Merge "Throw and log on some conditions that are error prone." into androidx-main
diff --git a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.java b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.java
index 95fb392..b73adf6 100644
--- a/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.java
+++ b/appactions/interaction/interaction-capabilities-core/src/test/java/androidx/appactions/interaction/capabilities/core/task/impl/TaskCapabilityImplTest.java
@@ -90,7 +90,6 @@
import androidx.concurrent.futures.CallbackToFutureAdapter.Completer;
import com.google.common.util.concurrent.ListenableFuture;
-import com.google.errorprone.annotations.CanIgnoreReturnValue;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -1703,7 +1702,6 @@
return RequiredTaskUpdater::new;
}
- @CanIgnoreReturnValue
public TaskHandlerBuilder setOnReadyToConfirmListener(
OnReadyToConfirmListenerInternal<Confirmation> listener) {
return super.setOnReadyToConfirmListenerInternal(listener);
@@ -1761,7 +1759,6 @@
return RequiredTaskUpdater::new;
}
- @CanIgnoreReturnValue
public TaskHandlerBuilderWithRequiredConfirmation setOnReadyToConfirmListener(
OnReadyToConfirmListenerInternal<Confirmation> listener) {
return super.setOnReadyToConfirmListenerInternal(listener);
diff --git a/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml b/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml
index 1f1625b..4fd7629 100644
--- a/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml
+++ b/bluetooth/integration-tests/testapp/src/main/AndroidManifest.xml
@@ -21,7 +21,8 @@
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
- <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
+ <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+ <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
</manifest>
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
index 3066bdc..9f655fb 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/MainActivity.kt
@@ -66,6 +66,7 @@
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.BLUETOOTH_ADVERTISE,
+ Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_SCAN,
)
)
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt
new file mode 100644
index 0000000..f2fb27d
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/AdvertiseResult.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.bluetooth.integration.testapp.experimental
+
+enum class AdvertiseResult {
+ ADVERTISE_STARTED,
+ ADVERTISE_FAILED_ALREADY_STARTED,
+ ADVERTISE_FAILED_DATA_TOO_LARGE,
+ ADVERTISE_FAILED_FEATURE_UNSUPPORTED,
+ ADVERTISE_FAILED_INTERNAL_ERROR,
+ ADVERTISE_FAILED_TOO_MANY_ADVERTISERS
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/BluetoothLe.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/BluetoothLe.kt
new file mode 100644
index 0000000..c6555f4
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/BluetoothLe.kt
@@ -0,0 +1,223 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.bluetooth.integration.testapp.experimental
+
+import android.annotation.SuppressLint
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGattCharacteristic
+import android.bluetooth.BluetoothGattDescriptor
+import android.bluetooth.BluetoothGattServerCallback
+import android.bluetooth.BluetoothGattService
+import android.bluetooth.BluetoothManager
+import android.bluetooth.le.AdvertiseCallback
+import android.bluetooth.le.AdvertiseData
+import android.bluetooth.le.AdvertiseSettings
+import android.content.Context
+import android.util.Log
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+
+/**
+ * Singleton class. Entry point for BLE related operations.
+ */
+class BluetoothLe(private val context: Context) {
+
+ companion object {
+ const val TAG = "BluetoothLe"
+ }
+
+ private val bluetoothManager =
+ context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
+
+ // Permissions are handled by MainActivity requestBluetoothPermissions
+ @SuppressLint("MissingPermission")
+ fun advertise(
+ settings: AdvertiseSettings,
+ data: AdvertiseData
+ ): Flow<AdvertiseResult> =
+ callbackFlow {
+ val callback = object : AdvertiseCallback() {
+ override fun onStartFailure(errorCode: Int) {
+ trySend(AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR)
+ }
+
+ override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
+ trySend(AdvertiseResult.ADVERTISE_STARTED)
+ }
+ }
+
+ val bluetoothAdapter = bluetoothManager?.adapter
+ val bleAdvertiser = bluetoothAdapter?.bluetoothLeAdvertiser
+
+ bleAdvertiser?.startAdvertising(settings, data, callback)
+
+ awaitClose {
+ Log.d(TAG, "awaitClose() called")
+ bleAdvertiser?.stopAdvertising(callback)
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ fun gattServer(): Flow<GattServerCallback> =
+ callbackFlow {
+ val callback = object : BluetoothGattServerCallback() {
+ override fun onConnectionStateChange(
+ device: BluetoothDevice?,
+ status: Int,
+ newState: Int
+ ) {
+ trySend(
+ GattServerCallback.OnConnectionStateChange(device, status, newState)
+ )
+ }
+
+ override fun onServiceAdded(status: Int, service: BluetoothGattService?) {
+ trySend(
+ GattServerCallback.OnServiceAdded(status, service)
+ )
+ }
+
+ override fun onCharacteristicReadRequest(
+ device: BluetoothDevice?,
+ requestId: Int,
+ offset: Int,
+ characteristic: BluetoothGattCharacteristic?
+ ) {
+ trySend(
+ GattServerCallback.OnCharacteristicReadRequest(
+ device,
+ requestId,
+ offset,
+ characteristic
+ )
+ )
+ }
+
+ override fun onCharacteristicWriteRequest(
+ device: BluetoothDevice?,
+ requestId: Int,
+ characteristic: BluetoothGattCharacteristic?,
+ preparedWrite: Boolean,
+ responseNeeded: Boolean,
+ offset: Int,
+ value: ByteArray?
+ ) {
+ trySend(
+ GattServerCallback.OnCharacteristicWriteRequest(
+ device,
+ requestId,
+ characteristic,
+ preparedWrite,
+ responseNeeded,
+ offset,
+ value
+ )
+ )
+ }
+
+ override fun onDescriptorReadRequest(
+ device: BluetoothDevice?,
+ requestId: Int,
+ offset: Int,
+ descriptor: BluetoothGattDescriptor?
+ ) {
+ trySend(
+ GattServerCallback.OnDescriptorReadRequest(
+ device,
+ requestId,
+ offset,
+ descriptor
+ )
+ )
+ }
+
+ override fun onDescriptorWriteRequest(
+ device: BluetoothDevice?,
+ requestId: Int,
+ descriptor: BluetoothGattDescriptor?,
+ preparedWrite: Boolean,
+ responseNeeded: Boolean,
+ offset: Int,
+ value: ByteArray?
+ ) {
+ trySend(
+ GattServerCallback.OnDescriptorWriteRequest(
+ device,
+ requestId,
+ descriptor,
+ preparedWrite,
+ responseNeeded,
+ offset,
+ value
+ )
+ )
+ }
+
+ override fun onExecuteWrite(
+ device: BluetoothDevice?,
+ requestId: Int,
+ execute: Boolean
+ ) {
+ trySend(
+ GattServerCallback.OnExecuteWrite(device, requestId, execute)
+ )
+ }
+
+ override fun onNotificationSent(device: BluetoothDevice?, status: Int) {
+ trySend(
+ GattServerCallback.OnNotificationSent(device, status)
+ )
+ }
+
+ override fun onMtuChanged(device: BluetoothDevice?, mtu: Int) {
+ trySend(
+ GattServerCallback.OnMtuChanged(device, mtu)
+ )
+ }
+
+ override fun onPhyUpdate(
+ device: BluetoothDevice?,
+ txPhy: Int,
+ rxPhy: Int,
+ status: Int
+ ) {
+ trySend(
+ GattServerCallback.OnPhyUpdate(device, txPhy, rxPhy, status)
+ )
+ }
+
+ override fun onPhyRead(
+ device: BluetoothDevice?,
+ txPhy: Int,
+ rxPhy: Int,
+ status: Int
+ ) {
+ trySend(
+ GattServerCallback.OnPhyRead(device, txPhy, rxPhy, status)
+ )
+ }
+ }
+
+ val bluetoothGattServer = bluetoothManager?.openGattServer(context, callback)
+
+ awaitClose {
+ Log.d(TAG, "awaitClose() called")
+ bluetoothGattServer?.close()
+ }
+ }
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/GattServerCallback.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/GattServerCallback.kt
new file mode 100644
index 0000000..317e917
--- /dev/null
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/experimental/GattServerCallback.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.bluetooth.integration.testapp.experimental
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGattCharacteristic
+import android.bluetooth.BluetoothGattDescriptor
+import android.bluetooth.BluetoothGattService
+
+sealed interface GattServerCallback {
+ data class OnConnectionStateChange(
+ val device: BluetoothDevice?,
+ val status: Int,
+ val newState: Int
+ ) : GattServerCallback
+
+ data class OnServiceAdded(
+ val status: Int,
+ val service: BluetoothGattService?
+ ) : GattServerCallback
+
+ data class OnCharacteristicReadRequest(
+ val device: BluetoothDevice?,
+ val requestId: Int,
+ val offset: Int,
+ val characteristic: BluetoothGattCharacteristic?
+ ) : GattServerCallback
+
+ data class OnCharacteristicWriteRequest(
+ val device: BluetoothDevice?,
+ val requestId: Int,
+ val characteristic: BluetoothGattCharacteristic?,
+ val preparedWrite: Boolean,
+ val responseNeeded: Boolean,
+ val offset: Int,
+ val value: ByteArray?
+ ) : GattServerCallback
+
+ data class OnDescriptorReadRequest(
+ val device: BluetoothDevice?,
+ val requestId: Int,
+ val offset: Int,
+ val descriptor: BluetoothGattDescriptor?
+ ) : GattServerCallback
+
+ data class OnDescriptorWriteRequest(
+ val device: BluetoothDevice?,
+ val requestId: Int,
+ val descriptor: BluetoothGattDescriptor?,
+ val preparedWrite: Boolean,
+ val responseNeeded: Boolean,
+ val offset: Int,
+ val value: ByteArray?
+ ) : GattServerCallback
+
+ data class OnExecuteWrite(
+ val device: BluetoothDevice?,
+ val requestId: Int,
+ val execute: Boolean
+ ) : GattServerCallback
+
+ data class OnNotificationSent(
+ val device: BluetoothDevice?,
+ val status: Int
+ ) : GattServerCallback
+
+ data class OnMtuChanged(
+ val device: BluetoothDevice?,
+ val mtu: Int
+ ) : GattServerCallback
+
+ data class OnPhyUpdate(
+ val device: BluetoothDevice?,
+ val txPhy: Int,
+ val rxPhy: Int,
+ val status: Int
+ ) : GattServerCallback
+
+ data class OnPhyRead(
+ val device: BluetoothDevice?,
+ val txPhy: Int,
+ val rxPhy: Int,
+ val status: Int
+ ) : GattServerCallback
+}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/bluetoothx/BtxFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/bluetoothx/BtxFragment.kt
index b10253f..800098e 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/bluetoothx/BtxFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/bluetoothx/BtxFragment.kt
@@ -18,7 +18,6 @@
import android.annotation.SuppressLint
import android.bluetooth.BluetoothManager
-import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
import android.bluetooth.le.ScanCallback
@@ -34,6 +33,9 @@
import androidx.bluetooth.integration.testapp.R
import androidx.bluetooth.integration.testapp.databinding.FragmentBtxBinding
+import androidx.bluetooth.integration.testapp.experimental.AdvertiseResult
+import androidx.bluetooth.integration.testapp.experimental.BluetoothLe
+import androidx.bluetooth.integration.testapp.experimental.GattServerCallback
import androidx.bluetooth.integration.testapp.ui.framework.FwkFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
@@ -53,6 +55,8 @@
const val TAG = "BtxFragment"
}
+ private lateinit var bluetoothLe: BluetoothLe
+
private lateinit var btxViewModel: BtxViewModel
private var _binding: FragmentBtxBinding? = null
@@ -78,6 +82,8 @@
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
+ bluetoothLe = BluetoothLe(requireContext())
+
binding.buttonScan.setOnClickListener {
startScan()
}
@@ -86,13 +92,19 @@
if (isChecked) startAdvertise()
else advertiseJob?.cancel()
}
+
+ binding.switchGattServer.setOnCheckedChangeListener { _, isChecked ->
+ if (isChecked) openGattServer()
+ else gattServerJob?.cancel()
+ }
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
- advertiseJob?.cancel()
scanJob?.cancel()
+ advertiseJob?.cancel()
+ gattServerJob?.cancel()
}
private val scanScope = CoroutineScope(Dispatchers.Main + Job())
@@ -143,42 +155,6 @@
private val advertiseScope = CoroutineScope(Dispatchers.Main + Job())
private var advertiseJob: Job? = null
- enum class AdvertiseResult {
- ADVERTISE_STARTED,
- ADVERTISE_FAILED_ALREADY_STARTED,
- ADVERTISE_FAILED_DATA_TOO_LARGE,
- ADVERTISE_FAILED_FEATURE_UNSUPPORTED,
- ADVERTISE_FAILED_INTERNAL_ERROR,
- ADVERTISE_FAILED_TOO_MANY_ADVERTISERS
- }
-
- // Permissions are handled by MainActivity requestBluetoothPermissions
- @SuppressLint("MissingPermission")
- fun advertise(settings: AdvertiseSettings, data: AdvertiseData): Flow<AdvertiseResult> =
- callbackFlow {
- val callback = object : AdvertiseCallback() {
- override fun onStartFailure(errorCode: Int) {
- trySend(AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR)
- }
-
- override fun onStartSuccess(settingsInEffect: AdvertiseSettings?) {
- trySend(AdvertiseResult.ADVERTISE_STARTED)
- }
- }
-
- val bluetoothManager =
- context?.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
- val bluetoothAdapter = bluetoothManager?.adapter
- val bleAdvertiser = bluetoothAdapter?.bluetoothLeAdvertiser
-
- bleAdvertiser?.startAdvertising(settings, data, callback)
-
- awaitClose {
- Log.d(TAG, "awaitClose() called")
- bleAdvertiser?.stopAdvertising(callback)
- }
- }
-
// Permissions are handled by MainActivity requestBluetoothPermissions
@SuppressLint("MissingPermission")
private fun startAdvertise() {
@@ -195,7 +171,7 @@
.build()
advertiseJob = advertiseScope.launch {
- advertise(advertiseSettings, advertiseData)
+ bluetoothLe.advertise(advertiseSettings, advertiseData)
.collect {
Log.d(TAG, "advertiseResult received: $it")
@@ -216,4 +192,119 @@
}
}
}
+
+ private val gattServerScope = CoroutineScope(Dispatchers.Main + Job())
+ private var gattServerJob: Job? = null
+
+ // Permissions are handled by MainActivity requestBluetoothPermissions
+ @SuppressLint("MissingPermission")
+ private fun openGattServer() {
+ Log.d(TAG, "openGattServer() called")
+
+ gattServerJob = gattServerScope.launch {
+ bluetoothLe.gattServer().collect { gattServerCallback ->
+ when (gattServerCallback) {
+ is GattServerCallback.OnCharacteristicReadRequest -> {
+ val onCharacteristicReadRequest:
+ GattServerCallback.OnCharacteristicReadRequest = gattServerCallback
+ Log.d(
+ TAG,
+ "openGattServer() called with: " +
+ "onCharacteristicReadRequest = $onCharacteristicReadRequest"
+ )
+ }
+ is GattServerCallback.OnCharacteristicWriteRequest -> {
+ val onCharacteristicWriteRequest:
+ GattServerCallback.OnCharacteristicWriteRequest = gattServerCallback
+ Log.d(
+ TAG,
+ "openGattServer() called with: " +
+ "onCharacteristicWriteRequest = $onCharacteristicWriteRequest"
+ )
+ }
+ is GattServerCallback.OnConnectionStateChange -> {
+ val onConnectionStateChange:
+ GattServerCallback.OnConnectionStateChange = gattServerCallback
+ Log.d(
+ TAG,
+ "openGattServer() called with: " +
+ "onConnectionStateChange = $onConnectionStateChange"
+ )
+ }
+ is GattServerCallback.OnDescriptorReadRequest -> {
+ val onDescriptorReadRequest:
+ GattServerCallback.OnDescriptorReadRequest = gattServerCallback
+ Log.d(
+ TAG,
+ "openGattServer() called with: " +
+ "onDescriptorReadRequest = $onDescriptorReadRequest"
+ )
+ }
+ is GattServerCallback.OnDescriptorWriteRequest -> {
+ val onDescriptorWriteRequest:
+ GattServerCallback.OnDescriptorWriteRequest = gattServerCallback
+ Log.d(
+ TAG,
+ "openGattServer() called with: " +
+ "onDescriptorWriteRequest = $onDescriptorWriteRequest"
+ )
+ }
+ is GattServerCallback.OnExecuteWrite -> {
+ val onExecuteWrite:
+ GattServerCallback.OnExecuteWrite = gattServerCallback
+ Log.d(
+ TAG,
+ "openGattServer() called with: " +
+ "onExecuteWrite = $onExecuteWrite"
+ )
+ }
+ is GattServerCallback.OnMtuChanged -> {
+ val onMtuChanged:
+ GattServerCallback.OnMtuChanged = gattServerCallback
+ Log.d(
+ TAG,
+ "openGattServer() called with: " +
+ "onMtuChanged = $onMtuChanged"
+ )
+ }
+ is GattServerCallback.OnNotificationSent -> {
+ val onNotificationSent:
+ GattServerCallback.OnNotificationSent = gattServerCallback
+ Log.d(
+ TAG,
+ "openGattServer() called with: " +
+ "onNotificationSent = $onNotificationSent"
+ )
+ }
+ is GattServerCallback.OnPhyRead -> {
+ val onPhyRead:
+ GattServerCallback.OnPhyRead = gattServerCallback
+ Log.d(
+ TAG,
+ "openGattServer() called with: " +
+ "onPhyRead = $onPhyRead"
+ )
+ }
+ is GattServerCallback.OnPhyUpdate -> {
+ val onPhyUpdate:
+ GattServerCallback.OnPhyUpdate = gattServerCallback
+ Log.d(
+ TAG,
+ "openGattServer() called with: " +
+ "onPhyUpdate = $onPhyUpdate"
+ )
+ }
+ is GattServerCallback.OnServiceAdded -> {
+ val onServiceAdded:
+ GattServerCallback.OnServiceAdded = gattServerCallback
+ Log.d(
+ TAG,
+ "openGattServer() called with: " +
+ "onServiceAdded = $onServiceAdded"
+ )
+ }
+ }
+ }
+ }
+ }
}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/framework/FwkFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/framework/FwkFragment.kt
index d066bbc..46e8e17 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/framework/FwkFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/framework/FwkFragment.kt
@@ -17,6 +17,12 @@
package androidx.bluetooth.integration.testapp.ui.framework
import android.annotation.SuppressLint
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothGattCharacteristic
+import android.bluetooth.BluetoothGattDescriptor
+import android.bluetooth.BluetoothGattServer
+import android.bluetooth.BluetoothGattServerCallback
+import android.bluetooth.BluetoothGattService
import android.bluetooth.BluetoothManager
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
@@ -80,8 +86,10 @@
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
- Log.d(TAG, "onCreateView() called with: inflater = $inflater, " +
- "container = $container, savedInstanceState = $savedInstanceState")
+ Log.d(
+ TAG, "onCreateView() called with: inflater = $inflater, " +
+ "container = $container, savedInstanceState = $savedInstanceState"
+ )
fwkViewModel = ViewModelProvider(this)[FwkViewModel::class.java]
_binding = FragmentFwkBinding.inflate(inflater, container, false)
@@ -99,11 +107,19 @@
if (isChecked) startAdvertise()
else stopAdvertise()
}
+
+ binding.switchGattServer.setOnCheckedChangeListener { _, isChecked ->
+ if (isChecked) openGattServer()
+ else closeGattServer()
+ }
}
+ // Permissions are handled by MainActivity requestBluetoothPermissions
+ @SuppressLint("MissingPermission")
override fun onDestroyView() {
super.onDestroyView()
_binding = null
+ bluetoothGattServer?.close()
}
// Permissions are handled by MainActivity requestBluetoothPermissions
@@ -170,4 +186,154 @@
bleAdvertiser?.stopAdvertising(advertiseCallback)
}
+
+ private var bluetoothGattServer: BluetoothGattServer? = null
+
+ // Permissions are handled by MainActivity requestBluetoothPermissions
+ @SuppressLint("MissingPermission")
+ private fun openGattServer() {
+ Log.d(TAG, "openGattServer() called")
+
+ val bluetoothManager =
+ context?.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager
+
+ bluetoothGattServer = bluetoothManager?.openGattServer(
+ requireContext(),
+ object : BluetoothGattServerCallback() {
+ override fun onConnectionStateChange(
+ device: BluetoothDevice?,
+ status: Int,
+ newState: Int
+ ) {
+ Log.d(
+ TAG,
+ "onConnectionStateChange() called with: device = $device" +
+ ", status = $status, newState = $newState"
+ )
+ }
+
+ override fun onServiceAdded(status: Int, service: BluetoothGattService?) {
+ Log.d(TAG, "onServiceAdded() called with: status = $status, service = $service")
+ }
+
+ override fun onCharacteristicReadRequest(
+ device: BluetoothDevice?,
+ requestId: Int,
+ offset: Int,
+ characteristic: BluetoothGattCharacteristic?
+ ) {
+ Log.d(
+ TAG,
+ "onCharacteristicReadRequest() called with: device = $device" +
+ ", requestId = $requestId, offset = $offset" +
+ ", characteristic = $characteristic"
+ )
+ }
+
+ override fun onCharacteristicWriteRequest(
+ device: BluetoothDevice?,
+ requestId: Int,
+ characteristic: BluetoothGattCharacteristic?,
+ preparedWrite: Boolean,
+ responseNeeded: Boolean,
+ offset: Int,
+ value: ByteArray?
+ ) {
+ Log.d(
+ TAG,
+ "onCharacteristicWriteRequest() called with: device = $device" +
+ ", requestId = $requestId, characteristic = $characteristic" +
+ ", preparedWrite = $preparedWrite, responseNeeded = $responseNeeded" +
+ ", offset = $offset, value = $value"
+ )
+ }
+
+ override fun onDescriptorReadRequest(
+ device: BluetoothDevice?,
+ requestId: Int,
+ offset: Int,
+ descriptor: BluetoothGattDescriptor?
+ ) {
+ Log.d(
+ TAG,
+ "onDescriptorReadRequest() called with: device = $device" +
+ ", requestId = $requestId, offset = $offset, descriptor = $descriptor"
+ )
+ }
+
+ override fun onDescriptorWriteRequest(
+ device: BluetoothDevice?,
+ requestId: Int,
+ descriptor: BluetoothGattDescriptor?,
+ preparedWrite: Boolean,
+ responseNeeded: Boolean,
+ offset: Int,
+ value: ByteArray?
+ ) {
+ Log.d(
+ TAG,
+ "onDescriptorWriteRequest() called with: device = $device" +
+ ", requestId = $requestId, descriptor = $descriptor" +
+ ", preparedWrite = $preparedWrite, responseNeeded = $responseNeeded" +
+ ", offset = $offset, value = $value"
+ )
+ }
+
+ override fun onExecuteWrite(
+ device: BluetoothDevice?,
+ requestId: Int,
+ execute: Boolean
+ ) {
+ Log.d(
+ TAG,
+ "onExecuteWrite() called with: device = $device, requestId = $requestId" +
+ ", execute = $execute"
+ )
+ }
+
+ override fun onNotificationSent(device: BluetoothDevice?, status: Int) {
+ Log.d(
+ TAG,
+ "onNotificationSent() called with: device = $device, status = $status"
+ )
+ }
+
+ override fun onMtuChanged(device: BluetoothDevice?, mtu: Int) {
+ Log.d(TAG, "onMtuChanged() called with: device = $device, mtu = $mtu")
+ }
+
+ override fun onPhyUpdate(
+ device: BluetoothDevice?,
+ txPhy: Int,
+ rxPhy: Int,
+ status: Int
+ ) {
+ Log.d(
+ TAG, "onPhyUpdate() called with: device = $device, txPhy = $txPhy" +
+ ", rxPhy = $rxPhy, status = $status"
+ )
+ }
+
+ override fun onPhyRead(
+ device: BluetoothDevice?,
+ txPhy: Int,
+ rxPhy: Int,
+ status: Int
+ ) {
+ Log.d(
+ TAG,
+ "onPhyRead() called with: device = $device, txPhy = $txPhy" +
+ ", rxPhy = $rxPhy, status = $status"
+ )
+ }
+ })
+ }
+
+ // Permissions are handled by MainActivity requestBluetoothPermissions
+ @SuppressLint("MissingPermission")
+ private fun closeGattServer() {
+ Log.d(TAG, "closeGattServer() called")
+
+ bluetoothGattServer?.close()
+ }
}
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
index 4bdca3c..5d9372f 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/activity_main.xml
@@ -21,7 +21,6 @@
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:paddingTop="?attr/actionBarSize"
tools:context=".MainActivity">
<com.google.android.material.bottomnavigation.BottomNavigationView
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_btx.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_btx.xml
index 2f53401..1a750d7 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_btx.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_btx.xml
@@ -20,27 +20,36 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:padding="16dp"
tools:context=".ui.bluetoothx.BtxFragment">
- <androidx.appcompat.widget.SwitchCompat
- android:id="@+id/switch_advertise"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/advertise_using_btx"
- android:layout_marginBottom="16dp"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintBottom_toTopOf="@+id/button_scan"
- app:layout_constraintEnd_toEndOf="parent" />
-
<Button
android:id="@+id/button_scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="16dp"
+ android:layout_marginTop="16dp"
android:text="@string/scan_using_btx"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent" />
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/switch_advertise"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/advertise_using_btx"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/button_scan" />
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/switch_gatt_server"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/open_gatt_server_using_btx"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/switch_advertise" />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_fwk.xml b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_fwk.xml
index 6a72bf2..48c20bf 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_fwk.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/layout/fragment_fwk.xml
@@ -20,27 +20,36 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:padding="16dp"
tools:context=".ui.framework.FwkFragment">
- <androidx.appcompat.widget.SwitchCompat
- android:id="@+id/switch_advertise"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/advertise_using_fwk"
- android:layout_marginBottom="16dp"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintBottom_toTopOf="@+id/button_scan"
- app:layout_constraintEnd_toEndOf="parent" />
-
<Button
android:id="@+id/button_scan"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_marginBottom="16dp"
+ android:layout_marginTop="16dp"
android:text="@string/scan_using_fwk"
- app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintEnd_toEndOf="parent" />
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/switch_advertise"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/advertise_using_fwk"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/button_scan" />
+
+ <androidx.appcompat.widget.SwitchCompat
+ android:id="@+id/switch_gatt_server"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="16dp"
+ android:text="@string/open_gatt_server_using_fwk"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/switch_advertise" />
</androidx.constraintlayout.widget.ConstraintLayout>
diff --git a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
index 170c6a7..913a1bb 100644
--- a/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
+++ b/bluetooth/integration-tests/testapp/src/main/res/values/donottranslate-strings.xml
@@ -27,4 +27,8 @@
<string name="advertise_using_fwk">Advertise using Framework Bluetooth APIs</string>
<string name="advertise_using_btx">Advertise using BluetoothX APIs</string>
<string name="advertise_start_message">Advertise started</string>
+
+ <string name="open_gatt_server_using_fwk">Open GATT Server using Framework Bluetooth APIs</string>
+ <string name="open_gatt_server_using_btx">Open GATT Server using BluetoothX APIs</string>
+ <string name="gatt_server_open">GATT Server open</string>
</resources>
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.java
index d3ef576..45701e8 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/FlashAvailabilityBufferUnderflowQuirk.java
@@ -20,11 +20,11 @@
import android.os.Build;
import android.util.Pair;
+import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.camera.core.impl.Quirk;
import java.nio.BufferUnderflowException;
-import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
@@ -44,12 +44,17 @@
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class FlashAvailabilityBufferUnderflowQuirk implements Quirk {
- private static final Set<Pair<String, String>> KNOWN_AFFECTED_MODELS = new HashSet<>(
- Arrays.asList(
- // Devices enumerated as Pair(Build.MANUFACTURER, Build.MODEL)
- new Pair<>("sprd", "lemp"),
- new Pair<>("sprd", "DM20C")
- ));
+ private static final Set<Pair<String, String>> KNOWN_AFFECTED_MODELS = new HashSet<>();
+ static {
+ // Devices enumerated as Pair(Build.MANUFACTURER, Build.MODEL).
+ addAffectedDevice("sprd", "lemp");
+ addAffectedDevice("sprd", "DM20C");
+ }
+
+ private static void addAffectedDevice(@NonNull String manufacturer, @NonNull String model) {
+ KNOWN_AFFECTED_MODELS.add(new Pair<>(manufacturer.toLowerCase(Locale.US),
+ model.toLowerCase(Locale.US)));
+ }
static boolean load() {
return KNOWN_AFFECTED_MODELS.contains(new Pair<>(Build.MANUFACTURER.toLowerCase(Locale.US),
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/AndroidUtil.java b/camera/camera-testing/src/main/java/androidx/camera/testing/AndroidUtil.java
index bf0380be..e76024c 100644
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/AndroidUtil.java
+++ b/camera/camera-testing/src/main/java/androidx/camera/testing/AndroidUtil.java
@@ -43,7 +43,8 @@
|| Build.MODEL.contains("Android SDK built for x86")
|| Build.MANUFACTURER.contains("Genymotion")
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
- || Build.PRODUCT.equals("google_sdk");
+ || Build.PRODUCT.equals("google_sdk")
+ || Build.HARDWARE.contains("ranchu");
}
/**
diff --git a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
index 5d0eb38..5a5e216 100644
--- a/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
+++ b/camera/integration-tests/viewtestapp/src/androidTest/java/androidx/camera/integration/view/CameraControllerFragmentTest.kt
@@ -37,6 +37,7 @@
import androidx.camera.core.impl.utils.futures.FutureCallback
import androidx.camera.core.impl.utils.futures.Futures
import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.camera.testing.AndroidUtil.isEmulator
import androidx.camera.testing.AndroidUtil.skipVideoRecordingTestIfNotSupportedByEmulator
import androidx.camera.testing.CameraPipeConfigTestRule
import androidx.camera.testing.CameraUtil
@@ -68,6 +69,7 @@
import org.junit.After
import org.junit.Assert
import org.junit.Assume
+import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Ignore
@@ -177,10 +179,7 @@
@Test
fun onPreviewViewTapped_previewIsFocused() {
- Assume.assumeFalse(
- "Ignore Cuttlefish",
- Build.MODEL.contains("Cuttlefish")
- )
+ assumeFalse("Ignore emulators", isEmulator())
// Arrange: listens to LiveData updates.
fragment.assertPreviewIsStreaming()
val focused = Semaphore(0)
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
index a2ce246..5647474 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/grid/LazyGridPinnableContainerTest.kt
@@ -39,6 +39,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -267,6 +268,7 @@
.assertIsNotPlaced()
}
+ @Ignore // b/268053147
@Test
fun pinnedItemIsStillPinnedWhenReorderedAndNotVisibleAnymore() {
val state = LazyGridState()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
index 4ef8154..67556ba 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyColumnTest.kt
@@ -49,13 +49,11 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performTouchInput
@@ -245,15 +243,13 @@
for (data in dataLists) {
rule.runOnIdle { dataModel = data }
- // Confirm the number of children to ensure there are no extra items
- val numItems = data.size
- rule.onNodeWithTag(tag)
- .onChildren()
- .assertCountEquals(numItems)
-
// Confirm the children's content
- for (item in data) {
- rule.onNodeWithText("$item").assertExists()
+ for (index in 1..8) {
+ if (index in data) {
+ rule.onNodeWithText("$index").assertIsDisplayed()
+ } else {
+ rule.onNodeWithText("$index").assertIsNotPlaced()
+ }
}
}
}
@@ -390,7 +386,7 @@
.assertIsDisplayed()
rule.onNodeWithTag("3")
- .assertDoesNotExist()
+ .assertIsNotPlaced()
}
@Test
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
index a743ac3c..ed50322 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListPinnableContainerTest.kt
@@ -41,6 +41,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -253,6 +254,7 @@
.assertIsNotPlaced()
}
+ @Ignore // b/268113792
@Test
fun pinnedItemIsStillPinnedWhenReorderedAndNotVisibleAnymore() {
val state = LazyListState()
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
index ecb6576..7784e47 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/list/LazyListTest.kt
@@ -721,9 +721,9 @@
// and has no children
rule.onNodeWithTag("1")
- .assertDoesNotExist()
+ .assertIsNotPlaced()
rule.onNodeWithTag("2")
- .assertDoesNotExist()
+ .assertIsNotPlaced()
}
@Test
diff --git a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
index 5796438..85b8635 100644
--- a/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
+++ b/compose/foundation/foundation/src/androidAndroidTest/kotlin/androidx/compose/foundation/lazy/staggeredgrid/LazyStaggeredGridPinnableContainerTest.kt
@@ -40,6 +40,7 @@
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.runBlocking
import org.junit.Before
+import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
@@ -269,6 +270,7 @@
.assertIsNotPlaced()
}
+ @Ignore // b/267698382
@Test
fun pinnedItemIsStillPinnedWhenReorderedAndNotVisibleAnymore() {
val state = LazyStaggeredGridState()
diff --git a/compose/material/material/api/public_plus_experimental_1.4.0-beta01.txt b/compose/material/material/api/public_plus_experimental_1.4.0-beta01.txt
index b032320..5423532 100644
--- a/compose/material/material/api/public_plus_experimental_1.4.0-beta01.txt
+++ b/compose/material/material/api/public_plus_experimental_1.4.0-beta01.txt
@@ -125,6 +125,7 @@
public final class BottomSheetScaffoldKt {
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomSheetScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomSheetScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit>? topBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional int floatingActionButtonPosition, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional float sheetPeekHeight, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.material.BottomSheetState BottomSheetScaffoldState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmStateChange);
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetScaffoldState rememberBottomSheetScaffoldState(optional androidx.compose.material.DrawerState drawerState, optional androidx.compose.material.BottomSheetState bottomSheetState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetState rememberBottomSheetState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmStateChange);
}
@@ -139,14 +140,21 @@
property public final androidx.compose.material.SnackbarHostState snackbarHostState;
}
- @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public final class BottomSheetState extends androidx.compose.material.SwipeableState<androidx.compose.material.BottomSheetValue> {
- ctor public BottomSheetState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmStateChange);
+ @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public final class BottomSheetState {
+ ctor public BottomSheetState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmValueChange);
method public suspend Object? collapse(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public androidx.compose.material.BottomSheetValue getCurrentValue();
+ method @Deprecated public float getOffset();
+ method public float getProgress();
method public boolean isCollapsed();
method public boolean isExpanded();
+ method public float requireOffset();
+ property public final androidx.compose.material.BottomSheetValue currentValue;
property public final boolean isCollapsed;
property public final boolean isExpanded;
+ property @Deprecated public final float offset;
+ property public final float progress;
field public static final androidx.compose.material.BottomSheetState.Companion Companion;
}
diff --git a/compose/material/material/api/public_plus_experimental_current.txt b/compose/material/material/api/public_plus_experimental_current.txt
index b032320..5423532 100644
--- a/compose/material/material/api/public_plus_experimental_current.txt
+++ b/compose/material/material/api/public_plus_experimental_current.txt
@@ -125,6 +125,7 @@
public final class BottomSheetScaffoldKt {
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static void BottomSheetScaffold(kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit> sheetContent, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.material.BottomSheetScaffoldState scaffoldState, optional kotlin.jvm.functions.Function0<kotlin.Unit>? topBar, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.SnackbarHostState,kotlin.Unit> snackbarHost, optional kotlin.jvm.functions.Function0<kotlin.Unit>? floatingActionButton, optional int floatingActionButtonPosition, optional boolean sheetGesturesEnabled, optional androidx.compose.ui.graphics.Shape sheetShape, optional float sheetElevation, optional long sheetBackgroundColor, optional long sheetContentColor, optional float sheetPeekHeight, optional kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.ColumnScope,kotlin.Unit>? drawerContent, optional boolean drawerGesturesEnabled, optional androidx.compose.ui.graphics.Shape drawerShape, optional float drawerElevation, optional long drawerBackgroundColor, optional long drawerContentColor, optional long drawerScrimColor, optional long backgroundColor, optional long contentColor, kotlin.jvm.functions.Function1<? super androidx.compose.foundation.layout.PaddingValues,kotlin.Unit> content);
+ method @Deprecated @androidx.compose.material.ExperimentalMaterialApi public static androidx.compose.material.BottomSheetState BottomSheetScaffoldState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmStateChange);
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetScaffoldState rememberBottomSheetScaffoldState(optional androidx.compose.material.DrawerState drawerState, optional androidx.compose.material.BottomSheetState bottomSheetState, optional androidx.compose.material.SnackbarHostState snackbarHostState);
method @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Composable public static androidx.compose.material.BottomSheetState rememberBottomSheetState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmStateChange);
}
@@ -139,14 +140,21 @@
property public final androidx.compose.material.SnackbarHostState snackbarHostState;
}
- @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public final class BottomSheetState extends androidx.compose.material.SwipeableState<androidx.compose.material.BottomSheetValue> {
- ctor public BottomSheetState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmStateChange);
+ @androidx.compose.material.ExperimentalMaterialApi @androidx.compose.runtime.Stable public final class BottomSheetState {
+ ctor public BottomSheetState(androidx.compose.material.BottomSheetValue initialValue, optional androidx.compose.animation.core.AnimationSpec<java.lang.Float> animationSpec, optional kotlin.jvm.functions.Function1<? super androidx.compose.material.BottomSheetValue,java.lang.Boolean> confirmValueChange);
method public suspend Object? collapse(kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public suspend Object? expand(kotlin.coroutines.Continuation<? super kotlin.Unit>);
+ method public androidx.compose.material.BottomSheetValue getCurrentValue();
+ method @Deprecated public float getOffset();
+ method public float getProgress();
method public boolean isCollapsed();
method public boolean isExpanded();
+ method public float requireOffset();
+ property public final androidx.compose.material.BottomSheetValue currentValue;
property public final boolean isCollapsed;
property public final boolean isExpanded;
+ property @Deprecated public final float offset;
+ property public final float progress;
field public static final androidx.compose.material.BottomSheetState.Companion Companion;
}
diff --git a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
index 7e1c569..75ab9f3 100644
--- a/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
+++ b/compose/material/material/src/androidAndroidTest/kotlin/androidx/compose/material/BottomSheetScaffoldTest.kt
@@ -35,7 +35,6 @@
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.layout.positionInParent
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.SemanticsActions
@@ -47,8 +46,8 @@
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onParent
-import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.performSemanticsAction
+import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.test.swipeDown
import androidx.compose.ui.test.swipeUp
import androidx.compose.ui.unit.Density
@@ -60,7 +59,6 @@
import androidx.test.filters.SdkSuppress
import com.google.common.truth.Truth
import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -352,47 +350,6 @@
}
@Test
- @Ignore("unignore once animation sync is ready (b/147291885)")
- fun bottomSheetScaffold_drawer_manualControl() = runBlocking {
- var drawerChildPosition: Offset = Offset.Zero
- lateinit var scaffoldState: BottomSheetScaffoldState
- rule.setContent {
- scaffoldState = rememberBottomSheetScaffoldState()
- Box {
- BottomSheetScaffold(
- scaffoldState = scaffoldState,
- sheetContent = {
- Box(Modifier.fillMaxWidth().requiredHeight(100.dp))
- },
- drawerContent = {
- Box(
- Modifier
- .fillMaxWidth()
- .height(50.dp)
- .background(color = Color.Blue)
- .onGloballyPositioned { positioned: LayoutCoordinates ->
- drawerChildPosition = positioned.positionInParent()
- }
- )
- }
- ) {
- Box(
- Modifier
- .fillMaxWidth()
- .height(50.dp)
- .background(color = Color.Blue)
- )
- }
- }
- }
- Truth.assertThat(drawerChildPosition.x).isLessThan(0f)
- scaffoldState.drawerState.open()
- Truth.assertThat(drawerChildPosition.x).isLessThan(0f)
- scaffoldState.drawerState.close()
- Truth.assertThat(drawerChildPosition.x).isLessThan(0f)
- }
-
- @Test
fun bottomSheetScaffold_AppbarAndContent_inColumn() {
var appbarPosition: Offset = Offset.Zero
var appbarSize: IntSize = IntSize.Zero
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
index 77fcbd42..7ffe514 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/BottomSheetScaffold.kt
@@ -13,13 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.compose.material
-
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
@@ -30,29 +26,29 @@
import androidx.compose.material.BottomSheetValue.Expanded
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
-import androidx.compose.runtime.State
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.onGloballyPositioned
+import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.semantics.collapse
import androidx.compose.ui.semantics.expand
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
+import kotlin.math.roundToInt
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.launch
-import kotlin.math.roundToInt
/**
* Possible values of [BottomSheetState].
@@ -63,60 +59,113 @@
* The bottom sheet is visible, but only showing its peek height.
*/
Collapsed,
-
/**
* The bottom sheet is visible at its maximum height.
*/
Expanded
}
+@Deprecated(
+ message = "This constructor is deprecated. confirmStateChange has been renamed to " +
+ "confirmValueChange.",
+ replaceWith = ReplaceWith("BottomSheetScaffoldState(initialValue, animationSpec, " +
+ "confirmStateChange)")
+)
+@ExperimentalMaterialApi
+fun BottomSheetScaffoldState(
+ initialValue: BottomSheetValue,
+ animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
+ confirmStateChange: (BottomSheetValue) -> Boolean
+) = BottomSheetState(
+ initialValue = initialValue,
+ animationSpec = animationSpec,
+ confirmValueChange = confirmStateChange
+)
+
/**
* State of the persistent bottom sheet in [BottomSheetScaffold].
*
* @param initialValue The initial value of the state.
* @param animationSpec The default animation that will be used to animate to a new state.
- * @param confirmStateChange Optional callback invoked to confirm or veto a pending state change.
+ * @param confirmValueChange Optional callback invoked to confirm or veto a pending state change.
*/
@ExperimentalMaterialApi
@Stable
class BottomSheetState(
initialValue: BottomSheetValue,
animationSpec: AnimationSpec<Float> = SwipeableDefaults.AnimationSpec,
- confirmStateChange: (BottomSheetValue) -> Boolean = { true }
-) : SwipeableState<BottomSheetValue>(
- initialValue = initialValue,
- animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
+ confirmValueChange: (BottomSheetValue) -> Boolean = { true }
) {
+ internal val swipeableState = SwipeableV2State(
+ initialValue = initialValue,
+ animationSpec = animationSpec,
+ confirmValueChange = confirmValueChange
+ )
+
+ val currentValue: BottomSheetValue
+ get() = swipeableState.currentValue
+
/**
* Whether the bottom sheet is expanded.
*/
val isExpanded: Boolean
- get() = currentValue == Expanded
+ get() = swipeableState.currentValue == Expanded
/**
* Whether the bottom sheet is collapsed.
*/
val isCollapsed: Boolean
- get() = currentValue == BottomSheetValue.Collapsed
+ get() = swipeableState.currentValue == Collapsed
/**
- * Expand the bottom sheet with animation and suspend until it if fully expanded or animation
- * has been cancelled. This method will throw [CancellationException] if the animation is
- * interrupted
- *
- * @return the reason the expand animation ended
+ * The fraction of the progress going from [currentValue] to the targetValue, within [0f..1f]
+ * bounds, or 1f if the sheet is in a settled state.
*/
- suspend fun expand() = animateTo(Expanded)
+ /*@FloatRange(from = 0f, to = 1f)*/
+ val progress: Float
+ get() = swipeableState.progress
+
+ /**
+ * Expand the bottom sheet with an animation and suspend until the animation finishes or is
+ * cancelled.
+ * Note: If the peek height is equal to the sheet height, this method will animate to the
+ * [Collapsed] state.
+ *
+ * This method will throw [CancellationException] if the animation is interrupted.
+ */
+ suspend fun expand() {
+ val target = if (swipeableState.hasAnchorForValue(Expanded)) Expanded else Collapsed
+ swipeableState.animateTo(target)
+ }
/**
* Collapse the bottom sheet with animation and suspend until it if fully collapsed or animation
* has been cancelled. This method will throw [CancellationException] if the animation is
- * interrupted
- *
- * @return the reason the collapse animation ended
+ * interrupted.
*/
- suspend fun collapse() = animateTo(BottomSheetValue.Collapsed)
+ suspend fun collapse() = swipeableState.animateTo(Collapsed)
+
+ @Deprecated(
+ message = "Use requireOffset() to access the offset.",
+ replaceWith = ReplaceWith("requireOffset()")
+ )
+ val offset: Float get() = error("Use requireOffset() to access the offset.")
+
+ /**
+ * Require the current offset.
+ *
+ * @throws IllegalStateException If the offset has not been initialized yet
+ */
+ fun requireOffset() = swipeableState.requireOffset()
+
+ internal suspend fun animateTo(
+ target: BottomSheetValue,
+ velocity: Float = swipeableState.lastVelocity
+ ) = swipeableState.animateTo(target, velocity)
+
+ internal suspend fun snapTo(target: BottomSheetValue) = swipeableState.snapTo(target)
+
+ internal val isAnimationRunning: Boolean get() = swipeableState.isAnimationRunning
companion object {
/**
@@ -126,20 +175,17 @@
animationSpec: AnimationSpec<Float>,
confirmStateChange: (BottomSheetValue) -> Boolean
): Saver<BottomSheetState, *> = Saver(
- save = { it.currentValue },
+ save = { it.swipeableState.currentValue },
restore = {
BottomSheetState(
initialValue = it,
animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
+ confirmValueChange = confirmStateChange
)
}
)
}
-
- internal val nestedScrollConnection = this.PreUpPostDownNestedScrollConnection
}
-
/**
* Create a [BottomSheetState] and [remember] it.
*
@@ -164,11 +210,10 @@
BottomSheetState(
initialValue = initialValue,
animationSpec = animationSpec,
- confirmStateChange = confirmStateChange
+ confirmValueChange = confirmStateChange
)
}
}
-
/**
* State of the [BottomSheetScaffold] composable.
*
@@ -183,7 +228,6 @@
val bottomSheetState: BottomSheetState,
val snackbarHostState: SnackbarHostState
)
-
/**
* Create and [remember] a [BottomSheetScaffoldState].
*
@@ -195,7 +239,7 @@
@ExperimentalMaterialApi
fun rememberBottomSheetScaffoldState(
drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
- bottomSheetState: BottomSheetState = rememberBottomSheetState(BottomSheetValue.Collapsed),
+ bottomSheetState: BottomSheetState = rememberBottomSheetState(Collapsed),
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }
): BottomSheetScaffoldState {
return remember(drawerState, bottomSheetState, snackbarHostState) {
@@ -206,7 +250,6 @@
)
}
}
-
/**
* <a href="https://material.io/components/sheets-bottom#standard-bottom-sheet" class="external" target="_blank">Material Design standard bottom sheet</a>.
*
@@ -241,7 +284,8 @@
* @param sheetContentColor The preferred content color provided by the bottom sheet to its
* children. Defaults to the matching content color for [sheetBackgroundColor], or if that is
* not a color from the theme, this will keep the same content color set above the bottom sheet.
- * @param sheetPeekHeight The height of the bottom sheet when it is collapsed.
+ * @param sheetPeekHeight The height of the bottom sheet when it is collapsed. If the peek height
+ * equals the sheet's full height, the sheet will only have a collapsed state.
* @param drawerContent The content of the drawer sheet.
* @param drawerGesturesEnabled Whether the drawer sheet can be interacted with by gestures.
* @param drawerShape The shape of the drawer sheet.
@@ -281,86 +325,59 @@
contentColor: Color = contentColorFor(backgroundColor),
content: @Composable (PaddingValues) -> Unit
) {
- val scope = rememberCoroutineScope()
- BoxWithConstraints(modifier) {
- val fullHeight = constraints.maxHeight.toFloat()
- val peekHeightPx = with(LocalDensity.current) { sheetPeekHeight.toPx() }
- var bottomSheetHeight by remember { mutableStateOf(fullHeight) }
-
- val swipeable = Modifier
- .nestedScroll(scaffoldState.bottomSheetState.nestedScrollConnection)
- .swipeable(
- state = scaffoldState.bottomSheetState,
- anchors = mapOf(
- fullHeight - peekHeightPx to BottomSheetValue.Collapsed,
- fullHeight - bottomSheetHeight to Expanded
- ),
- orientation = Orientation.Vertical,
- enabled = sheetGesturesEnabled,
- resistance = null
- )
- .semantics {
- if (peekHeightPx != bottomSheetHeight) {
- if (scaffoldState.bottomSheetState.isCollapsed) {
- expand {
- if (scaffoldState.bottomSheetState.confirmStateChange(Expanded)) {
- scope.launch { scaffoldState.bottomSheetState.expand() }
+ val peekHeightPx = with(LocalDensity.current) { sheetPeekHeight.toPx() }
+ val child = @Composable {
+ BottomSheetScaffoldLayout(
+ topBar = topBar,
+ body = content,
+ bottomSheet = { layoutHeight ->
+ BottomSheet(
+ state = scaffoldState.bottomSheetState,
+ modifier = Modifier
+ .nestedScroll(
+ remember(scaffoldState.bottomSheetState.swipeableState) {
+ ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
+ state = scaffoldState.bottomSheetState.swipeableState,
+ orientation = Orientation.Vertical
+ )
}
- true
- }
- } else {
- collapse {
- if (scaffoldState.bottomSheetState.confirmStateChange(Collapsed)) {
- scope.launch { scaffoldState.bottomSheetState.collapse() }
+ )
+ .fillMaxWidth()
+ .requiredHeightIn(min = sheetPeekHeight),
+ anchors = { state, sheetSize ->
+ when (state) {
+ Collapsed -> layoutHeight - peekHeightPx
+ Expanded -> if (sheetSize.height == peekHeightPx.roundToInt()) {
+ null
+ } else {
+ layoutHeight - sheetSize.height.toFloat()
}
- true
}
- }
- }
- }
-
- val child = @Composable {
- BottomSheetScaffoldStack(
- body = {
- Surface(
- color = backgroundColor,
- contentColor = contentColor
- ) {
- Column(Modifier.fillMaxSize()) {
- topBar?.invoke()
- content(PaddingValues(bottom = sheetPeekHeight))
- }
- }
- },
- bottomSheet = {
- Surface(
- swipeable
- .fillMaxWidth()
- .requiredHeightIn(min = sheetPeekHeight)
- .onGloballyPositioned {
- bottomSheetHeight = it.size.height.toFloat()
- },
- shape = sheetShape,
- elevation = sheetElevation,
- color = sheetBackgroundColor,
- contentColor = sheetContentColor,
- content = { Column(content = sheetContent) }
- )
- },
- floatingActionButton = {
- Box {
- floatingActionButton?.invoke()
- }
- },
- snackbarHost = {
- Box {
- snackbarHost(scaffoldState.snackbarHostState)
- }
- },
- bottomSheetOffset = scaffoldState.bottomSheetState.offset,
- floatingActionButtonPosition = floatingActionButtonPosition
- )
- }
+ },
+ sheetBackgroundColor = sheetBackgroundColor,
+ sheetContentColor = sheetContentColor,
+ sheetElevation = sheetElevation,
+ sheetGesturesEnabled = sheetGesturesEnabled,
+ sheetShape = sheetShape,
+ content = sheetContent
+ )
+ },
+ floatingActionButton = floatingActionButton,
+ snackbarHost = {
+ snackbarHost(scaffoldState.snackbarHostState)
+ },
+ sheetOffset = { scaffoldState.bottomSheetState.requireOffset() },
+ sheetPeekHeight = sheetPeekHeight,
+ sheetState = scaffoldState.bottomSheetState,
+ floatingActionButtonPosition = floatingActionButtonPosition
+ )
+ }
+ Surface(
+ modifier
+ .fillMaxSize(),
+ color = backgroundColor,
+ contentColor = contentColor
+ ) {
if (drawerContent == null) {
child()
} else {
@@ -378,68 +395,220 @@
}
}
}
-
+@OptIn(ExperimentalMaterialApi::class)
@Composable
-private fun BottomSheetScaffoldStack(
- body: @Composable () -> Unit,
- bottomSheet: @Composable () -> Unit,
- floatingActionButton: @Composable () -> Unit,
- snackbarHost: @Composable () -> Unit,
- bottomSheetOffset: State<Float>,
- floatingActionButtonPosition: FabPosition
+private fun BottomSheet(
+ state: BottomSheetState,
+ sheetGesturesEnabled: Boolean,
+ anchors: (state: BottomSheetValue, sheetSize: IntSize) -> Float?,
+ sheetShape: Shape,
+ sheetElevation: Dp,
+ sheetBackgroundColor: Color,
+ sheetContentColor: Color,
+ modifier: Modifier = Modifier,
+ content: @Composable ColumnScope.() -> Unit
) {
- Layout(
- content = {
- body()
- bottomSheet()
- floatingActionButton()
- snackbarHost()
- }
- ) { measurables, constraints ->
- val placeable = measurables.first().measure(constraints)
-
- layout(placeable.width, placeable.height) {
- placeable.placeRelative(0, 0)
-
- val (sheetPlaceable, fabPlaceable, snackbarPlaceable) =
- measurables.drop(1).map {
- it.measure(constraints.copy(minWidth = 0, minHeight = 0))
- }
-
- val sheetOffsetY = bottomSheetOffset.value.roundToInt()
-
- sheetPlaceable.placeRelative(0, sheetOffsetY)
-
- val fabOffsetX = when (floatingActionButtonPosition) {
- FabPosition.Center -> (placeable.width - fabPlaceable.width) / 2
- else -> placeable.width - fabPlaceable.width - FabEndSpacing.roundToPx()
- }
- val fabOffsetY = sheetOffsetY - fabPlaceable.height / 2
-
- fabPlaceable.placeRelative(fabOffsetX, fabOffsetY)
-
- val snackbarOffsetX = (placeable.width - snackbarPlaceable.width) / 2
- val snackbarOffsetY = placeable.height - snackbarPlaceable.height
-
- snackbarPlaceable.placeRelative(snackbarOffsetX, snackbarOffsetY)
- }
+ val scope = rememberCoroutineScope()
+ val anchorChangeHandler = remember(state, scope) {
+ BottomSheetScaffoldAnchorChangeHandler(
+ state = state,
+ animateTo = { target -> scope.launch { state.animateTo(target) } },
+ snapTo = { target -> scope.launch { state.snapTo(target) } }
+ )
}
+ Surface(
+ modifier
+ .swipeableV2(
+ state = state.swipeableState,
+ orientation = Orientation.Vertical,
+ enabled = sheetGesturesEnabled,
+ )
+ .swipeAnchors(
+ state = state.swipeableState,
+ possibleValues = setOf(Collapsed, Expanded),
+ calculateAnchor = anchors,
+ anchorChangeHandler = anchorChangeHandler
+ )
+ .semantics {
+ // If we don't have anchors yet, or have only one anchor we don't want any
+ // accessibility actions
+ if (state.swipeableState.anchors.size > 1) {
+ if (state.isCollapsed) {
+ expand {
+ if (state.swipeableState.confirmValueChange(Expanded)) {
+ scope.launch { state.expand() }
+ }
+ true
+ }
+ } else {
+ collapse {
+ if (state.swipeableState.confirmValueChange(Collapsed)) {
+ scope.launch { state.collapse() }
+ }
+ true
+ }
+ }
+ }
+ },
+ shape = sheetShape,
+ elevation = sheetElevation,
+ color = sheetBackgroundColor,
+ contentColor = sheetContentColor,
+ content = { Column(content = content) }
+ )
}
-private val FabEndSpacing = 16.dp
-
/**
* Contains useful defaults for [BottomSheetScaffold].
*/
object BottomSheetScaffoldDefaults {
-
/**
* The default elevation used by [BottomSheetScaffold].
*/
val SheetElevation = 8.dp
-
/**
* The default peek height used by [BottomSheetScaffold].
*/
val SheetPeekHeight = 56.dp
}
+private enum class BottomSheetScaffoldLayoutSlot { TopBar, Body, Sheet, Fab, Snackbar }
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+private fun BottomSheetScaffoldLayout(
+ topBar: @Composable (() -> Unit)?,
+ body: @Composable (innerPadding: PaddingValues) -> Unit,
+ bottomSheet: @Composable (layoutHeight: Int) -> Unit,
+ floatingActionButton: (@Composable () -> Unit)?,
+ snackbarHost: @Composable () -> Unit,
+ sheetPeekHeight: Dp,
+ floatingActionButtonPosition: FabPosition,
+ sheetOffset: () -> Float,
+ sheetState: BottomSheetState,
+) {
+ SubcomposeLayout { constraints ->
+ val layoutWidth = constraints.maxWidth
+ val layoutHeight = constraints.maxHeight
+ val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
+ val sheetPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Sheet) {
+ bottomSheet(layoutHeight)
+ }[0].measure(looseConstraints)
+ val sheetOffsetY = sheetOffset().roundToInt()
+ val topBarPlaceable = topBar?.let {
+ subcompose(BottomSheetScaffoldLayoutSlot.TopBar) { topBar() }[0]
+ .measure(looseConstraints)
+ }
+ val topBarHeight = topBarPlaceable?.height ?: 0
+ val bodyConstraints = looseConstraints.copy(maxHeight = layoutHeight - topBarHeight)
+ val bodyPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Body) {
+ body(PaddingValues(bottom = sheetPeekHeight))
+ }[0].measure(bodyConstraints)
+ val fabPlaceable = floatingActionButton?.let { fab ->
+ subcompose(BottomSheetScaffoldLayoutSlot.Fab, fab)[0].measure(looseConstraints)
+ }
+ val fabWidth = fabPlaceable?.width ?: 0
+ val fabHeight = fabPlaceable?.height ?: 0
+ val fabOffsetX = when (floatingActionButtonPosition) {
+ FabPosition.Center -> (layoutWidth - fabWidth) / 2
+ else -> layoutWidth - fabWidth - FabSpacing.roundToPx()
+ }
+ // In case sheet peek height < (FAB height / 2), give the FAB some minimum space
+ val fabOffsetY = if (sheetPeekHeight.toPx() < fabHeight / 2) {
+ sheetOffsetY - fabHeight - FabSpacing.roundToPx()
+ } else sheetOffsetY - (fabHeight / 2)
+ val snackbarPlaceable = subcompose(BottomSheetScaffoldLayoutSlot.Snackbar, snackbarHost)[0]
+ .measure(looseConstraints)
+ val snackbarOffsetX = (layoutWidth - snackbarPlaceable.width) / 2
+ val snackbarOffsetY = when (sheetState.currentValue) {
+ Collapsed -> fabOffsetY - snackbarPlaceable.height
+ Expanded -> layoutHeight - snackbarPlaceable.height
+ }
+ layout(layoutWidth, layoutHeight) {
+ // Placement order is important for elevation
+ bodyPlaceable.placeRelative(0, topBarHeight)
+ topBarPlaceable?.placeRelative(0, 0)
+ sheetPlaceable.placeRelative(0, sheetOffsetY)
+ fabPlaceable?.placeRelative(fabOffsetX, fabOffsetY)
+ snackbarPlaceable.placeRelative(snackbarOffsetX, snackbarOffsetY)
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+private fun ConsumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
+ state: SwipeableV2State<*>,
+ orientation: Orientation
+): NestedScrollConnection = object : NestedScrollConnection {
+ override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
+ val delta = available.toFloat()
+ return if (delta < 0 && source == NestedScrollSource.Drag) {
+ state.dispatchRawDelta(delta).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ return if (source == NestedScrollSource.Drag) {
+ state.dispatchRawDelta(available.toFloat()).toOffset()
+ } else {
+ Offset.Zero
+ }
+ }
+
+ override suspend fun onPreFling(available: Velocity): Velocity {
+ val toFling = available.toFloat()
+ val currentOffset = state.requireOffset()
+ return if (toFling < 0 && currentOffset > state.minOffset) {
+ state.settle(velocity = toFling)
+ // since we go to the anchor with tween settling, consume all for the best UX
+ available
+ } else {
+ Velocity.Zero
+ }
+ }
+
+ override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
+ state.settle(velocity = available.toFloat())
+ return available
+ }
+
+ private fun Float.toOffset(): Offset = Offset(
+ x = if (orientation == Orientation.Horizontal) this else 0f,
+ y = if (orientation == Orientation.Vertical) this else 0f
+ )
+
+ @JvmName("velocityToFloat")
+ private fun Velocity.toFloat() = if (orientation == Orientation.Horizontal) x else y
+
+ @JvmName("offsetToFloat")
+ private fun Offset.toFloat(): Float = if (orientation == Orientation.Horizontal) x else y
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+private fun BottomSheetScaffoldAnchorChangeHandler(
+ state: BottomSheetState,
+ animateTo: (target: BottomSheetValue) -> Unit,
+ snapTo: (target: BottomSheetValue) -> Unit,
+) = AnchorChangeHandler<BottomSheetValue> { previousTarget, previousAnchors, newAnchors ->
+ val previousTargetOffset = previousAnchors[previousTarget]
+ val newTarget = when (previousTarget) {
+ Collapsed -> Collapsed
+ Expanded -> if (newAnchors.containsKey(Expanded)) Expanded else Collapsed
+ }
+ val newTargetOffset = newAnchors.getValue(newTarget)
+ if (newTargetOffset != previousTargetOffset) {
+ if (state.isAnimationRunning) {
+ // Re-target the animation to the new offset if it changed
+ animateTo(newTarget)
+ } else {
+ // Snap to the new offset value of the target if no animation was running
+ snapTo(newTarget)
+ }
+ }
+}
+
+private val FabSpacing = 16.dp
\ No newline at end of file
diff --git a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
index 159b5ed..9789629b 100644
--- a/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
+++ b/compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/draw/PainterModifier.kt
@@ -17,6 +17,7 @@
package androidx.compose.ui.draw
import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.layout.MeasureScope
import androidx.compose.ui.Modifier
@@ -30,11 +31,14 @@
import androidx.compose.ui.layout.times
import androidx.compose.ui.layout.IntrinsicMeasurable
import androidx.compose.ui.layout.IntrinsicMeasureScope
-import androidx.compose.ui.layout.LayoutModifier
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.platform.InspectorInfo
-import androidx.compose.ui.platform.InspectorValueInfo
+import androidx.compose.ui.node.DrawModifierNode
+import androidx.compose.ui.node.LayoutModifierNode
+import androidx.compose.ui.node.ModifierNodeElement
+import androidx.compose.ui.node.invalidateDraw
+import androidx.compose.ui.node.invalidateLayer
+import androidx.compose.ui.node.invalidateLayout
import androidx.compose.ui.platform.debugInspectorInfo
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
@@ -54,6 +58,7 @@
*
* @sample androidx.compose.ui.samples.PainterModifierSample
*/
+@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.paint(
painter: Painter,
sizeToIntrinsics: Boolean = true,
@@ -61,39 +66,123 @@
contentScale: ContentScale = ContentScale.Inside,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null
-) = this.then(
- PainterModifier(
- painter = painter,
- sizeToIntrinsics = sizeToIntrinsics,
- alignment = alignment,
- contentScale = contentScale,
- alpha = alpha,
- colorFilter = colorFilter,
- inspectorInfo = debugInspectorInfo {
- name = "paint"
- properties["painter"] = painter
- properties["sizeToIntrinsics"] = sizeToIntrinsics
- properties["alignment"] = alignment
- properties["contentScale"] = contentScale
- properties["alpha"] = alpha
- properties["colorFilter"] = colorFilter
- }
- )
+) = this then PainterModifierNodeElement(
+ painter = painter,
+ sizeToIntrinsics = sizeToIntrinsics,
+ alignment = alignment,
+ contentScale = contentScale,
+ alpha = alpha,
+ colorFilter = colorFilter
)
/**
+ * Customized [ModifierNodeElement] for painting content using [painter].
+ *
+ * IMPORTANT NOTE: This class sets [androidx.compose.ui.node.ModifierNodeElement.autoInvalidate]
+ * to false which means it MUST invalidate both draw and the layout. It invalidates both in the
+ * [PainterModifierNodeElement.update] method through [LayoutModifierNode.invalidateLayer]
+ * (invalidates draw) and [LayoutModifierNode.invalidateLayout] (invalidates layout).
+ *
+ * @param painter used to paint content
+ * @param sizeToIntrinsics `true` to size the element relative to [Painter.intrinsicSize]
+ * @param alignment specifies alignment of the [painter] relative to content
+ * @param contentScale strategy for scaling [painter] if its size does not match the content size
+ * @param alpha opacity of [painter]
+ * @param colorFilter optional [ColorFilter] to apply to [painter]
+ *
+ * @sample androidx.compose.ui.samples.PainterModifierSample
+ */
+@ExperimentalComposeUiApi
+private class PainterModifierNodeElement(
+ val painter: Painter,
+ val sizeToIntrinsics: Boolean,
+ val alignment: Alignment,
+ val contentScale: ContentScale,
+ val alpha: Float,
+ val colorFilter: ColorFilter?
+) : ModifierNodeElement<PainterModifierNode>(
+ autoInvalidate = false,
+ inspectorInfo = debugInspectorInfo {
+ name = "paint"
+ properties["painter"] = painter
+ properties["sizeToIntrinsics"] = sizeToIntrinsics
+ properties["alignment"] = alignment
+ properties["contentScale"] = contentScale
+ properties["alpha"] = alpha
+ properties["colorFilter"] = colorFilter
+ }
+) {
+ override fun create(): PainterModifierNode {
+ return PainterModifierNode(
+ painter = painter,
+ sizeToIntrinsics = sizeToIntrinsics,
+ alignment = alignment,
+ contentScale = contentScale,
+ alpha = alpha,
+ colorFilter = colorFilter,
+ )
+ }
+
+ override fun update(node: PainterModifierNode): PainterModifierNode {
+ val invalidateLayout = node.sizeToIntrinsics != sizeToIntrinsics ||
+ (sizeToIntrinsics && node.painter.intrinsicSize != painter.intrinsicSize)
+
+ node.painter = painter
+ node.sizeToIntrinsics = sizeToIntrinsics
+ node.alignment = alignment
+ node.contentScale = contentScale
+ node.alpha = alpha
+ node.colorFilter = colorFilter
+
+ // Only invalidate layout if Intrinsics have changed.
+ if (invalidateLayout) {
+ node.invalidateLayout()
+ } else {
+ // Otherwise, redraw because one of the node properties has changed.
+ node.invalidateDraw()
+ }
+
+ return node
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is PainterModifierNodeElement) return false
+
+ if (painter !== other.painter) return false
+ if (sizeToIntrinsics != other.sizeToIntrinsics) return false
+ if (alignment != other.alignment) return false
+ if (contentScale != other.contentScale) return false
+ if (alpha != other.alpha) return false
+ if (colorFilter != other.colorFilter) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = painter.hashCode()
+ result = 31 * result + sizeToIntrinsics.hashCode()
+ result = 31 * result + alignment.hashCode()
+ result = 31 * result + contentScale.hashCode()
+ result = 31 * result + alpha.hashCode()
+ result = 31 * result + (colorFilter?.hashCode() ?: 0)
+ return result
+ }
+}
+
+/**
* [DrawModifier] used to draw the provided [Painter] followed by the contents
* of the component itself
*/
-private class PainterModifier(
- val painter: Painter,
- val sizeToIntrinsics: Boolean,
- val alignment: Alignment = Alignment.Center,
- val contentScale: ContentScale = ContentScale.Inside,
- val alpha: Float = DefaultAlpha,
- val colorFilter: ColorFilter? = null,
- inspectorInfo: InspectorInfo.() -> Unit
-) : LayoutModifier, DrawModifier, InspectorValueInfo(inspectorInfo) {
+@OptIn(ExperimentalComposeUiApi::class)
+private class PainterModifierNode(
+ var painter: Painter,
+ var sizeToIntrinsics: Boolean,
+ var alignment: Alignment = Alignment.Center,
+ var contentScale: ContentScale = ContentScale.Inside,
+ var alpha: Float = DefaultAlpha,
+ var colorFilter: ColorFilter? = null
+) : LayoutModifierNode, Modifier.Node(), DrawModifierNode {
/**
* Helper property to determine if we should size content to the intrinsic
@@ -289,26 +378,6 @@
private fun Size.hasSpecifiedAndFiniteWidth() = this != Size.Unspecified && width.isFinite()
private fun Size.hasSpecifiedAndFiniteHeight() = this != Size.Unspecified && height.isFinite()
- override fun hashCode(): Int {
- var result = painter.hashCode()
- result = 31 * result + sizeToIntrinsics.hashCode()
- result = 31 * result + alignment.hashCode()
- result = 31 * result + contentScale.hashCode()
- result = 31 * result + alpha.hashCode()
- result = 31 * result + (colorFilter?.hashCode() ?: 0)
- return result
- }
-
- override fun equals(other: Any?): Boolean {
- val otherModifier = other as? PainterModifier ?: return false
- return painter == otherModifier.painter &&
- sizeToIntrinsics == otherModifier.sizeToIntrinsics &&
- alignment == otherModifier.alignment &&
- contentScale == otherModifier.contentScale &&
- alpha == otherModifier.alpha &&
- colorFilter == otherModifier.colorFilter
- }
-
override fun toString(): String =
"PainterModifier(" +
"painter=$painter, " +
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnsupportedExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnsupportedExceptionJavaTest.java
new file mode 100644
index 0000000..79b8f4f
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnsupportedExceptionJavaTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.credentials.exceptions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ClearCredentialUnsupportedExceptionJavaTest {
+ @Test(expected = ClearCredentialUnsupportedException.class)
+ public void construct_inputNonEmpty_success() throws
+ ClearCredentialUnsupportedException {
+ throw new ClearCredentialUnsupportedException("msg");
+ }
+
+ @Test(expected = ClearCredentialUnsupportedException.class)
+ public void construct_errorMessageNull_success() throws
+ ClearCredentialUnsupportedException {
+ throw new ClearCredentialUnsupportedException(null);
+ }
+
+ @Test
+ public void getter_success() {
+ Class<ClearCredentialUnsupportedException> expectedClass =
+ ClearCredentialUnsupportedException.class;
+ String expectedMessage = "message";
+ ClearCredentialUnsupportedException exception = new
+ ClearCredentialUnsupportedException(expectedMessage);
+ assertThat(exception.getClass()).isEqualTo(expectedClass);
+ assertThat(exception.getErrorMessage()).isEqualTo(expectedMessage);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnsupportedExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnsupportedExceptionTest.kt
new file mode 100644
index 0000000..ca5684d
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/ClearCredentialUnsupportedExceptionTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.credentials.exceptions
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class ClearCredentialUnsupportedExceptionTest {
+ @Test(expected = ClearCredentialUnsupportedException::class)
+ fun construct_inputNonEmpty_success() {
+ throw ClearCredentialUnsupportedException("msg")
+ }
+
+ @Test(expected = ClearCredentialUnsupportedException::class)
+ fun construct_errorMessageNull_success() {
+ throw ClearCredentialUnsupportedException(null)
+ }
+
+ @Test
+ fun getter_success() {
+ val expectedType =
+ ClearCredentialUnsupportedException
+ .TYPE_CLEAR_CREDENTIAL_UNSUPPORTED_EXCEPTION
+ val expectedMessage = "message"
+ val exception = ClearCredentialUnsupportedException(expectedMessage)
+ assertThat(exception.type).isEqualTo(expectedType)
+ assertThat(exception.errorMessage).isEqualTo(expectedMessage)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnsupportedExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnsupportedExceptionJavaTest.java
new file mode 100644
index 0000000..c6ae770
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnsupportedExceptionJavaTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.credentials.exceptions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CreateCredentialUnsupportedExceptionJavaTest {
+ @Test(expected = CreateCredentialUnsupportedException.class)
+ public void construct_inputNonEmpty_success() throws
+ CreateCredentialUnsupportedException {
+ throw new CreateCredentialUnsupportedException("msg");
+ }
+
+ @Test(expected = CreateCredentialUnsupportedException.class)
+ public void construct_errorMessageNull_success() throws
+ CreateCredentialUnsupportedException {
+ throw new CreateCredentialUnsupportedException(null);
+ }
+
+ @Test
+ public void getter_success() {
+ Class<CreateCredentialUnsupportedException> expectedClass =
+ CreateCredentialUnsupportedException.class;
+ String expectedMessage = "message";
+ CreateCredentialUnsupportedException exception = new
+ CreateCredentialUnsupportedException(expectedMessage);
+ assertThat(exception.getClass()).isEqualTo(expectedClass);
+ assertThat(exception.getErrorMessage()).isEqualTo(expectedMessage);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnsupportedExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnsupportedExceptionTest.kt
new file mode 100644
index 0000000..e9e2417
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/CreateCredentialUnsupportedExceptionTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.credentials.exceptions
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class CreateCredentialUnsupportedExceptionTest {
+ @Test(expected = CreateCredentialUnsupportedException::class)
+ fun construct_inputNonEmpty_success() {
+ throw CreateCredentialUnsupportedException("msg")
+ }
+
+ @Test(expected = CreateCredentialUnsupportedException::class)
+ fun construct_errorMessageNull_success() {
+ throw CreateCredentialUnsupportedException(null)
+ }
+
+ @Test
+ fun getter_success() {
+ val expectedType =
+ CreateCredentialUnsupportedException
+ .TYPE_CREATE_CREDENTIAL_UNSUPPORTED_EXCEPTION
+ val expectedMessage = "message"
+ val exception = CreateCredentialUnsupportedException(expectedMessage)
+ assertThat(exception.type).isEqualTo(expectedType)
+ assertThat(exception.errorMessage).isEqualTo(expectedMessage)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnsupportedExceptionJavaTest.java b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnsupportedExceptionJavaTest.java
new file mode 100644
index 0000000..9ede916
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnsupportedExceptionJavaTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.credentials.exceptions;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class GetCredentialUnsupportedExceptionJavaTest {
+ @Test(expected = GetCredentialUnsupportedException.class)
+ public void construct_inputNonEmpty_success() throws
+ GetCredentialUnsupportedException {
+ throw new GetCredentialUnsupportedException("msg");
+ }
+
+ @Test(expected = GetCredentialUnsupportedException.class)
+ public void construct_errorMessageNull_success() throws
+ GetCredentialUnsupportedException {
+ throw new GetCredentialUnsupportedException(null);
+ }
+
+ @Test
+ public void getter_success() {
+ Class<GetCredentialUnsupportedException> expectedClass =
+ GetCredentialUnsupportedException.class;
+ String expectedMessage = "message";
+ GetCredentialUnsupportedException exception = new
+ GetCredentialUnsupportedException(expectedMessage);
+ assertThat(exception.getClass()).isEqualTo(expectedClass);
+ assertThat(exception.getErrorMessage()).isEqualTo(expectedMessage);
+ }
+}
diff --git a/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnsupportedExceptionTest.kt b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnsupportedExceptionTest.kt
new file mode 100644
index 0000000..f6c68d7
--- /dev/null
+++ b/credentials/credentials/src/androidTest/java/androidx/credentials/exceptions/GetCredentialUnsupportedExceptionTest.kt
@@ -0,0 +1,48 @@
+/*
+ * 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.credentials.exceptions
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class GetCredentialUnsupportedExceptionTest {
+ @Test(expected = GetCredentialUnsupportedException::class)
+ fun construct_inputNonEmpty_success() {
+ throw GetCredentialUnsupportedException("msg")
+ }
+
+ @Test(expected = GetCredentialUnsupportedException::class)
+ fun construct_errorMessageNull_success() {
+ throw GetCredentialUnsupportedException(null)
+ }
+
+ @Test
+ fun getter_success() {
+ val expectedType =
+ GetCredentialUnsupportedException
+ .TYPE_GET_CREDENTIAL_UNSUPPORTED_EXCEPTION
+ val expectedMessage = "message"
+ val exception = GetCredentialUnsupportedException(expectedMessage)
+ assertThat(exception.type).isEqualTo(expectedType)
+ assertThat(exception.errorMessage).isEqualTo(expectedMessage)
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialUnsupportedException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialUnsupportedException.kt
new file mode 100644
index 0000000..01fc50c
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/ClearCredentialUnsupportedException.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.exceptions
+
+/**
+ * During the clear credential flow, this is thrown when credential manager is unsupported, typically
+ * because the device has disabled it or did not ship with this feature enabled. A software update
+ * or a restart after enabling may fix this issue, but in certain cases, the device hardware may
+ * be the limiting factor.
+ *
+ * @see ClearCredentialException
+ * @hide
+ */
+class ClearCredentialUnsupportedException @JvmOverloads constructor(
+ errorMessage: CharSequence? = null
+) : GetCredentialException(TYPE_CLEAR_CREDENTIAL_UNSUPPORTED_EXCEPTION, errorMessage) {
+ /** @hide */
+ companion object {
+ internal const val TYPE_CLEAR_CREDENTIAL_UNSUPPORTED_EXCEPTION =
+ "androidx.credentials.TYPE_CLEAR_CREDENTIAL_UNSUPPORTED_EXCEPTION"
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialUnsupportedException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialUnsupportedException.kt
new file mode 100644
index 0000000..963dd8f
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/CreateCredentialUnsupportedException.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.exceptions
+
+/**
+ * During the create credential flow, this is thrown when credential manager is unsupported, typically
+ * because the device has disabled it or did not ship with this feature enabled. A software update
+ * or a restart after enabling may fix this issue, but in certain cases, the device hardware may
+ * be the limiting factor.
+ *
+ * @see CreateCredentialException
+ * @hide
+ */
+class CreateCredentialUnsupportedException @JvmOverloads constructor(
+ errorMessage: CharSequence? = null
+) : GetCredentialException(TYPE_CREATE_CREDENTIAL_UNSUPPORTED_EXCEPTION, errorMessage) {
+ /** @hide */
+ companion object {
+ internal const val TYPE_CREATE_CREDENTIAL_UNSUPPORTED_EXCEPTION =
+ "androidx.credentials.TYPE_CREATE_CREDENTIAL_UNSUPPORTED_EXCEPTION"
+ }
+}
\ No newline at end of file
diff --git a/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialUnsupportedException.kt b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialUnsupportedException.kt
new file mode 100644
index 0000000..a260503
--- /dev/null
+++ b/credentials/credentials/src/main/java/androidx/credentials/exceptions/GetCredentialUnsupportedException.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.credentials.exceptions
+
+/**
+ * During the get credential flow, this is thrown when credential manager is unsupported, typically
+ * because the device has disabled it or did not ship with this feature enabled. A software update
+ * or a restart after enabling may fix this issue, but in certain cases, the device hardware may
+ * be the limiting factor.
+ *
+ * @see GetCredentialException
+ * @hide
+ */
+class GetCredentialUnsupportedException @JvmOverloads constructor(
+ errorMessage: CharSequence? = null
+) : GetCredentialException(TYPE_GET_CREDENTIAL_UNSUPPORTED_EXCEPTION, errorMessage) {
+ /** @hide */
+ companion object {
+ internal const val TYPE_GET_CREDENTIAL_UNSUPPORTED_EXCEPTION =
+ "androidx.credentials.TYPE_GET_CREDENTIAL_UNSUPPORTED_EXCEPTION"
+ }
+}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
index 843d8d2..4f688fd 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerBodyAdapter.kt
@@ -68,13 +68,15 @@
onEmojiPickedListener(emojiViewItem)
},
onEmojiPickedFromPopupListener = { emoji ->
- with(
- emojiPickerItemsProvider().getBodyItem(bindingAdapterPosition)
- as EmojiViewData
- ) {
- if (updateToSticky) {
- this.emoji = emoji
- notifyItemChanged(bindingAdapterPosition)
+ val baseEmoji = BundledEmojiListLoader.getEmojiVariantsLookup()[emoji]!![0]
+ emojiPickerItemsProvider().forEachIndexed { index, itemViewData ->
+ if (itemViewData is EmojiViewData &&
+ BundledEmojiListLoader.getEmojiVariantsLookup()
+ [itemViewData.emoji]?.get(0) == baseEmoji &&
+ itemViewData.updateToSticky
+ ) {
+ itemViewData.emoji = emoji
+ notifyItemChanged(index)
}
}
})
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerItems.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerItems.kt
index a8d08ce..33ea012 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerItems.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerItems.kt
@@ -51,6 +51,8 @@
if (contentIndex == 0 && emptyPlaceholderItem != null) return emptyPlaceholderItem
return PlaceholderEmoji
}
+
+ fun getAll(): List<ItemViewData> = IntRange(0, size - 1).map { get(it) }
}
/**
@@ -58,7 +60,7 @@
*/
internal class EmojiPickerItems(
private val groups: List<ItemGroup>,
-) {
+) : Iterable<ItemViewData> {
val size: Int get() = groups.sumOf { it.size }
init {
@@ -103,4 +105,6 @@
val index = groups.indexOf(group)
return firstItemPositionByGroupIndex(index).let { it until it + group.size }
}
+
+ override fun iterator(): Iterator<ItemViewData> = groups.flatMap { it.getAll() }.iterator()
}
\ No newline at end of file
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
index 93f7bd1..999cb72 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiPickerView.kt
@@ -132,7 +132,7 @@
}
scope.launch(Dispatchers.IO) {
val load = launch { BundledEmojiListLoader.load(context) }
- refreshRecentItems()
+ refreshRecent()
load.join()
withContext(Dispatchers.Main) {
@@ -153,7 +153,6 @@
scope.launch {
recentEmojiProvider.recordSelection(emojiViewItem.emoji)
- refreshRecentItems()
}
}
)
@@ -194,19 +193,25 @@
).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
- return if (emojiPickerItems.getBodyItem(position).occupyEntireRow)
- emojiGridColumns
- else 1
+ return when (emojiPickerItems.getBodyItem(position).itemType) {
+ ItemType.PLACEHOLDER_EMOJI -> 0
+ ItemType.CATEGORY_TITLE, ItemType.PLACEHOLDER_TEXT -> emojiGridColumns
+ else -> 1
+ }
}
}
}
val headerAdapter =
EmojiPickerHeaderAdapter(context, emojiPickerItems, onHeaderIconClicked = {
- bodyLayoutManager.scrollToPositionWithOffset(
- emojiPickerItems.firstItemPositionByGroupIndex(it),
- 0
- )
+ with(emojiPickerItems.firstItemPositionByGroupIndex(it)) {
+ if (this == emojiPickerItems.groupRange(recentItemGroup).first) {
+ scope.launch {
+ refreshRecent()
+ }
+ }
+ bodyLayoutManager.scrollToPositionWithOffset(this, 0)
+ }
})
// clear view's children in case of resetting layout
@@ -242,6 +247,11 @@
bodyLayoutManager.findFirstCompletelyVisibleItemPosition()
headerAdapter.selectedGroupIndex =
emojiPickerItems.groupIndexByItemPosition(position)
+ if (position !in emojiPickerItems.groupRange(recentItemGroup)) {
+ scope.launch {
+ refreshRecent()
+ }
+ }
}
})
// Disable item insertion/deletion animation. This keeps view holder unchanged when
@@ -257,7 +267,7 @@
}
}
- private suspend fun refreshRecentItems() {
+ internal suspend fun refreshRecent() {
val recent = recentEmojiProvider.getRecentEmojiList()
recentItems.clear()
recentItems.addAll(recent.map {
@@ -266,6 +276,12 @@
updateToSticky = false,
)
})
+ if (isLaidOut) {
+ val range = emojiPickerItems.groupRange(recentItemGroup)
+ withContext(Dispatchers.Main) {
+ bodyAdapter.notifyItemRangeChanged(range.first, range.last + 1)
+ }
+ }
}
/**
@@ -278,15 +294,8 @@
fun setRecentEmojiProvider(recentEmojiProvider: RecentEmojiProvider) {
this.recentEmojiProvider = recentEmojiProvider
-
scope.launch {
- refreshRecentItems()
- if (::emojiPickerItems.isInitialized) {
- val range = emojiPickerItems.groupRange(recentItemGroup)
- withContext(Dispatchers.Main) {
- bodyAdapter.notifyItemRangeChanged(range.first, range.last + 1)
- }
- }
+ refreshRecent()
}
}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
index 6f54129..3813964 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/EmojiViewHolder.kt
@@ -76,7 +76,10 @@
itemView.layoutParams = LayoutParams(width, height)
emojiView = itemView.findViewById(R.id.emoji_view)
emojiView.isClickable = true
- emojiView.setOnClickListener { onEmojiPickedListener(emojiViewItem) }
+ emojiView.setOnClickListener {
+ it.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
+ onEmojiPickedListener(emojiViewItem)
+ }
indicator = itemView.findViewById(R.id.variant_availability_indicator)
}
diff --git a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt
index 4007a02..33cfcfb 100644
--- a/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt
+++ b/emoji2/emoji2-emojipicker/src/main/java/androidx/emoji2/emojipicker/ItemViewData.kt
@@ -26,21 +26,19 @@
/**
* Represents an item within the body RecyclerView.
*/
-internal sealed class ItemViewData(itemType: ItemType, val occupyEntireRow: Boolean = false) {
+internal sealed class ItemViewData(val itemType: ItemType) {
val viewType = itemType.ordinal
}
/**
* Title of each category.
*/
-internal data class CategoryTitle(val title: String) :
- ItemViewData(ItemType.CATEGORY_TITLE, occupyEntireRow = true)
+internal data class CategoryTitle(val title: String) : ItemViewData(ItemType.CATEGORY_TITLE)
/**
* Text to display when the category contains no items.
*/
-internal data class PlaceholderText(val text: String) :
- ItemViewData(ItemType.PLACEHOLDER_TEXT, occupyEntireRow = true)
+internal data class PlaceholderText(val text: String) : ItemViewData(ItemType.PLACEHOLDER_TEXT)
/**
* Represents an emoji.
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index b0febf5..c26632c 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -427,36 +427,21 @@
property public final String? title;
field public static final androidx.health.connect.client.records.ExerciseSessionRecord.Companion Companion;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.time.Duration> EXERCISE_DURATION_TOTAL;
- field public static final int EXERCISE_TYPE_BACK_EXTENSION = 1; // 0x1
field public static final int EXERCISE_TYPE_BADMINTON = 2; // 0x2
- field public static final int EXERCISE_TYPE_BARBELL_SHOULDER_PRESS = 3; // 0x3
field public static final int EXERCISE_TYPE_BASEBALL = 4; // 0x4
field public static final int EXERCISE_TYPE_BASKETBALL = 5; // 0x5
- field public static final int EXERCISE_TYPE_BENCH_PRESS = 6; // 0x6
- field public static final int EXERCISE_TYPE_BENCH_SIT_UP = 7; // 0x7
field public static final int EXERCISE_TYPE_BIKING = 8; // 0x8
field public static final int EXERCISE_TYPE_BIKING_STATIONARY = 9; // 0x9
field public static final int EXERCISE_TYPE_BOOT_CAMP = 10; // 0xa
field public static final int EXERCISE_TYPE_BOXING = 11; // 0xb
- field public static final int EXERCISE_TYPE_BURPEE = 12; // 0xc
field public static final int EXERCISE_TYPE_CALISTHENICS = 13; // 0xd
field public static final int EXERCISE_TYPE_CRICKET = 14; // 0xe
- field public static final int EXERCISE_TYPE_CRUNCH = 15; // 0xf
field public static final int EXERCISE_TYPE_DANCING = 16; // 0x10
- field public static final int EXERCISE_TYPE_DEADLIFT = 17; // 0x11
- field public static final int EXERCISE_TYPE_DUMBBELL_CURL_LEFT_ARM = 18; // 0x12
- field public static final int EXERCISE_TYPE_DUMBBELL_CURL_RIGHT_ARM = 19; // 0x13
- field public static final int EXERCISE_TYPE_DUMBBELL_FRONT_RAISE = 20; // 0x14
- field public static final int EXERCISE_TYPE_DUMBBELL_LATERAL_RAISE = 21; // 0x15
- field public static final int EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_LEFT_ARM = 22; // 0x16
- field public static final int EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_RIGHT_ARM = 23; // 0x17
- field public static final int EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_TWO_ARM = 24; // 0x18
field public static final int EXERCISE_TYPE_ELLIPTICAL = 25; // 0x19
field public static final int EXERCISE_TYPE_EXERCISE_CLASS = 26; // 0x1a
field public static final int EXERCISE_TYPE_FENCING = 27; // 0x1b
field public static final int EXERCISE_TYPE_FOOTBALL_AMERICAN = 28; // 0x1c
field public static final int EXERCISE_TYPE_FOOTBALL_AUSTRALIAN = 29; // 0x1d
- field public static final int EXERCISE_TYPE_FORWARD_TWIST = 30; // 0x1e
field public static final int EXERCISE_TYPE_FRISBEE_DISC = 31; // 0x1f
field public static final int EXERCISE_TYPE_GOLF = 32; // 0x20
field public static final int EXERCISE_TYPE_GUIDED_BREATHING = 33; // 0x21
@@ -466,16 +451,11 @@
field public static final int EXERCISE_TYPE_HIKING = 37; // 0x25
field public static final int EXERCISE_TYPE_ICE_HOCKEY = 38; // 0x26
field public static final int EXERCISE_TYPE_ICE_SKATING = 39; // 0x27
- field public static final int EXERCISE_TYPE_JUMPING_JACK = 40; // 0x28
- field public static final int EXERCISE_TYPE_JUMP_ROPE = 41; // 0x29
- field public static final int EXERCISE_TYPE_LAT_PULL_DOWN = 42; // 0x2a
- field public static final int EXERCISE_TYPE_LUNGE = 43; // 0x2b
field public static final int EXERCISE_TYPE_MARTIAL_ARTS = 44; // 0x2c
field public static final int EXERCISE_TYPE_OTHER_WORKOUT = 0; // 0x0
field public static final int EXERCISE_TYPE_PADDLING = 46; // 0x2e
field public static final int EXERCISE_TYPE_PARAGLIDING = 47; // 0x2f
field public static final int EXERCISE_TYPE_PILATES = 48; // 0x30
- field public static final int EXERCISE_TYPE_PLANK = 49; // 0x31
field public static final int EXERCISE_TYPE_RACQUETBALL = 50; // 0x32
field public static final int EXERCISE_TYPE_ROCK_CLIMBING = 51; // 0x33
field public static final int EXERCISE_TYPE_ROLLER_HOCKEY = 52; // 0x34
@@ -493,7 +473,6 @@
field public static final int EXERCISE_TYPE_SOCCER = 64; // 0x40
field public static final int EXERCISE_TYPE_SOFTBALL = 65; // 0x41
field public static final int EXERCISE_TYPE_SQUASH = 66; // 0x42
- field public static final int EXERCISE_TYPE_SQUAT = 67; // 0x43
field public static final int EXERCISE_TYPE_STAIR_CLIMBING = 68; // 0x44
field public static final int EXERCISE_TYPE_STAIR_CLIMBING_MACHINE = 69; // 0x45
field public static final int EXERCISE_TYPE_STRENGTH_TRAINING = 70; // 0x46
@@ -503,7 +482,6 @@
field public static final int EXERCISE_TYPE_SWIMMING_POOL = 74; // 0x4a
field public static final int EXERCISE_TYPE_TABLE_TENNIS = 75; // 0x4b
field public static final int EXERCISE_TYPE_TENNIS = 76; // 0x4c
- field public static final int EXERCISE_TYPE_UPPER_TWIST = 77; // 0x4d
field public static final int EXERCISE_TYPE_VOLLEYBALL = 78; // 0x4e
field public static final int EXERCISE_TYPE_WALKING = 79; // 0x4f
field public static final int EXERCISE_TYPE_WATER_POLO = 80; // 0x50
diff --git a/health/connect/connect-client/api/public_plus_experimental_current.txt b/health/connect/connect-client/api/public_plus_experimental_current.txt
index b0febf5..c26632c 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -427,36 +427,21 @@
property public final String? title;
field public static final androidx.health.connect.client.records.ExerciseSessionRecord.Companion Companion;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.time.Duration> EXERCISE_DURATION_TOTAL;
- field public static final int EXERCISE_TYPE_BACK_EXTENSION = 1; // 0x1
field public static final int EXERCISE_TYPE_BADMINTON = 2; // 0x2
- field public static final int EXERCISE_TYPE_BARBELL_SHOULDER_PRESS = 3; // 0x3
field public static final int EXERCISE_TYPE_BASEBALL = 4; // 0x4
field public static final int EXERCISE_TYPE_BASKETBALL = 5; // 0x5
- field public static final int EXERCISE_TYPE_BENCH_PRESS = 6; // 0x6
- field public static final int EXERCISE_TYPE_BENCH_SIT_UP = 7; // 0x7
field public static final int EXERCISE_TYPE_BIKING = 8; // 0x8
field public static final int EXERCISE_TYPE_BIKING_STATIONARY = 9; // 0x9
field public static final int EXERCISE_TYPE_BOOT_CAMP = 10; // 0xa
field public static final int EXERCISE_TYPE_BOXING = 11; // 0xb
- field public static final int EXERCISE_TYPE_BURPEE = 12; // 0xc
field public static final int EXERCISE_TYPE_CALISTHENICS = 13; // 0xd
field public static final int EXERCISE_TYPE_CRICKET = 14; // 0xe
- field public static final int EXERCISE_TYPE_CRUNCH = 15; // 0xf
field public static final int EXERCISE_TYPE_DANCING = 16; // 0x10
- field public static final int EXERCISE_TYPE_DEADLIFT = 17; // 0x11
- field public static final int EXERCISE_TYPE_DUMBBELL_CURL_LEFT_ARM = 18; // 0x12
- field public static final int EXERCISE_TYPE_DUMBBELL_CURL_RIGHT_ARM = 19; // 0x13
- field public static final int EXERCISE_TYPE_DUMBBELL_FRONT_RAISE = 20; // 0x14
- field public static final int EXERCISE_TYPE_DUMBBELL_LATERAL_RAISE = 21; // 0x15
- field public static final int EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_LEFT_ARM = 22; // 0x16
- field public static final int EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_RIGHT_ARM = 23; // 0x17
- field public static final int EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_TWO_ARM = 24; // 0x18
field public static final int EXERCISE_TYPE_ELLIPTICAL = 25; // 0x19
field public static final int EXERCISE_TYPE_EXERCISE_CLASS = 26; // 0x1a
field public static final int EXERCISE_TYPE_FENCING = 27; // 0x1b
field public static final int EXERCISE_TYPE_FOOTBALL_AMERICAN = 28; // 0x1c
field public static final int EXERCISE_TYPE_FOOTBALL_AUSTRALIAN = 29; // 0x1d
- field public static final int EXERCISE_TYPE_FORWARD_TWIST = 30; // 0x1e
field public static final int EXERCISE_TYPE_FRISBEE_DISC = 31; // 0x1f
field public static final int EXERCISE_TYPE_GOLF = 32; // 0x20
field public static final int EXERCISE_TYPE_GUIDED_BREATHING = 33; // 0x21
@@ -466,16 +451,11 @@
field public static final int EXERCISE_TYPE_HIKING = 37; // 0x25
field public static final int EXERCISE_TYPE_ICE_HOCKEY = 38; // 0x26
field public static final int EXERCISE_TYPE_ICE_SKATING = 39; // 0x27
- field public static final int EXERCISE_TYPE_JUMPING_JACK = 40; // 0x28
- field public static final int EXERCISE_TYPE_JUMP_ROPE = 41; // 0x29
- field public static final int EXERCISE_TYPE_LAT_PULL_DOWN = 42; // 0x2a
- field public static final int EXERCISE_TYPE_LUNGE = 43; // 0x2b
field public static final int EXERCISE_TYPE_MARTIAL_ARTS = 44; // 0x2c
field public static final int EXERCISE_TYPE_OTHER_WORKOUT = 0; // 0x0
field public static final int EXERCISE_TYPE_PADDLING = 46; // 0x2e
field public static final int EXERCISE_TYPE_PARAGLIDING = 47; // 0x2f
field public static final int EXERCISE_TYPE_PILATES = 48; // 0x30
- field public static final int EXERCISE_TYPE_PLANK = 49; // 0x31
field public static final int EXERCISE_TYPE_RACQUETBALL = 50; // 0x32
field public static final int EXERCISE_TYPE_ROCK_CLIMBING = 51; // 0x33
field public static final int EXERCISE_TYPE_ROLLER_HOCKEY = 52; // 0x34
@@ -493,7 +473,6 @@
field public static final int EXERCISE_TYPE_SOCCER = 64; // 0x40
field public static final int EXERCISE_TYPE_SOFTBALL = 65; // 0x41
field public static final int EXERCISE_TYPE_SQUASH = 66; // 0x42
- field public static final int EXERCISE_TYPE_SQUAT = 67; // 0x43
field public static final int EXERCISE_TYPE_STAIR_CLIMBING = 68; // 0x44
field public static final int EXERCISE_TYPE_STAIR_CLIMBING_MACHINE = 69; // 0x45
field public static final int EXERCISE_TYPE_STRENGTH_TRAINING = 70; // 0x46
@@ -503,7 +482,6 @@
field public static final int EXERCISE_TYPE_SWIMMING_POOL = 74; // 0x4a
field public static final int EXERCISE_TYPE_TABLE_TENNIS = 75; // 0x4b
field public static final int EXERCISE_TYPE_TENNIS = 76; // 0x4c
- field public static final int EXERCISE_TYPE_UPPER_TWIST = 77; // 0x4d
field public static final int EXERCISE_TYPE_VOLLEYBALL = 78; // 0x4e
field public static final int EXERCISE_TYPE_WALKING = 79; // 0x4f
field public static final int EXERCISE_TYPE_WATER_POLO = 80; // 0x50
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 3df3d9d..3f0d81f 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -427,36 +427,21 @@
property public final String? title;
field public static final androidx.health.connect.client.records.ExerciseSessionRecord.Companion Companion;
field public static final androidx.health.connect.client.aggregate.AggregateMetric<java.time.Duration> EXERCISE_DURATION_TOTAL;
- field public static final int EXERCISE_TYPE_BACK_EXTENSION = 1; // 0x1
field public static final int EXERCISE_TYPE_BADMINTON = 2; // 0x2
- field public static final int EXERCISE_TYPE_BARBELL_SHOULDER_PRESS = 3; // 0x3
field public static final int EXERCISE_TYPE_BASEBALL = 4; // 0x4
field public static final int EXERCISE_TYPE_BASKETBALL = 5; // 0x5
- field public static final int EXERCISE_TYPE_BENCH_PRESS = 6; // 0x6
- field public static final int EXERCISE_TYPE_BENCH_SIT_UP = 7; // 0x7
field public static final int EXERCISE_TYPE_BIKING = 8; // 0x8
field public static final int EXERCISE_TYPE_BIKING_STATIONARY = 9; // 0x9
field public static final int EXERCISE_TYPE_BOOT_CAMP = 10; // 0xa
field public static final int EXERCISE_TYPE_BOXING = 11; // 0xb
- field public static final int EXERCISE_TYPE_BURPEE = 12; // 0xc
field public static final int EXERCISE_TYPE_CALISTHENICS = 13; // 0xd
field public static final int EXERCISE_TYPE_CRICKET = 14; // 0xe
- field public static final int EXERCISE_TYPE_CRUNCH = 15; // 0xf
field public static final int EXERCISE_TYPE_DANCING = 16; // 0x10
- field public static final int EXERCISE_TYPE_DEADLIFT = 17; // 0x11
- field public static final int EXERCISE_TYPE_DUMBBELL_CURL_LEFT_ARM = 18; // 0x12
- field public static final int EXERCISE_TYPE_DUMBBELL_CURL_RIGHT_ARM = 19; // 0x13
- field public static final int EXERCISE_TYPE_DUMBBELL_FRONT_RAISE = 20; // 0x14
- field public static final int EXERCISE_TYPE_DUMBBELL_LATERAL_RAISE = 21; // 0x15
- field public static final int EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_LEFT_ARM = 22; // 0x16
- field public static final int EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_RIGHT_ARM = 23; // 0x17
- field public static final int EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_TWO_ARM = 24; // 0x18
field public static final int EXERCISE_TYPE_ELLIPTICAL = 25; // 0x19
field public static final int EXERCISE_TYPE_EXERCISE_CLASS = 26; // 0x1a
field public static final int EXERCISE_TYPE_FENCING = 27; // 0x1b
field public static final int EXERCISE_TYPE_FOOTBALL_AMERICAN = 28; // 0x1c
field public static final int EXERCISE_TYPE_FOOTBALL_AUSTRALIAN = 29; // 0x1d
- field public static final int EXERCISE_TYPE_FORWARD_TWIST = 30; // 0x1e
field public static final int EXERCISE_TYPE_FRISBEE_DISC = 31; // 0x1f
field public static final int EXERCISE_TYPE_GOLF = 32; // 0x20
field public static final int EXERCISE_TYPE_GUIDED_BREATHING = 33; // 0x21
@@ -466,16 +451,11 @@
field public static final int EXERCISE_TYPE_HIKING = 37; // 0x25
field public static final int EXERCISE_TYPE_ICE_HOCKEY = 38; // 0x26
field public static final int EXERCISE_TYPE_ICE_SKATING = 39; // 0x27
- field public static final int EXERCISE_TYPE_JUMPING_JACK = 40; // 0x28
- field public static final int EXERCISE_TYPE_JUMP_ROPE = 41; // 0x29
- field public static final int EXERCISE_TYPE_LAT_PULL_DOWN = 42; // 0x2a
- field public static final int EXERCISE_TYPE_LUNGE = 43; // 0x2b
field public static final int EXERCISE_TYPE_MARTIAL_ARTS = 44; // 0x2c
field public static final int EXERCISE_TYPE_OTHER_WORKOUT = 0; // 0x0
field public static final int EXERCISE_TYPE_PADDLING = 46; // 0x2e
field public static final int EXERCISE_TYPE_PARAGLIDING = 47; // 0x2f
field public static final int EXERCISE_TYPE_PILATES = 48; // 0x30
- field public static final int EXERCISE_TYPE_PLANK = 49; // 0x31
field public static final int EXERCISE_TYPE_RACQUETBALL = 50; // 0x32
field public static final int EXERCISE_TYPE_ROCK_CLIMBING = 51; // 0x33
field public static final int EXERCISE_TYPE_ROLLER_HOCKEY = 52; // 0x34
@@ -493,7 +473,6 @@
field public static final int EXERCISE_TYPE_SOCCER = 64; // 0x40
field public static final int EXERCISE_TYPE_SOFTBALL = 65; // 0x41
field public static final int EXERCISE_TYPE_SQUASH = 66; // 0x42
- field public static final int EXERCISE_TYPE_SQUAT = 67; // 0x43
field public static final int EXERCISE_TYPE_STAIR_CLIMBING = 68; // 0x44
field public static final int EXERCISE_TYPE_STAIR_CLIMBING_MACHINE = 69; // 0x45
field public static final int EXERCISE_TYPE_STRENGTH_TRAINING = 70; // 0x46
@@ -503,7 +482,6 @@
field public static final int EXERCISE_TYPE_SWIMMING_POOL = 74; // 0x4a
field public static final int EXERCISE_TYPE_TABLE_TENNIS = 75; // 0x4b
field public static final int EXERCISE_TYPE_TENNIS = 76; // 0x4c
- field public static final int EXERCISE_TYPE_UPPER_TWIST = 77; // 0x4d
field public static final int EXERCISE_TYPE_VOLLEYBALL = 78; // 0x4e
field public static final int EXERCISE_TYPE_WALKING = 79; // 0x4f
field public static final int EXERCISE_TYPE_WATER_POLO = 80; // 0x50
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
index 3f0b24d..f2b31a1 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/records/ExerciseSessionRecord.kt
@@ -31,6 +31,7 @@
* after each other, there can be gaps in between.
*
* Example code demonstrate how to read exercise session:
+ *
* @sample androidx.health.connect.client.samples.ReadExerciseSessions
*/
public class ExerciseSessionRecord(
@@ -98,38 +99,25 @@
* Can be used to represent any generic workout that does not fall into a specific category.
* Any unknown new value definition will also fall automatically into
* [EXERCISE_TYPE_OTHER_WORKOUT].
+ *
+ * Next Id: 84.
*/
const val EXERCISE_TYPE_OTHER_WORKOUT = 0
- const val EXERCISE_TYPE_BACK_EXTENSION = 1
const val EXERCISE_TYPE_BADMINTON = 2
- const val EXERCISE_TYPE_BARBELL_SHOULDER_PRESS = 3
const val EXERCISE_TYPE_BASEBALL = 4
const val EXERCISE_TYPE_BASKETBALL = 5
- const val EXERCISE_TYPE_BENCH_PRESS = 6
- const val EXERCISE_TYPE_BENCH_SIT_UP = 7
const val EXERCISE_TYPE_BIKING = 8
const val EXERCISE_TYPE_BIKING_STATIONARY = 9
const val EXERCISE_TYPE_BOOT_CAMP = 10
const val EXERCISE_TYPE_BOXING = 11
- const val EXERCISE_TYPE_BURPEE = 12
const val EXERCISE_TYPE_CALISTHENICS = 13
const val EXERCISE_TYPE_CRICKET = 14
- const val EXERCISE_TYPE_CRUNCH = 15
const val EXERCISE_TYPE_DANCING = 16
- const val EXERCISE_TYPE_DEADLIFT = 17
- const val EXERCISE_TYPE_DUMBBELL_CURL_LEFT_ARM = 18
- const val EXERCISE_TYPE_DUMBBELL_CURL_RIGHT_ARM = 19
- const val EXERCISE_TYPE_DUMBBELL_FRONT_RAISE = 20
- const val EXERCISE_TYPE_DUMBBELL_LATERAL_RAISE = 21
- const val EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_LEFT_ARM = 22
- const val EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_RIGHT_ARM = 23
- const val EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_TWO_ARM = 24
const val EXERCISE_TYPE_ELLIPTICAL = 25
const val EXERCISE_TYPE_EXERCISE_CLASS = 26
const val EXERCISE_TYPE_FENCING = 27
const val EXERCISE_TYPE_FOOTBALL_AMERICAN = 28
const val EXERCISE_TYPE_FOOTBALL_AUSTRALIAN = 29
- const val EXERCISE_TYPE_FORWARD_TWIST = 30
const val EXERCISE_TYPE_FRISBEE_DISC = 31
const val EXERCISE_TYPE_GOLF = 32
const val EXERCISE_TYPE_GUIDED_BREATHING = 33
@@ -139,15 +127,10 @@
const val EXERCISE_TYPE_HIKING = 37
const val EXERCISE_TYPE_ICE_HOCKEY = 38
const val EXERCISE_TYPE_ICE_SKATING = 39
- const val EXERCISE_TYPE_JUMPING_JACK = 40
- const val EXERCISE_TYPE_JUMP_ROPE = 41
- const val EXERCISE_TYPE_LAT_PULL_DOWN = 42
- const val EXERCISE_TYPE_LUNGE = 43
const val EXERCISE_TYPE_MARTIAL_ARTS = 44
const val EXERCISE_TYPE_PADDLING = 46
const val EXERCISE_TYPE_PARAGLIDING = 47
const val EXERCISE_TYPE_PILATES = 48
- const val EXERCISE_TYPE_PLANK = 49
const val EXERCISE_TYPE_RACQUETBALL = 50
const val EXERCISE_TYPE_ROCK_CLIMBING = 51
const val EXERCISE_TYPE_ROLLER_HOCKEY = 52
@@ -165,7 +148,6 @@
const val EXERCISE_TYPE_SOCCER = 64
const val EXERCISE_TYPE_SOFTBALL = 65
const val EXERCISE_TYPE_SQUASH = 66
- const val EXERCISE_TYPE_SQUAT = 67
const val EXERCISE_TYPE_STAIR_CLIMBING = 68
const val EXERCISE_TYPE_STAIR_CLIMBING_MACHINE = 69
const val EXERCISE_TYPE_STRENGTH_TRAINING = 70
@@ -175,7 +157,6 @@
const val EXERCISE_TYPE_SWIMMING_POOL = 74
const val EXERCISE_TYPE_TABLE_TENNIS = 75
const val EXERCISE_TYPE_TENNIS = 76
- const val EXERCISE_TYPE_UPPER_TWIST = 77
const val EXERCISE_TYPE_VOLLEYBALL = 78
const val EXERCISE_TYPE_WALKING = 79
const val EXERCISE_TYPE_WATER_POLO = 80
@@ -187,59 +168,53 @@
@JvmField
val EXERCISE_TYPE_STRING_TO_INT_MAP: Map<String, Int> =
mapOf(
- "back_extension" to EXERCISE_TYPE_BACK_EXTENSION,
+ "back_extension" to EXERCISE_TYPE_CALISTHENICS,
"badminton" to EXERCISE_TYPE_BADMINTON,
- "barbell_shoulder_press" to EXERCISE_TYPE_BARBELL_SHOULDER_PRESS,
+ "barbell_shoulder_press" to EXERCISE_TYPE_STRENGTH_TRAINING,
"baseball" to EXERCISE_TYPE_BASEBALL,
"basketball" to EXERCISE_TYPE_BASKETBALL,
- "bench_press" to EXERCISE_TYPE_BENCH_PRESS,
- "bench_sit_up" to EXERCISE_TYPE_BENCH_SIT_UP,
+ "bench_press" to EXERCISE_TYPE_STRENGTH_TRAINING,
+ "bench_sit_up" to EXERCISE_TYPE_CALISTHENICS,
"biking" to EXERCISE_TYPE_BIKING,
"biking_stationary" to EXERCISE_TYPE_BIKING_STATIONARY,
"boot_camp" to EXERCISE_TYPE_BOOT_CAMP,
"boxing" to EXERCISE_TYPE_BOXING,
- "burpee" to EXERCISE_TYPE_BURPEE,
- "calisthenics" to EXERCISE_TYPE_CALISTHENICS,
+ "burpee" to EXERCISE_TYPE_CALISTHENICS,
"cricket" to EXERCISE_TYPE_CRICKET,
- "crunch" to EXERCISE_TYPE_CRUNCH,
+ "crunch" to EXERCISE_TYPE_CALISTHENICS,
"dancing" to EXERCISE_TYPE_DANCING,
- "deadlift" to EXERCISE_TYPE_DEADLIFT,
- "dumbbell_curl_left_arm" to EXERCISE_TYPE_DUMBBELL_CURL_LEFT_ARM,
- "dumbbell_curl_right_arm" to EXERCISE_TYPE_DUMBBELL_CURL_RIGHT_ARM,
- "dumbbell_front_raise" to EXERCISE_TYPE_DUMBBELL_FRONT_RAISE,
- "dumbbell_lateral_raise" to EXERCISE_TYPE_DUMBBELL_LATERAL_RAISE,
- "dumbbell_triceps_extension_left_arm" to
- EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_LEFT_ARM,
- "dumbbell_triceps_extension_right_arm" to
- EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_RIGHT_ARM,
- "dumbbell_triceps_extension_two_arm" to
- EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_TWO_ARM,
+ "deadlift" to EXERCISE_TYPE_STRENGTH_TRAINING,
+ "dumbbell_curl_left_arm" to EXERCISE_TYPE_STRENGTH_TRAINING,
+ "dumbbell_curl_right_arm" to EXERCISE_TYPE_STRENGTH_TRAINING,
+ "dumbbell_front_raise" to EXERCISE_TYPE_STRENGTH_TRAINING,
+ "dumbbell_lateral_raise" to EXERCISE_TYPE_STRENGTH_TRAINING,
+ "dumbbell_triceps_extension_left_arm" to EXERCISE_TYPE_STRENGTH_TRAINING,
+ "dumbbell_triceps_extension_right_arm" to EXERCISE_TYPE_STRENGTH_TRAINING,
+ "dumbbell_triceps_extension_two_arm" to EXERCISE_TYPE_STRENGTH_TRAINING,
"elliptical" to EXERCISE_TYPE_ELLIPTICAL,
"exercise_class" to EXERCISE_TYPE_EXERCISE_CLASS,
"fencing" to EXERCISE_TYPE_FENCING,
"football_american" to EXERCISE_TYPE_FOOTBALL_AMERICAN,
"football_australian" to EXERCISE_TYPE_FOOTBALL_AUSTRALIAN,
- "forward_twist" to EXERCISE_TYPE_FORWARD_TWIST,
+ "forward_twist" to EXERCISE_TYPE_CALISTHENICS,
"frisbee_disc" to EXERCISE_TYPE_FRISBEE_DISC,
"golf" to EXERCISE_TYPE_GOLF,
"guided_breathing" to EXERCISE_TYPE_GUIDED_BREATHING,
"gymnastics" to EXERCISE_TYPE_GYMNASTICS,
"handball" to EXERCISE_TYPE_HANDBALL,
- "high_intensity_interval_training" to
- EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING,
"hiking" to EXERCISE_TYPE_HIKING,
"ice_hockey" to EXERCISE_TYPE_ICE_HOCKEY,
"ice_skating" to EXERCISE_TYPE_ICE_SKATING,
- "jumping_jack" to EXERCISE_TYPE_JUMPING_JACK,
- "jump_rope" to EXERCISE_TYPE_JUMP_ROPE,
- "lat_pull_down" to EXERCISE_TYPE_LAT_PULL_DOWN,
- "lunge" to EXERCISE_TYPE_LUNGE,
+ "jumping_jack" to EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING,
+ "jump_rope" to EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING,
+ "lat_pull_down" to EXERCISE_TYPE_STRENGTH_TRAINING,
+ "lunge" to EXERCISE_TYPE_CALISTHENICS,
"martial_arts" to EXERCISE_TYPE_MARTIAL_ARTS,
"paddling" to EXERCISE_TYPE_PADDLING,
"para_gliding" to
EXERCISE_TYPE_PARAGLIDING, // Historic typo in whs with para_gliding
"pilates" to EXERCISE_TYPE_PILATES,
- "plank" to EXERCISE_TYPE_PLANK,
+ "plank" to EXERCISE_TYPE_CALISTHENICS,
"racquetball" to EXERCISE_TYPE_RACQUETBALL,
"rock_climbing" to EXERCISE_TYPE_ROCK_CLIMBING,
"roller_hockey" to EXERCISE_TYPE_ROLLER_HOCKEY,
@@ -257,17 +232,16 @@
"soccer" to EXERCISE_TYPE_SOCCER,
"softball" to EXERCISE_TYPE_SOFTBALL,
"squash" to EXERCISE_TYPE_SQUASH,
- "squat" to EXERCISE_TYPE_SQUAT,
+ "squat" to EXERCISE_TYPE_CALISTHENICS,
"stair_climbing" to EXERCISE_TYPE_STAIR_CLIMBING,
"stair_climbing_machine" to EXERCISE_TYPE_STAIR_CLIMBING_MACHINE,
- "strength_training" to EXERCISE_TYPE_STRENGTH_TRAINING,
"stretching" to EXERCISE_TYPE_STRETCHING,
"surfing" to EXERCISE_TYPE_SURFING,
"swimming_open_water" to EXERCISE_TYPE_SWIMMING_OPEN_WATER,
"swimming_pool" to EXERCISE_TYPE_SWIMMING_POOL,
"table_tennis" to EXERCISE_TYPE_TABLE_TENNIS,
"tennis" to EXERCISE_TYPE_TENNIS,
- "upper_twist" to EXERCISE_TYPE_UPPER_TWIST,
+ "upper_twist" to EXERCISE_TYPE_CALISTHENICS,
"volleyball" to EXERCISE_TYPE_VOLLEYBALL,
"walking" to EXERCISE_TYPE_WALKING,
"water_polo" to EXERCISE_TYPE_WATER_POLO,
@@ -275,6 +249,12 @@
"wheelchair" to EXERCISE_TYPE_WHEELCHAIR,
"workout" to EXERCISE_TYPE_OTHER_WORKOUT,
"yoga" to EXERCISE_TYPE_YOGA,
+
+ // These should always be at the end so reverse mapping are correct.
+ "calisthenics" to EXERCISE_TYPE_CALISTHENICS,
+ "high_intensity_interval_training" to
+ EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING,
+ "strength_training" to EXERCISE_TYPE_STRENGTH_TRAINING,
)
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -285,6 +265,7 @@
/**
* List of supported activities on Health Platform.
+ *
* @suppress
*/
@Retention(AnnotationRetention.SOURCE)
@@ -292,36 +273,21 @@
@IntDef(
value =
[
- EXERCISE_TYPE_BACK_EXTENSION,
EXERCISE_TYPE_BADMINTON,
- EXERCISE_TYPE_BARBELL_SHOULDER_PRESS,
EXERCISE_TYPE_BASEBALL,
EXERCISE_TYPE_BASKETBALL,
- EXERCISE_TYPE_BENCH_PRESS,
- EXERCISE_TYPE_BENCH_SIT_UP,
EXERCISE_TYPE_BIKING,
EXERCISE_TYPE_BIKING_STATIONARY,
EXERCISE_TYPE_BOOT_CAMP,
EXERCISE_TYPE_BOXING,
- EXERCISE_TYPE_BURPEE,
EXERCISE_TYPE_CALISTHENICS,
EXERCISE_TYPE_CRICKET,
- EXERCISE_TYPE_CRUNCH,
EXERCISE_TYPE_DANCING,
- EXERCISE_TYPE_DEADLIFT,
- EXERCISE_TYPE_DUMBBELL_CURL_LEFT_ARM,
- EXERCISE_TYPE_DUMBBELL_CURL_RIGHT_ARM,
- EXERCISE_TYPE_DUMBBELL_FRONT_RAISE,
- EXERCISE_TYPE_DUMBBELL_LATERAL_RAISE,
- EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_LEFT_ARM,
- EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_RIGHT_ARM,
- EXERCISE_TYPE_DUMBBELL_TRICEPS_EXTENSION_TWO_ARM,
EXERCISE_TYPE_ELLIPTICAL,
EXERCISE_TYPE_EXERCISE_CLASS,
EXERCISE_TYPE_FENCING,
EXERCISE_TYPE_FOOTBALL_AMERICAN,
EXERCISE_TYPE_FOOTBALL_AUSTRALIAN,
- EXERCISE_TYPE_FORWARD_TWIST,
EXERCISE_TYPE_FRISBEE_DISC,
EXERCISE_TYPE_GOLF,
EXERCISE_TYPE_GUIDED_BREATHING,
@@ -331,15 +297,10 @@
EXERCISE_TYPE_HIKING,
EXERCISE_TYPE_ICE_HOCKEY,
EXERCISE_TYPE_ICE_SKATING,
- EXERCISE_TYPE_JUMPING_JACK,
- EXERCISE_TYPE_JUMP_ROPE,
- EXERCISE_TYPE_LAT_PULL_DOWN,
- EXERCISE_TYPE_LUNGE,
EXERCISE_TYPE_MARTIAL_ARTS,
EXERCISE_TYPE_PADDLING,
EXERCISE_TYPE_PARAGLIDING,
EXERCISE_TYPE_PILATES,
- EXERCISE_TYPE_PLANK,
EXERCISE_TYPE_RACQUETBALL,
EXERCISE_TYPE_ROCK_CLIMBING,
EXERCISE_TYPE_ROLLER_HOCKEY,
@@ -357,7 +318,6 @@
EXERCISE_TYPE_SOCCER,
EXERCISE_TYPE_SOFTBALL,
EXERCISE_TYPE_SQUASH,
- EXERCISE_TYPE_SQUAT,
EXERCISE_TYPE_STAIR_CLIMBING,
EXERCISE_TYPE_STAIR_CLIMBING_MACHINE,
EXERCISE_TYPE_STRENGTH_TRAINING,
@@ -367,7 +327,6 @@
EXERCISE_TYPE_SWIMMING_POOL,
EXERCISE_TYPE_TABLE_TENNIS,
EXERCISE_TYPE_TENNIS,
- EXERCISE_TYPE_UPPER_TWIST,
EXERCISE_TYPE_VOLLEYBALL,
EXERCISE_TYPE_WALKING,
EXERCISE_TYPE_WATER_POLO,
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
index 8730026..64d6f17 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/impl/converters/records/AllRecordsConverterTest.kt
@@ -31,7 +31,7 @@
import androidx.health.connect.client.records.DistanceRecord
import androidx.health.connect.client.records.ElevationGainedRecord
import androidx.health.connect.client.records.ExerciseSessionRecord
-import androidx.health.connect.client.records.ExerciseSessionRecord.Companion.EXERCISE_TYPE_BACK_EXTENSION
+import androidx.health.connect.client.records.ExerciseSessionRecord.Companion.EXERCISE_TYPE_BADMINTON
import androidx.health.connect.client.records.FloorsClimbedRecord
import androidx.health.connect.client.records.HeartRateRecord
import androidx.health.connect.client.records.HeartRateVariabilityRmssdRecord
@@ -579,7 +579,7 @@
fun testActivitySession() {
val data =
ExerciseSessionRecord(
- exerciseType = EXERCISE_TYPE_BACK_EXTENSION,
+ exerciseType = EXERCISE_TYPE_BADMINTON,
title = null,
notes = null,
startTime = START_TIME,
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
index 26d41ac..7d63ec1 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/records/ExerciseSessionRecordTest.kt
@@ -16,6 +16,11 @@
package androidx.health.connect.client.records
+import androidx.health.connect.client.records.ExerciseSessionRecord.Companion.EXERCISE_TYPE_CALISTHENICS
+import androidx.health.connect.client.records.ExerciseSessionRecord.Companion.EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING
+import androidx.health.connect.client.records.ExerciseSessionRecord.Companion.EXERCISE_TYPE_INT_TO_STRING_MAP
+import androidx.health.connect.client.records.ExerciseSessionRecord.Companion.EXERCISE_TYPE_STRENGTH_TRAINING
+import androidx.health.connect.client.records.ExerciseSessionRecord.Companion.EXERCISE_TYPE_STRING_TO_INT_MAP
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.google.common.truth.Truth.assertThat
import java.time.Instant
@@ -79,9 +84,20 @@
.map { it -> it.call(ExerciseSessionRecord.Companion) }
.toHashSet()
- assertThat(ExerciseSessionRecord.EXERCISE_TYPE_STRING_TO_INT_MAP.values)
+ assertThat(EXERCISE_TYPE_STRING_TO_INT_MAP.values.toSet())
.containsExactlyElementsIn(allEnums)
- assertThat(ExerciseSessionRecord.EXERCISE_TYPE_INT_TO_STRING_MAP.keys)
- .containsExactlyElementsIn(allEnums)
+ assertThat(EXERCISE_TYPE_INT_TO_STRING_MAP.keys).containsExactlyElementsIn(allEnums)
+ }
+
+ @Test
+ fun legacyTypesMapToRightValues() {
+ assertThat(EXERCISE_TYPE_INT_TO_STRING_MAP[EXERCISE_TYPE_STRENGTH_TRAINING])
+ .isEqualTo("strength_training")
+
+ assertThat(EXERCISE_TYPE_INT_TO_STRING_MAP[EXERCISE_TYPE_HIGH_INTENSITY_INTERVAL_TRAINING])
+ .isEqualTo("high_intensity_interval_training")
+
+ assertThat(EXERCISE_TYPE_INT_TO_STRING_MAP[EXERCISE_TYPE_CALISTHENICS])
+ .isEqualTo("calisthenics")
}
}
diff --git a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
index 34f191f..ed033e1 100644
--- a/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
+++ b/mediarouter/mediarouter/src/main/java/androidx/mediarouter/media/MediaRouteProvider.java
@@ -602,10 +602,6 @@
* the route controller.
* @param dynamicRoutes The dynamic route descriptors for published routes.
* At least a selected or selecting route should be included.
- *
- * @throws IllegalArgumentException Thrown when no dynamic route descriptors are {@link
- * DynamicRouteDescriptor#SELECTED SELECTED} or {@link DynamicRouteDescriptor#SELECTING
- * SELECTING}.
*/
public final void notifyDynamicRoutesChanged(
@NonNull MediaRouteDescriptor groupRoute,
@@ -616,23 +612,6 @@
if (dynamicRoutes == null) {
throw new NullPointerException("dynamicRoutes must not be null");
}
-
- boolean hasSelectedRoute = false;
- for (DynamicRouteDescriptor route: dynamicRoutes) {
- int state = route.getSelectionState();
- if (state == DynamicRouteDescriptor.SELECTED
- || state == DynamicRouteDescriptor.SELECTING) {
- hasSelectedRoute = true;
- break;
- }
- }
-
- if (!hasSelectedRoute) {
- throw new IllegalArgumentException("dynamicRoutes must have at least one selected"
- + " or selecting route.");
-
- }
-
synchronized (mLock) {
if (mExecutor != null) {
final OnDynamicRoutesChangedListener listener = mListener;
diff --git a/samples/MediaRoutingDemo/build.gradle b/samples/MediaRoutingDemo/build.gradle
index 5ec869d..4db96f4 100644
--- a/samples/MediaRoutingDemo/build.gradle
+++ b/samples/MediaRoutingDemo/build.gradle
@@ -9,6 +9,8 @@
implementation(project(":recyclerview:recyclerview"))
implementation("androidx.concurrent:concurrent-futures:1.1.0")
implementation(libs.material)
+
+ implementation ("androidx.multidex:multidex:2.0.1")
}
android {
@@ -20,6 +22,7 @@
}
defaultConfig {
vectorDrawables.useSupportLibrary = true
+ multiDexEnabled true
}
lint {
baseline = file("lint-baseline.xml")
diff --git a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
index bc93b71..b56a49f 100644
--- a/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
+++ b/samples/MediaRoutingDemo/src/main/AndroidManifest.xml
@@ -27,7 +27,8 @@
android:icon="@drawable/app_sample_code"
android:label="@string/media_router_app_name"
android:supportsRtl="true"
- android:theme="@style/Theme.SampleMediaRouter">
+ android:theme="@style/Theme.SampleMediaRouter"
+ android:name="androidx.multidex.MultiDexApplication">
<activity
android:name=".activities.MainActivity"
diff --git a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
index 20550af..90ccfc5 100644
--- a/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
+++ b/samples/MediaRoutingDemo/src/main/java/com/example/androidx/mediarouting/activities/MainActivity.java
@@ -504,6 +504,11 @@
return null;
}
+ private long getCurrentEstimatedPosition(@NonNull PlaylistItem item) {
+ return item.getPosition() + (mSessionManager.isPaused()
+ ? 0 : (SystemClock.elapsedRealtime() - item.getTimestamp()));
+ }
+
@NonNull
private MediaRouterParams getRouterParams() {
return new MediaRouterParams.Builder()
@@ -634,8 +639,7 @@
PlaylistItem item = getCheckedPlaylistItem();
if (item != null) {
- long pos = item.getPosition() + (mSessionManager.isPaused()
- ? 0 : (SystemClock.elapsedRealtime() - item.getTimestamp()));
+ long pos = getCurrentEstimatedPosition(item);
mSessionManager.suspend(pos);
}
if (isPresentationApiSupported()) {
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt
index 177a0a7..db1f1eb 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyColumnTest.kt
@@ -45,13 +45,11 @@
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.testTag
-import androidx.compose.ui.test.assertCountEquals
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.captureToImage
import androidx.compose.ui.test.getUnclippedBoundsInRoot
import androidx.compose.ui.test.junit4.createComposeRule
-import androidx.compose.ui.test.onChildren
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.unit.dp
@@ -254,15 +252,13 @@
for (data in dataLists) {
rule.runOnIdle { dataModel = data }
- // Confirm the number of children to ensure there are no extra items
- val numItems = data.size
- rule.onNodeWithTag(tag)
- .onChildren()
- .assertCountEquals(numItems)
-
// Confirm the children's content
- for (item in data) {
- rule.onNodeWithText("$item").assertExists()
+ for (index in 1..8) {
+ if (index in data) {
+ rule.onNodeWithText("$index").assertIsDisplayed()
+ } else {
+ rule.onNodeWithText("$index").assertIsNotPlaced()
+ }
}
}
}
@@ -362,7 +358,7 @@
.assertIsDisplayed()
rule.onNodeWithTag("3")
- .assertDoesNotExist()
+ .assertIsNotPlaced()
}
@Test
diff --git a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
index 876b8eb..51f87fd 100644
--- a/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
+++ b/tv/tv-foundation/src/androidTest/java/androidx/tv/foundation/lazy/list/LazyListTest.kt
@@ -84,7 +84,6 @@
import java.util.concurrent.CountDownLatch
import kotlin.math.roundToInt
import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.Parameterized
@@ -647,7 +646,6 @@
.assertStartPositionIsAlmost(0.dp)
}
- @Ignore("b/266124027")
@Test
fun whenItemsBecameEmpty() {
var items by mutableStateOf((1..10).toList())
@@ -676,9 +674,9 @@
// and has no children
rule.onNodeWithTag("1")
- .assertDoesNotExist()
+ .assertIsNotPlaced()
rule.onNodeWithTag("2")
- .assertDoesNotExist()
+ .assertIsNotPlaced()
}
@Test
diff --git a/wear/compose/compose-material/api/current.txt b/wear/compose/compose-material/api/current.txt
index 8a1701e..a4cfb31 100644
--- a/wear/compose/compose-material/api/current.txt
+++ b/wear/compose/compose-material/api/current.txt
@@ -321,6 +321,7 @@
@androidx.compose.runtime.Stable public final class PickerState implements androidx.compose.foundation.gestures.ScrollableState {
ctor public PickerState(int initialNumberOfOptions, optional int initiallySelectedOption, optional boolean repeatItems);
+ method public suspend Object? animateScrollToOption(int index, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public float dispatchRawDelta(float delta);
method public int getNumberOfOptions();
method public boolean getRepeatItems();
diff --git a/wear/compose/compose-material/api/public_plus_experimental_current.txt b/wear/compose/compose-material/api/public_plus_experimental_current.txt
index 8cc12b4..d192ef2 100644
--- a/wear/compose/compose-material/api/public_plus_experimental_current.txt
+++ b/wear/compose/compose-material/api/public_plus_experimental_current.txt
@@ -337,6 +337,7 @@
@androidx.compose.runtime.Stable public final class PickerState implements androidx.compose.foundation.gestures.ScrollableState {
ctor public PickerState(int initialNumberOfOptions, optional int initiallySelectedOption, optional boolean repeatItems);
+ method public suspend Object? animateScrollToOption(int index, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public float dispatchRawDelta(float delta);
method public int getNumberOfOptions();
method public boolean getRepeatItems();
diff --git a/wear/compose/compose-material/api/restricted_current.txt b/wear/compose/compose-material/api/restricted_current.txt
index 8a1701e..a4cfb31 100644
--- a/wear/compose/compose-material/api/restricted_current.txt
+++ b/wear/compose/compose-material/api/restricted_current.txt
@@ -321,6 +321,7 @@
@androidx.compose.runtime.Stable public final class PickerState implements androidx.compose.foundation.gestures.ScrollableState {
ctor public PickerState(int initialNumberOfOptions, optional int initiallySelectedOption, optional boolean repeatItems);
+ method public suspend Object? animateScrollToOption(int index, kotlin.coroutines.Continuation<? super kotlin.Unit>);
method public float dispatchRawDelta(float delta);
method public int getNumberOfOptions();
method public boolean getRepeatItems();
diff --git a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/PickerSample.kt b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/PickerSample.kt
index b9315ce..4e0485a 100644
--- a/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/PickerSample.kt
+++ b/wear/compose/compose-material/samples/src/main/java/androidx/wear/compose/material/samples/PickerSample.kt
@@ -92,6 +92,34 @@
}
}
+@Sampled
+@Composable
+fun AnimateOptionChangePicker() {
+ val coroutineScope = rememberCoroutineScope()
+ val state = rememberPickerState(initialNumberOfOptions = 10)
+ val contentDescription by remember { derivedStateOf { "${state.selectedOption + 1}" } }
+
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ Picker(
+ state = state,
+ separation = 4.dp,
+ contentDescription = contentDescription,
+ ) {
+ Chip(
+ onClick = {
+ coroutineScope.launch { state.animateScrollToOption(it) }
+ },
+ label = {
+ Text("$it")
+ }
+ )
+ }
+ }
+}
+
@OptIn(ExperimentalComposeUiApi::class)
@Sampled
@Composable
diff --git a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
index 7829b10..74c63e1 100644
--- a/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
+++ b/wear/compose/compose-material/src/androidAndroidTest/kotlin/androidx/wear/compose/material/PickerTest.kt
@@ -45,6 +45,8 @@
import androidx.test.filters.LargeTest
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.junit.Before
@@ -246,6 +248,118 @@
}
@Test
+ fun scroll_to_next_item_with_animation() {
+ animateScrollTo(
+ initialOption = 2,
+ targetOption = 3,
+ totalOptions = 10,
+ expectedItemsScrolled = 1,
+ )
+ }
+
+ @Test
+ fun scroll_forward_by_two_items_with_animation() {
+ animateScrollTo(
+ initialOption = 2,
+ targetOption = 4,
+ totalOptions = 10,
+ expectedItemsScrolled = 2,
+ )
+ }
+
+ @Test
+ fun scroll_to_prev_item_with_animation() {
+ animateScrollTo(
+ initialOption = 2,
+ targetOption = 1,
+ totalOptions = 5,
+ expectedItemsScrolled = -1,
+ )
+ }
+
+ @Test
+ fun scroll_backward_by_two_items_with_animation() {
+ animateScrollTo(
+ initialOption = 3,
+ targetOption = 1,
+ totalOptions = 5,
+ expectedItemsScrolled = -2,
+ )
+ }
+
+ @Test
+ fun scroll_forward_to_repeated_items_with_animation() {
+ animateScrollTo(
+ initialOption = 8,
+ targetOption = 2,
+ totalOptions = 10,
+ expectedItemsScrolled = 4,
+ )
+ }
+
+ @Test
+ fun scroll_backward_to_repeated_items_with_animation() {
+ animateScrollTo(
+ initialOption = 2,
+ targetOption = 8,
+ totalOptions = 10,
+ expectedItemsScrolled = -4,
+ )
+ }
+
+ @Test
+ fun scroll_to_the_closest_item_with_animation() {
+ animateScrollTo(
+ initialOption = 2,
+ targetOption = 0,
+ totalOptions = 4,
+ expectedItemsScrolled = -2,
+ )
+ }
+
+ @Test
+ fun animate_scroll_cancels_previous_animation() {
+ val initialOption = 5
+ val totalOptions = 10
+ val firstTarget = 7
+ val secondTarget = 9
+
+ val targetDelta = 4
+
+ lateinit var state: PickerState
+ lateinit var scope: CoroutineScope
+ rule.setContent {
+ scope = rememberCoroutineScope()
+ state = rememberPickerState(
+ initialNumberOfOptions = totalOptions,
+ initiallySelectedOption = initialOption
+ )
+ SimplePicker(state)
+ }
+ val initialItemIndex = state.scalingLazyListState.centerItemIndex
+
+ // The first animation starts, but before it's finished - a second animation starts,
+ // which cancels the first animation. In the end it doesn't matter how far picker was
+ // scrolled during first animation, because the second animation should bring
+ // picker to its final target.
+ rule.runOnIdle {
+ scope.launch {
+ async {
+ state.animateScrollToOption(firstTarget)
+ }
+ delay(100) // a short delay so that the first async will be triggered first
+ async {
+ state.animateScrollToOption(secondTarget)
+ }
+ }
+ }
+ rule.waitForIdle()
+ assertThat(state.selectedOption).isEqualTo(secondTarget)
+ assertThat(state.scalingLazyListState.centerItemIndex)
+ .isEqualTo(initialItemIndex + targetDelta)
+ }
+
+ @Test
fun scrolls_with_negative_separation() = scrolls_to_index_correctly(-1, 3)
@Test
@@ -289,6 +403,7 @@
}
}
+ val initialItemIndex = state.scalingLazyListState.centerItemIndex
rule.runOnIdle {
runBlocking {
state.numberOfOptions = 31
@@ -298,6 +413,7 @@
rule.waitForIdle()
assertThat(state.selectedOption).isEqualTo(initialOption)
+ assertThat(state.scalingLazyListState.centerItemIndex).isEqualTo(initialItemIndex)
}
@Test
@@ -581,6 +697,49 @@
rule.onNodeWithText("2").assertExists()
}
+ private fun animateScrollTo(
+ initialOption: Int,
+ targetOption: Int,
+ totalOptions: Int,
+ expectedItemsScrolled: Int,
+ ) {
+ lateinit var state: PickerState
+ lateinit var scope: CoroutineScope
+ rule.setContent {
+ scope = rememberCoroutineScope()
+ state = rememberPickerState(
+ initialNumberOfOptions = totalOptions,
+ initiallySelectedOption = initialOption
+ )
+ SimplePicker(state)
+ }
+
+ val initialItemIndex = state.scalingLazyListState.centerItemIndex
+ rule.runOnIdle {
+ scope.launch {
+ async {
+ state.animateScrollToOption(targetOption)
+ }
+ }
+ }
+ rule.waitForIdle()
+ assertThat(state.selectedOption).isEqualTo(targetOption)
+ assertThat(state.scalingLazyListState.centerItemIndex)
+ .isEqualTo(initialItemIndex + expectedItemsScrolled)
+ }
+
+ @Composable
+ private fun SimplePicker(state: PickerState) {
+ WithTouchSlop(0f) {
+ Picker(
+ state = state,
+ contentDescription = CONTENT_DESCRIPTION,
+ ) {
+ Box(Modifier.requiredSize(itemSizeDp))
+ }
+ }
+ }
+
private fun scroll_snaps(
separationSign: Int = 0,
touchInput: (TouchInjectionScope).() -> Unit,
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
index 31db6b9..963051d 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Picker.kt
@@ -494,7 +494,7 @@
verifyNumberOfOptions(newNumberOfOptions)
// We need to maintain the mapping between the currently selected item and the
// currently selected option.
- optionsOffset = positiveModule(
+ optionsOffset = positiveModulo(
selectedOption.coerceAtMost(newNumberOfOptions - 1) -
scalingLazyListState.centerItemIndex,
newNumberOfOptions
@@ -537,18 +537,29 @@
* @param index The index of the option to scroll to.
*/
public suspend fun scrollToOption(index: Int) {
- val itemIndex =
- if (!repeatItems) {
- index
- } else {
- // Pick the itemIndex closest to the current one, that it's congruent modulo
- // numberOfOptions with index - optionOffset.
- // This is to try to work around http://b/230582961
- val minTargetIndex = scalingLazyListState.centerItemIndex - numberOfOptions / 2
- minTargetIndex + positiveModule(index - minTargetIndex, numberOfOptions) -
- optionsOffset
- }
- scalingLazyListState.scrollToItem(itemIndex, 0)
+ scalingLazyListState.scrollToItem(getClosestTargetItemIndex(index), 0)
+ }
+
+ /**
+ * Animate (smooth scroll) to the given item at [index]
+ *
+ * A smooth scroll always happens to the closest item if PickerState has repeatItems=true.
+ * For example, picker values are :
+ * 0 1 2 3 0 1 2 [3] 0 1 2 3
+ * Target value is [0].
+ * 0 1 2 3 >0< 1 2 [3] >0< 1 2 3
+ * Picker can be scrolled forwards or backwards. To get to the target 0 it requires 1 step to
+ * scroll forwards and 3 steps to scroll backwards. Picker will be scrolled forwards
+ * as this is the closest destination.
+ *
+ * If the distance between possible targets is the same, picker will be scrolled backwards.
+ *
+ * @sample androidx.wear.compose.material.samples.AnimateOptionChangePicker
+ *
+ * @param index The index of the option to scroll to.
+ */
+ public suspend fun animateScrollToOption(index: Int) {
+ scalingLazyListState.animateScrollToItem(getClosestTargetItemIndex(index), 0)
}
public companion object {
@@ -593,6 +604,21 @@
override val canScrollBackward: Boolean
get() = scalingLazyListState.canScrollBackward
+ /**
+ * Function which calculates the real position of an option
+ */
+ private fun getClosestTargetItemIndex(option: Int): Int =
+ if (!repeatItems) {
+ option
+ } else {
+ // Calculating the distance to the target option in front or back.
+ // The minimum distance is then selected and picker is scrolled in that direction.
+ val stepsPrev = positiveModulo(selectedOption - option, numberOfOptions)
+ val stepsNext = positiveModulo(option - selectedOption, numberOfOptions)
+ scalingLazyListState.centerItemIndex +
+ if (stepsPrev <= stepsNext) -stepsPrev else stepsNext
+ }
+
private fun verifyNumberOfOptions(numberOfOptions: Int) {
require(numberOfOptions > 0) { "The picker should have at least one item." }
require(numberOfOptions < LARGE_NUMBER_OF_ITEMS / 3) {
@@ -704,7 +730,7 @@
public val selectedOption: Int
}
-private fun positiveModule(n: Int, mod: Int) = ((n % mod) + mod) % mod
+private fun positiveModulo(n: Int, mod: Int) = ((n % mod) + mod) % mod
private fun convertToDefaultFoundationScalingParams(
@Suppress("DEPRECATION")
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
index d2edf1d..2c06dbd 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/MaterialDemos.kt
@@ -30,6 +30,7 @@
import androidx.wear.compose.material.samples.AlertDialogSample
import androidx.wear.compose.material.samples.AlertWithButtons
import androidx.wear.compose.material.samples.AlertWithChips
+import androidx.wear.compose.material.samples.AnimateOptionChangePicker
import androidx.wear.compose.material.samples.AppCardWithIcon
import androidx.wear.compose.material.samples.ButtonWithIcon
import androidx.wear.compose.material.samples.ButtonWithText
@@ -210,6 +211,7 @@
},
ComposableDemo("Simple Picker") { SimplePicker() },
ComposableDemo("No gradient") { PickerWithoutGradient() },
+ ComposableDemo("Animate picker change") { AnimateOptionChangePicker() },
)
} else {
listOf(
diff --git a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
index 1eebca8..9e68e8f 100644
--- a/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
+++ b/wear/compose/integration-tests/demos/src/main/java/androidx/wear/compose/integration/demos/PickerDemo.kt
@@ -583,6 +583,9 @@
datePickerState.monthState.numberOfOptions = datePickerState.numOfMonths
}
if (datePickerState.numOfDays != datePickerState.dayState.numberOfOptions) {
+ if (datePickerState.dayState.selectedOption >= datePickerState.numOfDays) {
+ datePickerState.dayState.animateScrollToOption(datePickerState.numOfDays - 1)
+ }
datePickerState.dayState.numberOfOptions = datePickerState.numOfDays
}
}
@@ -595,7 +598,8 @@
datePickerState.currentYear(),
"${datePickerState.currentYear()}"
)
- } }
+ }
+ }
val monthContentDescription by remember(focusedElement, datePickerState.currentMonth()) {
derivedStateOf {
createDescriptionDatePicker(
diff --git a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
index ad4a176..7b6539c 100644
--- a/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
+++ b/wear/watchface/watchface-client/src/androidTest/java/androidx/wear/watchface/client/test/WatchFaceControlClientTest.kt
@@ -967,7 +967,7 @@
val mockIInteractiveWatchFace = mock(IInteractiveWatchFace::class.java)
val mockIBinder = mock(IBinder::class.java)
`when`(mockIInteractiveWatchFace.asBinder()).thenReturn(mockIBinder)
- `when`(mockIInteractiveWatchFace.apiVersion).thenReturn(6)
+ `when`(mockIInteractiveWatchFace.apiVersion).thenReturn(7)
val interactiveInstance = InteractiveWatchFaceClientImpl(
mockIInteractiveWatchFace,
diff --git a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
index 21d7a66..370ed48 100644
--- a/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
+++ b/wear/watchface/watchface-client/src/main/java/androidx/wear/watchface/client/InteractiveWatchFaceClient.kt
@@ -702,5 +702,5 @@
}
}
- override fun isComplicationDisplayPolicySupported() = iInteractiveWatchFace.apiVersion >= 7
+ override fun isComplicationDisplayPolicySupported() = iInteractiveWatchFace.apiVersion >= 8
}
diff --git a/wear/watchface/watchface-complications-data-source/build.gradle b/wear/watchface/watchface-complications-data-source/build.gradle
index e8b91c7..1726a58 100644
--- a/wear/watchface/watchface-complications-data-source/build.gradle
+++ b/wear/watchface/watchface-complications-data-source/build.gradle
@@ -34,8 +34,10 @@
testImplementation(libs.testRules)
testImplementation(libs.robolectric)
testImplementation(libs.mockitoCore4)
+ testImplementation(libs.mockitoKotlin4)
testImplementation(libs.truth)
testImplementation(libs.junit)
+ testImplementation(libs.kotlinTest)
}
android {
diff --git a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
index 442d906..6af62fd 100644
--- a/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
+++ b/wear/watchface/watchface-complications-data-source/src/main/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceService.kt
@@ -34,6 +34,7 @@
import androidx.annotation.RestrictTo
import androidx.wear.watchface.complications.data.ComplicationData
import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator
+import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator.Companion.hasExpression
import androidx.wear.watchface.complications.data.ComplicationType
import androidx.wear.watchface.complications.data.ComplicationType.Companion.fromWireType
import androidx.wear.watchface.complications.data.NoDataComplicationData
@@ -546,6 +547,9 @@
require(complicationData.validTimeRange == TimeRange.ALWAYS) {
"Preview data should have time range set to ALWAYS."
}
+ require(!hasExpression(complicationData.asWireComplicationData())) {
+ "Preview data must not have expressions."
+ }
}
return complicationData?.asWireComplicationData()
}
diff --git a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.java b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.java
deleted file mode 100644
index 7f4d0da..0000000
--- a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.java
+++ /dev/null
@@ -1,416 +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.wear.watchface.complications.datasource;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.ArgumentMatchers.argThat;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.verify;
-import static org.robolectric.shadows.ShadowLooper.runUiThreadTasks;
-
-import android.content.Intent;
-import android.content.res.Resources;
-import android.os.Build;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.RemoteException;
-import android.support.wearable.complications.IComplicationManager;
-import android.support.wearable.complications.IComplicationProvider;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString;
-import androidx.wear.watchface.complications.data.ComplicationData;
-import androidx.wear.watchface.complications.data.ComplicationText;
-import androidx.wear.watchface.complications.data.ComplicationTextExpression;
-import androidx.wear.watchface.complications.data.ComplicationType;
-import androidx.wear.watchface.complications.data.LongTextComplicationData;
-import androidx.wear.watchface.complications.data.PlainComplicationText;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
-import org.robolectric.annotation.Config;
-import org.robolectric.annotation.internal.DoNotInstrument;
-import org.robolectric.shadows.ShadowLog;
-import org.robolectric.shadows.ShadowLooper;
-
-import java.time.Instant;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-/** Tests for {@link ComplicationDataSourceService}. */
-@RunWith(ComplicationsTestRunner.class)
-@DoNotInstrument
-public class ComplicationDataSourceServiceTest {
-
- private static final String TAG = "ComplicationDataSourceServiceTest";
-
- HandlerThread mPretendMainThread = new HandlerThread("testThread");
- Handler mPretendMainThreadHandler;
-
- @Mock
- private IComplicationManager mRemoteManager;
-
- private final CountDownLatch mUpdateComplicationDataLatch = new CountDownLatch(1);
- private final IComplicationManager.Stub mLocalManager = new IComplicationManager.Stub() {
- @Override
- public void updateComplicationData(int complicationSlotId,
- android.support.wearable.complications.ComplicationData data)
- throws RemoteException {
- mRemoteManager.updateComplicationData(complicationSlotId, data);
- mUpdateComplicationDataLatch.countDown();
- }
- };
-
- private IComplicationProvider.Stub mProvider;
-
- /**
- * Mock implementation of ComplicationDataSourceService.
- *
- * <p>Can't use Mockito because it doesn't like partially implemented classes.
- */
- private class MockComplicationDataSourceService extends ComplicationDataSourceService {
- boolean respondWithTimeline = false;
-
- /**
- * Will be used to invoke {@link ComplicationRequestListener#onComplicationData} on
- * {@link #onComplicationRequest}.
- */
- @Nullable
- ComplicationData responseData;
-
- /**
- * Will be used to invoke {@link ComplicationRequestListener#onComplicationDataTimeline} on
- * {@link #onComplicationRequest}, if {@link #respondWithTimeline} is true.
- */
- @Nullable
- ComplicationDataTimeline responseDataTimeline;
-
- /** Last request provided to {@link #onComplicationRequest}. */
- @Nullable
- ComplicationRequest lastRequest;
-
- /** Will be returned from {@link #getPreviewData}. */
- @Nullable
- ComplicationData previewData;
-
- /** Last type provided to {@link #getPreviewData}. */
- @Nullable
- ComplicationType lastPreviewType;
-
- @NonNull
- @Override
- public Handler createMainThreadHandler() {
- return mPretendMainThreadHandler;
- }
-
- @Override
- public void onComplicationRequest(@NonNull ComplicationRequest request,
- @NonNull ComplicationRequestListener listener) {
- lastRequest = request;
- try {
- if (respondWithTimeline) {
- listener.onComplicationDataTimeline(responseDataTimeline);
- } else {
- listener.onComplicationData(responseData);
- }
- } catch (RemoteException e) {
- Log.e(TAG, "onComplicationRequest failed with error: ", e);
- }
- }
-
- @Nullable
- @Override
- public ComplicationData getPreviewData(@NonNull ComplicationType type) {
- lastPreviewType = type;
- return previewData;
- }
- }
-
- private final MockComplicationDataSourceService mService =
- new MockComplicationDataSourceService();
-
- @SuppressWarnings("deprecation") // b/251211092
- @Before
- public void setUp() {
- ShadowLog.setLoggable("ComplicationData", Log.DEBUG);
- MockitoAnnotations.initMocks(this);
- mProvider =
- (IComplicationProvider.Stub) mService.onBind(
- new Intent(
- ComplicationDataSourceService.ACTION_COMPLICATION_UPDATE_REQUEST));
-
- mPretendMainThread.start();
- mPretendMainThreadHandler = new Handler(mPretendMainThread.getLooper());
- }
-
- @After
- public void tearDown() {
- mPretendMainThread.quitSafely();
- }
-
- @Test
- public void testOnComplicationRequest() throws Exception {
- mService.responseData =
- new LongTextComplicationData.Builder(
- new PlainComplicationText.Builder("hello").build(),
- ComplicationText.EMPTY
- ).build();
-
- int id = 123;
- mProvider.onUpdate(
- id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager);
- assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
-
- ArgumentCaptor<android.support.wearable.complications.ComplicationData> data =
- ArgumentCaptor.forClass(
- android.support.wearable.complications.ComplicationData.class);
- verify(mRemoteManager).updateComplicationData(eq(id), data.capture());
- assertThat(data.getValue().getLongText().getTextAt(Resources.getSystem(), 0)).isEqualTo(
- "hello");
- }
-
- @Test
- @Config(sdk = Build.VERSION_CODES.TIRAMISU)
- public void testOnComplicationRequestWithExpression_doesNotEvaluateExpression()
- throws Exception {
- mService.responseData =
- new LongTextComplicationData.Builder(
- new ComplicationTextExpression(
- DynamicString.constant("hello").concat(
- DynamicString.constant(" world"))),
- ComplicationText.EMPTY)
- .build();
-
- mProvider.onUpdate(
- /* complicationInstanceId = */ 123,
- ComplicationType.LONG_TEXT.toWireComplicationType(),
- mLocalManager);
-
- assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
- verify(mRemoteManager).updateComplicationData(
- eq(123),
- eq(new LongTextComplicationData.Builder(
- new ComplicationTextExpression(
- DynamicString.constant("hello").concat(
- DynamicString.constant(" world"))),
- ComplicationText.EMPTY)
- .build()
- .asWireComplicationData()));
- }
-
- @Test
- @Config(sdk = Build.VERSION_CODES.S)
- public void testOnComplicationRequestWithExpressionPreT_evaluatesExpression()
- throws Exception {
- mService.responseData =
- new LongTextComplicationData.Builder(
- new ComplicationTextExpression(
- DynamicString.constant("hello").concat(
- DynamicString.constant(" world"))),
- ComplicationText.EMPTY)
- .build();
-
- mProvider.onUpdate(
- /* complicationInstanceId = */ 123,
- ComplicationType.LONG_TEXT.toWireComplicationType(),
- mLocalManager);
-
- runUiThreadTasksWhileAwaitingDataLatch(1000);
- verify(mRemoteManager).updateComplicationData(
- eq(123),
- argThat(data ->
- data.getLongText().getTextAt(Resources.getSystem(), 0)
- .equals("hello world")));
- }
-
- @Test
- public void testOnComplicationRequestWrongType() throws Exception {
- mService.responseData =
- new LongTextComplicationData.Builder(
- new PlainComplicationText.Builder("hello").build(),
- ComplicationText.EMPTY
- ).build();
- int id = 123;
- AtomicReference<Throwable> exception = new AtomicReference<>();
- CountDownLatch exceptionLatch = new CountDownLatch(1);
-
- mPretendMainThread.setUncaughtExceptionHandler((thread, throwable) -> {
- exception.set(throwable);
- exceptionLatch.countDown();
- });
-
- mProvider.onUpdate(
- id, ComplicationType.SHORT_TEXT.toWireComplicationType(), mLocalManager);
-
- assertThat(exceptionLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
- assertThat(exception.get()).isInstanceOf(IllegalArgumentException.class);
- }
-
- @Test
- public void testOnComplicationRequestNoUpdateRequired() throws Exception {
- mService.responseData = null;
-
- int id = 123;
- mProvider.onUpdate(
- id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager);
- assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
-
- ArgumentCaptor<android.support.wearable.complications.ComplicationData> data =
- ArgumentCaptor.forClass(
- android.support.wearable.complications.ComplicationData.class);
- verify(mRemoteManager).updateComplicationData(eq(id), data.capture());
- assertThat(data.getValue()).isNull();
- }
-
- @Test
- public void testGetComplicationPreviewData() throws Exception {
- mService.previewData = new LongTextComplicationData.Builder(
- new PlainComplicationText.Builder("hello preview").build(),
- ComplicationText.EMPTY
- ).build();
-
- assertThat(mProvider.getComplicationPreviewData(
- ComplicationType.LONG_TEXT.toWireComplicationType()
- ).getLongText().getTextAt(Resources.getSystem(), 0)).isEqualTo("hello preview");
- }
-
- @Test
- public void testTimelineTestService() throws Exception {
- mService.respondWithTimeline = true;
- ArrayList<TimelineEntry> timeline = new ArrayList<>();
- timeline.add(new TimelineEntry(
- new TimeInterval(
- Instant.ofEpochSecond(1000),
- Instant.ofEpochSecond(4000)
- ),
- new LongTextComplicationData.Builder(
- new PlainComplicationText.Builder(
- "A").build(),
- ComplicationText.EMPTY
- ).build()
- )
- );
- timeline.add(new TimelineEntry(
- new TimeInterval(
- Instant.ofEpochSecond(6000),
- Instant.ofEpochSecond(8000)
- ),
- new LongTextComplicationData.Builder(
- new PlainComplicationText.Builder(
- "B").build(),
- ComplicationText.EMPTY
- ).build()
- )
- );
- mService.responseDataTimeline = new ComplicationDataTimeline(
- new LongTextComplicationData.Builder(
- new PlainComplicationText.Builder(
- "default").build(),
- ComplicationText.EMPTY
- ).build(),
- timeline
- );
-
- int id = 123;
- mProvider.onUpdate(
- id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager);
- assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
-
- ArgumentCaptor<android.support.wearable.complications.ComplicationData> data =
- ArgumentCaptor.forClass(
- android.support.wearable.complications.ComplicationData.class);
- verify(mRemoteManager).updateComplicationData(eq(id), data.capture());
- assertThat(data.getValue().getLongText().getTextAt(Resources.getSystem(), 0)).isEqualTo(
- "default"
- );
- List<android.support.wearable.complications.ComplicationData> timeLineEntries =
- data.getValue().getTimelineEntries();
- assertThat(timeLineEntries).isNotNull();
- assertThat(timeLineEntries.size()).isEqualTo(2);
- assertThat(timeLineEntries.get(0).getTimelineStartEpochSecond()).isEqualTo(1000);
- assertThat(timeLineEntries.get(0).getTimelineEndEpochSecond()).isEqualTo(4000);
- assertThat(timeLineEntries.get(0).getLongText().getTextAt(Resources.getSystem(),
- 0)).isEqualTo(
- "A"
- );
-
- assertThat(timeLineEntries.get(1).getTimelineStartEpochSecond()).isEqualTo(6000);
- assertThat(timeLineEntries.get(1).getTimelineEndEpochSecond()).isEqualTo(8000);
- assertThat(timeLineEntries.get(1).getLongText().getTextAt(Resources.getSystem(),
- 0)).isEqualTo(
- "B"
- );
- }
-
- @Test
- public void testImmediateRequest() throws Exception {
- int id = 123;
- mService.responseData =
- new LongTextComplicationData.Builder(
- new PlainComplicationText.Builder("hello").build(),
- ComplicationText.EMPTY
- ).build();
- HandlerThread thread = new HandlerThread("testThread");
-
- try {
- thread.start();
- Handler threadHandler = new Handler(thread.getLooper());
- AtomicReference<android.support.wearable.complications.ComplicationData> response =
- new AtomicReference<>();
- CountDownLatch doneLatch = new CountDownLatch(1);
-
- threadHandler.post(() -> {
- try {
- response.set(mProvider.onSynchronousComplicationRequest(
- 123,
- ComplicationType.LONG_TEXT.toWireComplicationType()));
- doneLatch.countDown();
- } catch (RemoteException e) {
- // Should not happen
- }
- }
- );
-
- assertThat(doneLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue();
- assertThat(response.get().getLongText().getTextAt(Resources.getSystem(), 0)).isEqualTo(
- "hello");
- } finally {
- thread.quitSafely();
- }
- }
-
- private void runUiThreadTasksWhileAwaitingDataLatch(long timeout) throws InterruptedException {
- // Allowing UI thread to execute while we wait for the data latch.
- long attempts = 0;
- while (!mUpdateComplicationDataLatch.await(1, TimeUnit.MILLISECONDS)) {
- runUiThreadTasks();
- assertThat(attempts++).isLessThan(timeout); // In total waiting ~timeout.
- }
- }
-}
-
diff --git a/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.kt b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.kt
new file mode 100644
index 0000000..d099715
--- /dev/null
+++ b/wear/watchface/watchface-complications-data-source/src/test/java/androidx/wear/watchface/complications/datasource/ComplicationDataSourceServiceTest.kt
@@ -0,0 +1,433 @@
+/*
+ * 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.wear.watchface.complications.datasource
+
+import android.support.wearable.complications.ComplicationData as WireComplicationData
+import android.content.Intent
+import android.content.res.Resources
+import android.os.Build
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.RemoteException
+import android.support.wearable.complications.IComplicationManager
+import android.support.wearable.complications.IComplicationProvider
+import android.util.Log
+import androidx.wear.protolayout.expression.DynamicBuilders
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
+import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
+import androidx.wear.watchface.complications.data.ComplicationData
+import androidx.wear.watchface.complications.data.ComplicationText
+import androidx.wear.watchface.complications.data.ComplicationTextExpression
+import androidx.wear.watchface.complications.data.ComplicationType
+import androidx.wear.watchface.complications.data.LongTextComplicationData
+import androidx.wear.watchface.complications.data.PlainComplicationText
+import androidx.wear.watchface.complications.data.RangedValueComplicationData
+import androidx.wear.watchface.complications.data.ShortTextComplicationData
+import com.google.common.truth.Expect
+import com.google.common.truth.Truth.assertThat
+import java.time.Instant
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.test.assertFailsWith
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.argThat
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito.verify
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.mock
+import org.robolectric.annotation.Config
+import org.robolectric.annotation.internal.DoNotInstrument
+import org.robolectric.shadows.ShadowLog
+import org.robolectric.shadows.ShadowLooper.runUiThreadTasks
+
+/** Tests for [ComplicationDataSourceService]. */
+@RunWith(ComplicationsTestRunner::class)
+@DoNotInstrument
+class ComplicationDataSourceServiceTest {
+ @get:Rule
+ val expect = Expect.create()
+
+ private var mPretendMainThread = HandlerThread("testThread")
+ private lateinit var mPretendMainThreadHandler: Handler
+
+ private val mRemoteManager = mock<IComplicationManager>()
+ private val mUpdateComplicationDataLatch = CountDownLatch(1)
+ private val mLocalManager: IComplicationManager.Stub = object : IComplicationManager.Stub() {
+ override fun updateComplicationData(complicationSlotId: Int, data: WireComplicationData?) {
+ mRemoteManager.updateComplicationData(complicationSlotId, data)
+ mUpdateComplicationDataLatch.countDown()
+ }
+ }
+ private lateinit var mProvider: IComplicationProvider.Stub
+
+ /**
+ * Mock implementation of ComplicationDataSourceService.
+ *
+ * Can't use Mockito because it doesn't like partially implemented classes.
+ */
+ private inner class MockComplicationDataSourceService : ComplicationDataSourceService() {
+ var respondWithTimeline = false
+
+ /**
+ * Will be used to invoke [.ComplicationRequestListener.onComplicationData] on
+ * [onComplicationRequest].
+ */
+ var responseData: ComplicationData? = null
+
+ /**
+ * Will be used to invoke [.ComplicationRequestListener.onComplicationDataTimeline] on
+ * [onComplicationRequest], if [respondWithTimeline] is true.
+ */
+ var responseDataTimeline: ComplicationDataTimeline? = null
+
+ /** Last request provided to [onComplicationRequest]. */
+ var lastRequest: ComplicationRequest? = null
+
+ /** Will be returned from [previewData]. */
+ var previewData: ComplicationData? = null
+
+ /** Last type provided to [previewData]. */
+ var lastPreviewType: ComplicationType? = null
+
+ override fun createMainThreadHandler(): Handler = mPretendMainThreadHandler
+
+ override fun onComplicationRequest(
+ request: ComplicationRequest,
+ listener: ComplicationRequestListener
+ ) {
+ lastRequest = request
+ try {
+ if (respondWithTimeline) {
+ listener.onComplicationDataTimeline(responseDataTimeline)
+ } else {
+ listener.onComplicationData(responseData)
+ }
+ } catch (e: RemoteException) {
+ Log.e(TAG, "onComplicationRequest failed with error: ", e)
+ }
+ }
+
+ override fun getPreviewData(type: ComplicationType): ComplicationData? {
+ lastPreviewType = type
+ return previewData
+ }
+ }
+
+ private val mService = MockComplicationDataSourceService()
+
+ @Before
+ fun setUp() {
+ ShadowLog.setLoggable("ComplicationData", Log.DEBUG)
+ mProvider = mService.onBind(
+ Intent(ComplicationDataSourceService.ACTION_COMPLICATION_UPDATE_REQUEST)
+ ) as IComplicationProvider.Stub
+
+ mPretendMainThread.start()
+ mPretendMainThreadHandler = Handler(mPretendMainThread.looper)
+ }
+
+ @After
+ fun tearDown() {
+ mPretendMainThread.quitSafely()
+ }
+
+ @Test
+ fun testOnComplicationRequest() {
+ mService.responseData = LongTextComplicationData.Builder(
+ PlainComplicationText.Builder("hello").build(),
+ ComplicationText.EMPTY
+ ).build()
+ val id = 123
+ mProvider.onUpdate(id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager)
+ assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+ val data = argumentCaptor<WireComplicationData>()
+ verify(mRemoteManager).updateComplicationData(eq(id), data.capture())
+ assertThat(data.firstValue.longText!!.getTextAt(Resources.getSystem(), 0))
+ .isEqualTo("hello")
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.TIRAMISU])
+ fun testOnComplicationRequestWithExpression_doesNotEvaluateExpression() {
+ mService.responseData = LongTextComplicationData.Builder(
+ ComplicationTextExpression(
+ DynamicBuilders.DynamicString.constant("hello").concat(
+ DynamicBuilders.DynamicString.constant(" world")
+ )
+ ),
+ ComplicationText.EMPTY
+ ).build()
+ mProvider.onUpdate(
+ /* complicationInstanceId = */ 123,
+ ComplicationType.LONG_TEXT.toWireComplicationType(),
+ mLocalManager
+ )
+
+ assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+ verify(mRemoteManager).updateComplicationData(
+ eq(123),
+ eq(
+ LongTextComplicationData.Builder(
+ ComplicationTextExpression(
+ DynamicBuilders.DynamicString.constant("hello").concat(
+ DynamicBuilders.DynamicString.constant(" world")
+ )
+ ),
+ ComplicationText.EMPTY
+ ).build().asWireComplicationData()
+ )
+ )
+ }
+
+ @Test
+ @Config(sdk = [Build.VERSION_CODES.S])
+ fun testOnComplicationRequestWithExpressionPreT_evaluatesExpression() {
+ mService.responseData = LongTextComplicationData.Builder(
+ ComplicationTextExpression(
+ DynamicBuilders.DynamicString.constant("hello").concat(
+ DynamicBuilders.DynamicString.constant(" world")
+ )
+ ),
+ ComplicationText.EMPTY
+ ).build()
+
+ mProvider.onUpdate(
+ /* complicationInstanceId = */ 123,
+ ComplicationType.LONG_TEXT.toWireComplicationType(),
+ mLocalManager
+ )
+
+ runUiThreadTasksWhileAwaitingDataLatch(1000)
+ verify(mRemoteManager).updateComplicationData(
+ eq(123),
+ argThat { data ->
+ data.longText!!.getTextAt(Resources.getSystem(), 0) == "hello world"
+ })
+ }
+
+ @Test
+ fun testOnComplicationRequestWrongType() {
+ mService.responseData = LongTextComplicationData.Builder(
+ PlainComplicationText.Builder("hello").build(),
+ ComplicationText.EMPTY
+ ).build()
+ val id = 123
+ val exception = AtomicReference<Throwable>()
+ val exceptionLatch = CountDownLatch(1)
+
+ mPretendMainThread.uncaughtExceptionHandler =
+ Thread.UncaughtExceptionHandler { _, throwable ->
+ exception.set(throwable)
+ exceptionLatch.countDown()
+ }
+ mProvider.onUpdate(id, ComplicationType.SHORT_TEXT.toWireComplicationType(), mLocalManager)
+
+ assertThat(exceptionLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+ assertThat(exception.get()).isInstanceOf(IllegalArgumentException::class.java)
+ }
+
+ @Test
+ fun testOnComplicationRequestNoUpdateRequired() {
+ mService.responseData = null
+
+ val id = 123
+ mProvider.onUpdate(id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager)
+ assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+
+ val data = argumentCaptor<WireComplicationData>()
+ verify(mRemoteManager).updateComplicationData(eq(id), data.capture())
+ assertThat(data.allValues).containsExactly(null)
+ }
+
+ @Test
+ fun testGetComplicationPreviewData() {
+ mService.previewData = LongTextComplicationData.Builder(
+ PlainComplicationText.Builder("hello preview").build(),
+ ComplicationText.EMPTY
+ ).build()
+
+ assertThat(
+ mProvider.getComplicationPreviewData(
+ ComplicationType.LONG_TEXT.toWireComplicationType()
+ ).longText!!.getTextAt(Resources.getSystem(), 0)
+ ).isEqualTo("hello preview")
+ }
+
+ enum class DataWithExpressionScenario(val data: ComplicationData) {
+ RANGED_VALUE(
+ RangedValueComplicationData.Builder(
+ valueExpression = DynamicFloat.constant(1f),
+ min = 0f,
+ max = 10f,
+ contentDescription = ComplicationText.EMPTY
+ ).setText(ComplicationText.EMPTY).build()
+ ),
+ LONG_TEXT(
+ LongTextComplicationData.Builder(
+ text = ComplicationTextExpression(DynamicString.constant("Long Text")),
+ contentDescription = ComplicationText.EMPTY
+ ).build()
+ ),
+ LONG_TITLE(
+ LongTextComplicationData.Builder(
+ text = ComplicationText.EMPTY,
+ contentDescription = ComplicationText.EMPTY
+ ).setTitle(ComplicationTextExpression(DynamicString.constant("Long Title"))).build()
+ ),
+ SHORT_TEXT(
+ ShortTextComplicationData.Builder(
+ text = ComplicationTextExpression(DynamicString.constant("Short Text")),
+ contentDescription = ComplicationText.EMPTY
+ ).build()
+ ),
+ SHORT_TITLE(
+ ShortTextComplicationData.Builder(
+ text = ComplicationText.EMPTY,
+ contentDescription = ComplicationText.EMPTY
+ ).setTitle(ComplicationTextExpression(DynamicString.constant("Short Title"))).build()
+ ),
+ CONTENT_DESCRIPTION(
+ LongTextComplicationData.Builder(
+ text = ComplicationText.EMPTY,
+ contentDescription = ComplicationTextExpression(
+ DynamicString.constant("Long Text")
+ ),
+ ).build()
+ ),
+ }
+
+ @Test
+ fun testGetComplicationPreviewData_withExpression_fails() {
+ for (scenario in DataWithExpressionScenario.values()) {
+ mService.previewData = scenario.data
+
+ val exception = assertFailsWith<IllegalArgumentException> {
+ mProvider.getComplicationPreviewData(scenario.data.type.toWireComplicationType())
+ }
+
+ expect.withMessage(scenario.name)
+ .that(exception)
+ .hasMessageThat()
+ .isEqualTo("Preview data must not have expressions.")
+ }
+ }
+
+ @Test
+ fun testTimelineTestService() {
+ mService.respondWithTimeline = true
+ val timeline = ArrayList<TimelineEntry>()
+ timeline.add(
+ TimelineEntry(
+ TimeInterval(Instant.ofEpochSecond(1000), Instant.ofEpochSecond(4000)),
+ LongTextComplicationData.Builder(
+ PlainComplicationText.Builder("A").build(),
+ ComplicationText.EMPTY
+ ).build()
+ )
+ )
+ timeline.add(
+ TimelineEntry(
+ TimeInterval(Instant.ofEpochSecond(6000), Instant.ofEpochSecond(8000)),
+ LongTextComplicationData.Builder(
+ PlainComplicationText.Builder("B").build(),
+ ComplicationText.EMPTY
+ ).build()
+ )
+ )
+ mService.responseDataTimeline = ComplicationDataTimeline(
+ LongTextComplicationData.Builder(
+ PlainComplicationText.Builder("default").build(),
+ ComplicationText.EMPTY
+ ).build(),
+ timeline
+ )
+
+ val id = 123
+ mProvider.onUpdate(id, ComplicationType.LONG_TEXT.toWireComplicationType(), mLocalManager)
+ assertThat(mUpdateComplicationDataLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+ val data = argumentCaptor<WireComplicationData>()
+ verify(mRemoteManager)
+ .updateComplicationData(eq(id), data.capture())
+ assertThat(data.firstValue.longText!!.getTextAt(Resources.getSystem(), 0))
+ .isEqualTo("default")
+ val timeLineEntries: List<WireComplicationData?> = data.firstValue.timelineEntries!!
+ assertThat(timeLineEntries.size).isEqualTo(2)
+ assertThat(timeLineEntries[0]!!.timelineStartEpochSecond).isEqualTo(1000)
+ assertThat(timeLineEntries[0]!!.timelineEndEpochSecond).isEqualTo(4000)
+ assertThat(timeLineEntries[0]!!.longText!!.getTextAt(Resources.getSystem(), 0))
+ .isEqualTo("A")
+
+ assertThat(timeLineEntries[1]!!.timelineStartEpochSecond).isEqualTo(6000)
+ assertThat(timeLineEntries[1]!!.timelineEndEpochSecond).isEqualTo(8000)
+ assertThat(timeLineEntries[1]!!.longText!!.getTextAt(Resources.getSystem(), 0))
+ .isEqualTo("B")
+ }
+
+ @Test
+ fun testImmediateRequest() {
+ mService.responseData = LongTextComplicationData.Builder(
+ PlainComplicationText.Builder("hello").build(),
+ ComplicationText.EMPTY
+ ).build()
+ val thread = HandlerThread("testThread")
+
+ try {
+ thread.start()
+ val threadHandler = Handler(thread.looper)
+ val response = AtomicReference<WireComplicationData>()
+ val doneLatch = CountDownLatch(1)
+
+ threadHandler.post {
+ try {
+ response.set(
+ mProvider.onSynchronousComplicationRequest(
+ 123,
+ ComplicationType.LONG_TEXT.toWireComplicationType()
+ )
+ )
+ doneLatch.countDown()
+ } catch (e: RemoteException) {
+ // Should not happen
+ }
+ }
+
+ assertThat(doneLatch.await(1000, TimeUnit.MILLISECONDS)).isTrue()
+ assertThat(response.get().longText!!.getTextAt(Resources.getSystem(), 0))
+ .isEqualTo("hello")
+ } finally {
+ thread.quitSafely()
+ }
+ }
+
+ private fun runUiThreadTasksWhileAwaitingDataLatch(timeout: Long) {
+ // Allowing UI thread to execute while we wait for the data latch.
+ var attempts: Long = 0
+ while (!mUpdateComplicationDataLatch.await(1, TimeUnit.MILLISECONDS)) {
+ runUiThreadTasks()
+ assertThat(attempts++).isLessThan(timeout) // In total waiting ~timeout.
+ }
+ }
+
+ companion object {
+ private const val TAG = "ComplicationDataSourceServiceTest"
+ }
+}
\ No newline at end of file
diff --git a/wear/watchface/watchface-complications-data/build.gradle b/wear/watchface/watchface-complications-data/build.gradle
index fedd5c8..f4d254b 100644
--- a/wear/watchface/watchface-complications-data/build.gradle
+++ b/wear/watchface/watchface-complications-data/build.gradle
@@ -44,7 +44,7 @@
testImplementation(libs.testRules)
testImplementation(libs.robolectric)
testImplementation(libs.mockitoCore4)
- testImplementation(libs.mockitoKotlin)
+ testImplementation(libs.mockitoKotlin4)
testImplementation(libs.kotlinCoroutinesTest)
testImplementation(libs.truth)
testImplementation(libs.junit)
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
index 01aae9d..cafdea4e 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluator.kt
@@ -25,6 +25,7 @@
import androidx.wear.protolayout.expression.pipeline.DynamicTypeEvaluator
import androidx.wear.protolayout.expression.pipeline.DynamicTypeValueReceiver
import androidx.wear.protolayout.expression.pipeline.ObservableStateStore
+import androidx.wear.protolayout.expression.pipeline.sensor.SensorGateway
import java.util.concurrent.Executor
import java.util.function.Consumer
import kotlinx.coroutines.CoroutineScope
@@ -49,6 +50,8 @@
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
class ComplicationDataExpressionEvaluator(
val unevaluatedData: WireComplicationData,
+ private val sensorGateway: SensorGateway? = null,
+ private val stateStore: ObservableStateStore = ObservableStateStore(emptyMap()),
) : AutoCloseable {
/**
* Java compatibility class for [ComplicationDataExpressionEvaluator].
@@ -163,8 +166,8 @@
if (state.value.pending.isEmpty()) return
evaluator = DynamicTypeEvaluator(
/* platformDataSourcesInitiallyEnabled = */ true,
- /* sensorGateway = */ null,
- ObservableStateStore(emptyMap()),
+ sensorGateway,
+ stateStore,
)
for (receiver in state.value.pending) receiver.bind()
evaluator.enablePlatformDataSources()
@@ -231,5 +234,14 @@
companion object {
val INVALID_DATA: WireComplicationData = NoDataComplicationData().asWireComplicationData()
+
+ fun hasExpression(data: WireComplicationData): Boolean = data.run {
+ (hasRangedValueExpression() && rangedValueExpression != null) ||
+ (hasLongText() && longText?.stringExpression != null) ||
+ (hasLongTitle() && longTitle?.stringExpression != null) ||
+ (hasShortText() && shortText?.stringExpression != null) ||
+ (hasShortTitle() && shortTitle?.stringExpression != null) ||
+ (hasContentDescription() && contentDescription?.stringExpression != null)
+ }
}
}
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
index 69481cd..47abee2 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/ComplicationDataExpressionEvaluatorTest.kt
@@ -23,18 +23,27 @@
import androidx.test.core.app.ApplicationProvider.getApplicationContext
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicFloat
import androidx.wear.protolayout.expression.DynamicBuilders.DynamicString
+import androidx.wear.protolayout.expression.StateEntryBuilders.StateEntryValue
+import androidx.wear.protolayout.expression.pipeline.ObservableStateStore
+import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator.Companion.INVALID_DATA
+import androidx.wear.watchface.complications.data.ComplicationDataExpressionEvaluator.Companion.hasExpression
import com.google.common.truth.Expect
import com.google.common.truth.Truth.assertThat
-import com.nhaarman.mockitokotlin2.any
-import com.nhaarman.mockitokotlin2.mock
-import com.nhaarman.mockitokotlin2.never
-import com.nhaarman.mockitokotlin2.times
-import com.nhaarman.mockitokotlin2.verify
import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.shareIn
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
import org.robolectric.shadows.ShadowLog
import org.robolectric.shadows.ShadowLooper.runUiThreadTasks
@@ -68,79 +77,218 @@
}
/**
- * Scenarios for testing per-field static expressions.
+ * Scenarios for testing expressions.
*
- * Each scenario describes how to set the expression in the [WireComplicationData] and how to
- * set the evaluated value.
- *
- * Note that evaluated data retains the expression.
+ * Each scenario describes the expressed data, the flow of states, and the flow of the evaluated
+ * data output.
*/
- enum class StaticExpressionScenario(
- val expressed: WireComplicationData.Builder.() -> WireComplicationData.Builder,
- val evaluated: WireComplicationData.Builder.() -> WireComplicationData.Builder,
+ enum class DataExpressionScenario(
+ val expressed: WireComplicationData,
+ val states: List<Map<String, StateEntryValue>>,
+ val evaluated: List<WireComplicationData>,
) {
- RANGED_VALUE(
- expressed = { setRangedValueExpression(DynamicFloat.constant(10f)) },
- evaluated = {
- setRangedValue(10f).setRangedValueExpression(DynamicFloat.constant(10f))
- },
+ SET_IMMEDIATELY_WHEN_ALL_DATA_AVAILABLE(
+ expressed = WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setRangedValueExpression(DynamicFloat.constant(1f))
+ .setLongText(WireComplicationText(DynamicString.constant("Long Text")))
+ .setLongTitle(WireComplicationText(DynamicString.constant("Long Title")))
+ .setShortText(WireComplicationText(DynamicString.constant("Short Text")))
+ .setShortTitle(WireComplicationText(DynamicString.constant("Short Title")))
+ .setContentDescription(WireComplicationText(DynamicString.constant("Description")))
+ .build(),
+ states = listOf(),
+ evaluated = listOf(
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setRangedValue(1f)
+ .setRangedValueExpression(DynamicFloat.constant(1f))
+ .setLongText(
+ WireComplicationText("Long Text", DynamicString.constant("Long Text"))
+ )
+ .setLongTitle(
+ WireComplicationText("Long Title", DynamicString.constant("Long Title"))
+ )
+ .setShortText(
+ WireComplicationText("Short Text", DynamicString.constant("Short Text"))
+ )
+ .setShortTitle(
+ WireComplicationText("Short Title", DynamicString.constant("Short Title"))
+ )
+ .setContentDescription(
+ WireComplicationText("Description", DynamicString.constant("Description"))
+ )
+ .build()
+ ),
),
- LONG_TEXT(
- expressed = { setLongText(WireComplicationText(DynamicString.constant("hello"))) },
- evaluated = {
- setLongText(WireComplicationText("hello", DynamicString.constant("hello")))
- },
+ SET_ONLY_AFTER_ALL_FIELDS_EVALUATED(
+ expressed = WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setRangedValueExpression(DynamicFloat.fromState("ranged_value"))
+ .setLongText(WireComplicationText(DynamicString.fromState("long_text")))
+ .setLongTitle(WireComplicationText(DynamicString.fromState("long_title")))
+ .setShortText(WireComplicationText(DynamicString.fromState("short_text")))
+ .setShortTitle(WireComplicationText(DynamicString.fromState("short_title")))
+ .setContentDescription(WireComplicationText(DynamicString.fromState("description")))
+ .build(),
+ states = aggregate(
+ // Each map piles on top of the previous ones.
+ mapOf("ranged_value" to StateEntryValue.fromFloat(1f)),
+ mapOf("long_text" to StateEntryValue.fromString("Long Text")),
+ mapOf("long_title" to StateEntryValue.fromString("Long Title")),
+ mapOf("short_text" to StateEntryValue.fromString("Short Text")),
+ mapOf("short_title" to StateEntryValue.fromString("Short Title")),
+ // Only the last one will trigger an evaluated data.
+ mapOf("description" to StateEntryValue.fromString("Description")),
+ ),
+ evaluated = listOf(
+ INVALID_DATA, // Before state is available.
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setRangedValue(1f)
+ .setRangedValueExpression(DynamicFloat.fromState("ranged_value"))
+ .setLongText(
+ WireComplicationText("Long Text", DynamicString.fromState("long_text"))
+ )
+ .setLongTitle(
+ WireComplicationText("Long Title", DynamicString.fromState("long_title"))
+ )
+ .setShortText(
+ WireComplicationText("Short Text", DynamicString.fromState("short_text"))
+ )
+ .setShortTitle(
+ WireComplicationText("Short Title", DynamicString.fromState("short_title"))
+ )
+ .setContentDescription(
+ WireComplicationText("Description", DynamicString.fromState("description"))
+ )
+ .build()
+ ),
),
- LONG_TITLE(
- expressed = { setLongTitle(WireComplicationText(DynamicString.constant("hello"))) },
- evaluated = {
- setLongTitle(WireComplicationText("hello", DynamicString.constant("hello")))
- },
+ SET_TO_EVALUATED_IF_ALL_FIELDS_VALID(
+ expressed = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
+ .setShortText(WireComplicationText(DynamicString.fromState("valid")))
+ .build(),
+ states = listOf(
+ mapOf("valid" to StateEntryValue.fromString("Valid")),
+ ),
+ evaluated = listOf(
+ INVALID_DATA, // Before state is available.
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortTitle(WireComplicationText("Valid", DynamicString.fromState("valid")))
+ .setShortText(WireComplicationText("Valid", DynamicString.fromState("valid")))
+ .build(),
+ ),
),
- SHORT_TEXT(
- expressed = { setShortText(WireComplicationText(DynamicString.constant("hello"))) },
- evaluated = {
- setShortText(WireComplicationText("hello", DynamicString.constant("hello")))
- },
+ SET_TO_NO_DATA_IF_FIRST_STATE_IS_INVALID(
+ expressed = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
+ .setShortText(WireComplicationText(DynamicString.fromState("invalid")))
+ .build(),
+ states = listOf(
+ mapOf(),
+ mapOf("valid" to StateEntryValue.fromString("Valid")),
+ ),
+ evaluated = listOf(
+ INVALID_DATA, // States invalid after one field changed to valid.
+ ),
),
- SHORT_TITLE(
- expressed = { setShortTitle(WireComplicationText(DynamicString.constant("hello"))) },
- evaluated = {
- setShortTitle(WireComplicationText("hello", DynamicString.constant("hello")))
- },
- ),
- CONTENT_DESCRIPTION(
- expressed = {
- setContentDescription(WireComplicationText(DynamicString.constant("hello")))
- },
- evaluated = {
- setContentDescription(
- WireComplicationText("hello", DynamicString.constant("hello"))
- )
- },
+ SET_TO_NO_DATA_IF_LAST_STATE_IS_INVALID(
+ expressed = WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortTitle(WireComplicationText(DynamicString.fromState("valid")))
+ .setShortText(WireComplicationText(DynamicString.fromState("invalid")))
+ .build(),
+ states = listOf(
+ mapOf(
+ "valid" to StateEntryValue.fromString("Valid"),
+ "invalid" to StateEntryValue.fromString("Valid"),
+ ),
+ mapOf("valid" to StateEntryValue.fromString("Valid")),
+ ),
+ evaluated = listOf(
+ INVALID_DATA, // Before state is available.
+ WireComplicationData.Builder(WireComplicationData.TYPE_SHORT_TEXT)
+ .setShortTitle(WireComplicationText("Valid", DynamicString.fromState("valid")))
+ .setShortText(WireComplicationText("Valid", DynamicString.fromState("invalid")))
+ .build(),
+ INVALID_DATA, // After it was invalidated.
+ ),
),
}
@Test
- fun data_staticExpression_setToEvaluated() {
- for (scenario in StaticExpressionScenario.values()) {
- val base = WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA).build()
- val expressed = scenario.expressed(WireComplicationData.Builder(base)).build()
- val evaluated =
- scenario.evaluated(WireComplicationData.Builder(base)).build()
-
- ComplicationDataExpressionEvaluator(expressed).use { evaluator ->
+ fun data_expression_setToEvaluated() {
+ for (scenario in DataExpressionScenario.values()) {
+ // Defensive copy due to in-place evaluation.
+ val expressed = WireComplicationData.Builder(scenario.expressed).build()
+ val stateStore = ObservableStateStore(mapOf())
+ ComplicationDataExpressionEvaluator(
+ expressed,
+ stateStore = stateStore,
+ ).use { evaluator ->
+ val allEvaluations = evaluator.data.filterNotNull().shareIn(
+ CoroutineScope(Dispatchers.Main),
+ SharingStarted.Eagerly,
+ replay = 10,
+ )
evaluator.init()
- runUiThreadTasks()
+ runUiThreadTasks() // Ensures data sharing started.
+
+ for (state in scenario.states) {
+ stateStore.setStateEntryValues(state)
+ runUiThreadTasks() // Ensures data sharing ended.
+ }
expect
.withMessage(scenario.name)
- .that(evaluator.data.value)
- .isEqualTo(evaluated)
+ .that(allEvaluations.replayCache)
+ .isEqualTo(scenario.evaluated)
}
}
}
+ enum class HasExpressionDataWithExpressionScenario(val data: WireComplicationData) {
+ RANGED_VALUE(
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setRangedValueExpression(DynamicFloat.constant(1f))
+ .build()
+ ),
+ LONG_TEXT(
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setLongText(WireComplicationText(DynamicString.constant("Long Text")))
+ .build()
+ ),
+ LONG_TITLE(
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setLongTitle(WireComplicationText(DynamicString.constant("Long Title")))
+ .build()
+ ),
+ SHORT_TEXT(
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setShortText(WireComplicationText(DynamicString.constant("Short Text")))
+ .build()
+ ),
+ SHORT_TITLE(
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setShortTitle(WireComplicationText(DynamicString.constant("Short Title")))
+ .build()
+ ),
+ CONTENT_DESCRIPTION(
+ WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
+ .setContentDescription(WireComplicationText(DynamicString.constant("Description")))
+ .build()
+ ),
+ }
+
+ @Test
+ fun hasExpression_dataWithExpression_returnsTrue() {
+ for (scenario in HasExpressionDataWithExpressionScenario.values()) {
+ expect.withMessage(scenario.name).that(hasExpression(scenario.data)).isTrue()
+ }
+ }
+
+ @Test
+ fun hasExpression_dataWithoutExpression_returnsFalse() {
+ assertThat(hasExpression(DATA_WITH_NO_EXPRESSION)).isFalse()
+ }
+
@Test
fun compat_notInitialized_listenerNotInvoked() {
ComplicationDataExpressionEvaluator.Compat(
@@ -173,5 +321,9 @@
WireComplicationData.Builder(WireComplicationData.TYPE_NO_DATA)
.setRangedValue(10f)
.build()
+
+ /** Converts `[{a: A}, {b: B}, {c: C}]` to `[{a: A}, {a: A, b: B}, {a: A, b: B, c: C}]`. */
+ fun <K, V> aggregate(vararg maps: Map<K, V>): List<Map<K, V>> =
+ maps.fold(listOf()) { acc, map -> acc + ((acc.lastOrNull() ?: mapOf()) + map) }
}
}
diff --git a/wear/watchface/watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFace.aidl b/wear/watchface/watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFace.aidl
index f339375..e2b8183 100644
--- a/wear/watchface/watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFace.aidl
+++ b/wear/watchface/watchface-data/src/main/aidl/androidx/wear/watchface/control/IInteractiveWatchFace.aidl
@@ -40,7 +40,7 @@
/**
* API version number. This should be incremented every time a new method is added.
*/
- const int API_VERSION = 7;
+ const int API_VERSION = 8;
/** Indicates a "down" touch event on the watch face. */
const int TAP_TYPE_DOWN = 0;
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt
index baf683c..0f3a5fd 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsObserver.kt
@@ -132,8 +132,6 @@
}
override fun onActionScreenOn() {
- (watchState.isLocked as MutableStateFlow).value = false
-
// Before SysUI has connected, we use ActionScreenOn/ActionScreenOff as a trigger to query
// AMBIENT_ENABLED_PATH in order to determine if the device os ambient or not.
if (sysUiHasSentWatchUiState) {
@@ -143,4 +141,8 @@
val isAmbient = watchState.isAmbient as MutableStateFlow
isAmbient.value = false
}
+
+ override fun onActionUserPresent() {
+ (watchState.isLocked as MutableStateFlow).value = false
+ }
}
\ No newline at end of file
diff --git a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
index e8c9d5a..a22d479 100644
--- a/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
+++ b/wear/watchface/watchface/src/main/java/androidx/wear/watchface/BroadcastsReceiver.kt
@@ -78,6 +78,11 @@
@UiThread
public fun onActionScreenOn() {
}
+
+ /** Called when we receive [Intent.ACTION_USER_PRESENT] */
+ @UiThread
+ public fun onActionUserPresent() {
+ }
}
companion object {
@@ -101,6 +106,7 @@
Intent.ACTION_TIMEZONE_CHANGED -> observer.onActionTimeZoneChanged()
Intent.ACTION_SCREEN_OFF -> observer.onActionScreenOff()
Intent.ACTION_SCREEN_ON -> observer.onActionScreenOn()
+ Intent.ACTION_USER_PRESENT -> observer.onActionUserPresent()
WatchFaceImpl.MOCK_TIME_INTENT -> observer.onMockTime(intent)
else -> System.err.println("<< IGNORING $intent")
}
@@ -119,6 +125,7 @@
addAction(Intent.ACTION_BATTERY_OKAY)
addAction(Intent.ACTION_POWER_CONNECTED)
addAction(Intent.ACTION_POWER_DISCONNECTED)
+ addAction(Intent.ACTION_USER_PRESENT)
addAction(WatchFaceImpl.MOCK_TIME_INTENT)
}
)
diff --git a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
index a90f3c4..2681c8c 100644
--- a/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
+++ b/wear/watchface/watchface/src/test/java/androidx/wear/watchface/WatchFaceServiceTest.kt
@@ -19,6 +19,7 @@
import android.support.wearable.complications.ComplicationData as WireComplicationData
import android.support.wearable.complications.ComplicationText as WireComplicationText
import android.annotation.SuppressLint
+import android.app.KeyguardManager
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.ComponentName
@@ -6624,6 +6625,97 @@
.isInstanceOf(ShortTextComplicationData::class.java)
}
+ @Test
+ fun actionScreenOff_keyguardLocked() {
+ initWallpaperInteractiveWatchFaceInstance(
+ WatchFaceType.ANALOG,
+ listOf(leftComplication),
+ UserStyleSchema(emptyList()),
+ WallpaperInteractiveWatchFaceInstanceParams(
+ INTERACTIVE_INSTANCE_ID,
+ DeviceConfig(
+ false,
+ false,
+ 0,
+ 0
+ ),
+ WatchUiState(false, 0),
+ UserStyle(emptyMap()).toWireFormat(),
+ null,
+ null,
+ null
+ )
+ )
+
+ val shadowKeyguardManager = shadowOf(context.getSystemService(KeyguardManager::class.java))
+ shadowKeyguardManager.setIsDeviceLocked(true)
+
+ watchFaceImpl.broadcastsObserver.onActionScreenOff()
+
+ assertThat(watchState.isLocked.value).isTrue()
+ }
+
+ @Test
+ fun actionScreenOff_keyguardUnlocked() {
+ initWallpaperInteractiveWatchFaceInstance(
+ WatchFaceType.ANALOG,
+ listOf(leftComplication),
+ UserStyleSchema(emptyList()),
+ WallpaperInteractiveWatchFaceInstanceParams(
+ INTERACTIVE_INSTANCE_ID,
+ DeviceConfig(
+ false,
+ false,
+ 0,
+ 0
+ ),
+ WatchUiState(false, 0),
+ UserStyle(emptyMap()).toWireFormat(),
+ null,
+ null,
+ null
+ )
+ )
+
+ val shadowKeyguardManager = shadowOf(context.getSystemService(KeyguardManager::class.java))
+ shadowKeyguardManager.setIsDeviceLocked(false)
+
+ watchFaceImpl.broadcastsObserver.onActionScreenOff()
+
+ assertThat(watchState.isLocked.value).isFalse()
+ }
+
+ @Test
+ fun actionUserUnlocked_after_keyguardLocked() {
+ initWallpaperInteractiveWatchFaceInstance(
+ WatchFaceType.ANALOG,
+ listOf(leftComplication),
+ UserStyleSchema(emptyList()),
+ WallpaperInteractiveWatchFaceInstanceParams(
+ INTERACTIVE_INSTANCE_ID,
+ DeviceConfig(
+ false,
+ false,
+ 0,
+ 0
+ ),
+ WatchUiState(false, 0),
+ UserStyle(emptyMap()).toWireFormat(),
+ null,
+ null,
+ null
+ )
+ )
+
+ val shadowKeyguardManager = shadowOf(context.getSystemService(KeyguardManager::class.java))
+ shadowKeyguardManager.setIsDeviceLocked(true)
+
+ watchFaceImpl.broadcastsObserver.onActionScreenOff()
+ watchFaceImpl.broadcastsObserver.onActionUserPresent()
+
+ assertThat(watchState.isLocked.value).isFalse()
+ }
+
@SuppressLint("NewApi")
@Test
public fun createHeadlessSessionDelegate_onDestroy() {