Merge "Fix Rotary Encoder Stretch" into androidx-main
diff --git a/activity/activity/lint-baseline.xml b/activity/activity/lint-baseline.xml
deleted file mode 100644
index 7ac7250c..0000000
--- a/activity/activity/lint-baseline.xml
+++ /dev/null
@@ -1,40 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 7.4.0-alpha08" type="baseline" client="gradle" dependencies="false" name="AGP (7.4.0-alpha08)" variant="all" version="7.4.0-alpha08">
-
- <issue
- id="NewApi"
- message="Call requires API level 33 (current min is 19): `android.provider.MediaStore#getPickImagesMaxLimit`"
- errorLine1=" require(maxItems <= MediaStore.getPickImagesMaxLimit()) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 33 (current min is 19): `android.provider.MediaStore#getPickImagesMaxLimit`"
- errorLine1=" MediaStore.getPickImagesMaxLimit()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" require(maxItems <= MediaStore.getPickImagesMaxLimit()) {"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt"/>
- </issue>
-
- <issue
- id="ClassVerificationFailure"
- message="This call references a method added in API level 33; however, the containing class androidx.activity.result.contract.ActivityResultContracts.PickMultipleVisualMedia.Companion is reachable from earlier API levels and will fail run-time class verification."
- errorLine1=" MediaStore.getPickImagesMaxLimit()"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt"/>
- </issue>
-
-</issues>
diff --git a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
index 47ef4d1..fe92989 100644
--- a/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
+++ b/activity/activity/src/main/java/androidx/activity/result/contract/ActivityResultContracts.kt
@@ -734,6 +734,7 @@
}
@CallSuper
+ @SuppressLint("NewApi", "ClassVerificationFailure")
override fun createIntent(context: Context, input: PickVisualMediaRequest): Intent {
// Check to see if the photo picker is available
return if (PickVisualMedia.isPhotoPickerAvailable()) {
@@ -783,6 +784,7 @@
*
* @see MediaStore.EXTRA_PICK_IMAGES_MAX
*/
+ @SuppressLint("NewApi", "ClassVerificationFailure")
internal fun getMaxItems() = if (PickVisualMedia.isPhotoPickerAvailable()) {
MediaStore.getPickImagesMaxLimit()
} else {
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
index b704ffa..9524df3 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXComposeImplPlugin.kt
@@ -347,7 +347,7 @@
":compose:compiler:compiler"
)
} else {
- project.rootProject.findProject(":compose:compiler:compiler")!!
+ project.rootProject.resolveProject(":compose:compiler:compiler")
}
)
val kotlinPlugin = configuration.incoming.artifactView { view ->
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
index b8e6838..727552a 100644
--- a/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
+++ b/buildSrc/private/src/main/kotlin/androidx/build/AndroidXImplPlugin.kt
@@ -160,7 +160,7 @@
KotlinClosure1<String, Project>(
function = {
// this refers to the first parameter of the closure.
- project(this)
+ project.resolveProject(this)
}
)
)
diff --git a/buildSrc/private/src/main/kotlin/androidx/build/ProjectResolver.kt b/buildSrc/private/src/main/kotlin/androidx/build/ProjectResolver.kt
new file mode 100644
index 0000000..7858465
--- /dev/null
+++ b/buildSrc/private/src/main/kotlin/androidx/build/ProjectResolver.kt
@@ -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.build
+
+import org.gradle.api.Project
+import org.gradle.api.UnknownProjectException
+
+// Resolves the given project, and if it is not found,
+// throws an exception that mentions the active project subset, if any (MAIN, COMPOSE, ...)
+public fun Project.resolveProject(projectSpecification: String): Project {
+ try {
+ return project.project(projectSpecification)
+ } catch (e: UnknownProjectException) {
+ val subset = project.getProjectSubset()
+ val subsetDescription = if (subset == null) {
+ ""
+ } else {
+ " in subset $subset"
+ }
+ throw UnknownProjectException(
+ "Project $projectSpecification not found$subsetDescription",
+ e
+ )
+ }
+}
+
+private fun Project.getProjectSubset(): String? {
+ val prop = project.providers.gradleProperty("androidx.projects")
+ if (prop.isPresent()) {
+ return prop.get().uppercase()
+ }
+
+ val envProp = project.providers.environmentVariable("ANDROIDX_PROJECTS")
+ if (envProp.isPresent()) {
+ return envProp.get().uppercase()
+ }
+ return null
+}
diff --git a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
index b4af3e0..b6d77efc 100644
--- a/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
+++ b/camera/camera-camera2-pipe-integration/src/main/java/androidx/camera/camera2/pipe/integration/impl/UseCaseManager.kt
@@ -213,7 +213,9 @@
it?.close()?.let { closingJob ->
closingCameraJobs.add(closingJob)
closingJob.invokeOnCompletion {
- closingCameraJobs.remove(closingJob)
+ synchronized(lock) {
+ closingCameraJobs.remove(closingJob)
+ }
}
}
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
index cb4f1cd..6d5f28a 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/Camera2CameraInfoImpl.java
@@ -37,6 +37,8 @@
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
import androidx.camera.camera2.internal.compat.CameraManagerCompat;
import androidx.camera.camera2.internal.compat.quirk.CameraQuirks;
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.camera2.internal.compat.quirk.ZslDisablerQuirk;
import androidx.camera.camera2.internal.compat.workaround.FlashAvailabilityChecker;
import androidx.camera.camera2.interop.Camera2CameraInfo;
import androidx.camera.camera2.interop.ExperimentalCamera2Interop;
@@ -358,7 +360,8 @@
@Override
public boolean isZslSupported() {
- return Build.VERSION.SDK_INT >= 23 && isPrivateReprocessingSupported();
+ return Build.VERSION.SDK_INT >= 23 && isPrivateReprocessingSupported()
+ && (DeviceQuirks.get(ZslDisablerQuirk.class) == null);
}
@Override
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java
index 12ecb98..bcf5423 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/ZslControlImpl.java
@@ -38,6 +38,8 @@
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.camera.camera2.internal.compat.CameraCharacteristicsCompat;
+import androidx.camera.camera2.internal.compat.quirk.DeviceQuirks;
+import androidx.camera.camera2.internal.compat.quirk.ZslDisablerQuirk;
import androidx.camera.core.ExperimentalGetImage;
import androidx.camera.core.ImageProxy;
import androidx.camera.core.Logger;
@@ -86,6 +88,8 @@
private boolean mIsZslDisabledByFlashMode = false;
private boolean mIsPrivateReprocessingSupported = false;
+ private boolean mShouldZslDisabledByQuirks = false;
+
@SuppressWarnings("WeakerAccess")
SafeCloseImageReaderProxy mReprocessingImageReader;
private CameraCaptureCallback mMetadataMatchingCaptureCallback;
@@ -102,6 +106,8 @@
mReprocessingInputSizeMap = createReprocessingInputSizeMap(mCameraCharacteristicsCompat);
+ mShouldZslDisabledByQuirks = DeviceQuirks.get(ZslDisablerQuirk.class) != null;
+
mImageRingBuffer = new ZslRingBuffer(
RING_BUFFER_CAPACITY,
imageProxy -> imageProxy.close());
@@ -139,6 +145,10 @@
return;
}
+ if (mShouldZslDisabledByQuirks) {
+ return;
+ }
+
// Due to b/232268355 and feedback from pixel team that private format will have better
// performance, we will use private only for zsl.
if (!mIsPrivateReprocessingSupported
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
index fe2ab38..706f1a6 100644
--- a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/DeviceQuirksLoader.java
@@ -80,6 +80,9 @@
if (TorchIsClosedAfterImageCapturingQuirk.load()) {
quirks.add(new TorchIsClosedAfterImageCapturingQuirk());
}
+ if (ZslDisablerQuirk.load()) {
+ quirks.add(new ZslDisablerQuirk());
+ }
return quirks;
}
diff --git a/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ZslDisablerQuirk.java b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ZslDisablerQuirk.java
new file mode 100644
index 0000000..6a4ec43
--- /dev/null
+++ b/camera/camera-camera2/src/main/java/androidx/camera/camera2/internal/compat/quirk/ZslDisablerQuirk.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.camera2.internal.compat.quirk;
+
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.CameraInfo;
+import androidx.camera.core.impl.Quirk;
+
+import java.util.Locale;
+
+/**
+ * <p>QuirkSummary
+ * Bug Id: 252818931
+ * Description: On certain devices, the captured image has color issue for reprocessing. We
+ * need to disable zero-shutter lag and return false for
+ * {@link CameraInfo#isZslSupported()}.
+ * Device(s): Samsung Fold4
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+public class ZslDisablerQuirk implements Quirk {
+
+ static boolean load() {
+ return isSamsungFold4();
+ }
+
+ private static boolean isSamsungFold4() {
+ return "samsung".equalsIgnoreCase(Build.BRAND)
+ && android.os.Build.MODEL.toUpperCase(Locale.US).startsWith("SM-F936");
+ }
+}
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
index 5cea42a..0062a4b 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/Camera2CameraInfoImplTest.java
@@ -61,6 +61,7 @@
import org.robolectric.shadow.api.Shadow;
import org.robolectric.shadows.ShadowCameraCharacteristics;
import org.robolectric.shadows.ShadowCameraManager;
+import org.robolectric.util.ReflectionHelpers;
import java.util.Arrays;
import java.util.HashMap;
@@ -512,6 +513,36 @@
assertThat(cameraInfo.isZslSupported()).isFalse();
}
+ @Config(minSdk = 23)
+ @Test
+ public void isZslSupported_hasZslDisablerQuirk_returnFalse()
+ throws CameraAccessExceptionCompat {
+ ReflectionHelpers.setStaticField(Build.class, "BRAND", "samsung");
+ ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-F936B");
+
+ init(/* hasReprocessingCapabilities = */ true);
+
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+ CAMERA0_ID, mCameraManagerCompat);
+
+ assertThat(cameraInfo.isZslSupported()).isFalse();
+ }
+
+ @Config(minSdk = 23)
+ @Test
+ public void isZslSupported_hasNoZslDisablerQuirk_returnTrue()
+ throws CameraAccessExceptionCompat {
+ ReflectionHelpers.setStaticField(Build.class, "BRAND", "samsung");
+ ReflectionHelpers.setStaticField(Build.class, "MODEL", "SM-G973");
+
+ init(/* hasReprocessingCapabilities = */ true);
+
+ final Camera2CameraInfoImpl cameraInfo = new Camera2CameraInfoImpl(
+ CAMERA0_ID, mCameraManagerCompat);
+
+ assertThat(cameraInfo.isZslSupported()).isTrue();
+ }
+
private CameraManagerCompat initCameraManagerWithPhysicalIds(
List<Pair<String, CameraCharacteristics>> cameraIdsAndCharacteristicsList) {
FakeCameraManagerImpl cameraManagerImpl = new FakeCameraManagerImpl();
diff --git a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt
index 33ddaf5..5804642 100644
--- a/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt
+++ b/camera/camera-camera2/src/test/java/androidx/camera/camera2/internal/ZslControlImplTest.kt
@@ -39,6 +39,7 @@
import org.robolectric.annotation.internal.DoNotInstrument
import org.robolectric.shadow.api.Shadow
import org.robolectric.shadows.ShadowCameraCharacteristics
+import org.robolectric.util.ReflectionHelpers
val YUV_REPROCESSING_MAXIMUM_SIZE = Size(4000, 3000)
val PRIVATE_REPROCESSING_MAXIMUM_SIZE = Size(3000, 2000)
@@ -49,20 +50,20 @@
@RunWith(RobolectricTestRunner::class)
@DoNotInstrument
@Config(minSdk = Build.VERSION_CODES.M)
-public class ZslControlImplTest {
+class ZslControlImplTest {
private lateinit var zslControl: ZslControlImpl
private lateinit var sessionConfigBuilder: SessionConfig.Builder
@Before
- public fun setUp() {
+ fun setUp() {
sessionConfigBuilder = SessionConfig.Builder().also { sessionConfigBuilder ->
sessionConfigBuilder.setTemplateType(CameraDevice.TEMPLATE_ZERO_SHUTTER_LAG)
}
}
@Test
- public fun isPrivateReprocessingSupported_addZslConfig() {
+ fun isPrivateReprocessingSupported_addZslConfig() {
zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
hasCapabilities = true,
isYuvReprocessingSupported = false,
@@ -85,7 +86,7 @@
}
@Test
- public fun isYuvReprocessingSupported_notAddZslConfig() {
+ fun isYuvReprocessingSupported_notAddZslConfig() {
zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
hasCapabilities = true,
isYuvReprocessingSupported = true,
@@ -99,7 +100,7 @@
}
@Test
- public fun isJpegNotValidOutputFormat_notAddZslConfig() {
+ fun isJpegNotValidOutputFormat_notAddZslConfig() {
zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
hasCapabilities = true,
isYuvReprocessingSupported = true,
@@ -113,7 +114,7 @@
}
@Test
- public fun isReprocessingNotSupported_notAddZslConfig() {
+ fun isReprocessingNotSupported_notAddZslConfig() {
zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
hasCapabilities = true,
isYuvReprocessingSupported = false,
@@ -127,7 +128,7 @@
}
@Test
- public fun isZslDisabledByUserCaseConfig_notAddZslConfig() {
+ fun isZslDisabledByUserCaseConfig_notAddZslConfig() {
zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
hasCapabilities = true,
isYuvReprocessingSupported = false,
@@ -142,7 +143,7 @@
}
@Test
- public fun isZslDisabledByFlashMode_addZslConfig() {
+ fun isZslDisabledByFlashMode_addZslConfig() {
zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
hasCapabilities = true,
isYuvReprocessingSupported = false,
@@ -166,7 +167,7 @@
}
@Test
- public fun isZslDisabled_clearZslConfig() {
+ fun isZslDisabled_clearZslConfig() {
zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
hasCapabilities = true,
isYuvReprocessingSupported = false,
@@ -182,6 +183,49 @@
assertThat(zslControl.mReprocessingImageReader).isNull()
}
+ @Test
+ fun hasZslDisablerQuirk_notAddZslConfig() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-F936B")
+
+ zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
+ hasCapabilities = true,
+ isYuvReprocessingSupported = false,
+ isPrivateReprocessingSupported = true,
+ isJpegValidOutputFormat = true
+ ))
+
+ zslControl.addZslConfig(sessionConfigBuilder)
+
+ assertThat(zslControl.mReprocessingImageReader).isNull()
+ }
+
+ @Test
+ fun hasNoZslDisablerQuirk_addZslConfig() {
+ ReflectionHelpers.setStaticField(Build::class.java, "BRAND", "samsung")
+ ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-G973")
+
+ zslControl = ZslControlImpl(createCameraCharacteristicsCompat(
+ hasCapabilities = true,
+ isYuvReprocessingSupported = false,
+ isPrivateReprocessingSupported = true,
+ isJpegValidOutputFormat = true
+ ))
+
+ zslControl.addZslConfig(sessionConfigBuilder)
+
+ assertThat(zslControl.mReprocessingImageReader).isNotNull()
+ assertThat(zslControl.mReprocessingImageReader.imageFormat).isEqualTo(PRIVATE)
+ assertThat(zslControl.mReprocessingImageReader.maxImages).isEqualTo(
+ MAX_IMAGES)
+ assertThat(zslControl.mReprocessingImageReader.width).isEqualTo(
+ PRIVATE_REPROCESSING_MAXIMUM_SIZE.width)
+ assertThat(zslControl.mReprocessingImageReader.height).isEqualTo(
+ PRIVATE_REPROCESSING_MAXIMUM_SIZE.height)
+ assertThat(zslControl.mImageRingBuffer.maxCapacity).isEqualTo(
+ RING_BUFFER_CAPACITY)
+ }
+
private fun createCameraCharacteristicsCompat(
hasCapabilities: Boolean,
isYuvReprocessingSupported: Boolean,
diff --git a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
index 455c714..9063d8d 100644
--- a/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
+++ b/camera/camera-core/src/androidTest/java/androidx/camera/core/imagecapture/ProcessingNodeDeviceTest.kt
@@ -13,23 +13,30 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
package androidx.camera.core.imagecapture
+import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapFactory.decodeByteArray
import android.graphics.ImageFormat
+import android.graphics.Rect
+import androidx.camera.core.ImageCapture.OutputFileOptions
+import androidx.camera.core.ImageProxy
import androidx.camera.core.imagecapture.Utils.CAMERA_CAPTURE_RESULT
import androidx.camera.core.imagecapture.Utils.CROP_RECT
import androidx.camera.core.imagecapture.Utils.HEIGHT
import androidx.camera.core.imagecapture.Utils.OUTPUT_FILE_OPTIONS
-import androidx.camera.core.imagecapture.Utils.ROTATION_DEGREES
import androidx.camera.core.imagecapture.Utils.SENSOR_TO_BUFFER
import androidx.camera.core.imagecapture.Utils.WIDTH
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.core.internal.CameraCaptureResultImageInfo
-import androidx.camera.core.internal.utils.ImageUtil
+import androidx.camera.core.internal.utils.ImageUtil.jpegImageToJpegByteArray
+import androidx.camera.core.processing.InternalImageProcessor
+import androidx.camera.testing.TestImageUtil.createJpegBytes
+import androidx.camera.testing.TestImageUtil.createJpegFakeImageProxy
import androidx.camera.testing.TestImageUtil.createYuvFakeImageProxy
+import androidx.camera.testing.TestImageUtil.getAverageDiff
+import androidx.camera.testing.fakes.GrayscaleImageEffect
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
@@ -39,77 +46,95 @@
import org.junit.runner.RunWith
/**
- * Instrumented tests for [JpegBytes2Image].
+ * Instrumented tests for [ProcessingNode].
*/
@SmallTest
@RunWith(AndroidJUnit4::class)
@SdkSuppress(minSdkVersion = 21)
class ProcessingNodeDeviceTest {
+ @Test
+ fun applyBitmapEffectInMemory_effectApplied() = runBlocking {
+ processJpegAndVerifyEffectApplied(null)
+ }
+
+ @Test
+ fun applyBitmapEffectOnDisk_effectApplied() = runBlocking {
+ processJpegAndVerifyEffectApplied(OUTPUT_FILE_OPTIONS)
+ }
@Test
fun processYuvInputInMemory_getsJpegOutput() = runBlocking {
- // Arrange: create a YUV input.
- val yuvIn = ProcessingNode.In.of(ImageFormat.YUV_420_888)
- ProcessingNode(mainThreadExecutor()).also { it.transform(yuvIn) }
- val takePictureCallback = FakeTakePictureCallback()
- val imageIn = createYuvFakeImageProxy(
- CameraCaptureResultImageInfo(CAMERA_CAPTURE_RESULT),
- WIDTH,
- HEIGHT
- )
- val processingRequest = ProcessingRequest(
- { listOf() },
- /*outputFileOptions=*/ null,
- CROP_RECT,
- ROTATION_DEGREES,
- /*jpegQuality=*/100,
- SENSOR_TO_BUFFER,
- takePictureCallback
- )
- val input = ProcessingNode.InputPacket.of(processingRequest, imageIn)
-
- // Act.
- yuvIn.edge.accept(input)
- val imageOut = takePictureCallback.getInMemoryResult()
-
- // Assert: image content is cropped correctly
- // TODO(b/245940015): verify the content of the restored image.
- val jpegOut = ImageUtil.jpegImageToJpegByteArray(imageOut)
- val bitmapOut = decodeByteArray(jpegOut, 0, jpegOut.size)
- assertThat(bitmapOut.width).isEqualTo(WIDTH)
- assertThat(bitmapOut.height).isEqualTo(HEIGHT / 2)
+ processYuvAndVerifyOutputSize(null)
}
@Test
fun processYuvInputOnDisk_getsJpegOutput() = runBlocking {
- // Arrange: create a YUV input.
- val yuvIn = ProcessingNode.In.of(ImageFormat.YUV_420_888)
- ProcessingNode(mainThreadExecutor()).also { it.transform(yuvIn) }
- val takePictureCallback = FakeTakePictureCallback()
+ processYuvAndVerifyOutputSize(OUTPUT_FILE_OPTIONS)
+ }
+
+ private suspend fun processYuvAndVerifyOutputSize(outputFileOptions: OutputFileOptions?) {
+ // Arrange: create node with JPEG input and grayscale effect.
+ val node = ProcessingNode(mainThreadExecutor())
+ val nodeIn = ProcessingNode.In.of(ImageFormat.YUV_420_888)
val imageIn = createYuvFakeImageProxy(
CameraCaptureResultImageInfo(CAMERA_CAPTURE_RESULT),
WIDTH,
HEIGHT
)
+ // Act.
+ val bitmap = processAndGetBitmap(node, nodeIn, imageIn, outputFileOptions)
+ // Assert: image content is cropped correctly
+ // TODO(b/245940015): verify the content of the restored image.
+ assertThat(bitmap.width).isEqualTo(WIDTH)
+ assertThat(bitmap.height).isEqualTo(HEIGHT / 2)
+ }
+
+ private suspend fun processJpegAndVerifyEffectApplied(outputFileOptions: OutputFileOptions?) {
+ // Arrange: create node with JPEG input and grayscale effect.
+ val node = ProcessingNode(
+ mainThreadExecutor(),
+ InternalImageProcessor(GrayscaleImageEffect())
+ )
+ val nodeIn = ProcessingNode.In.of(ImageFormat.JPEG)
+ val imageIn = createJpegFakeImageProxy(
+ CameraCaptureResultImageInfo(CAMERA_CAPTURE_RESULT),
+ createJpegBytes(WIDTH, HEIGHT)
+ )
+ // Act.
+ val bitmap = processAndGetBitmap(node, nodeIn, imageIn, outputFileOptions)
+ // Assert: the output is a cropped grayscale image.
+ assertThat(getAverageDiff(bitmap, Rect(0, 0, 320, 240), 0X555555)).isEqualTo(0)
+ assertThat(getAverageDiff(bitmap, Rect(321, 0, WIDTH, 240), 0XAAAAAA)).isEqualTo(0)
+ }
+
+ private suspend fun processAndGetBitmap(
+ node: ProcessingNode,
+ nodeIn: ProcessingNode.In,
+ imageIn: ImageProxy,
+ outputFileOptions: OutputFileOptions?
+ ): Bitmap {
+ // Arrange: create a YUV input.
+ node.transform(nodeIn)
+ val takePictureCallback = FakeTakePictureCallback()
val processingRequest = ProcessingRequest(
{ listOf() },
- OUTPUT_FILE_OPTIONS,
+ outputFileOptions,
CROP_RECT,
- ROTATION_DEGREES,
+ /*rotationDegrees=*/0, // 0 because exif does not have rotation.
/*jpegQuality=*/100,
SENSOR_TO_BUFFER,
takePictureCallback
)
val input = ProcessingNode.InputPacket.of(processingRequest, imageIn)
-
- // Act.
- yuvIn.edge.accept(input)
- val filePath = takePictureCallback.getOnDiskResult().savedUri!!.path!!
-
- // Assert: image content is cropped correctly
- // TODO(b/245940015): verify the content of the restored image.
- val bitmap = BitmapFactory.decodeFile(filePath)
- assertThat(bitmap.width).isEqualTo(WIDTH)
- assertThat(bitmap.height).isEqualTo(HEIGHT / 2)
+ // Act and return.
+ nodeIn.edge.accept(input)
+ return if (outputFileOptions == null) {
+ val imageOut = takePictureCallback.getInMemoryResult()
+ val jpegOut = jpegImageToJpegByteArray(imageOut)
+ decodeByteArray(jpegOut, 0, jpegOut.size)
+ } else {
+ val filePath = takePictureCallback.getOnDiskResult().savedUri!!.path!!
+ BitmapFactory.decodeFile(filePath)
+ }
}
-}
\ No newline at end of file
+}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
index 2457f15..0d4049b 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/ImageCapture.java
@@ -337,7 +337,7 @@
/**
* Whether the software JPEG pipeline will be used.
*/
- private boolean mUseSoftwareJpeg = false;
+ private boolean mUseSoftwareJpeg = true;
////////////////////////////////////////////////////////////////////////////////////////////
// [UseCase attached dynamic] - Can change but is only available when the UseCase is attached.
@@ -1937,7 +1937,8 @@
Log.d(TAG, String.format("createPipelineWithNode(cameraId: %s, resolution: %s)",
cameraId, resolution));
checkState(mImagePipeline == null);
- mImagePipeline = new ImagePipeline(config, resolution);
+ // TODO: set CameraEffect
+ mImagePipeline = new ImagePipeline(config, resolution, null);
checkState(mTakePictureManager == null);
mTakePictureManager = new TakePictureManager(mImageCaptureControl, mImagePipeline);
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
index af12fa8..8cc2c12 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ImagePipeline.java
@@ -29,8 +29,10 @@
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
+import androidx.camera.core.CameraEffect;
import androidx.camera.core.ForwardingImageProxy;
import androidx.camera.core.ImageCapture;
import androidx.camera.core.impl.CaptureBundle;
@@ -40,6 +42,7 @@
import androidx.camera.core.impl.SessionConfig;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.internal.compat.workaround.ExifRotationAvailability;
+import androidx.camera.core.processing.InternalImageProcessor;
import androidx.core.util.Pair;
import java.util.ArrayList;
@@ -78,9 +81,18 @@
// ===== public methods =====
@MainThread
+ @VisibleForTesting
public ImagePipeline(
@NonNull ImageCaptureConfig useCaseConfig,
@NonNull Size cameraSurfaceSize) {
+ this(useCaseConfig, cameraSurfaceSize, /*cameraEffect=*/ null);
+ }
+
+ @MainThread
+ public ImagePipeline(
+ @NonNull ImageCaptureConfig useCaseConfig,
+ @NonNull Size cameraSurfaceSize,
+ @Nullable CameraEffect cameraEffect) {
checkMainThread();
mUseCaseConfig = useCaseConfig;
mCaptureConfig = CaptureConfig.Builder.createFrom(useCaseConfig).build();
@@ -89,7 +101,8 @@
mCaptureNode = new CaptureNode();
mBundlingNode = new SingleBundlingNode();
mProcessingNode = new ProcessingNode(
- requireNonNull(mUseCaseConfig.getIoExecutor(CameraXExecutors.ioExecutor())));
+ requireNonNull(mUseCaseConfig.getIoExecutor(CameraXExecutors.ioExecutor())),
+ cameraEffect != null ? new InternalImageProcessor(cameraEffect) : null);
// Connect nodes
mPipelineIn = CaptureNode.In.of(cameraSurfaceSize, mUseCaseConfig.getInputFormat());
@@ -133,6 +146,7 @@
/**
* Sets a listener for close calls on this image.
+ *
* @param listener to set
*/
@MainThread
@@ -270,4 +284,9 @@
return mCaptureNode;
}
+ @NonNull
+ @VisibleForTesting
+ ProcessingNode getProcessingNode() {
+ return mProcessingNode;
+ }
}
diff --git a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
index 5e12339..fbe5b37 100644
--- a/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
+++ b/camera/camera-core/src/main/java/androidx/camera/core/imagecapture/ProcessingNode.java
@@ -20,13 +20,16 @@
import static androidx.camera.core.ImageCapture.ERROR_UNKNOWN;
import static androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor;
+import static androidx.core.util.Preconditions.checkState;
import static java.util.Objects.requireNonNull;
import android.graphics.Bitmap;
+import android.graphics.ImageFormat;
import android.os.Build;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
@@ -35,6 +38,7 @@
import androidx.camera.core.ImageProxy;
import androidx.camera.core.impl.utils.executor.CameraXExecutors;
import androidx.camera.core.processing.Edge;
+import androidx.camera.core.processing.InternalImageProcessor;
import androidx.camera.core.processing.Node;
import androidx.camera.core.processing.Operation;
import androidx.camera.core.processing.Packet;
@@ -54,6 +58,8 @@
@NonNull
private final Executor mBlockingExecutor;
+ @Nullable
+ final InternalImageProcessor mImageProcessor;
private Operation<InputPacket, Packet<ImageProxy>> mInput2Packet;
private Operation<Image2JpegBytes.In, Packet<byte[]>> mImage2JpegBytes;
@@ -62,13 +68,26 @@
private Operation<Packet<byte[]>, Packet<Bitmap>> mJpegBytes2CroppedBitmap;
private Operation<Packet<ImageProxy>, ImageProxy> mJpegImage2Result;
private Operation<Packet<byte[]>, Packet<ImageProxy>> mJpegBytes2Image;
+ private Operation<Packet<Bitmap>, Packet<Bitmap>> mBitmapEffect;
/**
* @param blockingExecutor a executor that can be blocked by long running tasks. e.g.
* {@link CameraXExecutors#ioExecutor()}
*/
+ @VisibleForTesting
ProcessingNode(@NonNull Executor blockingExecutor) {
+ this(blockingExecutor, /*imageProcessor=*/null);
+ }
+
+ /**
+ * @param blockingExecutor a executor that can be blocked by long running tasks. e.g.
+ * {@link CameraXExecutors#ioExecutor()}
+ * @param imageProcessor external effect for post-processing.
+ */
+ ProcessingNode(@NonNull Executor blockingExecutor,
+ @Nullable InternalImageProcessor imageProcessor) {
mBlockingExecutor = blockingExecutor;
+ mImageProcessor = imageProcessor;
}
@NonNull
@@ -90,9 +109,15 @@
mBitmap2JpegBytes = new Bitmap2JpegBytes();
mJpegBytes2Disk = new JpegBytes2Disk();
mJpegImage2Result = new JpegImage2Result();
- if (inputEdge.getFormat() == YUV_420_888) {
+ if (inputEdge.getFormat() == YUV_420_888 || mImageProcessor != null) {
+ // Convert JPEG bytes to ImageProxy for:
+ // - YUV input: YUV -> JPEG -> ImageProxy
+ // - Effects: JPEG -> Bitmap -> effect -> Bitmap -> JPEG -> ImageProxy
mJpegBytes2Image = new JpegBytes2Image();
}
+ if (mImageProcessor != null) {
+ mBitmapEffect = new BitmapEffect(mImageProcessor);
+ }
// No output. The request callback will be invoked to deliver the final result.
return null;
}
@@ -131,10 +156,8 @@
Packet<ImageProxy> originalImage = mInput2Packet.apply(inputPacket);
Packet<byte[]> jpegBytes = mImage2JpegBytes.apply(
Image2JpegBytes.In.of(originalImage, request.getJpegQuality()));
- if (jpegBytes.hasCropping()) {
- Packet<Bitmap> croppedBitmap = mJpegBytes2CroppedBitmap.apply(jpegBytes);
- jpegBytes = mBitmap2JpegBytes.apply(
- Bitmap2JpegBytes.In.of(croppedBitmap, request.getJpegQuality()));
+ if (jpegBytes.hasCropping() || mBitmapEffect != null) {
+ jpegBytes = cropAndMaybeApplyEffect(jpegBytes, request.getJpegQuality());
}
return mJpegBytes2Disk.apply(
JpegBytes2Disk.In.of(jpegBytes, requireNonNull(request.getOutputFileOptions())));
@@ -146,15 +169,33 @@
throws ImageCaptureException {
ProcessingRequest request = inputPacket.getProcessingRequest();
Packet<ImageProxy> image = mInput2Packet.apply(inputPacket);
- if (image.getFormat() == YUV_420_888) {
- Packet<byte[]> jpegPacket = mImage2JpegBytes.apply(
+ if (image.getFormat() == YUV_420_888 || mBitmapEffect != null) {
+ Packet<byte[]> jpegBytes = mImage2JpegBytes.apply(
Image2JpegBytes.In.of(image, request.getJpegQuality()));
- image = mJpegBytes2Image.apply(jpegPacket);
+ if (mBitmapEffect != null) {
+ jpegBytes = cropAndMaybeApplyEffect(jpegBytes, request.getJpegQuality());
+ }
+ image = mJpegBytes2Image.apply(jpegBytes);
}
return mJpegImage2Result.apply(image);
}
/**
+ * Crops JPEG byte array and apply effect if present.
+ */
+ private Packet<byte[]> cropAndMaybeApplyEffect(Packet<byte[]> jpegPacket, int jpegQuality)
+ throws ImageCaptureException {
+ checkState(jpegPacket.getFormat() == ImageFormat.JPEG);
+ Packet<Bitmap> bitmapPacket = mJpegBytes2CroppedBitmap.apply(jpegPacket);
+ if (mBitmapEffect != null) {
+ // Apply effect if present.
+ bitmapPacket = mBitmapEffect.apply(bitmapPacket);
+ }
+ return mBitmap2JpegBytes.apply(
+ Bitmap2JpegBytes.In.of(bitmapPacket, jpegQuality));
+ }
+
+ /**
* Sends {@link ImageCaptureException} to {@link TakePictureManager}.
*/
private static void sendError(@NonNull ProcessingRequest request,
diff --git a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
index 4976d49..a40665a 100644
--- a/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
+++ b/camera/camera-core/src/test/java/androidx/camera/core/imagecapture/ImagePipelineTest.kt
@@ -44,6 +44,7 @@
import androidx.camera.core.imagecapture.Utils.injectRotationOptionQuirk
import androidx.camera.core.impl.CaptureConfig
import androidx.camera.core.impl.CaptureConfig.OPTION_ROTATION
+import androidx.camera.core.impl.ImageCaptureConfig
import androidx.camera.core.impl.ImageInputConfig
import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor
import androidx.camera.core.internal.IoConfig.OPTION_IO_EXECUTOR
@@ -51,6 +52,7 @@
import androidx.camera.testing.TestImageUtil.createJpegFakeImageProxy
import androidx.camera.testing.fakes.FakeImageInfo
import androidx.camera.testing.fakes.FakeImageReaderProxy
+import androidx.camera.testing.fakes.GrayscaleImageEffect
import com.google.common.truth.Truth.assertThat
import org.junit.After
import org.junit.Before
@@ -78,6 +80,7 @@
}
private lateinit var imagePipeline: ImagePipeline
+ private lateinit var imageCaptureConfig: ImageCaptureConfig
@Before
fun setUp() {
@@ -87,7 +90,8 @@
}
builder.mutableConfig.insertOption(OPTION_IO_EXECUTOR, mainThreadExecutor())
builder.mutableConfig.insertOption(ImageInputConfig.OPTION_INPUT_FORMAT, ImageFormat.JPEG)
- imagePipeline = ImagePipeline(builder.useCaseConfig, SIZE)
+ imageCaptureConfig = builder.useCaseConfig
+ imagePipeline = ImagePipeline(imageCaptureConfig, SIZE)
}
@After
@@ -96,6 +100,22 @@
}
@Test
+ fun createPipelineWithoutEffect_processingNodeHasNoEffect() {
+ assertThat(imagePipeline.processingNode.mImageProcessor).isNull()
+ }
+
+ @Test
+ fun createPipelineWithEffect_processingNodeContainsEffect() {
+ assertThat(
+ ImagePipeline(
+ imageCaptureConfig,
+ SIZE,
+ GrayscaleImageEffect()
+ ).processingNode.mImageProcessor
+ ).isNotNull()
+ }
+
+ @Test
fun createRequests_verifyCameraRequest() {
// Arrange.
val captureInput = imagePipeline.captureNode.inputEdge
diff --git a/camera/camera-testing/src/main/java/androidx/camera/testing/GrantPermissionRuleUtil.kt b/camera/camera-testing/src/main/java/androidx/camera/testing/GrantPermissionRuleUtil.kt
deleted file mode 100644
index 9d92cd4..0000000
--- a/camera/camera-testing/src/main/java/androidx/camera/testing/GrantPermissionRuleUtil.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- * Copyright 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.testing
-
-import android.Manifest
-import android.os.Build
-import androidx.test.rule.GrantPermissionRule
-
-/**
- * Util functions for grant permission
- */
-object GrantPermissionRuleUtil {
- /**
- * Returns a GrantPermissionRule after filtering the available permissions according to API
- * level.
- */
- @JvmStatic
- fun grantWithApiLevelFilter(vararg permissions: String): GrantPermissionRule =
- GrantPermissionRule.grant(*(permissions.filter {
- Build.VERSION.SDK_INT <= 32 || it != Manifest.permission.WRITE_EXTERNAL_STORAGE
- }).toTypedArray())
-}
\ No newline at end of file
diff --git a/camera/camera-video/api/current.txt b/camera/camera-video/api/current.txt
index f8a2ad5..b591b5e 100644
--- a/camera/camera-video/api/current.txt
+++ b/camera/camera-video/api/current.txt
@@ -9,6 +9,7 @@
field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
+ field public static final int AUDIO_STATE_SOURCE_ERROR = 4; // 0x4
field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
}
diff --git a/camera/camera-video/api/public_plus_experimental_current.txt b/camera/camera-video/api/public_plus_experimental_current.txt
index f8a2ad5..b591b5e 100644
--- a/camera/camera-video/api/public_plus_experimental_current.txt
+++ b/camera/camera-video/api/public_plus_experimental_current.txt
@@ -9,6 +9,7 @@
field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
+ field public static final int AUDIO_STATE_SOURCE_ERROR = 4; // 0x4
field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
}
diff --git a/camera/camera-video/api/restricted_current.txt b/camera/camera-video/api/restricted_current.txt
index f8a2ad5..b591b5e 100644
--- a/camera/camera-video/api/restricted_current.txt
+++ b/camera/camera-video/api/restricted_current.txt
@@ -9,6 +9,7 @@
field public static final int AUDIO_STATE_ACTIVE = 0; // 0x0
field public static final int AUDIO_STATE_DISABLED = 1; // 0x1
field public static final int AUDIO_STATE_ENCODER_ERROR = 3; // 0x3
+ field public static final int AUDIO_STATE_SOURCE_ERROR = 4; // 0x4
field public static final int AUDIO_STATE_SOURCE_SILENCED = 2; // 0x2
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java b/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
index b3d05d8..327396a 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/AudioStats.java
@@ -76,17 +76,26 @@
/**
* The recording is muted because the audio encoder encountered errors.
*
- * <p>When the audio encoder encountered errors, the recording will keep being recorded
- * without audio for the rest of the recording. The audio stats generated after the audio
- * encoder failed will contain this audio state.
+ * <p>If the audio source encounters errors during recording, audio stats generated after the
+ * error will contain this audio state, and the recording will proceed without audio.
*
* <p>Use {@link #getErrorCause()} to get the error cause.
*/
public static final int AUDIO_STATE_ENCODER_ERROR = 3;
+ /**
+ * The recording is muted because the audio source encountered errors.
+ *
+ * <p>If the audio source encounters errors during recording, audio stats generated after the
+ * error will contain this audio state, and the recording will proceed without audio.
+ *
+ * <p>Use {@link #getErrorCause()} to get the error cause.
+ */
+ public static final int AUDIO_STATE_SOURCE_ERROR = 4;
+
/** @hide */
@IntDef({AUDIO_STATE_ACTIVE, AUDIO_STATE_DISABLED, AUDIO_STATE_SOURCE_SILENCED,
- AUDIO_STATE_ENCODER_ERROR})
+ AUDIO_STATE_ENCODER_ERROR, AUDIO_STATE_SOURCE_ERROR})
@Retention(RetentionPolicy.SOURCE)
@RestrictTo(RestrictTo.Scope.LIBRARY)
public @interface AudioState {
@@ -99,7 +108,7 @@
*/
private static final Set<Integer> ERROR_STATES =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList(AUDIO_STATE_SOURCE_SILENCED,
- AUDIO_STATE_ENCODER_ERROR)));
+ AUDIO_STATE_ENCODER_ERROR, AUDIO_STATE_SOURCE_ERROR)));
/**
* Indicates whether the recording is being recorded with audio.
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
index d85ba2e..1c7eb36 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/Recorder.java
@@ -73,7 +73,6 @@
import androidx.camera.video.StreamInfo.StreamState;
import androidx.camera.video.internal.AudioSource;
import androidx.camera.video.internal.AudioSourceAccessException;
-import androidx.camera.video.internal.ResourceCreationException;
import androidx.camera.video.internal.compat.Api26Impl;
import androidx.camera.video.internal.compat.quirk.DeactivateEncoderSurfaceBeforeStopEncoderQuirk;
import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
@@ -220,9 +219,13 @@
*/
ACTIVE,
/**
- * The audio source or the audio encoder encountered errors.
+ * The audio encoder encountered errors.
*/
- ERROR
+ ERROR_ENCODER,
+ /**
+ * The audio source encountered errors.
+ */
+ ERROR_SOURCE,
}
/**
@@ -1109,13 +1112,13 @@
/**
* Setup audio related resources.
*
- * @throws ResourceCreationException if the necessary resource for audio to work failed to be
- * setup.
+ * @throws AudioSourceAccessException if the audio source failed to be setup.
+ * @throws InvalidConfigException if the audio encoder failed to be setup.
*/
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
@ExecutedBy("mSequentialExecutor")
private void setupAudio(@NonNull RecordingRecord recordingToStart)
- throws ResourceCreationException {
+ throws AudioSourceAccessException, InvalidConfigException {
MediaSpec mediaSpec = getObservableData(mMediaSpec);
// Resolve the audio mime info
MimeInfo audioMimeInfo = resolveAudioMimeInfo(mediaSpec, mResolvedCamcorderProfile);
@@ -1124,26 +1127,18 @@
// Select and create the audio source
AudioSource.Settings audioSourceSettings =
resolveAudioSourceSettings(audioMimeInfo, mediaSpec.getAudioSpec());
- try {
- if (mAudioSource != null) {
- releaseCurrentAudioSource();
- }
- // TODO: set audioSourceTimebase to AudioSource. Currently AudioSource hard code
- // AudioTimestamp.TIMEBASE_MONOTONIC.
- mAudioSource = setupAudioSource(recordingToStart, audioSourceSettings);
- Logger.d(TAG, String.format("Set up new audio source: 0x%x", mAudioSource.hashCode()));
- } catch (AudioSourceAccessException e) {
- throw new ResourceCreationException(e);
+ if (mAudioSource != null) {
+ releaseCurrentAudioSource();
}
+ // TODO: set audioSourceTimebase to AudioSource. Currently AudioSource hard code
+ // AudioTimestamp.TIMEBASE_MONOTONIC.
+ mAudioSource = setupAudioSource(recordingToStart, audioSourceSettings);
+ Logger.d(TAG, String.format("Set up new audio source: 0x%x", mAudioSource.hashCode()));
// Select and create the audio encoder
AudioEncoderConfig audioEncoderConfig = resolveAudioEncoderConfig(audioMimeInfo,
audioSourceTimebase, audioSourceSettings, mediaSpec.getAudioSpec());
- try {
- mAudioEncoder = mAudioEncoderFactory.createEncoder(mExecutor, audioEncoderConfig);
- } catch (InvalidConfigException e) {
- throw new ResourceCreationException(e);
- }
+ mAudioEncoder = mAudioEncoderFactory.createEncoder(mExecutor, audioEncoderConfig);
// Connect the audio source to the audio encoder
Encoder.EncoderInput bufferProvider = mAudioEncoder.getInput();
@@ -1158,11 +1153,8 @@
private AudioSource setupAudioSource(@NonNull RecordingRecord recordingToStart,
@NonNull AudioSource.Settings audioSourceSettings)
throws AudioSourceAccessException {
-
- AudioSource audioSource = recordingToStart.performOneTimeAudioSourceCreation(
- audioSourceSettings, AUDIO_EXECUTOR);
-
- return audioSource;
+ return recordingToStart.performOneTimeAudioSourceCreation(audioSourceSettings,
+ AUDIO_EXECUTOR);
}
private void releaseCurrentAudioSource() {
@@ -1207,7 +1199,7 @@
mVideoEncoder = mVideoEncoderFactory.createEncoder(mExecutor, config);
} catch (InvalidConfigException e) {
Logger.e(TAG, "Unable to initialize video encoder.", e);
- onEncoderSetupError(new ResourceCreationException(e));
+ onEncoderSetupError(e);
return;
}
@@ -1477,7 +1469,9 @@
// Configure audio based on the current audio state.
switch (mAudioState) {
- case ERROR:
+ case ERROR_ENCODER:
+ // Fall-through
+ case ERROR_SOURCE:
// Fall-through
case ACTIVE:
// Fall-through
@@ -1497,9 +1491,15 @@
try {
setupAudio(recordingToStart);
setAudioState(AudioState.ACTIVE);
- } catch (ResourceCreationException e) {
+ } catch (AudioSourceAccessException | InvalidConfigException e) {
Logger.e(TAG, "Unable to create audio resource with error: ", e);
- setAudioState(AudioState.ERROR);
+ AudioState audioState;
+ if (e instanceof InvalidConfigException) {
+ audioState = AudioState.ERROR_ENCODER;
+ } else {
+ audioState = AudioState.ERROR_SOURCE;
+ }
+ setAudioState(audioState);
mAudioErrorCause = e;
}
}
@@ -1623,7 +1623,11 @@
// If the audio source or encoder encounters error, update the
// status event to notify users. Then continue recording without
// audio data.
- setAudioState(AudioState.ERROR);
+ if (throwable instanceof EncodeException) {
+ setAudioState(AudioState.ERROR_ENCODER);
+ } else {
+ setAudioState(AudioState.ERROR_SOURCE);
+ }
mAudioErrorCause = throwable;
updateInProgressStatusEvent();
completer.set(null);
@@ -1955,8 +1959,10 @@
} else {
return AudioStats.AUDIO_STATE_ACTIVE;
}
- case ERROR:
+ case ERROR_ENCODER:
return AudioStats.AUDIO_STATE_ENCODER_ERROR;
+ case ERROR_SOURCE:
+ return AudioStats.AUDIO_STATE_SOURCE_ERROR;
case IDLING:
// AudioStats should not be produced when audio is in IDLING state.
break;
@@ -2051,7 +2057,9 @@
setAudioState(AudioState.IDLING);
mAudioSource.stop();
break;
- case ERROR:
+ case ERROR_ENCODER:
+ // Fall-through
+ case ERROR_SOURCE:
// Reset audio state to INITIALIZING if the audio encoder encountered error, so
// that it can be setup again when the next recording with audio enabled is started.
setAudioState(AudioState.INITIALIZING);
@@ -2431,7 +2439,7 @@
@SuppressWarnings("WeakerAccess") /* synthetic accessor */
@ExecutedBy("mSequentialExecutor")
- void setAudioState(AudioState audioState) {
+ void setAudioState(@NonNull AudioState audioState) {
Logger.d(TAG, "Transitioning audio state: " + mAudioState + " --> " + audioState);
mAudioState = audioState;
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/ResourceCreationException.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/ResourceCreationException.java
deleted file mode 100644
index 5853fa6..0000000
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/ResourceCreationException.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Copyright 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.video.internal;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.RequiresApi;
-
-/** An exception thrown to indicate an error has occurred during creating necessary resource. */
-@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
-public class ResourceCreationException extends Exception {
-
- public ResourceCreationException(@Nullable String message) {
- super(message);
- }
-
- public ResourceCreationException(@Nullable String message, @Nullable Throwable cause) {
- super(message, cause);
- }
-
- public ResourceCreationException(@Nullable Throwable cause) {
- super(cause);
- }
-}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
index 41b0c0e..2a9f7f3 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/DeviceQuirksLoader.java
@@ -89,6 +89,9 @@
if (ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.load()) {
quirks.add(new ImageCaptureFailedWhenVideoCaptureIsBoundQuirk());
}
+ if (MediaCodecDoesNotSendEos.load()) {
+ quirks.add(new MediaCodecDoesNotSendEos());
+ }
return quirks;
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.java
index a72f29f..e4b6a17 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/ImageCaptureFailedWhenVideoCaptureIsBoundQuirk.java
@@ -26,13 +26,13 @@
* Bug Id: b/239369953
* Description: When taking image with VideoCapture is bound, the capture result is returned
* but the resulting image can not be obtained.
- * Device(s): BLU Studio X10, Itel w6004, and Vivo 1805.
+ * Device(s): BLU Studio X10, Itel w6004, Twist 2 Pro, and Vivo 1805.
*/
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
public class ImageCaptureFailedWhenVideoCaptureIsBoundQuirk implements Quirk {
static boolean load() {
- return isBluStudioX10() || isItelW6004() || isVivo1805();
+ return isBluStudioX10() || isItelW6004() || isVivo1805() || isPositivoTwist2Pro();
}
public static boolean isBluStudioX10() {
@@ -46,4 +46,9 @@
public static boolean isVivo1805() {
return "vivo".equalsIgnoreCase(Build.BRAND) && "vivo 1805".equalsIgnoreCase(Build.MODEL);
}
+
+ public static boolean isPositivoTwist2Pro() {
+ return "positivo".equalsIgnoreCase(Build.BRAND) && "twist 2 pro".equalsIgnoreCase(
+ Build.MODEL);
+ }
}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java
new file mode 100644
index 0000000..46ccb85
--- /dev/null
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/compat/quirk/MediaCodecDoesNotSendEos.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.video.internal.compat.quirk;
+
+import android.media.MediaCodec;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.camera.core.impl.Quirk;
+
+/**
+ * <p>QuirkSummary
+ * Bug Id: b/248189542
+ * Description: When recording video with effect pipeline enabled, calling
+ * {@link MediaCodec#signalEndOfInputStream()} doesn't trigger an EOS buffer to
+ * {@link MediaCodec.Callback}.
+ * Device(s): twist 2 pro.
+ */
+@RequiresApi(21)
+public class MediaCodecDoesNotSendEos implements Quirk {
+
+ public static boolean isPositivoTwist2Pro() {
+ return "positivo".equalsIgnoreCase(Build.BRAND) && "twist 2 pro".equalsIgnoreCase(
+ Build.MODEL);
+ }
+
+ static boolean load() {
+ return isPositivoTwist2Pro();
+ }
+}
diff --git a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
index bf72036..c5d5bf1 100644
--- a/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
+++ b/camera/camera-video/src/main/java/androidx/camera/video/internal/encoder/EncoderImpl.java
@@ -26,6 +26,8 @@
import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.STARTED;
import static androidx.camera.video.internal.encoder.EncoderImpl.InternalState.STOPPING;
+import static java.util.Objects.requireNonNull;
+
import android.annotation.SuppressLint;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
@@ -50,6 +52,7 @@
import androidx.camera.video.internal.compat.quirk.CameraUseInconsistentTimebaseQuirk;
import androidx.camera.video.internal.compat.quirk.DeviceQuirks;
import androidx.camera.video.internal.compat.quirk.EncoderNotUsePersistentInputSurfaceQuirk;
+import androidx.camera.video.internal.compat.quirk.MediaCodecDoesNotSendEos;
import androidx.camera.video.internal.compat.quirk.VideoEncoderSuspendDoesNotIncludeSuspendTimeQuirk;
import androidx.camera.video.internal.workaround.EncoderFinder;
import androidx.camera.video.internal.workaround.VideoTimebaseConverter;
@@ -144,28 +147,29 @@
private static final Range<Long> NO_RANGE = Range.create(NO_LIMIT_LONG, NO_LIMIT_LONG);
private static final long STOP_TIMEOUT_MS = 1000L;
private static final long TIMESTAMP_ANY = -1;
+ private static final int FAKE_BUFFER_INDEX = -9999;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
final String mTag;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
final Object mLock = new Object();
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
final boolean mIsVideoEncoder;
private final MediaFormat mMediaFormat;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
final MediaCodec mMediaCodec;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
final EncoderInput mEncoderInput;
private final EncoderInfo mEncoderInfo;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
final Executor mEncoderExecutor;
private final ListenableFuture<Void> mReleasedFuture;
private final Completer<Void> mReleasedCompleter;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
final Queue<Integer> mFreeInputBufferIndexQueue = new ArrayDeque<>();
private final Queue<Completer<InputBuffer>> mAcquisitionQueue = new ArrayDeque<>();
private final Set<InputBuffer> mInputBufferSet = new HashSet<>();
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
final Set<EncodedDataImpl> mEncodedDataSet = new HashSet<>();
/*
* mActivePauseResumeTimeRanges is a queue used to track all active pause/resume time ranges.
@@ -173,31 +177,33 @@
* range, so this range is still needed to check for later output buffers. The first element
* in the queue is the oldest range and the last element is the newest.
*/
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
final Deque<Range<Long>> mActivePauseResumeTimeRanges = new ArrayDeque<>();
final Timebase mInputTimebase;
final TimeProvider mTimeProvider = new SystemTimeProvider();
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@GuardedBy("mLock")
EncoderCallback mEncoderCallback = EncoderCallback.EMPTY;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@GuardedBy("mLock")
Executor mEncoderCallbackExecutor = CameraXExecutors.directExecutor();
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
InternalState mState;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
Range<Long> mStartStopTimeRangeUs = NO_RANGE;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
long mTotalPausedDurationUs = 0L;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
boolean mPendingCodecStop = false;
// The data timestamp that an encoding stops at. If this timestamp is null, it means the
// encoding hasn't receiving enough data to be stopped.
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
Long mLastDataStopTimestamp = null;
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
Future<?> mStopTimeoutFuture = null;
+ @Nullable
+ private MediaCodecCallback mMediaCodecCallback;
private boolean mIsFlushedAfterEndOfStream = false;
private boolean mSourceStoppedSignalled = false;
@@ -276,7 +282,8 @@
mStopTimeoutFuture.cancel(true);
mStopTimeoutFuture = null;
}
- mMediaCodec.setCallback(new MediaCodecCallback());
+ mMediaCodecCallback = new MediaCodecCallback();
+ mMediaCodec.setCallback(mMediaCodecCallback);
mMediaCodec.configure(mMediaFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
if (mEncoderInput instanceof SurfaceInput) {
@@ -306,6 +313,7 @@
* released. It can call {@link #pause} to pause the encoding after started. If the encoder is
* in paused state, then calling this method will resume the encoding.
*/
+ @SuppressWarnings("StatementWithEmptyBody") // to better organize the logic and comments
@Override
public void start() {
final long startTriggerTimeUs = generatePresentationTimeUs();
@@ -314,8 +322,7 @@
case CONFIGURED:
mLastDataStopTimestamp = null;
- final long startTimeUs = startTriggerTimeUs;
- Logger.d(mTag, "Start on " + DebugUtils.readableUs(startTimeUs));
+ Logger.d(mTag, "Start on " + DebugUtils.readableUs(startTriggerTimeUs));
try {
if (mIsFlushedAfterEndOfStream) {
// If the codec is flushed after an end-of-stream, it was never
@@ -323,7 +330,7 @@
// before starting it again.
reset();
}
- mStartStopTimeRangeUs = Range.create(startTimeUs, NO_LIMIT_LONG);
+ mStartStopTimeRangeUs = Range.create(startTriggerTimeUs, NO_LIMIT_LONG);
mMediaCodec.start();
} catch (MediaCodec.CodecException e) {
handleEncodeError(e);
@@ -345,14 +352,14 @@
pauseRange != null && pauseRange.getUpper() == NO_LIMIT_LONG,
"There should be a \"pause\" before \"resume\"");
final long pauseTimeUs = pauseRange.getLower();
- final long resumeTimeUs = startTriggerTimeUs;
- mActivePauseResumeTimeRanges.addLast(Range.create(pauseTimeUs, resumeTimeUs));
+ mActivePauseResumeTimeRanges.addLast(
+ Range.create(pauseTimeUs, startTriggerTimeUs));
// Do not update total paused duration here since current output buffer may
// still before the pause range.
- Logger.d(mTag, "Resume on " + DebugUtils.readableUs(resumeTimeUs)
+ Logger.d(mTag, "Resume on " + DebugUtils.readableUs(startTriggerTimeUs)
+ "\nPaused duration = " + DebugUtils.readableUs(
- (resumeTimeUs - pauseTimeUs))
+ (startTriggerTimeUs - pauseTimeUs))
);
if (!mIsVideoEncoder && DeviceQuirks.get(
@@ -496,7 +503,7 @@
});
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
void signalCodecStop() {
if (mEncoderInput instanceof ByteBufferInput) {
@@ -509,10 +516,15 @@
Futures.successfulAsList(futures).addListener(this::signalEndOfInputStream,
mEncoderExecutor);
} else if (mEncoderInput instanceof SurfaceInput) {
- try {
- mMediaCodec.signalEndOfInputStream();
- } catch (MediaCodec.CodecException e) {
- handleEncodeError(e);
+ if (DeviceQuirks.get(MediaCodecDoesNotSendEos.class) != null) {
+ requireNonNull(mMediaCodecCallback).onOutputBufferAvailable(mMediaCodec,
+ FAKE_BUFFER_INDEX, createFakeEosBufferInfo());
+ } else {
+ try {
+ mMediaCodec.signalEndOfInputStream();
+ } catch (MediaCodec.CodecException e) {
+ handleEncodeError(e);
+ }
}
}
}
@@ -540,9 +552,9 @@
break;
case STARTED:
// Create and insert a pause/resume range.
- final long pauseTimeUs = pauseTriggerTimeUs;
- Logger.d(mTag, "Pause on " + DebugUtils.readableUs(pauseTimeUs));
- mActivePauseResumeTimeRanges.addLast(Range.create(pauseTimeUs, NO_LIMIT_LONG));
+ Logger.d(mTag, "Pause on " + DebugUtils.readableUs(pauseTriggerTimeUs));
+ mActivePauseResumeTimeRanges.addLast(
+ Range.create(pauseTriggerTimeUs, NO_LIMIT_LONG));
setState(PAUSED);
break;
case PENDING_RELEASE:
@@ -678,7 +690,7 @@
mState = state;
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
void setMediaCodecPaused(boolean paused) {
Bundle bundle = new Bundle();
@@ -686,7 +698,7 @@
mMediaCodec.setParameters(bundle);
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
void requestKeyFrameToMediaCodec() {
Bundle bundle = new Bundle();
@@ -734,13 +746,13 @@
}, mEncoderExecutor);
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
void handleEncodeError(@NonNull MediaCodec.CodecException e) {
handleEncodeError(EncodeException.ERROR_CODEC, e.getMessage(), e);
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
void handleEncodeError(@EncodeException.ErrorType int error, @Nullable String message,
@Nullable Throwable throwable) {
@@ -760,6 +772,7 @@
stopMediaCodec(() -> notifyError(error, message, throwable));
break;
case ERROR:
+ //noinspection ConstantConditions
Logger.w(mTag, "Get more than one error: " + message + "(" + error + ")",
throwable);
break;
@@ -769,7 +782,7 @@
}
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
void notifyError(@EncodeException.ErrorType int error, @Nullable String message,
@Nullable Throwable throwable) {
EncoderCallback callback;
@@ -786,7 +799,7 @@
}
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
void stopMediaCodec(@Nullable Runnable afterStop) {
/*
@@ -812,12 +825,12 @@
Logger.d(mTag, "encoded data and input buffers are returned");
}
if (mEncoderInput instanceof SurfaceInput && !mSourceStoppedSignalled) {
- // For a SurfaceInput, the codec is in control of dequeuing buffers from the
- // underlying BufferQueue. If we stop the codec, then it will stop dequeuing buffers
- // and the BufferQueue may run out of input buffers, causing the camera pipeline
- // to stall. Instead of stopping, we will flush the codec. Since the codec is
- // operating in asynchronous mode, this will cause the codec to continue to
- // discard buffers. We should have already received the end-of-stream signal on
+ // For a SurfaceInput, the codec is in control of de-queuing buffers from the
+ // underlying BufferQueue. If we stop the codec, then it will stop de-queuing
+ // buffers and the BufferQueue may run out of input buffers, causing the camera
+ // pipeline to stall. Instead of stopping, we will flush the codec. Since the
+ // codec is operating in asynchronous mode, this will cause the codec to continue
+ // to discard buffers. We should have already received the end-of-stream signal on
// an output buffer at this point, so those buffers are not needed anyways. We will
// defer resetting the codec until just before starting the codec again.
mMediaCodec.flush();
@@ -836,7 +849,7 @@
}, mEncoderExecutor);
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
void handleStopped() {
if (mState == PENDING_RELEASE) {
@@ -859,7 +872,7 @@
}
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
void updateTotalPausedDuration(long bufferPresentationTimeUs) {
while (!mActivePauseResumeTimeRanges.isEmpty()) {
@@ -876,7 +889,7 @@
}
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
long getAdjustedTimeUs(@NonNull MediaCodec.BufferInfo bufferInfo) {
long adjustedTimeUs;
@@ -888,7 +901,7 @@
return adjustedTimeUs;
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
boolean isInPauseRange(long timeUs) {
for (Range<Long> range : mActivePauseResumeTimeRanges) {
@@ -903,7 +916,7 @@
return false;
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
@NonNull
ListenableFuture<InputBuffer> acquireInputBuffer() {
@@ -940,12 +953,19 @@
}
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @NonNull
+ private MediaCodec.BufferInfo createFakeEosBufferInfo() {
+ MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
+ bufferInfo.set(0, 0, generatePresentationTimeUs(), MediaCodec.BUFFER_FLAG_END_OF_STREAM);
+ return bufferInfo;
+ }
+
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@ExecutedBy("mEncoderExecutor")
void matchAcquisitionsAndFreeBufferIndexes() {
while (!mAcquisitionQueue.isEmpty() && !mFreeInputBufferIndexQueue.isEmpty()) {
- Completer<InputBuffer> completer = mAcquisitionQueue.poll();
- int bufferIndex = mFreeInputBufferIndexQueue.poll();
+ Completer<InputBuffer> completer = requireNonNull(mAcquisitionQueue.poll());
+ int bufferIndex = requireNonNull(mFreeInputBufferIndexQueue.poll());
InputBufferImpl inputBuffer;
try {
@@ -971,22 +991,22 @@
: new AudioEncoderInfoImpl(codecInfo, mime);
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
long generatePresentationTimeUs() {
return mTimeProvider.uptimeUs();
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
static boolean isKeyFrame(@NonNull MediaCodec.BufferInfo bufferInfo) {
return (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
static boolean isEndOfStream(@NonNull MediaCodec.BufferInfo bufferInfo) {
return (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
class MediaCodecCallback extends MediaCodec.Callback {
@Nullable
@@ -1101,11 +1121,14 @@
return;
}
} else {
- try {
- mMediaCodec.releaseOutputBuffer(index, false);
- } catch (MediaCodec.CodecException e) {
- handleEncodeError(e);
- return;
+ // Not necessary to return fake buffer
+ if (index != FAKE_BUFFER_INDEX) {
+ try {
+ mMediaCodec.releaseOutputBuffer(index, false);
+ } catch (MediaCodec.CodecException e) {
+ handleEncodeError(e);
+ return;
+ }
}
}
@@ -1252,6 +1275,7 @@
return true;
}
+ @SuppressWarnings("StatementWithEmptyBody") // to better organize the logic and comments
@ExecutedBy("mEncoderExecutor")
private boolean updatePauseRangeStateAndCheckIfBufferPaused(
@NonNull MediaCodec.BufferInfo bufferInfo) {
@@ -1372,7 +1396,7 @@
}
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
class SurfaceInput implements Encoder.SurfaceInput {
@@ -1468,7 +1492,7 @@
}
}
- @SuppressWarnings("WeakerAccess") /* synthetic accessor */
+ @SuppressWarnings("WeakerAccess") // synthetic accessor
class ByteBufferInput implements Encoder.ByteBufferInput {
private final Map<Observer<? super State>, Executor> mStateObservers =
diff --git a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
index b9f5851..d8cbb3e 100644
--- a/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
+++ b/camera/integration-tests/coretestapp/src/main/java/androidx/camera/integration/core/CameraXActivity.java
@@ -41,6 +41,7 @@
import android.hardware.display.DisplayManager;
import android.media.MediaScannerConnection;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
@@ -154,12 +155,24 @@
*/
public class CameraXActivity extends AppCompatActivity {
private static final String TAG = "CameraXActivity";
- private static final String[] REQUIRED_PERMISSIONS =
- new String[]{
+ private static final String[] REQUIRED_PERMISSIONS;
+ static {
+ // From Android T, skips the permission check of WRITE_EXTERNAL_STORAGE since it won't be
+ // granted any more.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ REQUIRED_PERMISSIONS = new String[]{
+ Manifest.permission.CAMERA,
+ Manifest.permission.RECORD_AUDIO
+ };
+ } else {
+ REQUIRED_PERMISSIONS = new String[]{
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
+ }
+ }
+
// Possible values for this intent key: "backward" or "forward".
private static final String INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction";
// Possible values for this intent key: "switch_test_case", "preview_test_case" or
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
index ad7a355..9cf35f4 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/ImageCaptureTest.kt
@@ -29,10 +29,10 @@
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
import androidx.camera.testing.CoreAppTestUtil
-import androidx.camera.testing.GrantPermissionRuleUtil
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice
import java.util.concurrent.TimeUnit
import org.junit.After
@@ -57,8 +57,7 @@
)
@get:Rule
- val permissionRule =
- GrantPermissionRuleUtil.grantWithApiLevelFilter(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
private val context = ApplicationProvider.getApplicationContext<Context>()
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
index e6096d7..7ade0d5 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/LifecycleStatusChangeStressTest.kt
@@ -33,12 +33,12 @@
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
import androidx.camera.testing.CoreAppTestUtil
-import androidx.camera.testing.GrantPermissionRuleUtil
import androidx.camera.testing.LabTestRule
import androidx.lifecycle.Lifecycle
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice
import androidx.testutils.RepeatRule
import androidx.testutils.withActivity
@@ -72,8 +72,7 @@
)
@get:Rule
- val permissionRule =
- GrantPermissionRuleUtil.grantWithApiLevelFilter(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
@get:Rule
val labTest: LabTestRule = LabTestRule()
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt
index 2a009f6..eb30443 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchAvailableModesStressTest.kt
@@ -31,7 +31,6 @@
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
import androidx.camera.testing.CoreAppTestUtil
-import androidx.camera.testing.GrantPermissionRuleUtil
import androidx.camera.testing.LabTestRule
import androidx.camera.testing.StressTestRule
import androidx.test.core.app.ApplicationProvider
@@ -40,6 +39,7 @@
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice
import androidx.testutils.RepeatRule
import androidx.testutils.withActivity
@@ -70,8 +70,7 @@
)
@get:Rule
- val permissionRule =
- GrantPermissionRuleUtil.grantWithApiLevelFilter(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
@get:Rule
val labTest: LabTestRule = LabTestRule()
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt
index 7a832d46..12a63e2 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/SwitchCameraStressTest.kt
@@ -32,12 +32,12 @@
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CameraUtil.PreTestCameraIdList
import androidx.camera.testing.CoreAppTestUtil
-import androidx.camera.testing.GrantPermissionRuleUtil
import androidx.camera.testing.LabTestRule
import androidx.camera.testing.StressTestRule
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice
import androidx.testutils.RepeatRule
import androidx.testutils.withActivity
@@ -67,8 +67,7 @@
)
@get:Rule
- val permissionRule =
- GrantPermissionRuleUtil.grantWithApiLevelFilter(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
@get:Rule
val labTest: LabTestRule = LabTestRule()
diff --git a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt
index 4218633..2ba7a62 100644
--- a/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt
+++ b/camera/integration-tests/extensionstestapp/src/androidTest/java/androidx/camera/integration/extensions/camera2extensions/Camera2ExtensionsActivityTest.kt
@@ -32,7 +32,6 @@
import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.isCamera2ExtensionModeSupported
import androidx.camera.testing.CameraUtil
import androidx.camera.testing.CoreAppTestUtil
-import androidx.camera.testing.GrantPermissionRuleUtil
import androidx.camera.testing.LabTestRule
import androidx.camera.testing.StressTestRule
import androidx.lifecycle.Lifecycle
@@ -41,6 +40,7 @@
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.rule.GrantPermissionRule
import androidx.test.uiautomator.UiDevice
import org.junit.After
import org.junit.Assume
@@ -70,8 +70,7 @@
)
@get:Rule
- val permissionRule =
- GrantPermissionRuleUtil.grantWithApiLevelFilter(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ val permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE)
@get:Rule
val labTest: LabTestRule = LabTestRule()
diff --git a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
index 1bba88b..7f63e72 100644
--- a/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/extensionstestapp/src/main/AndroidManifest.xml
@@ -17,10 +17,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.camera" />
- <uses-permission
- android:name="android.permission.WRITE_EXTERNAL_STORAGE"
- android:maxSdkVersion="32" />
-
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<application
diff --git a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
index 5416004..85d64c1 100644
--- a/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
+++ b/camera/integration-tests/extensionstestapp/src/main/java/androidx/camera/integration/extensions/CameraExtensionsActivity.java
@@ -27,6 +27,7 @@
import static androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_DELETE_CAPTURED_IMAGE;
import static androidx.camera.integration.extensions.IntentExtraKey.INTENT_EXTRA_KEY_EXTENSION_MODE;
+import android.Manifest;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.PackageInfo;
@@ -92,8 +93,10 @@
import java.io.File;
import java.text.Format;
import java.text.SimpleDateFormat;
+import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -616,8 +619,31 @@
Log.e(TAG, "Failed to obtain all required permissions.", exception);
return new String[0];
}
- String[] permissions = info.requestedPermissions;
- if (permissions != null && permissions.length > 0) {
+
+ if (info.requestedPermissions == null || info.requestedPermissions.length == 0) {
+ return new String[0];
+ }
+
+ List<String> requiredPermissions = new ArrayList<>();
+
+ // From Android T, skips the permission check of WRITE_EXTERNAL_STORAGE since it won't be
+ // granted any more. When querying the requested permissions from PackageManager,
+ // READ_EXTERNAL_STORAGE will also be included if we specify WRITE_EXTERNAL_STORAGE
+ // requirement in AndroidManifest.xml. Therefore, also need to skip the permission check
+ // of READ_EXTERNAL_STORAGE.
+ for (String permission : info.requestedPermissions) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && (
+ Manifest.permission.WRITE_EXTERNAL_STORAGE.equals(permission)
+ || Manifest.permission.READ_EXTERNAL_STORAGE.equals(permission))) {
+ continue;
+ }
+
+ requiredPermissions.add(permission);
+ }
+
+ String[] permissions = requiredPermissions.toArray(new String[0]);
+
+ if (permissions.length > 0) {
return permissions;
} else {
return new String[0];
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
index 70a7ae4..ca918fdd 100644
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
@@ -22,6 +22,7 @@
import android.content.pm.ActivityInfo
import android.content.pm.PackageManager
import android.content.res.Configuration
+import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.os.Handler
@@ -264,12 +265,14 @@
// Launch the permission request for CAMERA
requestPermission.launch(Manifest.permission.CAMERA)
return false
- } else if (ContextCompat.checkSelfPermission(
+ } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
+ ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
- // Launch the permission request for WRITE_EXTERNAL_STORAGE
+ // Launch the permission request for WRITE_EXTERNAL_STORAGE. From Android T, skips to
+ // request WRITE_EXTERNAL_STORAGE permission since it won't be granted any more.
requestPermission.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
return false
}
diff --git a/camera/integration-tests/viewfindertestapp/src/main/AndroidManifest.xml b/camera/integration-tests/viewfindertestapp/src/main/AndroidManifest.xml
index cc49044..c788afa 100644
--- a/camera/integration-tests/viewfindertestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/viewfindertestapp/src/main/AndroidManifest.xml
@@ -25,6 +25,7 @@
<application
android:allowBackup="true"
+ android:requestLegacyExternalStorage="true"
android:label="@string/app_name"
android:theme="@style/AppTheme">
diff --git a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
index faa2325..22a0b7c 100644
--- a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
+++ b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
@@ -222,12 +222,16 @@
return
}
- val storagePermission = activity?.let {
- ContextCompat.checkSelfPermission(it, Manifest.permission.WRITE_EXTERNAL_STORAGE)
- }
- if (storagePermission != PackageManager.PERMISSION_GRANTED) {
- requestStoragePermission()
- return
+ // From Android T, skips the permission check of WRITE_EXTERNAL_STORAGE since it won't be
+ // granted any more.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
+ val storagePermission = activity?.let {
+ ContextCompat.checkSelfPermission(it, Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ }
+ if (storagePermission != PackageManager.PERMISSION_GRANTED) {
+ requestStoragePermission()
+ return
+ }
}
sendSurfaceRequest(false)
diff --git a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
index b164c35..eb5dc31 100644
--- a/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
+++ b/camera/integration-tests/viewtestapp/src/main/java/androidx/camera/integration/view/MainActivity.java
@@ -49,12 +49,23 @@
private static final String PREVIEW_VIEW_FRAGMENT = "PreviewView";
private static final String COMPOSE_UI_FRAGMENT = "ComposeUi";
- private static final String[] REQUIRED_PERMISSIONS =
- new String[]{
+ private static final String[] REQUIRED_PERMISSIONS;
+ static {
+ // From Android T, skips the permission check of WRITE_EXTERNAL_STORAGE since it won't be
+ // granted any more.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ REQUIRED_PERMISSIONS = new String[]{
+ Manifest.permission.CAMERA,
+ Manifest.permission.RECORD_AUDIO
+ };
+ } else {
+ REQUIRED_PERMISSIONS = new String[]{
Manifest.permission.CAMERA,
Manifest.permission.RECORD_AUDIO,
Manifest.permission.WRITE_EXTERNAL_STORAGE
};
+ }
+ }
private static final int REQUEST_CODE_PERMISSIONS = 10;
// Possible values for this intent key are the name values of LensFacing encoded as
diff --git a/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt b/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
index 75681de..447d006 100644
--- a/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
+++ b/car/app/app/api/public_plus_experimental_1.3.0-beta02.txt
@@ -38,6 +38,7 @@
method public final boolean onUnbind(android.content.Intent);
field @Deprecated public static final String CATEGORY_CHARGING_APP = "androidx.car.app.category.CHARGING";
field @androidx.car.app.annotations.RequiresCarApi(6) public static final String CATEGORY_FEATURE_CLUSTER = "androidx.car.app.category.FEATURE_CLUSTER";
+ field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_IOT_APP = "androidx.car.app.category.IOT";
field public static final String CATEGORY_NAVIGATION_APP = "androidx.car.app.category.NAVIGATION";
field @Deprecated public static final String CATEGORY_PARKING_APP = "androidx.car.app.category.PARKING";
field public static final String CATEGORY_POI_APP = "androidx.car.app.category.POI";
diff --git a/car/app/app/api/public_plus_experimental_current.txt b/car/app/app/api/public_plus_experimental_current.txt
index 75681de..447d006 100644
--- a/car/app/app/api/public_plus_experimental_current.txt
+++ b/car/app/app/api/public_plus_experimental_current.txt
@@ -38,6 +38,7 @@
method public final boolean onUnbind(android.content.Intent);
field @Deprecated public static final String CATEGORY_CHARGING_APP = "androidx.car.app.category.CHARGING";
field @androidx.car.app.annotations.RequiresCarApi(6) public static final String CATEGORY_FEATURE_CLUSTER = "androidx.car.app.category.FEATURE_CLUSTER";
+ field @androidx.car.app.annotations.ExperimentalCarApi public static final String CATEGORY_IOT_APP = "androidx.car.app.category.IOT";
field public static final String CATEGORY_NAVIGATION_APP = "androidx.car.app.category.NAVIGATION";
field @Deprecated public static final String CATEGORY_PARKING_APP = "androidx.car.app.category.PARKING";
field public static final String CATEGORY_POI_APP = "androidx.car.app.category.POI";
diff --git a/car/app/app/src/main/java/androidx/car/app/CarAppService.java b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
index 3da6251..4a8c423 100644
--- a/car/app/app/src/main/java/androidx/car/app/CarAppService.java
+++ b/car/app/app/src/main/java/androidx/car/app/CarAppService.java
@@ -122,6 +122,13 @@
public static final String CATEGORY_POI_APP = "androidx.car.app.category.POI";
/**
+ * Used in the app manifest. It declares that this app declares physical objects with sensors,
+ * that connect and exchange data with other devices and systems.
+ */
+ @ExperimentalCarApi
+ public static final String CATEGORY_IOT_APP = "androidx.car.app.category.IOT";
+
+ /**
* Used to declare that this app is a settings app in the manifest. This app can be used to
* provide screens corresponding to the settings page and/or any error resolution screens e.g.
* sign-in screen.
diff --git a/compose/compiler/compiler-hosted/integration-tests/build.gradle b/compose/compiler/compiler-hosted/integration-tests/build.gradle
index 2086707..149e956 100644
--- a/compose/compiler/compiler-hosted/integration-tests/build.gradle
+++ b/compose/compiler/compiler-hosted/integration-tests/build.gradle
@@ -53,6 +53,7 @@
testImplementation(libs.protobufLite)
testImplementation(libs.guavaAndroid)
testImplementation(project(":compose:compiler:compiler-hosted"))
+ testImplementation(projectOrArtifact(":compose:foundation:foundation"))
testImplementation(projectOrArtifact(":compose:material:material"))
testImplementation(project(":compose:runtime:runtime"))
testImplementation(projectOrArtifact(":compose:ui:ui"))
diff --git a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
index fa7fea9..ff87214 100644
--- a/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
+++ b/compose/compiler/compiler-hosted/integration-tests/src/test/java/androidx/compose/compiler/plugins/kotlin/TargetAnnotationsTransformTests.kt
@@ -1073,7 +1073,7 @@
}
BasicText(AnnotatedString(
text = "Some text"
- ), null, null, null, <unsafe-coerce>(0), false, 0, null, %composer, 0, 0b11111110)
+ ), null, null, null, <unsafe-coerce>(0), false, 0, 0, null, %composer, 0, 0b000111111110)
if (isTraceInProgress()) {
traceEventEnd()
}
diff --git a/compose/foundation/foundation/api/current.txt b/compose/foundation/foundation/api/current.txt
index d7dd537..15b1f37 100644
--- a/compose/foundation/foundation/api/current.txt
+++ b/compose/foundation/foundation/api/current.txt
@@ -888,13 +888,17 @@
}
public final class BasicTextFieldKt {
- method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
- method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
}
public final class BasicTextKt {
- method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
- method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+ method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines);
+ method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent);
}
public final class ClickableTextKt {
diff --git a/compose/foundation/foundation/api/public_plus_experimental_current.txt b/compose/foundation/foundation/api/public_plus_experimental_current.txt
index d23f77a..361a9e6 100644
--- a/compose/foundation/foundation/api/public_plus_experimental_current.txt
+++ b/compose/foundation/foundation/api/public_plus_experimental_current.txt
@@ -1132,13 +1132,17 @@
}
public final class BasicTextFieldKt {
- method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
- method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
}
public final class BasicTextKt {
- method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
- method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+ method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines);
+ method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent);
}
public final class ClickableTextKt {
diff --git a/compose/foundation/foundation/api/restricted_current.txt b/compose/foundation/foundation/api/restricted_current.txt
index d7dd537..15b1f37 100644
--- a/compose/foundation/foundation/api/restricted_current.txt
+++ b/compose/foundation/foundation/api/restricted_current.txt
@@ -888,13 +888,17 @@
}
public final class BasicTextFieldKt {
- method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
- method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional int minLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<kotlin.Unit>,kotlin.Unit> decorationBox);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(String value, kotlin.jvm.functions.Function1<? super java.lang.String,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicTextField(androidx.compose.ui.text.input.TextFieldValue value, kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.input.TextFieldValue,? extends kotlin.Unit> onValueChange, optional androidx.compose.ui.Modifier modifier, optional boolean enabled, optional boolean readOnly, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional boolean singleLine, optional int maxLines, optional androidx.compose.ui.text.input.VisualTransformation visualTransformation, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional androidx.compose.foundation.interaction.MutableInteractionSource interactionSource, optional androidx.compose.ui.graphics.Brush cursorBrush, optional kotlin.jvm.functions.Function1<? super kotlin.jvm.functions.Function0<? extends kotlin.Unit>,? extends kotlin.Unit> decorationBox);
}
public final class BasicTextKt {
- method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
- method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+ method @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines);
+ method @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional int minLines, optional java.util.Map<java.lang.String,androidx.compose.foundation.text.InlineTextContent> inlineContent);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(String text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines);
+ method @Deprecated @androidx.compose.runtime.Composable public static void BasicText(androidx.compose.ui.text.AnnotatedString text, optional androidx.compose.ui.Modifier modifier, optional androidx.compose.ui.text.TextStyle style, optional kotlin.jvm.functions.Function1<? super androidx.compose.ui.text.TextLayoutResult,? extends kotlin.Unit> onTextLayout, optional int overflow, optional boolean softWrap, optional int maxLines, optional java.util.Map<java.lang.String,? extends androidx.compose.foundation.text.InlineTextContent> inlineContent);
}
public final class ClickableTextKt {
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BasicTextMinMaxLinesDemo.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BasicTextMinMaxLinesDemo.kt
new file mode 100644
index 0000000..a4a4281
--- /dev/null
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/BasicTextMinMaxLinesDemo.kt
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2020 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.compose.foundation.demos.text
+
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.text.BasicText
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.Button
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.withStyle
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+
+@Preview
+@Composable
+fun BasicTextMinMaxLinesDemo() {
+ Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
+ TagLine("maxLines == line count")
+ TextWithMinMaxLines("Line 1\nLine 2", maxLines = 2)
+
+ TagLine("maxLines > line count")
+ TextWithMinMaxLines("abc", maxLines = 3)
+
+ TagLine("maxLines < line count")
+ TextWithMinMaxLines("Line 1\nLine 2\nLine 3", maxLines = 2)
+
+ TagLine("maxLines < line count with different line heights")
+ TextWithMinMaxLines(
+ text = buildAnnotatedString {
+ append("Line 1\n")
+ withStyle(SpanStyle(fontSize = fontSize8)) {
+ append("Line 2\n")
+ }
+ append("Line 3")
+ },
+ maxLines = 2
+ )
+
+ TagLine("minLines == line count")
+ TextWithMinMaxLines("First line\nSecond line", minLines = 2)
+
+ TagLine("minLines < line count")
+ TextWithMinMaxLines("First line\nSecond line\nThird line", minLines = 2)
+
+ TagLine("minLines > line count")
+ var sameLineHeightsHasExtraLine by remember { mutableStateOf(false) }
+ val extraLine = if (sameLineHeightsHasExtraLine) "\nLine 4" else ""
+ TextWithMinMaxLines(
+ text = "Line 1\nLine 2\nLine 3$extraLine",
+ minLines = 4
+ )
+ Button(onClick = { sameLineHeightsHasExtraLine = !sameLineHeightsHasExtraLine }) {
+ Text(text = "Toggle last line")
+ }
+
+ TagLine("minLines > line count with different line heights")
+ var diffLineHeightsHasExtraLine by remember { mutableStateOf(false) }
+ TextWithMinMaxLines(
+ text = buildAnnotatedString {
+ append("Line 1\n")
+ withStyle(SpanStyle(fontSize = fontSize6)) {
+ append("Line 2\n")
+ }
+ append("Line 3")
+ if (diffLineHeightsHasExtraLine) append("\nLine 4")
+ },
+ minLines = 4
+ )
+ Button(onClick = { diffLineHeightsHasExtraLine = !diffLineHeightsHasExtraLine }) {
+ Text(text = "Toggle last line")
+ }
+
+ TagLine("minLines < maxLines")
+ TextWithMinMaxLines(
+ "Line 1\nLine 2\nLine 3\nLine 4",
+ minLines = 2,
+ maxLines = 3
+ )
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+private fun TextWithMinMaxLines(
+ text: AnnotatedString,
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE
+) {
+ BasicText(
+ text = text,
+ modifier = Modifier
+ .border(1.dp, Color.Gray)
+ .padding(2.dp),
+ maxLines = maxLines,
+ minLines = minLines
+ )
+}
+
+@Composable
+private fun TextWithMinMaxLines(
+ text: String = "",
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE
+) {
+ TextWithMinMaxLines(
+ text = AnnotatedString(text),
+ minLines = minLines,
+ maxLines = maxLines
+ )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
index 4c56976..d760925 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/ComposeInputFieldMinMaxLines.kt
@@ -16,6 +16,7 @@
package androidx.compose.foundation.demos.text
+import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.runtime.Composable
@@ -24,8 +25,14 @@
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TransformedText
+import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.sp
@Preview
@Composable
@@ -33,36 +40,122 @@
LazyColumn {
item {
TagLine("empty text, no maxLines")
- TextFieldWithMaxLines("", maxLines = Int.MAX_VALUE)
+ TextFieldWithMinMaxLines("", maxLines = Int.MAX_VALUE)
}
item {
TagLine("maxLines == line count")
- TextFieldWithMaxLines("abc", maxLines = 1)
+ TextFieldWithMinMaxLines("abc", maxLines = 1)
}
item {
TagLine("empty text, maxLines > line count")
- TextFieldWithMaxLines("", maxLines = 2)
+ TextFieldWithMinMaxLines("", maxLines = 2)
}
item {
TagLine("maxLines > line count")
- TextFieldWithMaxLines("abc", maxLines = 4)
+ TextFieldWithMinMaxLines("abc", maxLines = 3)
}
item {
TagLine("maxLines < line count")
- TextFieldWithMaxLines("abc".repeat(20), maxLines = 1)
+ TextFieldWithMinMaxLines("abc".repeat(20), maxLines = 1)
+ }
+ item {
+ TagLine("empty text, no minLines")
+ TextFieldWithMinMaxLines("", minLines = 1)
+ }
+ item {
+ TagLine("minLines == line count")
+ TextFieldWithMinMaxLines(createMultilineText(2), minLines = 2)
+ }
+ item {
+ TagLine("empty text, minLines > line count")
+ TextFieldWithMinMaxLines("", minLines = 2)
+ }
+ item {
+ TagLine("minLines > line count")
+ TextFieldWithMinMaxLines(
+ createMultilineText(4),
+ minLines = 5
+ )
+ }
+ item {
+ TagLine("minLines < line count")
+ TextFieldWithMinMaxLines(createMultilineText(3), minLines = 2)
+ }
+ item {
+ TagLine("minLines < maxLines")
+ TextFieldWithMinMaxLines(
+ createMultilineText(4),
+ minLines = 2,
+ maxLines = 3
+ )
+ }
+ item {
+ TagLine("minLines == maxLines")
+ TextFieldWithMinMaxLines(
+ createMultilineText(2),
+ minLines = 3,
+ maxLines = 3
+ )
+ }
+ item {
+ TagLine("maxLines=4 with different line heights")
+ TextFieldWithMinMaxLines(
+ createMultilineText(5),
+ maxLines = 4,
+ spanStyles = listOf(
+ AnnotatedString.Range(SpanStyle(fontSize = 40.sp), 14, 21)
+ )
+ )
+ }
+ item {
+ TagLine("minLines=5 with different line heights")
+ TextFieldWithMinMaxLines(
+ createMultilineText(4),
+ minLines = 5,
+ spanStyles = listOf(
+ AnnotatedString.Range(SpanStyle(fontSize = 40.sp), 14, 21)
+ )
+ )
}
}
}
+@OptIn(ExperimentalFoundationApi::class)
@Composable
-private fun TextFieldWithMaxLines(str: String? = null, maxLines: Int) {
+private fun TextFieldWithMinMaxLines(
+ str: String? = null,
+ minLines: Int = 1,
+ maxLines: Int = Int.MAX_VALUE,
+ spanStyles: List<AnnotatedString.Range<SpanStyle>>? = null
+) {
val state = rememberSaveable { mutableStateOf(str ?: "abc ".repeat(20)) }
+
+ val visualTransformation: VisualTransformation =
+ if (spanStyles == null) {
+ VisualTransformation.None
+ } else {
+ VisualTransformation { annotatedString ->
+ TransformedText(
+ AnnotatedString(
+ annotatedString.text,
+ spanStyles = spanStyles
+ ),
+ OffsetMapping.Identity
+ )
+ }
+ }
+
BasicTextField(
modifier = demoTextFieldModifiers.clipToBounds(),
value = state.value,
onValueChange = { state.value = it },
textStyle = TextStyle(fontSize = fontSize8),
cursorBrush = SolidColor(Color.Red),
- maxLines = maxLines
+ minLines = minLines,
+ maxLines = maxLines,
+ visualTransformation = visualTransformation
)
-}
\ No newline at end of file
+}
+
+private fun createMultilineText(lineCount: Int) =
+ (1..lineCount).joinToString("\n") { "Line $it" }
\ No newline at end of file
diff --git a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
index e539ce9..54610b2 100644
--- a/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
+++ b/compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/text/TextDemos.kt
@@ -44,6 +44,7 @@
ComposableDemo("Layout Reuse") { TextReuseLayoutDemo() },
ComposableDemo("Line Height Behavior") { TextLineHeightDemo() },
ComposableDemo("Interactive text") { InteractiveTextDemo() },
+ ComposableDemo("Min/max lines") { BasicTextMinMaxLinesDemo() },
ComposableDemo("Ellipsize and letterspacing") { EllipsizeWithLetterSpacing() },
ComposableDemo("Line breaking") { TextLineBreakingDemo() },
DemoCategory(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
index a4fdebf..d499986 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicText.kt
@@ -52,7 +52,9 @@
* [overflow] and TextAlign may have unexpected effects.
* @param maxLines An optional maximum number of lines for the text to span, wrapping if
* necessary. If the text exceeds the given number of lines, it will be truncated according to
- * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * [overflow] and [softWrap]. It is required that 1 <= [minLines] <= [maxLines].
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
*/
@OptIn(InternalFoundationTextApi::class)
@Composable
@@ -64,11 +66,16 @@
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
+ minLines: Int = 1
) {
// NOTE(text-perf-review): consider precomputing layout here by pushing text to a channel...
// something like:
// remember(text) { precomputeTextLayout(text) }
- require(maxLines > 0) { "maxLines should be greater than 0" }
+
+ // Unlike text field for which validation happens inside the 'heightInLines' modifier, in text
+ // 'maxLines' are not handled by the modifier but instead passed to the StaticLayout, therefore
+ // we perform validation here
+ validateMinMaxLines(minLines, maxLines)
// selection registrar, if no SelectionContainer is added ambient value will be null
val selectionRegistrar = LocalSelectionRegistrar.current
@@ -104,6 +111,7 @@
fontFamilyResolver = fontFamilyResolver,
overflow = overflow,
maxLines = maxLines,
+ minLines = minLines,
),
selectableId
)
@@ -121,6 +129,7 @@
fontFamilyResolver = fontFamilyResolver,
overflow = overflow,
maxLines = maxLines,
+ minLines = minLines,
)
)
}
@@ -130,7 +139,7 @@
state.selectionBackgroundColor = LocalTextSelectionColors.current.backgroundColor
}
- Layout(modifier.then(controller.modifiers), controller.measurePolicy)
+ Layout(modifier = modifier.then(controller.modifiers), measurePolicy = controller.measurePolicy)
}
/**
@@ -151,7 +160,9 @@
* [overflow] and TextAlign may have unexpected effects.
* @param maxLines An optional maximum number of lines for the text to span, wrapping if
* necessary. If the text exceeds the given number of lines, it will be truncated according to
- * [overflow] and [softWrap]. If it is not null, then it must be greater than zero.
+ * [overflow] and [softWrap]. It is required that 1 <= [minLines] <= [maxLines].
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
* @param inlineContent A map store composables that replaces certain ranges of the text. It's
* used to insert composables into text layout. Check [InlineTextContent] for more information.
*/
@@ -165,9 +176,13 @@
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
- inlineContent: Map<String, InlineTextContent> = mapOf(),
+ minLines: Int = 1,
+ inlineContent: Map<String, InlineTextContent> = mapOf()
) {
- require(maxLines > 0) { "maxLines should be greater than 0" }
+ // Unlike text field for which validation happens inside the 'heightInLines' modifier, in text
+ // 'maxLines' are not handled by the modifier but instead passed to the StaticLayout, therefore
+ // we perform validation here
+ validateMinMaxLines(minLines, maxLines)
// selection registrar, if no SelectionContainer is added ambient value will be null
val selectionRegistrar = LocalSelectionRegistrar.current
@@ -244,10 +259,58 @@
)
}
+@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
+@Composable
+fun BasicText(
+ text: String,
+ modifier: Modifier = Modifier,
+ style: TextStyle = TextStyle.Default,
+ onTextLayout: (TextLayoutResult) -> Unit = {},
+ overflow: TextOverflow = TextOverflow.Clip,
+ softWrap: Boolean = true,
+ maxLines: Int = Int.MAX_VALUE
+) {
+ BasicText(
+ text = text,
+ modifier = modifier,
+ style = style,
+ onTextLayout = onTextLayout,
+ overflow = overflow,
+ softWrap = softWrap,
+ minLines = 1,
+ maxLines = maxLines
+ )
+}
+
+@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
+@Composable
+fun BasicText(
+ text: AnnotatedString,
+ modifier: Modifier = Modifier,
+ style: TextStyle = TextStyle.Default,
+ onTextLayout: (TextLayoutResult) -> Unit = {},
+ overflow: TextOverflow = TextOverflow.Clip,
+ softWrap: Boolean = true,
+ maxLines: Int = Int.MAX_VALUE,
+ inlineContent: Map<String, InlineTextContent> = mapOf(),
+) {
+ BasicText(
+ text = text,
+ modifier = modifier,
+ style = style,
+ onTextLayout = onTextLayout,
+ overflow = overflow,
+ softWrap = softWrap,
+ minLines = 1,
+ maxLines = maxLines,
+ inlineContent = inlineContent
+ )
+}
+
/**
* A custom saver that won't save if no selection is active.
*/
private fun selectionIdSaver(selectionRegistrar: SelectionRegistrar?) = Saver<Long, Long>(
save = { if (selectionRegistrar.hasSelection(it)) it else null },
restore = { it }
-)
\ No newline at end of file
+)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
index 5ce4c33..e68d5b4 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/BasicTextField.kt
@@ -94,11 +94,12 @@
* [KeyboardOptions.imeAction].
* @param singleLine when set to true, this text field becomes a single horizontally scrolling
* text field instead of wrapping onto multiple lines. The keyboard will be informed to not show
- * the return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the
- * maxLines attribute will be automatically set to 1.
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true.
+ * the return key as the [ImeAction]. [maxLines] and [minLines] are ignored as both are
+ * automatically set to 1.
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
* @param visualTransformation The visual transformation filter for changing the visual
* representation of the input. By default no visual transformation is applied.
* @param onTextLayout Callback that is executed when a new text layout is calculated. A
@@ -129,7 +130,8 @@
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
- maxLines: Int = Int.MAX_VALUE,
+ maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+ minLines: Int = 1,
visualTransformation: VisualTransformation = VisualTransformation.None,
onTextLayout: (TextLayoutResult) -> Unit = {},
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@@ -177,6 +179,7 @@
imeOptions = keyboardOptions.toImeOptions(singleLine = singleLine),
keyboardActions = keyboardActions,
softWrap = !singleLine,
+ minLines = if (singleLine) 1 else minLines,
maxLines = if (singleLine) 1 else maxLines,
decorationBox = decorationBox,
enabled = enabled,
@@ -237,11 +240,12 @@
* [KeyboardOptions.imeAction].
* @param singleLine when set to true, this text field becomes a single horizontally scrolling
* text field instead of wrapping onto multiple lines. The keyboard will be informed to not show
- * the return key as the [ImeAction]. Note that [maxLines] parameter will be ignored as the
- * maxLines attribute will be automatically set to 1.
- * @param maxLines the maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1. Note that this parameter will be ignored and instead maxLines will be
- * set to 1 if [singleLine] is set to true.
+ * the return key as the [ImeAction]. [maxLines] and [minLines] are ignored as both are
+ * automatically set to 1.
+ * @param maxLines the maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
+ * @param minLines the minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true.
* @param visualTransformation The visual transformation filter for changing the visual
* representation of the input. By default no visual transformation is applied.
* @param onTextLayout Callback that is executed when a new text layout is calculated. A
@@ -272,7 +276,8 @@
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
singleLine: Boolean = false,
- maxLines: Int = Int.MAX_VALUE,
+ maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE,
+ minLines: Int = 1,
visualTransformation: VisualTransformation = VisualTransformation.None,
onTextLayout: (TextLayoutResult) -> Unit = {},
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
@@ -296,9 +301,90 @@
imeOptions = keyboardOptions.toImeOptions(singleLine = singleLine),
keyboardActions = keyboardActions,
softWrap = !singleLine,
+ minLines = if (singleLine) 1 else minLines,
maxLines = if (singleLine) 1 else maxLines,
decorationBox = decorationBox,
enabled = enabled,
readOnly = readOnly
)
}
+
+@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
+@Composable
+fun BasicTextField(
+ value: String,
+ onValueChange: (String) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ readOnly: Boolean = false,
+ textStyle: TextStyle = TextStyle.Default,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ singleLine: Boolean = false,
+ maxLines: Int = Int.MAX_VALUE,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ onTextLayout: (TextLayoutResult) -> Unit = {},
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ cursorBrush: Brush = SolidColor(Color.Black),
+ decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
+ @Composable { innerTextField -> innerTextField() }
+) {
+ BasicTextField(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = modifier,
+ enabled = enabled,
+ readOnly = readOnly,
+ textStyle = textStyle,
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions,
+ singleLine = singleLine,
+ minLines = 1,
+ maxLines = maxLines,
+ visualTransformation = visualTransformation,
+ onTextLayout = onTextLayout,
+ interactionSource = interactionSource,
+ cursorBrush = cursorBrush,
+ decorationBox = decorationBox
+ )
+}
+
+@Deprecated("Maintained for binary compatibility", level = DeprecationLevel.HIDDEN)
+@Composable
+fun BasicTextField(
+ value: TextFieldValue,
+ onValueChange: (TextFieldValue) -> Unit,
+ modifier: Modifier = Modifier,
+ enabled: Boolean = true,
+ readOnly: Boolean = false,
+ textStyle: TextStyle = TextStyle.Default,
+ keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
+ keyboardActions: KeyboardActions = KeyboardActions.Default,
+ singleLine: Boolean = false,
+ maxLines: Int = Int.MAX_VALUE,
+ visualTransformation: VisualTransformation = VisualTransformation.None,
+ onTextLayout: (TextLayoutResult) -> Unit = {},
+ interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
+ cursorBrush: Brush = SolidColor(Color.Black),
+ decorationBox: @Composable (innerTextField: @Composable () -> Unit) -> Unit =
+ @Composable { innerTextField -> innerTextField() }
+) {
+ BasicTextField(
+ value = value,
+ onValueChange = onValueChange,
+ modifier = modifier,
+ enabled = enabled,
+ readOnly = readOnly,
+ textStyle = textStyle,
+ keyboardOptions = keyboardOptions,
+ keyboardActions = keyboardActions,
+ singleLine = singleLine,
+ minLines = 1,
+ maxLines = maxLines,
+ visualTransformation = visualTransformation,
+ onTextLayout = onTextLayout,
+ interactionSource = interactionSource,
+ cursorBrush = cursorBrush,
+ decorationBox = decorationBox
+ )
+}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
index 203924d..f9a41e9 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreText.kt
@@ -462,7 +462,7 @@
}
state.previousGlobalPosition = newGlobalPosition
}
- }
+ }.heightInLines(state.textDelegate.style, state.textDelegate.minLines)
/*@VisibleForTesting*/
internal var semanticsModifier = createSemanticsModifierFor(state.textDelegate.text)
@@ -560,6 +560,7 @@
softWrap: Boolean = true,
overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
+ minLines: Int = DefaultMinLines,
placeholders: List<AnnotatedString.Range<Placeholder>>
): TextDelegate {
// NOTE(text-perf-review): whenever we have remember intrinsic implemented, this might be a
@@ -569,6 +570,7 @@
current.softWrap != softWrap ||
current.overflow != overflow ||
current.maxLines != maxLines ||
+ current.minLines != minLines ||
current.density != density ||
current.placeholders != placeholders ||
current.fontFamilyResolver !== fontFamilyResolver
@@ -579,6 +581,7 @@
softWrap = softWrap,
overflow = overflow,
maxLines = maxLines,
+ minLines = minLines,
density = density,
fontFamilyResolver = fontFamilyResolver,
placeholders = placeholders,
@@ -598,6 +601,7 @@
softWrap: Boolean = true,
overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
+ minLines: Int = DefaultMinLines,
): TextDelegate {
// NOTE(text-perf-review): whenever we have remember intrinsic implemented, this might be a
// lot slower than the equivalent `remember(a, b, c, ...) { ... }` call.
@@ -606,6 +610,7 @@
current.softWrap != softWrap ||
current.overflow != overflow ||
current.maxLines != maxLines ||
+ current.minLines != minLines ||
current.density != density ||
current.fontFamilyResolver !== fontFamilyResolver
) {
@@ -615,6 +620,7 @@
softWrap = softWrap,
overflow = overflow,
maxLines = maxLines,
+ minLines = minLines,
density = density,
fontFamilyResolver = fontFamilyResolver,
)
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
index cc57d5d..9bb0804 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/CoreTextField.kt
@@ -154,8 +154,10 @@
* provided, there will be no cursor drawn
* @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
* text will be positioned as if there was unlimited horizontal space.
- * @param maxLines The maximum height in terms of maximum number of visible lines. Should be
- * equal or greater than 1.
+ * @param maxLines The maximum height in terms of maximum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
* @param imeOptions Contains different IME configuration options.
* @param keyboardActions when the input service emits an IME action, the corresponding callback
* is called. Note that this IME action may be different from what you specified in
@@ -185,6 +187,7 @@
cursorBrush: Brush = SolidColor(Color.Unspecified),
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
+ minLines: Int = DefaultMinLines,
imeOptions: ImeOptions = ImeOptions.Default,
keyboardActions: KeyboardActions = KeyboardActions.Default,
enabled: Boolean = true,
@@ -550,6 +553,7 @@
.heightIn(min = state.minHeightForSingleLineField)
.heightInLines(
textStyle = textStyle,
+ minLines = minLines,
maxLines = maxLines
)
.textFieldScroll(
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
index 2092f2d..7284b68 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/HeightInLinesModifier.kt
@@ -34,8 +34,10 @@
/**
* The default minimum height in terms of minimum number of visible lines.
+ *
+ * Should not be used in public API and samples unless it's public, too.
*/
-internal const val DefaultMinLines: Int = 1
+internal const val DefaultMinLines = 1
/**
* Constraint the height of the text field so that it vertically occupies at least [minLines]
@@ -53,15 +55,7 @@
properties["textStyle"] = textStyle
}
) {
- require(minLines > 0) {
- "minLines must be greater than 0"
- }
- require(maxLines > 0) {
- "maxLines must be greater than 0"
- }
- require(minLines <= maxLines) {
- "minLines $minLines must be lower than or equal to maxLines $maxLines"
- }
+ validateMinMaxLines(minLines, maxLines)
if (minLines == DefaultMinLines && maxLines == Int.MAX_VALUE) return@composed Modifier
val density = LocalDensity.current
@@ -126,4 +120,13 @@
max = precomputedMaxLinesHeight?.toDp() ?: Dp.Unspecified
)
}
+}
+
+internal fun validateMinMaxLines(minLines: Int, maxLines: Int) {
+ require(minLines > 0 && maxLines > 0) {
+ "both minLines $minLines and maxLines $maxLines must be greater than zero"
+ }
+ require(minLines <= maxLines) {
+ "minLines $minLines must be less than or equal to maxLines $maxLines"
+ }
}
\ No newline at end of file
diff --git a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
index 7a5b425..cdfbddf 100644
--- a/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
+++ b/compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/text/TextDelegate.kt
@@ -63,7 +63,10 @@
*
* @param maxLines An optional maximum number of lines for the text to span, wrapping if
* necessary. If the text exceeds the given number of lines, it is truncated such that subsequent
- * lines are dropped.
+ * lines are dropped. It is required that 1 <= [minLines] <= [maxLines].
+ *
+ * @param minLines The minimum height in terms of minimum number of visible lines. It is required
+ * that 1 <= [minLines] <= [maxLines].
*
* @param softWrap Whether the text should break at soft line breaks. If false, the glyphs in the
* text will be positioned as if there was unlimited horizontal space. If [softWrap] is false,
@@ -86,6 +89,7 @@
val text: AnnotatedString,
val style: TextStyle,
val maxLines: Int = Int.MAX_VALUE,
+ val minLines: Int = DefaultMinLines,
val softWrap: Boolean = true,
val overflow: TextOverflow = TextOverflow.Clip,
val density: Density,
@@ -118,6 +122,8 @@
init {
check(maxLines > 0)
+ check(minLines > 0)
+ check(minLines <= maxLines)
}
fun layoutIntrinsics(layoutDirection: LayoutDirection) {
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
index 9ff9b59..e371923 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextControllerTest.kt
@@ -17,6 +17,7 @@
package androidx.compose.foundation.text
import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.TextStyle
import com.google.common.truth.Truth.assertThat
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
@@ -31,10 +32,12 @@
fun `semantics modifier recreated when TextDelegate is set`() {
val textDelegateBefore = mock<TextDelegate>() {
whenever(it.text).thenReturn(AnnotatedString("Example Text String 1"))
+ whenever(it.style).thenReturn(TextStyle.Default)
}
val textDelegateAfter = mock<TextDelegate>() {
whenever(it.text).thenReturn(AnnotatedString("Example Text String 2"))
+ whenever(it.style).thenReturn(TextStyle.Default)
}
// Make sure that mock doesn't do smart memory management:
diff --git a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt
index d2ceef2..a878a1c 100644
--- a/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt
+++ b/compose/foundation/foundation/src/test/kotlin/androidx/compose/foundation/text/TextDelegateTest.kt
@@ -43,6 +43,7 @@
)
assertThat(textDelegate.maxLines).isEqualTo(Int.MAX_VALUE)
+ assertThat(textDelegate.minLines).isEqualTo(DefaultMinLines)
assertThat(textDelegate.overflow).isEqualTo(TextOverflow.Clip)
}
@@ -75,6 +76,21 @@
}
@Test
+ fun `constructor with customized minLines`() {
+ val minLines = 8
+
+ val textDelegate = TextDelegate(
+ text = AnnotatedString(text = ""),
+ style = TextStyle.Default,
+ minLines = minLines,
+ density = density,
+ fontFamilyResolver = fontFamilyResolver
+ )
+
+ assertThat(textDelegate.minLines).isEqualTo(minLines)
+ }
+
+ @Test
fun `constructor with customized overflow`() {
val overflow = TextOverflow.Ellipsis
diff --git a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
index c8ef43b..ec1bd54 100644
--- a/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
+++ b/compose/material/material/src/commonMain/kotlin/androidx/compose/material/Text.kt
@@ -128,13 +128,13 @@
)
)
BasicText(
- text,
- modifier,
- mergedStyle,
- onTextLayout,
- overflow,
- softWrap,
- maxLines,
+ text = text,
+ modifier = modifier,
+ style = mergedStyle,
+ onTextLayout = onTextLayout,
+ overflow = overflow,
+ softWrap = softWrap,
+ maxLines = maxLines,
)
}
@@ -231,14 +231,14 @@
)
)
BasicText(
- text,
- modifier,
- mergedStyle,
- onTextLayout,
- overflow,
- softWrap,
- maxLines,
- inlineContent
+ text = text,
+ modifier = modifier,
+ style = mergedStyle,
+ onTextLayout = onTextLayout,
+ overflow = overflow,
+ softWrap = softWrap,
+ maxLines = maxLines,
+ inlineContent = inlineContent
)
}
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
index c77e134..1705992 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/AppBar.kt
@@ -27,7 +27,6 @@
import androidx.compose.animation.core.animateTo
import androidx.compose.animation.core.spring
import androidx.compose.animation.rememberSplineBasedDecay
-import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.draggable
import androidx.compose.foundation.gestures.rememberDraggableState
@@ -1163,11 +1162,10 @@
Modifier
}
- Surface(modifier = modifier.then(appBarDragModifier)) {
+ Surface(modifier = modifier.then(appBarDragModifier), color = appBarContainerColor) {
Column {
TopAppBarLayout(
modifier = Modifier
- .background(color = appBarContainerColor)
.windowInsetsPadding(windowInsets)
// clip after padding so we don't show the title over the inset area
.clipToBounds(),
diff --git a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
index f482132..060651b 100644
--- a/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
+++ b/compose/material3/material3/src/commonMain/kotlin/androidx/compose/material3/Text.kt
@@ -227,14 +227,14 @@
)
)
BasicText(
- text,
- modifier,
- mergedStyle,
- onTextLayout,
- overflow,
- softWrap,
- maxLines,
- inlineContent
+ text = text,
+ modifier = modifier,
+ style = mergedStyle,
+ onTextLayout = onTextLayout,
+ overflow = overflow,
+ softWrap = softWrap,
+ maxLines = maxLines,
+ inlineContent = inlineContent
)
}
diff --git a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt
index 70b82b1..0f04167 100644
--- a/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt
+++ b/compose/ui/ui-text/src/androidAndroidTest/kotlin/androidx/compose/ui/text/platform/ApplySpanStyleTest.kt
@@ -17,14 +17,23 @@
package androidx.compose.ui.text.platform
import android.graphics.Typeface
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Shadow
+import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontSynthesis
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.platform.extensions.applySpanStyle
+import androidx.compose.ui.text.style.BaselineShift
import androidx.compose.ui.text.style.TextDecoration
+import androidx.compose.ui.text.style.TextGeometricTransform
import androidx.compose.ui.unit.Density
+import androidx.compose.ui.unit.TextUnit
+import androidx.compose.ui.unit.em
+import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
@@ -44,6 +53,212 @@
}
@Test
+ fun fontSizeSp_shouldBeAppliedTo_textSize() {
+ val fontSize = 24.sp
+ val spanStyle = SpanStyle(fontSize = fontSize)
+ val tp = AndroidTextPaint(0, density.density)
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.textSize).isEqualTo(with(density) { fontSize.toPx() })
+ assertThat(notApplied.fontSize).isEqualTo(TextUnit.Unspecified)
+ }
+
+ @Test
+ fun fontSizeEm_shouldBeAppliedTo_textSize() {
+ val fontSize = 2.em
+ val spanStyle = SpanStyle(fontSize = fontSize)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.textSize = 30f
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.textSize).isEqualTo(60f)
+ assertThat(notApplied.fontSize).isEqualTo(TextUnit.Unspecified)
+ }
+
+ @Test
+ fun textGeometricTransform_shouldBeAppliedTo_scaleSkew() {
+ val textGeometricTransform = TextGeometricTransform(
+ scaleX = 1.5f,
+ skewX = 1f
+ )
+ val spanStyle = SpanStyle(textGeometricTransform = textGeometricTransform)
+ val tp = AndroidTextPaint(0, density.density)
+ val originalSkew = tp.textSkewX
+ val originalScale = tp.textScaleX
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.textSkewX).isEqualTo(originalSkew + textGeometricTransform.skewX)
+ assertThat(tp.textScaleX).isEqualTo(originalScale * textGeometricTransform.scaleX)
+ assertThat(notApplied.textGeometricTransform).isNull()
+ }
+
+ @Test
+ fun letterSpacingSp_shouldBeLeftAsSpan() {
+ val letterSpacing = 10.sp
+ val spanStyle = SpanStyle(letterSpacing = letterSpacing)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.letterSpacing = 4f
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.letterSpacing).isEqualTo(4f)
+ assertThat(notApplied.letterSpacing).isEqualTo(letterSpacing)
+ }
+
+ @Test
+ fun letterSpacingEm_shouldBeAppliedTo_letterSpacing() {
+ val letterSpacing = 1.5.em
+ val spanStyle = SpanStyle(letterSpacing = letterSpacing)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.letterSpacing = 4f
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.letterSpacing).isEqualTo(1.5f)
+ assertThat(notApplied.letterSpacing).isEqualTo(TextUnit.Unspecified)
+ }
+
+ @Test
+ fun letterSpacingUnspecified_shouldBeNoOp() {
+ val letterSpacing = TextUnit.Unspecified
+ val spanStyle = SpanStyle(letterSpacing = letterSpacing)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.letterSpacing = 4f
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.letterSpacing).isEqualTo(4f)
+ assertThat(notApplied.letterSpacing).isEqualTo(TextUnit.Unspecified)
+ }
+
+ @Test
+ fun nonEmptyFontFeatureSettings_shouldBeAppliedTo_fontFeatureSettings() {
+ val fontFeatureSettings = "\"kern\" 0"
+ val spanStyle = SpanStyle(fontFeatureSettings = fontFeatureSettings)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.fontFeatureSettings = ""
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.fontFeatureSettings).isEqualTo(fontFeatureSettings)
+ assertThat(notApplied.fontFeatureSettings).isNull()
+ }
+
+ @Test
+ fun emptyFontFeatureSettings_shouldBeNotAppliedTo_fontFeatureSettings() {
+ val fontFeatureSettings = ""
+ val spanStyle = SpanStyle(fontFeatureSettings = fontFeatureSettings)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.fontFeatureSettings = "\"kern\" 0"
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.fontFeatureSettings).isEqualTo("\"kern\" 0")
+ assertThat(notApplied.fontFeatureSettings).isNull()
+ }
+
+ @Test
+ fun fontSettings_shouldBeAppliedTo_typeface() {
+ val fontFamily = FontFamily.Cursive
+ val fontWeight = FontWeight.W800
+ val fontStyle = FontStyle.Italic
+ val fontSynthesis = FontSynthesis.Style
+
+ val spanStyle = SpanStyle(
+ fontFamily = fontFamily,
+ fontWeight = fontWeight,
+ fontStyle = fontStyle,
+ fontSynthesis = fontSynthesis
+ )
+
+ val tp = AndroidTextPaint(0, density.density)
+ tp.typeface = Typeface.DEFAULT
+
+ var calledFontFamily: FontFamily? = null
+ var calledFontWeight: FontWeight? = null
+ var calledFontStyle: FontStyle? = null
+ var calledFontSynthesis: FontSynthesis? = null
+
+ val notApplied = tp.applySpanStyle(
+ spanStyle,
+ { family, weight, style, synthesis ->
+ calledFontFamily = family
+ calledFontWeight = weight
+ calledFontStyle = style
+ calledFontSynthesis = synthesis
+ Typeface.MONOSPACE
+ },
+ density
+ )
+
+ assertThat(tp.typeface).isEqualTo(Typeface.MONOSPACE)
+ assertThat(calledFontFamily).isEqualTo(fontFamily)
+ assertThat(calledFontWeight).isEqualTo(fontWeight)
+ assertThat(calledFontStyle).isEqualTo(fontStyle)
+ assertThat(calledFontSynthesis).isEqualTo(fontSynthesis)
+
+ assertThat(notApplied.fontFamily).isNull()
+ assertThat(notApplied.fontWeight).isNull()
+ assertThat(notApplied.fontStyle).isNull()
+ assertThat(notApplied.fontSynthesis).isNull()
+ }
+
+ @Test
+ fun baselineShift_shouldBeLeftAsSpan() {
+ val baselineShift = BaselineShift(0.8f)
+ val spanStyle = SpanStyle(baselineShift = baselineShift)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.baselineShift = 0
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.baselineShift).isEqualTo(0)
+ assertThat(notApplied.baselineShift).isEqualTo(baselineShift)
+ }
+
+ @Test
+ fun baselineShiftNone_shouldNotBeLeftAsSpan() {
+ val baselineShift = BaselineShift.None
+ val spanStyle = SpanStyle(baselineShift = baselineShift)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.baselineShift = 0
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.baselineShift).isEqualTo(0)
+ assertThat(notApplied.baselineShift).isNull()
+ }
+
+ @Test
+ fun background_shouldBeLeftAsSpan() {
+ val background = Color.Red
+ val spanStyle = SpanStyle(background = background)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.color = Color.Black.toArgb()
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.color).isEqualTo(Color.Black.toArgb())
+ assertThat(notApplied.background).isEqualTo(background)
+ }
+
+ @Test
+ fun backgroundTransparent_shouldNotBeLeftAsSpan() {
+ val background = Color.Transparent
+ val spanStyle = SpanStyle(background = background)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.color = Color.Black.toArgb()
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.color).isEqualTo(Color.Black.toArgb())
+ assertThat(notApplied.background).isEqualTo(Color.Unspecified)
+ }
+
+ @Test
fun textDecorationUnderline_shouldBeLeftAsSpan() {
val textDecoration = TextDecoration.Underline
val spanStyle = SpanStyle(textDecoration = textDecoration)
@@ -83,4 +298,30 @@
assertThat(tp.isStrikeThruText).isEqualTo(false)
assertThat(notApplied.textDecoration).isNull()
}
+
+ @Test
+ fun shadow_shouldBeAppliedTo_shadowLayer() {
+ val shadow = Shadow(Color.Red, Offset(4f, 4f), blurRadius = 8f)
+ val spanStyle = SpanStyle(shadow = shadow)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.clearShadowLayer()
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.shadow).isEqualTo(shadow)
+ assertThat(notApplied.shadow).isNull()
+ }
+
+ @Test
+ fun color_shouldBeAppliedTo_color() {
+ val color = Color.Red
+ val spanStyle = SpanStyle(color = color)
+ val tp = AndroidTextPaint(0, density.density)
+ tp.color = Color.Black.toArgb()
+
+ val notApplied = tp.applySpanStyle(spanStyle, resolveTypeface, density)
+
+ assertThat(tp.color).isEqualTo(Color.Red.toArgb())
+ assertThat(notApplied.color).isEqualTo(Color.Unspecified)
+ }
}
\ No newline at end of file
diff --git a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt
index 3b6a1cc..e7e664d 100644
--- a/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt
+++ b/compose/ui/ui-text/src/androidMain/kotlin/androidx/compose/ui/text/platform/AndroidTextPaint.android.kt
@@ -45,7 +45,9 @@
}
private var textDecoration: TextDecoration = TextDecoration.None
- private var shadow: Shadow = Shadow.None
+
+ @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
+ internal var shadow: Shadow = Shadow.None
@VisibleForTesting
internal var brush: Brush? = null
diff --git a/development/importMaven/build.gradle.kts b/development/importMaven/build.gradle.kts
index a2f23ee..38f9bc1 100644
--- a/development/importMaven/build.gradle.kts
+++ b/development/importMaven/build.gradle.kts
@@ -65,7 +65,7 @@
}
tasks.withType<KotlinCompile> {
- kotlinOptions.jvmTarget = "17"
+ kotlinOptions.jvmTarget = "11"
}
// b/250726951 Gradle ProjectBuilder needs reflection access to java.lang.
@@ -82,4 +82,4 @@
// some jars will be duplicate, we can pick any since they are
// versioned.
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
-}
\ No newline at end of file
+}
diff --git a/health/connect/connect-client-proto/src/main/proto/permission.proto b/health/connect/connect-client-proto/src/main/proto/permission.proto
index 97c4903..14fd285 100644
--- a/health/connect/connect-client-proto/src/main/proto/permission.proto
+++ b/health/connect/connect-client-proto/src/main/proto/permission.proto
@@ -28,7 +28,11 @@
ACCESS_TYPE_WRITE = 2;
}
+// Represents both the new and the old permission format.
+// If "permission" is set, the other 2 fields will be ignored.
message Permission {
optional DataType data_type = 1;
optional AccessType access_type = 2;
+
+ optional string permission = 3;
}
diff --git a/health/connect/connect-client/api/current.txt b/health/connect/connect-client/api/current.txt
index ca671b0..3578818 100644
--- a/health/connect/connect-client/api/current.txt
+++ b/health/connect/connect-client/api/current.txt
@@ -13,8 +13,9 @@
method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
method public androidx.health.connect.client.PermissionController getPermissionController();
method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
- method public default static boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
- method public default static boolean isAvailable(android.content.Context context);
+ method public default static boolean isApiSupported();
+ method public default static boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+ method public default static boolean isProviderAvailable(android.content.Context context);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -25,8 +26,9 @@
public static final class HealthConnectClient.Companion {
method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
- method public boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
- method public boolean isAvailable(android.content.Context context);
+ method public boolean isApiSupported();
+ method public boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+ method public boolean isProviderAvailable(android.content.Context context);
}
public interface PermissionController {
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 ca671b0..3578818 100644
--- a/health/connect/connect-client/api/public_plus_experimental_current.txt
+++ b/health/connect/connect-client/api/public_plus_experimental_current.txt
@@ -13,8 +13,9 @@
method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
method public androidx.health.connect.client.PermissionController getPermissionController();
method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
- method public default static boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
- method public default static boolean isAvailable(android.content.Context context);
+ method public default static boolean isApiSupported();
+ method public default static boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+ method public default static boolean isProviderAvailable(android.content.Context context);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -25,8 +26,9 @@
public static final class HealthConnectClient.Companion {
method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
- method public boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
- method public boolean isAvailable(android.content.Context context);
+ method public boolean isApiSupported();
+ method public boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+ method public boolean isProviderAvailable(android.content.Context context);
}
public interface PermissionController {
diff --git a/health/connect/connect-client/api/restricted_current.txt b/health/connect/connect-client/api/restricted_current.txt
index 5965aa4..4acb5a6 100644
--- a/health/connect/connect-client/api/restricted_current.txt
+++ b/health/connect/connect-client/api/restricted_current.txt
@@ -13,8 +13,9 @@
method public default static androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
method public androidx.health.connect.client.PermissionController getPermissionController();
method public suspend Object? insertRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.InsertRecordsResponse>);
- method public default static boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
- method public default static boolean isAvailable(android.content.Context context);
+ method public default static boolean isApiSupported();
+ method public default static boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+ method public default static boolean isProviderAvailable(android.content.Context context);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecord(kotlin.reflect.KClass<T> recordType, String recordId, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordResponse<T>>);
method public suspend <T extends androidx.health.connect.client.records.Record> Object? readRecords(androidx.health.connect.client.request.ReadRecordsRequest<T> request, kotlin.coroutines.Continuation<? super androidx.health.connect.client.response.ReadRecordsResponse<T>>);
method public suspend Object? updateRecords(java.util.List<? extends androidx.health.connect.client.records.Record> records, kotlin.coroutines.Continuation<? super kotlin.Unit>);
@@ -25,8 +26,9 @@
public static final class HealthConnectClient.Companion {
method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
method public androidx.health.connect.client.HealthConnectClient getOrCreate(android.content.Context context);
- method public boolean isAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
- method public boolean isAvailable(android.content.Context context);
+ method public boolean isApiSupported();
+ method public boolean isProviderAvailable(android.content.Context context, optional java.util.List<java.lang.String> providerPackageNames);
+ method public boolean isProviderAvailable(android.content.Context context);
}
public interface PermissionController {
diff --git a/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/AvailabilitySamples.kt b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/AvailabilitySamples.kt
new file mode 100644
index 0000000..6bff47b
--- /dev/null
+++ b/health/connect/connect-client/samples/src/main/java/androidx/health/connect/client/samples/AvailabilitySamples.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.
+ */
+
+@file:Suppress("UNUSED_VARIABLE")
+
+package androidx.health.connect.client.samples
+
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import androidx.annotation.Sampled
+import androidx.health.connect.client.HealthConnectClient
+
+@Sampled
+suspend fun AvailabilityCheckSamples(context: Context, providerPackageName: String) {
+ if (!HealthConnectClient.isApiSupported()) {
+ return // early return as there is no viable integration
+ }
+ if (!HealthConnectClient.isProviderAvailable(context)) {
+ // Optionally redirect to package installer to find a provider, for example:
+ val uriString =
+ "market://details?id=$providerPackageName&url=healthconnect%3A%2F%2Fonboarding"
+ context.startActivity(
+ Intent(Intent.ACTION_VIEW).apply {
+ setPackage("com.android.vending")
+ data = Uri.parse(uriString)
+ putExtra("overlay", true)
+ putExtra("callerId", context.packageName)
+ }
+ )
+ return
+ }
+ val healthConnectClient = HealthConnectClient.getOrCreate(context)
+ // Issue operations with healthConnectClient
+}
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IFilterGrantedPermissionsCallback.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IFilterGrantedPermissionsCallback.aidl
new file mode 100644
index 0000000..ce4a20c
--- /dev/null
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IFilterGrantedPermissionsCallback.aidl
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 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.health.platform.client.service;
+
+import androidx.health.platform.client.permission.Permission;
+import androidx.health.platform.client.error.ErrorStatus;
+
+oneway interface IFilterGrantedPermissionsCallback {
+ void onSuccess(in List<Permission> permissions) = 0;
+ void onError(in ErrorStatus status) = 1;
+}
diff --git a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
index da62ba3..a3806cb 100644
--- a/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
+++ b/health/connect/connect-client/src/main/aidl/androidx/health/platform/client/service/IHealthDataService.aidl
@@ -36,6 +36,7 @@
import androidx.health.platform.client.service.IDeleteDataRangeCallback;
import androidx.health.platform.client.service.IReadDataRangeCallback;
import androidx.health.platform.client.service.IUpdateDataCallback;
+import androidx.health.platform.client.service.IFilterGrantedPermissionsCallback;
import androidx.health.platform.client.service.IUpsertExerciseRouteCallback;
import androidx.health.platform.client.service.IInsertDataCallback;
import androidx.health.platform.client.service.IReadDataCallback;
@@ -50,11 +51,11 @@
* API version of the AIDL interface. Should be incremented every time a new
* method is added.
*/
- const int CURRENT_API_VERSION = 3;
+ const int CURRENT_API_VERSION = 4;
const int MIN_API_VERSION = 1;
- // Next Id: 22
+ // Next Id: 23
/**
* Returns version of this AIDL interface.
@@ -66,6 +67,8 @@
void getGrantedPermissions(in RequestContext context, in List<Permission> permissions, in IGetGrantedPermissionsCallback callback) = 3;
+ void filterGrantedPermissions(in RequestContext context, in List<Permission> permissions, in IFilterGrantedPermissionsCallback callback) = 22;
+
void revokeAllPermissions(in RequestContext context, in IRevokeAllPermissionsCallback callback) = 8;
void insertData(in RequestContext context, in UpsertDataRequest request, in IInsertDataCallback callback) = 9;
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
index 4154168..77cc947 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/HealthConnectClient.kt
@@ -357,19 +357,40 @@
internal const val DEFAULT_PROVIDER_PACKAGE_NAME = "com.google.android.apps.healthdata"
/**
+ * Determines whether the current Health Connect SDK is supported on this device. If it is
+ * not supported, then installing any provider will not help - instead disable the
+ * integration.
+ *
+ * @return whether the api is supported on the device.
+ */
+ @JvmStatic
+ public fun isApiSupported(): Boolean {
+ if (!isSdkVersionSufficient()) {
+ return false
+ }
+ // We will redirect implementations on U or above, which isn't implemented yet.
+ // When developers guard their app with the isSupported check, we can safely know they
+ // will not use Jetpack code that is not upgraded to U.
+ return Build.VERSION.SDK_INT <= Build.VERSION_CODES.TIRAMISU
+ }
+
+ /**
* Determines whether an implementation of [HealthConnectClient] is available on this device
- * at the moment.
+ * at the moment. If none is available, apps may choose to redirect to package installers to
+ * find suitable providers.
+ *
+ * @sample androidx.health.connect.client.samples.AvailabilityCheckSamples
*
* @param providerPackageNames optional package provider to choose implementation from
* @return whether the api is available
*/
@JvmOverloads
@JvmStatic
- public fun isAvailable(
+ public fun isProviderAvailable(
context: Context,
providerPackageNames: List<String> = listOf(DEFAULT_PROVIDER_PACKAGE_NAME),
): Boolean {
- if (!isSdkVersionSufficient()) {
+ if (!isApiSupported()) {
return false
}
return providerPackageNames.any { isPackageInstalled(context.packageManager, it) }
@@ -385,7 +406,7 @@
* @throws UnsupportedOperationException if service not available due to SDK version too low
* @throws IllegalStateException if service not available due to not installed
*
- * @see isAvailable
+ * @see isProviderAvailable
*/
@JvmOverloads
@JvmStatic
@@ -393,10 +414,10 @@
context: Context,
providerPackageNames: List<String> = listOf(DEFAULT_PROVIDER_PACKAGE_NAME),
): HealthConnectClient {
- if (!isSdkVersionSufficient()) {
+ if (!isApiSupported()) {
throw UnsupportedOperationException("SDK version too low")
}
- if (!isAvailable(context, providerPackageNames)) {
+ if (!isProviderAvailable(context, providerPackageNames)) {
throw IllegalStateException("Service not available")
}
val enabledPackage =
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
index a9def5a..d08c1f1 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/PermissionController.kt
@@ -16,6 +16,7 @@
package androidx.health.connect.client
import androidx.activity.result.contract.ActivityResultContract
+import androidx.annotation.RestrictTo
import androidx.health.connect.client.HealthConnectClient.Companion.DEFAULT_PROVIDER_PACKAGE_NAME
import androidx.health.connect.client.permission.HealthDataRequestPermissions
import androidx.health.connect.client.permission.HealthPermission
@@ -37,6 +38,20 @@
suspend fun getGrantedPermissions(permissions: Set<HealthPermission>): Set<HealthPermission>
/**
+ * Filters and returns a subset of permissions granted by the user to the calling app, out of
+ * the input permissions set.
+ *
+ * @param permissions set of permissions to filter
+ * @return filtered set of granted permissions.
+ *
+ * @throws android.os.RemoteException For any IPC transportation failures.
+ * @throws java.io.IOException For any disk I/O issues.
+ * @throws IllegalStateException If service is not available.
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY) // Not yet ready for public
+ suspend fun filterGrantedPermissions(permissions: Set<String>): Set<String>
+
+ /**
* Revokes all previously granted [HealthPermission] by the user to the calling app.
*
* @throws android.os.RemoteException For any IPC transportation failures.
diff --git a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
index 5ed5ce1..cd206a5 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/connect/client/impl/HealthConnectClientImpl.kt
@@ -51,6 +51,7 @@
import androidx.health.platform.client.HealthDataAsyncClient
import androidx.health.platform.client.impl.logger.Logger
import androidx.health.platform.client.proto.DataProto
+import androidx.health.platform.client.proto.PermissionProto
import androidx.health.platform.client.proto.RequestProto
import kotlin.reflect.KClass
import kotlinx.coroutines.guava.await
@@ -83,6 +84,25 @@
return grantedPermissions
}
+ override suspend fun filterGrantedPermissions(
+ permissions: Set<String>
+ ): Set<String> {
+ val grantedPermissions =
+ delegate
+ .filterGrantedPermissions(
+ permissions.map {
+ PermissionProto.Permission.newBuilder().setPermission(it).build() }
+ .toSet())
+ .await()
+ .map { it.permission }
+ .toSet()
+ Logger.debug(
+ HEALTH_CONNECT_CLIENT_TAG,
+ "Granted ${grantedPermissions.size} out of ${permissions.size} permissions."
+ )
+ return grantedPermissions
+ }
+
override suspend fun revokeAllPermissions() {
delegate.revokeAllPermissions().await()
Logger.debug(HEALTH_CONNECT_CLIENT_TAG, "Revoked all permissions.")
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/HealthDataAsyncClient.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/HealthDataAsyncClient.kt
index 08074e2..3f5ac3f 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/HealthDataAsyncClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/HealthDataAsyncClient.kt
@@ -38,6 +38,14 @@
permissions: Set<PermissionProto.Permission>
): ListenableFuture<Set<PermissionProto.Permission>>
+ /**
+ * Returns a set of [Permission] granted by the user to this app, out of the input [Permission]
+ * set.
+ */
+ fun filterGrantedPermissions(
+ permissions: Set<PermissionProto.Permission>
+ ): ListenableFuture<Set<PermissionProto.Permission>>
+
/** Allows an app to relinquish app permissions granted to itself by calling this method. */
fun revokeAllPermissions(): ListenableFuture<Unit>
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/FilterGrantedPermissionsCallback.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/FilterGrantedPermissionsCallback.kt
new file mode 100644
index 0000000..805bcd5
--- /dev/null
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/FilterGrantedPermissionsCallback.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 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.health.platform.client.impl
+
+import androidx.health.platform.client.error.ErrorStatus
+import androidx.health.platform.client.impl.error.toException
+import androidx.health.platform.client.permission.Permission
+import androidx.health.platform.client.proto.PermissionProto
+import androidx.health.platform.client.service.IFilterGrantedPermissionsCallback
+import com.google.common.util.concurrent.SettableFuture
+
+/** Wrapper to convert [IFilterGrantedPermissionsCallback] to listenable futures. */
+internal class FilterGrantedPermissionsCallback(
+ private val resultFuture: SettableFuture<Set<PermissionProto.Permission>>,
+) : IFilterGrantedPermissionsCallback.Stub() {
+ override fun onSuccess(response: List<Permission>) {
+ resultFuture.set(response.map { it.proto }.toSet())
+ }
+
+ override fun onError(error: ErrorStatus) {
+ resultFuture.setException(error.toException())
+ }
+}
diff --git a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
index 60ac65a..599b99d 100644
--- a/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
+++ b/health/connect/connect-client/src/main/java/androidx/health/platform/client/impl/ServiceBackedHealthDataClient.kt
@@ -44,6 +44,7 @@
import androidx.health.platform.client.service.IHealthDataService
import androidx.health.platform.client.service.IHealthDataService.MIN_API_VERSION
import com.google.common.util.concurrent.ListenableFuture
+import kotlin.math.min
/** An IPC backed HealthDataClient implementation. */
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -89,6 +90,18 @@
}
}
+ override fun filterGrantedPermissions(
+ permissions: Set<PermissionProto.Permission>,
+ ): ListenableFuture<Set<PermissionProto.Permission>> {
+ return executeWithVersionCheck(min(MIN_API_VERSION, 4)) { service, resultFuture ->
+ service.filterGrantedPermissions(
+ getRequestContext(),
+ permissions.map { Permission(it) }.toList(),
+ FilterGrantedPermissionsCallback(resultFuture)
+ )
+ }
+ }
+
override fun revokeAllPermissions(): ListenableFuture<Unit> {
return executeWithVersionCheck(MIN_API_VERSION) { service, resultFuture ->
service.revokeAllPermissions(
diff --git a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
index a6dd148..39574d6 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/connect/client/HealthConnectClientTest.kt
@@ -50,7 +50,8 @@
fun noBackingImplementation_unavailable() {
val packageManager = context.packageManager
Shadows.shadowOf(packageManager).removePackage(PROVIDER_PACKAGE_NAME)
- assertThat(HealthConnectClient.isAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+ assertThat(HealthConnectClient.isApiSupported()).isTrue()
+ assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
.isFalse()
assertThrows(IllegalStateException::class.java) {
HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
@@ -61,7 +62,7 @@
@Config(sdk = [Build.VERSION_CODES.P])
fun backingImplementation_notEnabled_unavailable() {
installPackage(context, PROVIDER_PACKAGE_NAME, enabled = false)
- assertThat(HealthConnectClient.isAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+ assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
.isFalse()
assertThrows(IllegalStateException::class.java) {
HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
@@ -72,7 +73,7 @@
@Config(sdk = [Build.VERSION_CODES.P])
fun backingImplementation_enabledNoService_unavailable() {
installPackage(context, PROVIDER_PACKAGE_NAME, enabled = true)
- assertThat(HealthConnectClient.isAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+ assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
.isFalse()
assertThrows(IllegalStateException::class.java) {
HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
@@ -84,14 +85,16 @@
fun backingImplementation_enabled_isAvailable() {
installPackage(context, PROVIDER_PACKAGE_NAME, enabled = true)
installService(context, PROVIDER_PACKAGE_NAME)
- assertThat(HealthConnectClient.isAvailable(context, listOf(PROVIDER_PACKAGE_NAME))).isTrue()
+ assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+ .isTrue()
HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
}
@Test
@Config(sdk = [Build.VERSION_CODES.O_MR1])
fun sdkVersionTooOld_unavailable() {
- assertThat(HealthConnectClient.isAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
+ assertThat(HealthConnectClient.isApiSupported()).isFalse()
+ assertThat(HealthConnectClient.isProviderAvailable(context, listOf(PROVIDER_PACKAGE_NAME)))
.isFalse()
assertThrows(UnsupportedOperationException::class.java) {
HealthConnectClient.getOrCreate(context, listOf(PROVIDER_PACKAGE_NAME))
diff --git a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt
index 08bfe2c..49bb23a 100644
--- a/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt
+++ b/health/connect/connect-client/src/test/java/androidx/health/platform/client/impl/testing/FakeHealthDataService.kt
@@ -42,6 +42,7 @@
import androidx.health.platform.client.service.IAggregateDataCallback
import androidx.health.platform.client.service.IDeleteDataCallback
import androidx.health.platform.client.service.IDeleteDataRangeCallback
+import androidx.health.platform.client.service.IFilterGrantedPermissionsCallback
import androidx.health.platform.client.service.IGetChangesCallback
import androidx.health.platform.client.service.IGetChangesTokenCallback
import androidx.health.platform.client.service.IGetGrantedPermissionsCallback
@@ -60,7 +61,7 @@
@RestrictTo(RestrictTo.Scope.LIBRARY)
class FakeHealthDataService : IHealthDataService.Stub() {
/** Change this state to control permission responses. Not thread safe */
- val grantedPermissions: MutableSet<Permission> = mutableSetOf()
+ private val grantedPermissions: MutableSet<Permission> = mutableSetOf()
/** State retaining last requested parameters. */
var lastRequestContext: RequestContext? = null
@@ -108,6 +109,21 @@
callback.onSuccess(granted)
}
+ override fun filterGrantedPermissions(
+ context: RequestContext,
+ permissions: List<Permission>,
+ callback: IFilterGrantedPermissionsCallback,
+ ) {
+ lastRequestContext = context
+ errorCode?.let {
+ callback.onError(ErrorStatus.create(it, "" + it))
+ return@filterGrantedPermissions
+ }
+
+ val granted = permissions.filter { it in grantedPermissions }.toList()
+ callback.onSuccess(granted)
+ }
+
override fun revokeAllPermissions(
context: RequestContext,
callback: IRevokeAllPermissionsCallback,
diff --git a/health/health-services-client/api/1.0.0-beta01.txt b/health/health-services-client/api/1.0.0-beta01.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/1.0.0-beta01.txt
+++ b/health/health-services-client/api/1.0.0-beta01.txt
@@ -85,11 +85,6 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
}
- public final class VersionApiService extends android.app.Service {
- ctor public VersionApiService();
- method public android.os.IBinder? onBind(android.content.Intent? intent);
- }
-
}
package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
}
public final class LocationData {
- ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+ ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
method public double getAltitude();
method public double getBearing();
method public double getLatitude();
@@ -669,8 +664,8 @@
property public final double bearing;
property public final double latitude;
property public final double longitude;
- field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
- field public static final double BEARING_UNAVAILABLE = -1.0;
+ field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+ field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
}
public final class MeasureCapabilities {
diff --git a/health/health-services-client/api/current.txt b/health/health-services-client/api/current.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/current.txt
+++ b/health/health-services-client/api/current.txt
@@ -85,11 +85,6 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
}
- public final class VersionApiService extends android.app.Service {
- ctor public VersionApiService();
- method public android.os.IBinder? onBind(android.content.Intent? intent);
- }
-
}
package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
}
public final class LocationData {
- ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+ ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
method public double getAltitude();
method public double getBearing();
method public double getLatitude();
@@ -669,8 +664,8 @@
property public final double bearing;
property public final double latitude;
property public final double longitude;
- field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
- field public static final double BEARING_UNAVAILABLE = -1.0;
+ field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+ field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
}
public final class MeasureCapabilities {
diff --git a/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt b/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt
+++ b/health/health-services-client/api/public_plus_experimental_1.0.0-beta01.txt
@@ -85,11 +85,6 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
}
- public final class VersionApiService extends android.app.Service {
- ctor public VersionApiService();
- method public android.os.IBinder? onBind(android.content.Intent? intent);
- }
-
}
package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
}
public final class LocationData {
- ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+ ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
method public double getAltitude();
method public double getBearing();
method public double getLatitude();
@@ -669,8 +664,8 @@
property public final double bearing;
property public final double latitude;
property public final double longitude;
- field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
- field public static final double BEARING_UNAVAILABLE = -1.0;
+ field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+ field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
}
public final class MeasureCapabilities {
diff --git a/health/health-services-client/api/public_plus_experimental_current.txt b/health/health-services-client/api/public_plus_experimental_current.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/public_plus_experimental_current.txt
+++ b/health/health-services-client/api/public_plus_experimental_current.txt
@@ -85,11 +85,6 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
}
- public final class VersionApiService extends android.app.Service {
- ctor public VersionApiService();
- method public android.os.IBinder? onBind(android.content.Intent? intent);
- }
-
}
package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
}
public final class LocationData {
- ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+ ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
method public double getAltitude();
method public double getBearing();
method public double getLatitude();
@@ -669,8 +664,8 @@
property public final double bearing;
property public final double latitude;
property public final double longitude;
- field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
- field public static final double BEARING_UNAVAILABLE = -1.0;
+ field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+ field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
}
public final class MeasureCapabilities {
diff --git a/health/health-services-client/api/restricted_1.0.0-beta01.txt b/health/health-services-client/api/restricted_1.0.0-beta01.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/restricted_1.0.0-beta01.txt
+++ b/health/health-services-client/api/restricted_1.0.0-beta01.txt
@@ -85,11 +85,6 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
}
- public final class VersionApiService extends android.app.Service {
- ctor public VersionApiService();
- method public android.os.IBinder? onBind(android.content.Intent? intent);
- }
-
}
package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
}
public final class LocationData {
- ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+ ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
method public double getAltitude();
method public double getBearing();
method public double getLatitude();
@@ -669,8 +664,8 @@
property public final double bearing;
property public final double latitude;
property public final double longitude;
- field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
- field public static final double BEARING_UNAVAILABLE = -1.0;
+ field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+ field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
}
public final class MeasureCapabilities {
diff --git a/health/health-services-client/api/restricted_current.txt b/health/health-services-client/api/restricted_current.txt
index 84e9862..e37c668 100644
--- a/health/health-services-client/api/restricted_current.txt
+++ b/health/health-services-client/api/restricted_current.txt
@@ -85,11 +85,6 @@
method public com.google.common.util.concurrent.ListenableFuture<java.lang.Void> setPassiveListenerServiceAsync(Class<? extends androidx.health.services.client.PassiveListenerService> service, androidx.health.services.client.data.PassiveListenerConfig config);
}
- public final class VersionApiService extends android.app.Service {
- ctor public VersionApiService();
- method public android.os.IBinder? onBind(android.content.Intent? intent);
- }
-
}
package androidx.health.services.client.data {
@@ -660,7 +655,7 @@
}
public final class LocationData {
- ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional @FloatRange double altitude, optional @FloatRange(from=-1.0, to=360.0, toInclusive=false) double bearing);
+ ctor public LocationData(@FloatRange(from=-90.0, to=90.0) double latitude, @FloatRange(from=-180.0, to=180.0) double longitude, optional double altitude, optional double bearing);
method public double getAltitude();
method public double getBearing();
method public double getLatitude();
@@ -669,8 +664,8 @@
property public final double bearing;
property public final double latitude;
property public final double longitude;
- field public static final double ALTITUDE_UNAVAILABLE = 1.7976931348623157E308;
- field public static final double BEARING_UNAVAILABLE = -1.0;
+ field public static final double ALTITUDE_UNAVAILABLE = (0.0/0.0);
+ field public static final double BEARING_UNAVAILABLE = (0.0/0.0);
}
public final class MeasureCapabilities {
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerService.kt b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerService.kt
index a497e64..423cebe 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerService.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/PassiveListenerService.kt
@@ -91,7 +91,7 @@
*/
public open fun onPermissionLost() {}
- private inner class IPassiveListenerServiceWrapper : IPassiveListenerService.Stub() {
+ internal inner class IPassiveListenerServiceWrapper : IPassiveListenerService.Stub() {
override fun onPassiveListenerEvent(event: PassiveListenerEvent) {
val proto = event.proto
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/VersionApiService.kt b/health/health-services-client/src/main/java/androidx/health/services/client/VersionApiService.kt
index 48f6af7..91d58a2 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/VersionApiService.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/VersionApiService.kt
@@ -20,10 +20,13 @@
import android.content.Intent
import android.os.IBinder
import android.util.Log
+import androidx.annotation.RestrictTo
+import androidx.annotation.RestrictTo.Scope
import androidx.health.services.client.impl.IVersionApiService
import androidx.health.services.client.impl.IpcConstants
/** Service that allows querying the canonical SDK version used to compile this app. */
+@RestrictTo(Scope.LIBRARY)
public class VersionApiService : Service() {
private val stub: VersionApiServiceStub = VersionApiServiceStub()
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
index 07c0479..9d188b9 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/DataPoints.kt
@@ -391,9 +391,8 @@
@FloatRange(from = -90.0, to = 90.0) latitude: Double,
@FloatRange(from = -180.0, to = 180.0) longitude: Double,
timeDurationFromBoot: Duration,
- @FloatRange altitude: Double = LocationData.ALTITUDE_UNAVAILABLE,
- @FloatRange(from = -1.0, to = 360.0, toInclusive = false) bearing: Double =
- LocationData.BEARING_UNAVAILABLE,
+ altitude: Double = LocationData.ALTITUDE_UNAVAILABLE,
+ bearing: Double = LocationData.BEARING_UNAVAILABLE,
accuracy: LocationAccuracy? = null
): SampleDataPoint<LocationData> {
if (latitude !in -90.0..90.0) {
diff --git a/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationData.kt b/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationData.kt
index c1a68a5..8537b1b 100644
--- a/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationData.kt
+++ b/health/health-services-client/src/main/java/androidx/health/services/client/data/LocationData.kt
@@ -27,12 +27,11 @@
/** Longitude of location. Range from -180.0 to = 180.0. */
@FloatRange(from = -180.0, to = 180.0) public val longitude: Double,
/** Altitude of location in meters or [ALTITUDE_UNAVAILABLE] if not available. */
- @FloatRange public val altitude: Double = ALTITUDE_UNAVAILABLE,
+ public val altitude: Double = ALTITUDE_UNAVAILABLE,
/** Bearing in degrees within the range of [0.0 (inclusive), 360.0(exclusive)] or
* [BEARING_UNAVAILABLE] if not available.
*/
- @FloatRange(from = -1.0, to = 360.0, toInclusive = false) public val bearing: Double =
- BEARING_UNAVAILABLE,
+ public val bearing: Double = BEARING_UNAVAILABLE,
) {
init {
if (latitude !in -90.0..90.0) {
@@ -114,10 +113,10 @@
private const val BEARING_INDEX: Int = 3
/** When using [DataType.LOCATION], the default value if altitude value is not available. */
- public const val ALTITUDE_UNAVAILABLE: Double = Double.MAX_VALUE
+ public const val ALTITUDE_UNAVAILABLE: Double = Double.NaN
/** When using [DataType.LOCATION], the default value if bearing value is not available. */
- public const val BEARING_UNAVAILABLE: Double = -1.0
+ public const val BEARING_UNAVAILABLE: Double = Double.NaN
internal fun fromDataProtoValue(proto: DataProto.Value): LocationData {
require(proto.hasDoubleArrayVal())
diff --git a/health/health-services-client/src/test/java/androidx/health/services/client/PassiveListenerServiceTest.kt b/health/health-services-client/src/test/java/androidx/health/services/client/PassiveListenerServiceTest.kt
new file mode 100644
index 0000000..a4479a2
--- /dev/null
+++ b/health/health-services-client/src/test/java/androidx/health/services/client/PassiveListenerServiceTest.kt
@@ -0,0 +1,226 @@
+/*
+ * 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.health.services.client
+
+import android.app.Application
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.os.IBinder
+import androidx.health.services.client.data.ComparisonType
+import androidx.health.services.client.data.DataPointContainer
+import androidx.health.services.client.data.DataPoints
+import androidx.health.services.client.data.DataType
+import androidx.health.services.client.data.DataType.Companion.STEPS_DAILY
+import androidx.health.services.client.data.DataTypeCondition
+import androidx.health.services.client.data.HealthEvent
+import androidx.health.services.client.data.HealthEvent.Type.Companion.FALL_DETECTED
+import androidx.health.services.client.data.PassiveGoal
+import androidx.health.services.client.data.PassiveMonitoringUpdate
+import androidx.health.services.client.data.UserActivityInfo
+import androidx.health.services.client.data.UserActivityState.Companion.USER_ACTIVITY_PASSIVE
+import androidx.health.services.client.impl.IPassiveListenerService
+import androidx.health.services.client.impl.event.PassiveListenerEvent
+import androidx.health.services.client.impl.response.HealthEventResponse
+import androidx.health.services.client.impl.response.PassiveMonitoringGoalResponse
+import androidx.health.services.client.impl.response.PassiveMonitoringUpdateResponse
+import androidx.test.core.app.ApplicationProvider
+import com.google.common.truth.Truth.assertThat
+import java.time.Duration
+import java.time.Instant
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.Shadows
+
+@RunWith(RobolectricTestRunner::class)
+class PassiveListenerServiceTest {
+ private fun Int.duration() = Duration.ofSeconds(this.toLong())
+ private fun Int.instant() = Instant.ofEpochMilli(this.toLong())
+
+ private val context = ApplicationProvider.getApplicationContext<Application>()
+ private lateinit var service: FakeService
+ private lateinit var stub: IPassiveListenerService
+
+ private val connection: ServiceConnection = object : ServiceConnection {
+ override fun onServiceConnected(componentName: ComponentName, binder: IBinder) {
+ stub = IPassiveListenerService.Stub.asInterface(binder)
+ }
+
+ override fun onServiceDisconnected(componentName: ComponentName) {}
+ }
+
+ @Before
+ fun setUp() {
+ service = FakeService()
+
+ Shadows.shadowOf(context).setBindServiceCallsOnServiceConnectedDirectly(true)
+ Shadows.shadowOf(context)
+ .setComponentNameAndServiceForBindService(
+ ComponentName(context, FakeService::class.java),
+ service.IPassiveListenerServiceWrapper()
+ )
+ }
+
+ @Test
+ fun receivesDataPoints() {
+ context.bindService(
+ Intent(context, FakeService::class.java),
+ connection,
+ Context.BIND_AUTO_CREATE
+ )
+ val listenerEvent = PassiveListenerEvent.createPassiveUpdateResponse(
+ PassiveMonitoringUpdateResponse(
+ PassiveMonitoringUpdate(
+ DataPointContainer(
+ listOf(
+ DataPoints.dailySteps(100, 10.duration(), 20.duration())
+ )
+ ),
+ listOf()
+ )
+ )
+ )
+
+ stub.onPassiveListenerEvent(listenerEvent)
+
+ val dataPoint = service.dataPointsReceived!!.getData(STEPS_DAILY).first()
+ assertThat(dataPoint.value).isEqualTo(100)
+ assertThat(dataPoint.startDurationFromBoot).isEqualTo(10.duration())
+ assertThat(dataPoint.endDurationFromBoot).isEqualTo(20.duration())
+ }
+
+ @Test
+ fun receivesUserActivityState() {
+ context.bindService(
+ Intent(context, FakeService::class.java),
+ connection,
+ Context.BIND_AUTO_CREATE
+ )
+ val listenerEvent = PassiveListenerEvent.createPassiveUpdateResponse(
+ PassiveMonitoringUpdateResponse(
+ PassiveMonitoringUpdate(
+ DataPointContainer(listOf()),
+ listOf(UserActivityInfo(USER_ACTIVITY_PASSIVE, null, 42.instant()))
+ )
+ )
+ )
+
+ stub.onPassiveListenerEvent(listenerEvent)
+
+ assertThat(service.userActivityReceived!!.userActivityState)
+ .isEqualTo(USER_ACTIVITY_PASSIVE)
+ assertThat(service.userActivityReceived!!.exerciseInfo).isNull()
+ assertThat(service.userActivityReceived!!.stateChangeTime).isEqualTo(42.instant())
+ }
+
+ @Test
+ fun receivesGoalCompleted() {
+ context.bindService(
+ Intent(context, FakeService::class.java),
+ connection,
+ Context.BIND_AUTO_CREATE
+ )
+ val listenerEvent = PassiveListenerEvent.createPassiveGoalResponse(
+ PassiveMonitoringGoalResponse(
+ PassiveGoal(
+ DataTypeCondition(
+ STEPS_DAILY,
+ 100,
+ ComparisonType.GREATER_THAN
+ )
+ )
+ )
+ )
+
+ stub.onPassiveListenerEvent(listenerEvent)
+
+ assertThat(service.goalReceived!!.dataTypeCondition.dataType).isEqualTo(STEPS_DAILY)
+ assertThat(service.goalReceived!!.dataTypeCondition.threshold).isEqualTo(100)
+ assertThat(service.goalReceived!!.dataTypeCondition.comparisonType)
+ .isEqualTo(ComparisonType.GREATER_THAN)
+ }
+
+ @Test
+ fun receivesHealthEvent() {
+ context.bindService(
+ Intent(context, FakeService::class.java),
+ connection,
+ Context.BIND_AUTO_CREATE
+ )
+ val listenerEvent = PassiveListenerEvent.createHealthEventResponse(
+ HealthEventResponse(
+ HealthEvent(
+ FALL_DETECTED,
+ 42.instant(),
+ DataPointContainer(listOf(DataPoints.heartRate(42.0, 84.duration())))
+ )
+ )
+ )
+
+ stub.onPassiveListenerEvent(listenerEvent)
+
+ assertThat(service.healthEventReceived!!.type).isEqualTo(FALL_DETECTED)
+ assertThat(service.healthEventReceived!!.eventTime).isEqualTo(42.instant())
+ val hrDataPoint =
+ service.healthEventReceived!!.metrics.getData(DataType.HEART_RATE_BPM).first()
+ assertThat(hrDataPoint.value).isEqualTo(42.0)
+ }
+
+ @Test
+ fun isNotifiedWhenPermissionsAreLost() {
+ context.bindService(
+ Intent(context, FakeService::class.java),
+ connection,
+ Context.BIND_AUTO_CREATE
+ )
+
+ stub.onPassiveListenerEvent(PassiveListenerEvent.createPermissionLostResponse())
+
+ assertThat(service.permissionLostCount).isEqualTo(1)
+ }
+
+ class FakeService : PassiveListenerService() {
+ var dataPointsReceived: DataPointContainer? = null
+ var userActivityReceived: UserActivityInfo? = null
+ var goalReceived: PassiveGoal? = null
+ var healthEventReceived: HealthEvent? = null
+ var permissionLostCount = 0
+
+ override fun onNewDataPointsReceived(dataPoints: DataPointContainer) {
+ dataPointsReceived = dataPoints
+ }
+
+ override fun onUserActivityInfoReceived(info: UserActivityInfo) {
+ userActivityReceived = info
+ }
+
+ override fun onGoalCompleted(goal: PassiveGoal) {
+ goalReceived = goal
+ }
+
+ override fun onHealthEventReceived(event: HealthEvent) {
+ healthEventReceived = event
+ }
+
+ override fun onPermissionLost() {
+ permissionLostCount++
+ }
+ }
+}
\ No newline at end of file
diff --git a/libraryversions.toml b/libraryversions.toml
index 66c401d..0bb99db 100644
--- a/libraryversions.toml
+++ b/libraryversions.toml
@@ -13,7 +13,7 @@
BLUETOOTH = "1.0.0-alpha01"
BROWSER = "1.5.0-alpha01"
BUILDSRC_TESTS = "1.0.0-alpha01"
-CAMERA = "1.2.0-rc01"
+CAMERA = "1.3.0-alpha01"
CAMERA_PIPE = "1.0.0-alpha01"
CARDVIEW = "1.1.0-alpha01"
CAR_APP = "1.3.0-beta02"
diff --git a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt
index d8e59d1..5999737 100644
--- a/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/main/java/androidx/privacysandbox/tools/apicompiler/generator/StubDelegatesGenerator.kt
@@ -32,6 +32,7 @@
import androidx.privacysandbox.tools.core.model.AnnotatedInterface
import androidx.privacysandbox.tools.core.model.Method
import androidx.privacysandbox.tools.core.model.ParsedApi
+import androidx.privacysandbox.tools.core.model.Types
import androidx.privacysandbox.tools.core.model.getOnlyService
import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
@@ -112,13 +113,17 @@
add(getDelegateCallBlock(method))
}
val value = api.valueMap[method.returnType]
- if (value != null) {
- addStatement(
- "transactionCallback.onSuccess(%M(result))",
- value.toParcelableNameSpec()
- )
- } else {
- addStatement("transactionCallback.onSuccess(result)")
+ when {
+ value != null -> {
+ addStatement(
+ "transactionCallback.onSuccess(%M(result))",
+ value.toParcelableNameSpec()
+ )
+ }
+ method.returnType == Types.unit -> {
+ addStatement("transactionCallback.onSuccess()")
+ }
+ else -> addStatement("transactionCallback.onSuccess(result)")
}
}
addControlFlow("catch (t: Throwable)") {
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
index 73bde7b..8b88411 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/java/androidx/privacysandbox/tools/apicompiler/PrivacySandboxKspCompilerTest.kt
@@ -49,6 +49,7 @@
"com/mysdk/IMySdk.java",
"com/mysdk/ICancellationSignal.java",
"com/mysdk/IStringTransactionCallback.java",
+ "com/mysdk/IUnitTransactionCallback.java",
"com/mysdk/AbstractSandboxedSdkProvider.kt",
"com/mysdk/MySdkStubDelegate.kt",
"com/mysdk/TransportCancellationCallback.kt",
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
index cf15f15..8a358cf 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/input/com/mysdk/MySdk.kt
@@ -9,6 +9,8 @@
suspend fun handleRequest(request: Request): Response
+ suspend fun logRequest(request: Request)
+
fun doMoreStuff()
}
diff --git a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
index 4b22ebc..84a9d3b 100644
--- a/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
+++ b/privacysandbox/tools/tools-apicompiler/src/test/test-data/testinterface/output/com/mysdk/MySdkStubDelegate.kt
@@ -1,5 +1,6 @@
package com.mysdk
+import com.mysdk.RequestConverter
import com.mysdk.RequestConverter.fromParcelable
import com.mysdk.ResponseConverter.toParcelable
import kotlin.Int
@@ -44,6 +45,21 @@
transactionCallback.onCancellable(cancellationSignal)
}
+ public override fun logRequest(request: ParcelableRequest,
+ transactionCallback: IUnitTransactionCallback): Unit {
+ val job = GlobalScope.launch(Dispatchers.Main) {
+ try {
+ val result = delegate.logRequest(fromParcelable(request))
+ transactionCallback.onSuccess()
+ }
+ catch (t: Throwable) {
+ transactionCallback.onFailure(404, t.message)
+ }
+ }
+ val cancellationSignal = TransportCancellationCallback() { job.cancel() }
+ transactionCallback.onCancellable(cancellationSignal)
+ }
+
public override fun doMoreStuff(): Unit {
delegate.doMoreStuff()
}
diff --git a/privacysandbox/tools/tools-apigenerator/build.gradle b/privacysandbox/tools/tools-apigenerator/build.gradle
index 7c22295..04b0326 100644
--- a/privacysandbox/tools/tools-apigenerator/build.gradle
+++ b/privacysandbox/tools/tools-apigenerator/build.gradle
@@ -34,6 +34,7 @@
implementation project(path: ':privacysandbox:tools:tools-core')
testImplementation(project(":internal-testutils-truth"))
+ testImplementation(project(":privacysandbox:tools:tools-apipackager"))
testImplementation(project(":privacysandbox:tools:tools-testing"))
testImplementation(project(":room:room-compiler-processing-testing"))
testImplementation(libs.kotlinCoroutinesCore)
diff --git a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
index 941a432..5c13a3f 100644
--- a/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
+++ b/privacysandbox/tools/tools-apigenerator/src/test/java/androidx/privacysandbox/tools/apigenerator/TestUtils.kt
@@ -16,33 +16,18 @@
package androidx.privacysandbox.tools.apigenerator
+import androidx.privacysandbox.tools.apipackager.PrivacySandboxApiPackager
import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertCompiles
import androidx.room.compiler.processing.util.Source
-import com.google.common.truth.Truth.assertThat
-import java.io.File
import java.nio.file.Files.createTempDirectory
import java.nio.file.Path
-import java.util.zip.ZipEntry
-import java.util.zip.ZipOutputStream
-import kotlin.io.path.name
fun compileIntoInterfaceDescriptorsJar(vararg sources: Source): Path {
- val tempDir = createTempDirectory("compile").toFile().also { it.deleteOnExit() }
-
+ val tempDir = createTempDirectory("compile").also { it.toFile().deleteOnExit() }
val result = assertCompiles(sources.toList())
- val sdkInterfaceDescriptors = File(tempDir, "sdk-interface-descriptors.jar")
- assertThat(sdkInterfaceDescriptors.createNewFile()).isTrue()
-
- ZipOutputStream(sdkInterfaceDescriptors.outputStream()).use { zipOutputStream ->
- result.outputClasspath.forEach { classPathDir ->
- classPathDir.walk().filter(File::isFile).forEach { file ->
- val zipEntry = ZipEntry(classPathDir.toPath().relativize(file.toPath()).name)
- zipOutputStream.putNextEntry(zipEntry)
- file.inputStream().copyTo(zipOutputStream)
- zipOutputStream.closeEntry()
- }
- }
- }
-
- return sdkInterfaceDescriptors.toPath()
+ val sdkInterfaceDescriptors = tempDir.resolve("sdk-interface-descriptors.jar")
+ PrivacySandboxApiPackager().packageSdkDescriptors(
+ result.outputClasspath.first().toPath(), sdkInterfaceDescriptors
+ )
+ return sdkInterfaceDescriptors
}
diff --git a/privacysandbox/tools/tools-apipackager/build.gradle b/privacysandbox/tools/tools-apipackager/build.gradle
new file mode 100644
index 0000000..4bcd717
--- /dev/null
+++ b/privacysandbox/tools/tools-apipackager/build.gradle
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 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.
+ */
+
+import androidx.build.LibraryType
+
+plugins {
+ id("AndroidXPlugin")
+ id("kotlin")
+}
+
+dependencies {
+ api(libs.kotlinStdlib)
+ implementation(libs.kotlinStdlibJdk8)
+ implementation(libs.asm)
+ implementation(libs.asmCommons)
+
+ implementation project(path: ':privacysandbox:tools:tools')
+
+ testImplementation(project(":internal-testutils-truth"))
+ testImplementation(project(":privacysandbox:tools:tools-testing"))
+ testImplementation(project(":room:room-compiler-processing-testing"))
+ testImplementation(libs.junit)
+ testImplementation(libs.truth)
+}
+
+androidx {
+ name = "androidx.privacysandbox.tools:tools-apipackager"
+ type = LibraryType.OTHER_CODE_PROCESSOR
+ mavenGroup = LibraryGroups.PRIVACYSANDBOX_TOOLS
+ inceptionYear = "2022"
+ description = "Packages API descriptions from SDKs compiled with Privacy Sandbox annotations."
+}
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
new file mode 100644
index 0000000..0a1ad9f
--- /dev/null
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/AnnotationInspector.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.privacysandbox.tools.apipackager
+
+import androidx.privacysandbox.tools.PrivacySandboxCallback
+import androidx.privacysandbox.tools.PrivacySandboxService
+import androidx.privacysandbox.tools.PrivacySandboxValue
+import java.io.File
+import org.objectweb.asm.AnnotationVisitor
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.Type
+
+internal object AnnotationInspector {
+ private val privacySandboxAnnotations = setOf(
+ PrivacySandboxCallback::class,
+ PrivacySandboxService::class,
+ PrivacySandboxValue::class,
+ )
+
+ fun hasPrivacySandboxAnnotation(classFile: File): Boolean {
+ val reader = ClassReader(classFile.readBytes())
+ val annotationExtractor = AnnotationExtractor()
+ reader.accept(
+ annotationExtractor,
+ ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES
+ )
+ return annotationExtractor.hasPrivacySandboxAnnotation
+ }
+
+ private class AnnotationExtractor : ClassVisitor(Opcodes.ASM9) {
+ private val privacySandboxAnnotationDescriptors =
+ privacySandboxAnnotations.map { Type.getDescriptor(it.java) }.toSet()
+ var hasPrivacySandboxAnnotation = false
+ private set
+
+ override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
+ if (!hasPrivacySandboxAnnotation) {
+ descriptor?.let {
+ hasPrivacySandboxAnnotation = privacySandboxAnnotationDescriptors.contains(it)
+ }
+ }
+ return super.visitAnnotation(descriptor, visible)
+ }
+ }
+}
diff --git a/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
new file mode 100644
index 0000000..cb5f049
--- /dev/null
+++ b/privacysandbox/tools/tools-apipackager/src/main/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackager.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.privacysandbox.tools.apipackager
+
+import androidx.privacysandbox.tools.apipackager.AnnotationInspector.hasPrivacySandboxAnnotation
+import java.nio.file.Path
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+import kotlin.io.path.exists
+import kotlin.io.path.isDirectory
+import kotlin.io.path.notExists
+
+class PrivacySandboxApiPackager {
+
+ /**
+ * Extracts API descriptors from SDKs annotated with Privacy Sandbox annotations.
+ *
+ * This function will output a file with SDK interface descriptors, which can later be used to
+ * generate the client-side sources for communicating with this SDK over IPC through the
+ * privacy sandbox.
+ *
+ * @param sdkClasspath path to the root directory that contains the SDK's compiled classes.
+ * Non-class files will be safely ignored.
+ * @param sdkInterfaceDescriptorsOutput output path for SDK Interface descriptors file.
+ */
+ fun packageSdkDescriptors(
+ sdkClasspath: Path,
+ sdkInterfaceDescriptorsOutput: Path,
+ ) {
+ require(sdkClasspath.exists() && sdkClasspath.isDirectory()) {
+ "$sdkClasspath is not a valid classpath."
+ }
+ require(sdkInterfaceDescriptorsOutput.notExists()) {
+ "$sdkInterfaceDescriptorsOutput already exists."
+ }
+
+ val outputFile = sdkInterfaceDescriptorsOutput.toFile().also {
+ it.parentFile.mkdirs()
+ it.createNewFile()
+ }
+
+ ZipOutputStream(outputFile.outputStream()).use { zipOutputStream ->
+ sdkClasspath.toFile().walk()
+ .filter { it.extension == "class" }
+ .filter { hasPrivacySandboxAnnotation(it) }
+ .forEach { classFile ->
+ val zipEntry = ZipEntry(sdkClasspath.relativize(classFile.toPath()).toString())
+ zipOutputStream.putNextEntry(zipEntry)
+ classFile.inputStream().copyTo(zipOutputStream)
+ zipOutputStream.closeEntry()
+ }
+ }
+ }
+}
diff --git a/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
new file mode 100644
index 0000000..5469653
--- /dev/null
+++ b/privacysandbox/tools/tools-apipackager/src/test/java/androidx/privacysandbox/tools/apipackager/PrivacySandboxApiPackagerTest.kt
@@ -0,0 +1,203 @@
+/*
+ * 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.privacysandbox.tools.apipackager
+
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.assertThat
+import androidx.privacysandbox.tools.testing.CompilationTestHelper.compileAll
+import androidx.room.compiler.processing.util.Source
+import androidx.room.compiler.processing.util.compiler.TestCompilationResult
+import androidx.testutils.assertThrows
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.zip.ZipInputStream
+import kotlin.io.path.createFile
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class PrivacySandboxApiPackagerTest {
+
+ @Test
+ fun validSdkClasspath_onlyAnnotatedClassesAreReturned() {
+ val packagedSdkClasspath = compileAndReturnUnzippedPackagedClasspath(
+ Source.kotlin(
+ "com/mysdk/TestSandboxSdk.kt", """
+ |package com.mysdk
+ |
+ |import androidx.privacysandbox.tools.PrivacySandboxCallback
+ |import androidx.privacysandbox.tools.PrivacySandboxService
+ |import androidx.privacysandbox.tools.PrivacySandboxValue
+ |
+ |@PrivacySandboxService
+ |interface MySdk
+ |
+ |@PrivacySandboxValue
+ |data class Value(val id: Int)
+ |
+ |@PrivacySandboxCallback
+ |interface MySdkCallback
+ |
+ |interface InterfaceThatShouldBeIgnored {
+ | val sdk: MySdk
+ |}
+ |
+ |data class DataClassThatShouldBeIgnored(val id: Int)
+ """.trimMargin()
+ )
+ )
+
+ val relativeDescriptorPaths = packagedSdkClasspath
+ .walk()
+ .filter { it.isFile }
+ .map { packagedSdkClasspath.toPath().relativize(it.toPath()).toString() }
+ .toList()
+ assertThat(relativeDescriptorPaths).containsExactly(
+ "com/mysdk/MySdk.class",
+ "com/mysdk/MySdkCallback.class",
+ "com/mysdk/Value.class",
+ )
+ }
+
+ @Test
+ fun validSdkClasspath_packagedDescriptorsCanBeLinkedAgainst() {
+ val packagedSdkClasspath = compileAndReturnUnzippedPackagedClasspath(
+ Source.kotlin(
+ "com/mysdk/TestSandboxSdk.kt", """
+ |package com.mysdk
+ |
+ |import androidx.privacysandbox.tools.PrivacySandboxCallback
+ |import androidx.privacysandbox.tools.PrivacySandboxService
+ |import androidx.privacysandbox.tools.PrivacySandboxValue
+ |
+ |@PrivacySandboxService
+ |interface MySdk
+ |
+ |@PrivacySandboxValue
+ |data class Value(val id: Int)
+ |
+ |@PrivacySandboxCallback
+ |interface MySdkCallback
+ """.trimMargin()
+ )
+ )
+
+ val appSource =
+ Source.kotlin(
+ "com/exampleapp/App.kt", """
+ |package com.exampleapp
+ |
+ |import com.mysdk.MySdk
+ |import com.mysdk.Value
+ |import com.mysdk.MySdkCallback
+ |
+ |class App(
+ | val sdk: MySdk,
+ | val sdkValue: Value,
+ | val callback: MySdkCallback,
+ |)
+ """.trimMargin()
+ )
+ assertThat(compileWithExtraClasspath(appSource, packagedSdkClasspath)).succeeds()
+ }
+
+ @Test
+ fun sdkClasspathDoesNotExist_throwException() {
+ val invalidClasspathFile = makeTestDirectory().resolve("dir_that_does_not_exist")
+ val validSdkDescriptor = makeTestDirectory().resolve("sdk-descriptors.jar")
+ assertThrows<IllegalArgumentException> {
+ PrivacySandboxApiPackager().packageSdkDescriptors(
+ invalidClasspathFile, validSdkDescriptor
+ )
+ }
+ }
+
+ @Test
+ fun sdkClasspathNotADirectory_throwException() {
+ val invalidClasspathFile = makeTestDirectory().resolve("invalid-file.txt").also {
+ it.createFile()
+ }
+ val validSdkDescriptor = makeTestDirectory().resolve("sdk-descriptors.jar")
+ assertThrows<IllegalArgumentException> {
+ PrivacySandboxApiPackager().packageSdkDescriptors(
+ invalidClasspathFile, validSdkDescriptor
+ )
+ }
+ }
+
+ @Test
+ fun outputAlreadyExists_throwException() {
+ val source = Source.kotlin(
+ "com/mysdk/Valid.kt", """
+ |package com.mysdk
+ |interface Valid
+ """.trimMargin()
+ )
+ val sdkClasspath = compileAll(listOf(source), includePrivacySandboxPlatformSources = false)
+ .outputClasspath.first().toPath()
+ val descriptorPathThatAlreadyExists =
+ makeTestDirectory().resolve("sdk-descriptors.jar").also {
+ it.createFile()
+ }
+ assertThrows<IllegalArgumentException> {
+ PrivacySandboxApiPackager().packageSdkDescriptors(
+ sdkClasspath, descriptorPathThatAlreadyExists
+ )
+ }
+ }
+
+ /** Compiles the given source files and returns a classpath with the results. */
+ private fun compileAndReturnUnzippedPackagedClasspath(vararg sources: Source): File {
+ val result = compileAll(sources.toList(), includePrivacySandboxPlatformSources = false)
+ assertThat(result).succeeds()
+ assertThat(result.outputClasspath).hasSize(1)
+
+ val originalClasspath = result.outputClasspath.first().toPath()
+ val descriptors = makeTestDirectory().resolve("sdk-descriptors.jar")
+ PrivacySandboxApiPackager().packageSdkDescriptors(originalClasspath, descriptors)
+
+ val outputDir = makeTestDirectory().toFile()
+ ZipInputStream(descriptors.toFile().inputStream()).use { input ->
+ generateSequence { input.nextEntry }
+ .forEach {
+ val file: File = outputDir.resolve(it.name)
+ file.parentFile.mkdirs()
+ file.createNewFile()
+ input.copyTo(file.outputStream())
+ }
+ }
+ return outputDir
+ }
+
+ private fun makeTestDirectory(): Path {
+ return Files.createTempDirectory("test")
+ .also {
+ it.toFile().deleteOnExit()
+ }
+ }
+
+ private fun compileWithExtraClasspath(source: Source, extraClasspath: File):
+ TestCompilationResult {
+ return compileAll(
+ listOf(source),
+ extraClasspath = listOf(extraClasspath),
+ includePrivacySandboxPlatformSources = false
+ )
+ }
+}
diff --git a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
index 0c15531..11df687 100644
--- a/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
+++ b/privacysandbox/tools/tools-testing/src/main/java/androidx/privacysandbox/tools/testing/CompilationTestHelper.kt
@@ -25,6 +25,7 @@
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import java.io.File
import java.nio.file.Files
import javax.tools.Diagnostic
@@ -37,14 +38,20 @@
fun compileAll(
sources: List<Source>,
+ extraClasspath: List<File> = emptyList(),
+ includePrivacySandboxPlatformSources: Boolean = true,
symbolProcessorProviders: List<SymbolProcessorProvider> = emptyList(),
processorOptions: Map<String, String> = emptyMap()
): TestCompilationResult {
val tempDir = Files.createTempDirectory("compile").toFile().also { it.deleteOnExit() }
+ val targetSources = if (includePrivacySandboxPlatformSources) {
+ sources + syntheticPrivacySandboxSources
+ } else sources
return compile(
tempDir,
TestCompilationArguments(
- sources = sources + syntheticPrivacySandboxSources,
+ sources = targetSources,
+ classpath = extraClasspath,
symbolProcessorProviders = symbolProcessorProviders,
processorOptions = processorOptions,
)
diff --git a/privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/PrivacySandboxInterface.kt b/privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/PrivacySandboxInterface.kt
new file mode 100644
index 0000000..281a4d0
--- /dev/null
+++ b/privacysandbox/tools/tools/src/main/java/androidx/privacysandbox/tools/PrivacySandboxInterface.kt
@@ -0,0 +1,24 @@
+/*
+ * 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.privacysandbox.tools
+
+/**
+ * Annotated interfaces used by the client to communicate with the SDK in the privacy sandbox.
+ */
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.CLASS)
+annotation class PrivacySandboxInterface
\ No newline at end of file
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/build.gradle b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/build.gradle
new file mode 100644
index 0000000..75b2f84
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/build.gradle
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+import com.android.build.api.artifact.SingleArtifact
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.application")
+ id("kotlin-android")
+}
+
+// This project can be removed once b/239659205 has landed and use only the
+// profile-verification-sample project.
+android {
+ buildTypes {
+ release {
+ // Minification and shrinking are disabled to avoid r8 removing unused methods and
+ // speed up build process.
+ minifyEnabled false
+ shrinkResources false
+ }
+ }
+
+ flavorDimensions = ["version"]
+ productFlavors {
+ v1 {
+ dimension "version"
+ versionCode 1
+ }
+ v2 {
+ dimension "version"
+ versionCode 2
+ }
+ v3 {
+ dimension "version"
+ versionCode 3
+ }
+ }
+
+ namespace "androidx.profileinstaller.integration.profileverification.target.no_initializer"
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+ implementation(project(":profileinstaller:profileinstaller"))
+
+ // These projects are not used directly but added to baseline-prof.txt to increase number of
+ // methods, in order to have dex opt to run.
+ implementation(project(":core:core"))
+ implementation(project(":core:core-ktx"))
+}
+
+// Define a configuration that can be consumed, as this project is a provider of test apks for
+// profile verification integration test.
+configurations {
+ apkAssets {
+ canBeConsumed = true
+ canBeResolved = false
+ attributes {
+ attribute(
+ LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+ objects.named(LibraryElements, 'profileverification-apkAssets')
+ )
+ }
+ }
+}
+
+// Release apk variants are added as output artifacts to the apkAssets configuration.
+// The apkAssets configuration is consumed by profile-verification integration test and
+// artifacts are placed in the assets folder.
+androidComponents {
+ onVariants(selector().all().withBuildType("release"), { variant ->
+ artifacts {
+ apkAssets(variant.artifacts.get(SingleArtifact.APK.INSTANCE))
+ }
+ })
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1c2eb27
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/AndroidManifest.xml
@@ -0,0 +1,46 @@
+<!--
+ ~ 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <application
+ android:testOnly="false"
+ android:allowBackup="false"
+ android:label="Profileinstaller verification sample no profile"
+ android:supportsRtl="true"
+ tools:ignore="MissingApplicationIcon">
+
+ <activity
+ android:name=".SampleActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <provider
+ android:name="androidx.startup.InitializationProvider"
+ android:authorities="${applicationId}.androidx-startup"
+ android:exported="false"
+ tools:node="merge">
+ <meta-data
+ android:name="androidx.profileinstaller.ProfileInstallerInitializer"
+ tools:node="remove" />
+ </provider>
+
+ </application>
+</manifest>
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/baseline-prof.txt b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/baseline-prof.txt
new file mode 100644
index 0000000..11d9e76
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/baseline-prof.txt
@@ -0,0 +1,2 @@
+Landroidx/**;
+HSPLandroidx/**;->**(**)**
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/java/androidx/profileinstaller/integration/profileverification/target/no_initializer/SampleActivity.kt b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/java/androidx/profileinstaller/integration/profileverification/target/no_initializer/SampleActivity.kt
new file mode 100644
index 0000000..b742486
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/java/androidx/profileinstaller/integration/profileverification/target/no_initializer/SampleActivity.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.profileinstaller.integration.profileverification.target.no_initializer
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import androidx.profileinstaller.ProfileVerifier
+import java.util.concurrent.Executors
+
+class SampleActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ Executors.newSingleThreadExecutor().submit {
+ val result = ProfileVerifier.writeProfileVerification(this)
+ runOnUiThread {
+ findViewById<TextView>(R.id.txtNotice).text = """
+ Profile installed: ${result.profileInstallResultCode}
+ Has reference profile: ${result.isCompiledWithProfile}
+ Has current profile: ${result.hasProfileEnqueuedForCompilation()}
+ """.trimIndent()
+ }
+ }
+ }
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/layout/activity_main.xml b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..b0a97a6
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/layout/activity_main.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/txtNotice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="22sp"
+ android:layout_gravity="center" />
+
+</FrameLayout>
+
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/values/donottranslate-strings.xml b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..1ade21e
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <string name="app_notice">profileinstaller verification app.</string>
+</resources>
diff --git a/profileinstaller/integration-tests/profile-verification-sample/build.gradle b/profileinstaller/integration-tests/profile-verification-sample/build.gradle
new file mode 100644
index 0000000..8d9c645
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/build.gradle
@@ -0,0 +1,90 @@
+/*
+ * 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.
+ */
+
+import com.android.build.api.artifact.SingleArtifact
+
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.application")
+ id("kotlin-android")
+}
+android {
+ buildTypes {
+ release {
+ // Minification and shrinking are disabled to avoid r8 removing unused methods and
+ // speed up build process.
+ minifyEnabled false
+ shrinkResources false
+ }
+ }
+
+ flavorDimensions = ["version"]
+ productFlavors {
+ v1 {
+ dimension "version"
+ versionCode 1
+ }
+ v2 {
+ dimension "version"
+ versionCode 2
+ }
+ v3 {
+ dimension "version"
+ versionCode 3
+ }
+ }
+
+ namespace "androidx.profileinstaller.integration.profileverification.target"
+}
+
+dependencies {
+ implementation(libs.kotlinStdlib)
+ implementation(project(":profileinstaller:profileinstaller"))
+
+ // These projects are not used directly but added to baseline-prof.txt to increase number of
+ // methods, in order to have dex opt to run.
+ implementation(project(":core:core"))
+ implementation(project(":core:core-ktx"))
+}
+
+// Define a configuration that can be consumed, as this project is a provider of test apks for
+// profile verification integration test.
+configurations {
+ apkAssets {
+ canBeConsumed = true
+ canBeResolved = false
+ attributes {
+ attribute(
+ LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+ objects.named(LibraryElements, 'profileverification-apkAssets')
+ )
+ }
+ }
+}
+
+// Release apk variants are added as output artifacts to the apkAssets configuration.
+// The apkAssets configuration is consumed by profile-verification integration test and
+// artifacts are placed in the assets folder.
+// Release apk variants are added as output artifacts to the apkAssets configuration.
+// The apkAssets configuration is consumed by profile-verification integration test and
+// artifacts are placed in the assets folder.
+androidComponents {
+ onVariants(selector().all().withBuildType("release"), { variant ->
+ artifacts {
+ apkAssets(variant.artifacts.get(SingleArtifact.APK.INSTANCE))
+ }
+ })
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification-sample/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..26fa0b3
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<!--
+ ~ 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.
+ -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools">
+
+ <application
+ android:allowBackup="false"
+ android:label="Profileinstaller verification sample"
+ android:supportsRtl="true"
+ android:testOnly="false"
+ tools:ignore="MissingApplicationIcon">
+
+ <activity
+ android:name=".SampleActivity"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/baseline-prof.txt b/profileinstaller/integration-tests/profile-verification-sample/src/main/baseline-prof.txt
new file mode 100644
index 0000000..11d9e76
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/baseline-prof.txt
@@ -0,0 +1,2 @@
+Landroidx/**;
+HSPLandroidx/**;->**(**)**
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/java/androidx/profileinstaller/integration/profileverification/target/SampleActivity.kt b/profileinstaller/integration-tests/profile-verification-sample/src/main/java/androidx/profileinstaller/integration/profileverification/target/SampleActivity.kt
new file mode 100644
index 0000000..fd1a1e1
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/java/androidx/profileinstaller/integration/profileverification/target/SampleActivity.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.profileinstaller.integration.profileverification.target
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import androidx.profileinstaller.ProfileVerifier
+import java.util.concurrent.Executors
+
+class SampleActivity : Activity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.activity_main)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ Executors.newSingleThreadExecutor().submit {
+ val result = ProfileVerifier.getCompilationStatusAsync().get()
+ runOnUiThread {
+ findViewById<TextView>(R.id.txtNotice).text = """
+ Profile installed: ${result.profileInstallResultCode}
+ Has reference profile: ${result.isCompiledWithProfile}
+ Has current profile: ${result.hasProfileEnqueuedForCompilation()}
+ """.trimIndent()
+ }
+ }
+ }
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/res/layout/activity_main.xml b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..b0a97a6
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/layout/activity_main.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <TextView
+ android:id="@+id/txtNotice"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textSize="22sp"
+ android:layout_gravity="center" />
+
+</FrameLayout>
+
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/res/values/donottranslate-strings.xml b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..1ade21e
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright 2022 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ -->
+
+<resources>
+ <string name="app_notice">profileinstaller verification app.</string>
+</resources>
diff --git a/profileinstaller/integration-tests/profile-verification/build.gradle b/profileinstaller/integration-tests/profile-verification/build.gradle
new file mode 100644
index 0000000..a25c998
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/build.gradle
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 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.
+ */
+plugins {
+ id("AndroidXPlugin")
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+// This task copies the apks provided by the `apkAssets` configuration and places them in the
+// assets folder. This allows a build time generation of the sample apps.
+def copyApkTaskProvider = tasks.register("copyApkAssets", Copy) {
+ description = "Copies the asset apks provided by profile-verification-sample projects"
+ dependsOn(configurations.getByName("apkAssets"))
+ from(configurations.getByName("apkAssets").incoming.artifactView {}.files)
+ into(layout.buildDirectory.dir("intermediates/apkAssets"))
+
+ // Note that the artifact directory included contain multiple output-metadata.json files built
+ // with the apks. Since we're not interested in those we can simply exclude duplicates.
+ duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
+}
+
+android {
+ defaultConfig {
+ minSdkVersion 23
+ }
+ sourceSets.androidTest.assets.srcDir(copyApkTaskProvider)
+ namespace "androidx.profileinstaller.integration.profileverification"
+}
+
+// Define a configuration that can be resolved. This project is the consumer of test apks, i.e. it
+// contains the integration tests.
+configurations {
+ apkAssets {
+ canBeConsumed = false
+ canBeResolved = true
+ attributes {
+ attribute(
+ LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+ objects.named(LibraryElements, 'profileverification-apkAssets')
+ )
+ }
+ }
+}
+
+dependencies {
+ androidTestImplementation(project(":profileinstaller:profileinstaller"))
+ androidTestImplementation(libs.testRules)
+ androidTestImplementation(libs.testExtJunit)
+ androidTestImplementation(libs.testCore)
+ androidTestImplementation(libs.testRunner)
+ androidTestImplementation(libs.testUiautomator)
+ androidTestImplementation(libs.testExtTruth)
+ apkAssets(project(":profileinstaller:integration-tests:profile-verification-sample"))
+ apkAssets(project(":profileinstaller:integration-tests:profile-verification-sample-no-initializer"))
+}
+
+// It makes sure that the apks are generated before the assets are packed.
+afterEvaluate {
+ tasks.named("generateDebugAndroidTestAssets").configure { it.dependsOn(copyApkTaskProvider) }
+}
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..bae036b
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 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.
+ -->
+<manifest />
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.prof b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.prof
new file mode 100644
index 0000000..dafbbbb
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.prof
Binary files differ
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.profm b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.profm
new file mode 100644
index 0000000..d72fd91
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.profm
Binary files differ
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationOnUnsupportedApiVersions.kt b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationOnUnsupportedApiVersions.kt
new file mode 100644
index 0000000..e7af5ff
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationOnUnsupportedApiVersions.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.profileinstaller.integration.profileverification
+
+import android.os.Build
+import androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
+import androidx.test.filters.LargeTest
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Test
+
+@LargeTest
+class ProfileVerificationOnUnsupportedApiVersions {
+
+ @Before
+ fun setUp() {
+
+ // This test runs only on selected api version currently unsupported by profile verifier
+ Assume.assumeTrue(
+ Build.VERSION.SDK_INT < Build.VERSION_CODES.P ||
+ Build.VERSION.SDK_INT == Build.VERSION_CODES.R
+ )
+
+ withPackageName(PACKAGE_NAME_WITH_INITIALIZER) { uninstall() }
+ withPackageName(PACKAGE_NAME_WITHOUT_INITIALIZER) { uninstall() }
+ }
+
+ @After
+ fun tearDown() {
+ withPackageName(PACKAGE_NAME_WITH_INITIALIZER) { uninstall() }
+ withPackageName(PACKAGE_NAME_WITHOUT_INITIALIZER) { uninstall() }
+ }
+
+ @Test
+ fun unsupportedApiWithInitializer() = withPackageName(PACKAGE_NAME_WITH_INITIALIZER) {
+ install(apkName = APK_WITH_INITIALIZER, withProfile = false)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(false)
+ }
+ }
+
+ @Test
+ fun unsupportedApiWithoutInitializer() = withPackageName(PACKAGE_NAME_WITHOUT_INITIALIZER) {
+ install(apkName = APK_WITHOUT_INITIALIZER, withProfile = false)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(false)
+ }
+ }
+
+ companion object {
+ private const val PACKAGE_NAME_WITH_INITIALIZER =
+ "androidx.profileinstaller.integration.profileverification.target"
+ private const val PACKAGE_NAME_WITHOUT_INITIALIZER =
+ "androidx.profileinstaller.integration.profileverification.target.no_initializer"
+ private const val ACTIVITY_NAME =
+ ".SampleActivity"
+
+ private const val APK_WITHOUT_INITIALIZER =
+ "profile-verification-sample-no-initializer-v1-release.apk"
+ private const val APK_WITH_INITIALIZER =
+ "profile-verification-sample-v1-release.apk"
+ }
+}
\ No newline at end of file
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithProfileInstallerInitializer.kt b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithProfileInstallerInitializer.kt
new file mode 100644
index 0000000..190a008
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithProfileInstallerInitializer.kt
@@ -0,0 +1,238 @@
+/*
+ * 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.profileinstaller.integration.profileverification
+
+import androidx.profileinstaller.ProfileVersion
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * This test uses the project "profileinstaller:integration-tests:profile-verification-sample". The
+ * release version of it has been embedded in the assets in 3 different versions with increasing
+ * versionCode to allow updating the app through pm command.
+ *
+ * The SampleActivity invoked displays the status of the reference profile install on the UI after
+ * the callback from {@link ProfileVerifier} returns. This test checks the status visualized to
+ * confirm if the reference profile has been installed.
+ *
+ * This test needs min sdk version `P` because it's first version to introduce support for dm file:
+ * https://googleplex-android-review.git.corp.google.com/c/platform/frameworks/base/+/3368431/
+ */
+@SdkSuppress(
+ minSdkVersion = android.os.Build.VERSION_CODES.P,
+ maxSdkVersion = ProfileVersion.MAX_SUPPORTED_SDK
+)
+@LargeTest
+class ProfileVerificationTestWithProfileInstallerInitializer {
+
+ @Before
+ fun setUp() = withPackageName(PACKAGE_NAME) {
+ // Note that this test fails on emulator api 30 (b/251540646)
+ assumeTrue(!isApi30)
+ uninstall()
+ }
+
+ @After
+ fun tearDown() = withPackageName(PACKAGE_NAME) {
+ uninstall()
+ }
+
+ @Test
+ fun installNewApp() = withPackageName(PACKAGE_NAME) {
+ // Install without reference profile
+ install(apkName = V1_APK, withProfile = false)
+
+ // Start
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(AWAITING_COMPILATION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(true)
+ }
+ }
+
+ @Test
+ fun installNewAppAndWaitForCompilation() = withPackageName(PACKAGE_NAME) {
+
+ // Install without reference profile
+ install(apkName = V1_APK, withProfile = false)
+
+ // Start once to install profile
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(AWAITING_COMPILATION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(true)
+ }
+ stop()
+
+ // Start again, should still be awaiting compilation
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(AWAITING_COMPILATION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(true)
+ }
+ stop()
+
+ // Compile
+ compileCurrentProfile()
+
+ // Start again to check profile is compiled
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(false)
+ }
+
+ // Profile should still be compiled
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(false)
+ }
+ }
+
+ @Test
+ fun installAppWithReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+ // Install with reference profile.
+ install(apkName = V1_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(true)
+ }
+ }
+
+ @Test
+ fun updateFromReferenceProfileToReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+ // Install without reference profile
+ install(apkName = V1_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(true)
+ }
+
+ // Updates adding reference profile
+ install(apkName = V2_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(true)
+ }
+ }
+
+ @Test
+ fun updateFromNoReferenceProfileToReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+ // Install without reference profile
+ install(apkName = V2_APK, withProfile = false)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(AWAITING_COMPILATION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(true)
+ }
+
+ // Updates adding reference profile
+ install(apkName = V3_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(true)
+ }
+ }
+
+ @Test
+ fun updateFromReferenceProfileToNoReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+ // Install with reference profile
+ install(apkName = V1_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(true)
+ }
+
+ // Updates removing reference profile
+ install(apkName = V2_APK, withProfile = false)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(AWAITING_COMPILATION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(true)
+ }
+ }
+
+ @Test
+ fun installWithReferenceProfileThenUpdateNoProfileThenUpdateProfileAgain() =
+ withPackageName(PACKAGE_NAME) {
+
+ // Install with reference profile
+ install(apkName = V1_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(true)
+ }
+
+ // Updates removing reference profile
+ install(apkName = V2_APK, withProfile = false)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(AWAITING_COMPILATION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(true)
+ }
+
+ // Reinstall with reference profile
+ install(apkName = V3_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(true)
+ }
+ }
+
+ companion object {
+ private const val PACKAGE_NAME =
+ "androidx.profileinstaller.integration.profileverification.target"
+ private const val ACTIVITY_NAME =
+ ".SampleActivity"
+
+ // Note that these version differ only for version code 1..3 to allow update
+ private const val V1_APK = "profile-verification-sample-v1-release.apk"
+ private const val V2_APK = "profile-verification-sample-v2-release.apk"
+ private const val V3_APK = "profile-verification-sample-v3-release.apk"
+ }
+}
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithoutProfileInstallerInitializer.kt b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithoutProfileInstallerInitializer.kt
new file mode 100644
index 0000000..c83ac77
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithoutProfileInstallerInitializer.kt
@@ -0,0 +1,309 @@
+/*
+ * 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.profileinstaller.integration.profileverification
+
+import androidx.profileinstaller.ProfileVersion
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * This test uses the project
+ * "profileinstaller:integration-tests:profile-verification-sample-no-initializer". The release
+ * version of it has been embedded in the assets in 3 different versions with increasing
+ * versionCode to allow updating the app through pm command. In this test the
+ * ProfileInstallerInitializer has been disabled from the target app manifest.
+ *
+ * The SampleActivity invoked displays the status of the reference profile install on the UI after
+ * the callback from {@link ProfileVerifier} returns. This test checks the status visualized to
+ * confirm if the reference profile has been installed.
+ *
+ * This test needs min sdk version `P` because it's first version to introduce support for dm file:
+ * https://googleplex-android-review.git.corp.google.com/c/platform/frameworks/base/+/3368431/
+ */
+@SdkSuppress(
+ minSdkVersion = android.os.Build.VERSION_CODES.P,
+ maxSdkVersion = ProfileVersion.MAX_SUPPORTED_SDK
+)
+@LargeTest
+class ProfileVerificationTestWithoutProfileInstallerInitializer {
+
+ @Before
+ fun setUp() = withPackageName(PACKAGE_NAME) {
+ // Note that this test fails on emulator api 30 (b/251540646)
+ assumeTrue(!isApi30)
+ uninstall()
+ }
+
+ @After
+ fun tearDown() = withPackageName(PACKAGE_NAME) {
+ uninstall()
+ }
+
+ @Test
+ fun installNewAppWithoutReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+ // Install without reference profile
+ install(apkName = V1_APK, withProfile = false)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(NONE)
+ hasReferenceProfile(false)
+ hasCurrentProfile(false)
+ }
+ }
+
+ @Test
+ fun installNewAppAndWaitForCompilation() = withPackageName(PACKAGE_NAME) {
+
+ // Install without reference profile
+ install(apkName = V1_APK, withProfile = false)
+
+ // Start once to check there is no profile
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(NONE)
+ hasReferenceProfile(false)
+ hasCurrentProfile(false)
+ }
+ stop()
+
+ // Start again to check there is no profile
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(NONE)
+ hasReferenceProfile(false)
+ hasCurrentProfile(false)
+ }
+ stop()
+
+ // Install profile through broadcast receiver
+ broadcastProfileInstallAction()
+
+ // Start again to check there it's now awaiting compilation
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(AWAITING_COMPILATION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(true)
+ }
+ stop()
+
+ // Start again to check there it's now awaiting compilation
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(AWAITING_COMPILATION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(true)
+ }
+ stop()
+
+ // Compile
+ compileCurrentProfile()
+
+ // Start again to check profile is compiled
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(false)
+ }
+
+ // Start again to check profile is compiled
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(false)
+ }
+ }
+
+ @Test
+ fun installAppWithReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+ // Install with reference profile
+ install(apkName = V1_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(false)
+ }
+ }
+
+ @Test
+ fun updateFromNoReferenceProfileToReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+ // Install without reference profile
+ install(apkName = V2_APK, withProfile = false)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(NONE)
+ hasReferenceProfile(false)
+ hasCurrentProfile(false)
+ }
+
+ // Updates adding reference profile
+ install(apkName = V3_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(false)
+ }
+ }
+
+ @Test
+ fun updateFromReferenceProfileToNoReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+ // Install with reference profile
+ install(apkName = V1_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(false)
+ }
+
+ // Updates removing reference profile
+ install(apkName = V2_APK, withProfile = false)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(NONE)
+ hasReferenceProfile(false)
+ hasCurrentProfile(false)
+ }
+ }
+
+ @Test
+ fun installWithReferenceProfileThenUpdateNoProfileThenUpdateProfileAgain() =
+ withPackageName(PACKAGE_NAME) {
+
+ // Install with reference profile
+ install(apkName = V1_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(false)
+ }
+
+ // Updates removing reference profile
+ install(apkName = V2_APK, withProfile = false)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(NONE)
+ hasReferenceProfile(false)
+ hasCurrentProfile(false)
+ }
+
+ // Reinstall with reference profile
+ install(apkName = V3_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(false)
+ }
+ }
+
+ @Test
+ fun forceInstallCurrentProfileThroughBroadcastReceiver() = withPackageName(PACKAGE_NAME) {
+
+ // Install without reference profile
+ install(apkName = V1_APK, withProfile = false)
+
+ // Start and assess there is no profile
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(NONE)
+ hasReferenceProfile(false)
+ hasCurrentProfile(false)
+ }
+ stop()
+
+ // Force update through broadcast receiver
+ broadcastProfileInstallAction()
+
+ // Start and assess there is a current profile
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(AWAITING_COMPILATION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(true)
+ }
+ }
+
+ @Test
+ fun forceInstallCurrentProfileThroughBroadcastReceiverAndUpdateWithReference() =
+ withPackageName(PACKAGE_NAME) {
+
+ // Install without reference profile, start and assess there is no profile
+ install(apkName = V1_APK, withProfile = false)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(NONE)
+ hasReferenceProfile(false)
+ hasCurrentProfile(false)
+ }
+ stop()
+
+ // Force update through ProfileInstallerReceiver
+ broadcastProfileInstallAction()
+
+ // Start again and assess there is a current profile now installed
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(AWAITING_COMPILATION)
+ hasReferenceProfile(false)
+ hasCurrentProfile(true)
+ }
+
+ // Update to v2 and assert that the current profile was uninstalled
+ install(apkName = V2_APK, withProfile = false)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(NONE)
+ hasReferenceProfile(false)
+ hasCurrentProfile(false)
+ }
+
+ // Update to v3 with reference profile and assess this is correctly recognized
+ install(apkName = V3_APK, withProfile = true)
+ start(ACTIVITY_NAME)
+ evaluateUI {
+ profileInstalled(COMPILED)
+ hasReferenceProfile(true)
+ hasCurrentProfile(false)
+ }
+ }
+
+ companion object {
+ private const val PACKAGE_NAME =
+ "androidx.profileinstaller.integration.profileverification.target.no_initializer"
+ private const val ACTIVITY_NAME =
+ ".SampleActivity"
+
+ // Note that these version differ only for version code 1..3 to allow update
+ private const val V1_APK = "profile-verification-sample-no-initializer-v1-release.apk"
+ private const val V2_APK = "profile-verification-sample-no-initializer-v2-release.apk"
+ private const val V3_APK = "profile-verification-sample-no-initializer-v3-release.apk"
+ }
+}
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/TestManager.kt b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/TestManager.kt
new file mode 100644
index 0000000..d289ce0
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/TestManager.kt
@@ -0,0 +1,309 @@
+/*
+ * 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.profileinstaller.integration.profileverification
+
+import android.os.Build
+import android.os.Environment
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import androidx.concurrent.futures.DirectExecutor
+import androidx.profileinstaller.DeviceProfileWriter
+import androidx.profileinstaller.ProfileInstaller
+import androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE
+import androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE
+import androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.io.File
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+
+fun withPackageName(packageName: String, block: WithPackageBlock.() -> Unit) {
+ block(WithPackageBlock(packageName))
+}
+
+class WithPackageBlock internal constructor(private val packageName: String) {
+
+ private val instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val uiAutomation = instrumentation.uiAutomation
+ private val uiDevice = UiDevice.getInstance(instrumentation)
+
+ // `externalMediaDirs` is deprecated. Docs encourage to put media in MediaStore instead. Here
+ // we just need a folder that can be accessed by both app and shell user so should be fine.
+ @Suppress("deprecation")
+ private val dirUsableByAppAndShell by lazy {
+ when {
+ Build.VERSION.SDK_INT >= 29 -> {
+ // On Android Q+ we are using the media directory because that is
+ // the directory that the shell has access to. Context: b/181601156
+ // This is the same logic of Outputs#init to determine `dirUsableByAppAndShell`.
+ InstrumentationRegistry.getInstrumentation().context.externalMediaDirs.firstOrNull {
+ Environment.getExternalStorageState(it) == Environment.MEDIA_MOUNTED
+ }
+ }
+ Build.VERSION.SDK_INT <= 22 -> {
+ // prior to API 23, shell didn't have access to externalCacheDir
+ InstrumentationRegistry.getInstrumentation().context.cacheDir
+ }
+ else -> InstrumentationRegistry.getInstrumentation().context.externalCacheDir
+ } ?: throw IllegalStateException("Unable to select a directory for writing files.")
+ }
+
+ fun uninstall() = executeCommand("pm uninstall $packageName")
+
+ fun install(apkName: String, withProfile: Boolean) {
+
+ // Contains all the clean up actions to perform at the end of the execution
+ val cleanUpBlocks = mutableListOf<() -> (Unit)>()
+
+ try {
+
+ // First writes in a temp file the apk from the assets
+ val tmpApkFile = File(dirUsableByAppAndShell, "tmp_$apkName").also { file ->
+ file.delete()
+ file.createNewFile()
+ file.deleteOnExit()
+ file.outputStream().use { instrumentation.context.assets.open(apkName).copyTo(it) }
+ }
+ cleanUpBlocks.add { tmpApkFile.delete() }
+
+ // Then moves it to a destination that can be used to install it
+ val destApkPath = "$TEMP_DIR/$apkName"
+ assertThat(executeCommand("mv ${tmpApkFile.absolutePath} $destApkPath")).isEmpty()
+ cleanUpBlocks.add { executeCommand("rm $destApkPath") }
+
+ // This mimes the behaviour of `adb install-multiple` using an install session.
+ // For reference:
+ // https://source.corp.google.com/android-internal/packages/modules/adb/client/adb_install.cpp
+
+ // Creates an install session
+ val installCreateOutput = executeCommand("pm install-create -t").first().trim()
+ val sessionId = REGEX_SESSION_ID
+ .find(installCreateOutput)
+ .throwIfNull("pm install session is invalid.")
+ .groups[1]
+ .throwIfNull("pm install session is invalid.")
+ .value
+ .toLong()
+
+ // Adds the base apk to the install session
+ val successBaseApk =
+ executeCommand("pm install-write $sessionId base.apk $TEMP_DIR/$apkName")
+ .first()
+ .trim()
+ .startsWith("Success")
+ if (!successBaseApk) {
+ throw IllegalStateException("Could not add $apkName to install session $sessionId")
+ }
+
+ // Adds the base dm (profile) to the install session
+ if (withProfile) {
+
+ // Generates the profiles using device profile writer
+ val tmpProfileProfFile = File(dirUsableByAppAndShell, "tmp_profile.prof")
+ .also {
+ it.delete()
+ it.createNewFile()
+ it.deleteOnExit()
+ }
+ cleanUpBlocks.add { tmpProfileProfFile.delete() }
+
+ val deviceProfileWriter = DeviceProfileWriter(
+ instrumentation.context.assets,
+ DirectExecutor.INSTANCE,
+ EMPTY_DIAGNOSTICS,
+ apkName,
+ BASELINE_PROF,
+ BASELINE_PROFM,
+ tmpProfileProfFile
+ )
+ if (!deviceProfileWriter.deviceAllowsProfileInstallerAotWrites()) {
+ throw IllegalStateException(
+ "The device does not allow profile installer aot writes"
+ )
+ }
+ val success = deviceProfileWriter.read()
+ .transcodeIfNeeded()
+ .write()
+ if (!success) {
+ throw IllegalStateException(
+ "Profile was not installed correctly."
+ )
+ }
+
+ // Compress the profile to generate the dex metadata file
+ val tmpDmFile = File(dirUsableByAppAndShell, "tmp_base.dm")
+ .also {
+ it.delete()
+ it.createNewFile()
+ it.deleteOnExit()
+ it.outputStream().use { os ->
+ ZipOutputStream(os).use {
+
+ // One single zip entry named `primary.prof`
+ it.putNextEntry(ZipEntry("primary.prof"))
+ it.write(tmpProfileProfFile.readBytes())
+ }
+ }
+ }
+ cleanUpBlocks.add { tmpDmFile.delete() }
+
+ // Then moves it to a destination that can be used to install it
+ val dmFilePath = "$TEMP_DIR/$DM_FILE_NAME"
+ executeCommand("mv ${tmpDmFile.absolutePath} $dmFilePath")
+ cleanUpBlocks.add { executeCommand("rm $dmFilePath") }
+
+ // Tries to install using pm install write
+ val successBaseDm =
+ executeCommand("pm install-write $sessionId base.dm $dmFilePath")
+ .first()
+ .trim()
+ .startsWith("Success")
+
+ if (!successBaseDm) {
+ throw IllegalStateException(
+ "Could not add $dmFilePath to install session $sessionId"
+ )
+ }
+ }
+
+ // Commit the install transaction. Note that install-commit may not print any output
+ // if it fails.
+ val commitCommandOutput = executeCommand("pm install-commit $sessionId")
+ val firstLine = commitCommandOutput.firstOrNull()?.trim()
+ if (firstLine == null || firstLine != "Success") {
+ throw IllegalStateException(
+ "pm install-commit failed: ${commitCommandOutput.joinToString("\n")}"
+ )
+ }
+ } finally {
+
+ // Runs all the clean up blocks. This will clean up also partial operations in case
+ // there is an issue during install
+ cleanUpBlocks.forEach { it() }
+ }
+ }
+
+ fun start(activityName: String) {
+ val error = executeCommand("am start -n $packageName/$activityName")
+ .any { it.startsWith("Error") }
+ assertThat(error).isFalse()
+ uiDevice.waitForIdle()
+ }
+
+ fun stop() {
+ val error = executeCommand("am force-stop $packageName")
+ .any { it.startsWith("Error") }
+ assertThat(error).isFalse()
+ uiDevice.waitForIdle()
+ }
+
+ fun compileCurrentProfile() {
+ val stdout = executeCommand("cmd package compile -f -m speed-profile $packageName")
+ val success = stdout.first().trim() == "Success"
+ assertWithMessage("Profile compilation failed: `$stdout`").that(success).isTrue()
+ }
+
+ fun broadcastProfileInstallAction() {
+ val result = broadcast(
+ packageName = packageName,
+ action = "androidx.profileinstaller.action.INSTALL_PROFILE",
+ receiverClass = "androidx.profileinstaller.ProfileInstallReceiver"
+ )
+ assertWithMessage("Profile install action broadcast failed with code.")
+ .that(result)
+ .isEqualTo(ProfileInstaller.RESULT_INSTALL_SUCCESS)
+ }
+
+ private fun broadcast(packageName: String, action: String, receiverClass: String) =
+ executeCommand("am broadcast -a $action $packageName/$receiverClass")
+ .first { it.contains("Broadcast completed: result=") }
+ .split("=")[1]
+ .trim()
+ .toInt()
+
+ private fun executeCommand(command: String): List<String> {
+ Log.d(TAG, "Executing command: `$command`")
+ return ParcelFileDescriptor
+ .AutoCloseInputStream(uiAutomation.executeShellCommand(command))
+ .bufferedReader()
+ .lineSequence()
+ .toList()
+ }
+
+ fun evaluateUI(block: AssertUiBlock.() -> Unit) {
+ val resourceId = "id/txtNotice"
+ val lines = uiDevice
+ .wait(Until.findObject(By.res("$packageName:$resourceId")), UI_TIMEOUT)
+ .text
+ .lines()
+ .map { it.split(":")[1].trim() }
+ assertThat(lines).hasSize(3)
+ block(AssertUiBlock(lines))
+ }
+
+ companion object {
+ private const val TAG = "TestManager"
+ private const val TEMP_DIR = "/data/local/tmp/"
+ private const val UI_TIMEOUT = 20000L
+ private const val BASELINE_PROF = "baseline.prof"
+ private const val BASELINE_PROFM = "baseline.profm"
+ private const val DM_FILE_NAME = "base.dm"
+ private val REGEX_SESSION_ID = """\[(\d+)\]""".toRegex()
+ }
+
+ class AssertUiBlock(private val lines: List<String>) {
+ fun profileInstalled(resultCode: Int) =
+ assertWithMessage("Unexpected profile verification result code")
+ .that(lines[0].toInt())
+ .isEqualTo(resultCode)
+
+ fun hasReferenceProfile(value: Boolean) =
+ assertWithMessage("Unexpected hasReferenceProfile value")
+ .that(lines[1].toBoolean())
+ .isEqualTo(value)
+
+ fun hasCurrentProfile(value: Boolean) =
+ assertWithMessage("Unexpected hasCurrentProfile value")
+ .that(lines[2].toBoolean())
+ .isEqualTo(value)
+ }
+}
+
+val isApi30 by lazy { Build.VERSION.SDK_INT == Build.VERSION_CODES.R }
+
+const val COMPILED = RESULT_CODE_COMPILED_WITH_PROFILE
+const val AWAITING_COMPILATION = RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
+const val NONE = RESULT_CODE_NO_PROFILE
+
+private fun <T> T?.throwIfNull(message: String): T = this ?: throw Exception(message)
+
+private val EMPTY_DIAGNOSTICS: ProfileInstaller.DiagnosticsCallback =
+ object : ProfileInstaller.DiagnosticsCallback {
+ private val TAG = "ProfileVerifierDiagnosticsCallback"
+ override fun onDiagnosticReceived(code: Int, data: Any?) {
+ Log.d(TAG, "onDiagnosticReceived: $code")
+ }
+
+ override fun onResultReceived(code: Int, data: Any?) {
+ Log.d(TAG, "onResultReceived: $code")
+ }
+ }
diff --git a/profileinstaller/integration-tests/profile-verification/src/main/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d4c1970
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 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.
+ -->
+<manifest />
diff --git a/profileinstaller/profileinstaller/api/current.txt b/profileinstaller/profileinstaller/api/current.txt
index ea6199b..8eed65c 100644
--- a/profileinstaller/profileinstaller/api/current.txt
+++ b/profileinstaller/profileinstaller/api/current.txt
@@ -46,5 +46,24 @@
ctor public ProfileInstallerInitializer.Result();
}
+ public final class ProfileVerifier {
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+ method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+ }
+
+ public static class ProfileVerifier.CompilationStatus {
+ method public int getProfileInstallResultCode();
+ method public boolean hasProfileEnqueuedForCompilation();
+ method public boolean isCompiledWithProfile();
+ field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+ field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+ field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+ field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+ field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+ field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+ field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+ field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+ }
+
}
diff --git a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
index ea6199b..8eed65c 100644
--- a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
+++ b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
@@ -46,5 +46,24 @@
ctor public ProfileInstallerInitializer.Result();
}
+ public final class ProfileVerifier {
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+ method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+ }
+
+ public static class ProfileVerifier.CompilationStatus {
+ method public int getProfileInstallResultCode();
+ method public boolean hasProfileEnqueuedForCompilation();
+ method public boolean isCompiledWithProfile();
+ field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+ field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+ field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+ field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+ field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+ field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+ field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+ field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+ }
+
}
diff --git a/profileinstaller/profileinstaller/api/restricted_current.txt b/profileinstaller/profileinstaller/api/restricted_current.txt
index ea6199b..8eed65c 100644
--- a/profileinstaller/profileinstaller/api/restricted_current.txt
+++ b/profileinstaller/profileinstaller/api/restricted_current.txt
@@ -46,5 +46,24 @@
ctor public ProfileInstallerInitializer.Result();
}
+ public final class ProfileVerifier {
+ method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+ method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+ }
+
+ public static class ProfileVerifier.CompilationStatus {
+ method public int getProfileInstallResultCode();
+ method public boolean hasProfileEnqueuedForCompilation();
+ method public boolean isCompiledWithProfile();
+ field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+ field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+ field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+ field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+ field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+ field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+ field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+ field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+ }
+
}
diff --git a/profileinstaller/profileinstaller/build.gradle b/profileinstaller/profileinstaller/build.gradle
index 06356df..4a26114 100644
--- a/profileinstaller/profileinstaller/build.gradle
+++ b/profileinstaller/profileinstaller/build.gradle
@@ -24,6 +24,8 @@
dependencies {
annotationProcessor(libs.nullaway)
api("androidx.startup:startup-runtime:1.1.1")
+ api(libs.guavaListenableFuture)
+ implementation("androidx.concurrent:concurrent-futures:1.1.0")
implementation("androidx.annotation:annotation:1.2.0")
testImplementation(libs.junit)
testImplementation(libs.truth)
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
index 0bcb376..f1465e1 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
@@ -286,8 +286,9 @@
}
private static @Nullable byte[] desiredVersion() {
- // If SDK is pre-N, we don't want to do anything, so return null.
- if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK) {
+ // If SDK is pre or post supported version, we don't want to do anything, so return null.
+ if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK
+ || Build.VERSION.SDK_INT > ProfileVersion.MAX_SUPPORTED_SDK) {
return null;
}
@@ -318,7 +319,8 @@
private static boolean requiresMetadata() {
// If SDK is pre-N, we don't want to do anything, so return null.
- if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK) {
+ if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK
+ || Build.VERSION.SDK_INT > ProfileVersion.MAX_SUPPORTED_SDK) {
return false;
}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
index 6f103d4..357b96f 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
@@ -388,8 +388,9 @@
* @param filesDir for noting successful installation
* @param apkName The apk file name the profile is targeting
* @param diagnostics The diagnostics callback to pass diagnostics to
+ * @return True whether the operation was successful, false otherwise
*/
- private static void transcodeAndWrite(
+ private static boolean transcodeAndWrite(
@NonNull AssetManager assets,
@NonNull String packageName,
@NonNull PackageInfo packageInfo,
@@ -400,7 +401,7 @@
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
result(executor, diagnostics, ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION, null);
- return;
+ return false;
}
File curProfile = new File(new File(PROFILE_BASE_DIR, packageName), PROFILE_FILE);
@@ -408,7 +409,7 @@
diagnostics, apkName, PROFILE_SOURCE_LOCATION, PROFILE_META_LOCATION, curProfile);
if (!deviceProfileWriter.deviceAllowsProfileInstallerAotWrites()) {
- return; /* nothing else to do here */
+ return false; /* nothing else to do here */
}
boolean success = deviceProfileWriter.read()
@@ -418,6 +419,7 @@
if (success) {
noteProfileWrittenFor(packageInfo, filesDir);
}
+ return success;
}
/**
@@ -531,16 +533,23 @@
packageInfo = packageManager.getPackageInfo(packageName, 0);
} catch (PackageManager.NameNotFoundException e) {
diagnostics.onResultReceived(RESULT_IO_EXCEPTION, e);
+
+ // Calls the verification. Note that in this case since the force install failed we
+ // don't need to report it to the ProfileVerifier.
+ ProfileVerifier.writeProfileVerification(context, false);
return;
}
File filesDir = context.getFilesDir();
if (forceWriteProfile
|| !hasAlreadyWrittenProfileForThisInstall(packageInfo, filesDir, diagnostics)) {
Log.d(TAG, "Installing profile for " + context.getPackageName());
- transcodeAndWrite(assetManager, packageName, packageInfo, filesDir, apkName, executor,
- diagnostics);
+ boolean profileWritten = transcodeAndWrite(assetManager, packageName, packageInfo,
+ filesDir, apkName, executor, diagnostics);
+ ProfileVerifier.writeProfileVerification(
+ context, profileWritten && forceWriteProfile);
} else {
Log.d(TAG, "Skipping profile installation for " + context.getPackageName());
+ ProfileVerifier.writeProfileVerification(context, false);
}
}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVerifier.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVerifier.java
new file mode 100644
index 0000000..671e131
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVerifier.java
@@ -0,0 +1,562 @@
+/*
+ * 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.profileinstaller;
+
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+import androidx.concurrent.futures.ResolvableFuture;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Provides API to verify whether a compilation profile was installed with the app. This does not
+ * make a distinction between cloud or baseline profile. The output of
+ * {@link #getCompilationStatusAsync()} allows to check if the app has been compiled with a
+ * compiled profile or whether there is a profile enqueued for compilation.
+ *
+ * If {@link ProfileInstallerInitializer} was disabled, it's necessary to manually trigger the
+ * method {@link #writeProfileVerification(Context)} or the {@link ListenableFuture} returned by
+ * {@link ProfileVerifier#getCompilationStatusAsync()} will hang or timeout.
+ *
+ * Note that {@link ProfileVerifier} requires {@link Build.VERSION_CODES#P} due to a permission
+ * issue: the reference profile folder is not accessible to pre api 28. When calling this api on
+ * unsupported api, {@link #getCompilationStatusAsync()} returns
+ * {@link CompilationStatus#RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION}.
+ */
+public final class ProfileVerifier {
+ private static final String REF_PROFILES_BASE_DIR = "/data/misc/profiles/ref/";
+ private static final String CUR_PROFILES_BASE_DIR = "/data/misc/profiles/cur/0/";
+ private static final String PROFILE_FILE_NAME = "primary.prof";
+ private static final String PROFILE_INSTALLED_CACHE_FILE_NAME = "profileInstalled";
+ private static final ResolvableFuture<CompilationStatus> sFuture = ResolvableFuture.create();
+ private static final Object SYNC_OBJ = new Object();
+ private static final String TAG = "ProfileVerifier";
+
+ @Nullable
+ private static CompilationStatus sCompilationStatus = null;
+
+ private ProfileVerifier() {
+ }
+
+ /**
+ * Caches the information on whether a reference profile exists for this app. This method
+ * performs IO operations and should not be executed on main thread. Note that this method
+ * should be called manually a few seconds after app startup if
+ * {@link ProfileInstallerInitializer} has been disabled.
+ *
+ * @param context an instance of the {@link Context}.
+ * @return the {@link CompilationStatus} of the app profile. Note that this is the same
+ * {@link CompilationStatus} obtained through {@link #getCompilationStatusAsync()}.
+ */
+ @WorkerThread
+ @NonNull
+ public static CompilationStatus writeProfileVerification(@NonNull Context context
+ ) {
+ return writeProfileVerification(context, false);
+ }
+
+ /**
+ * Caches the information on whether a reference profile exists for this app. This method
+ * performs IO operations and should not be executed on main thread. This specific api is for
+ * internal usage of this package only. The flag {@code forceVerifyCurrentProfile} should
+ * be triggered only when installing from broadcast receiver to force a current profile
+ * verification.
+ *
+ * @param context an instance of the {@link Context}.
+ * @param forceVerifyCurrentProfile requests a force verification for current profile. This
+ * should be used when installing profile through
+ * {@link ProfileInstallReceiver}.
+ * @return the {@link CompilationStatus} of the app profile. Note that this is the same
+ * {@link CompilationStatus} obtained through {@link #getCompilationStatusAsync()}.
+ * @hide
+ */
+ @NonNull
+ @WorkerThread
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ static CompilationStatus writeProfileVerification(
+ @NonNull Context context,
+ boolean forceVerifyCurrentProfile
+ ) {
+
+ // `forceVerifyCurrentProfile` can force a verification for the current profile only.
+ // Current profile can be installed at any time through the ProfileInstallerReceiver so
+ // the cached result won't work.
+ if (!forceVerifyCurrentProfile && sCompilationStatus != null) {
+ return sCompilationStatus;
+ }
+
+ synchronized (SYNC_OBJ) {
+
+ if (!forceVerifyCurrentProfile && sCompilationStatus != null) {
+ return sCompilationStatus;
+ }
+
+ // ProfileVerifier supports only api 28 and above.
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
+ || Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+ return setCompilationStatus(
+ RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION,
+ false,
+ false
+ );
+ }
+
+ // Check reference profile file existence. Note that when updating from a version with
+ // profile to a version without profile, a new reference profile of size zero is
+ // created. This should be equivalent to no reference profile.
+ File referenceProfileFile = new File(
+ new File(REF_PROFILES_BASE_DIR, context.getPackageName()), PROFILE_FILE_NAME);
+ long referenceProfileSize = referenceProfileFile.length();
+ boolean hasReferenceProfile =
+ referenceProfileFile.exists() && referenceProfileSize > 0;
+
+ // Check current profile file existence
+ File currentProfileFile = new File(
+ new File(CUR_PROFILES_BASE_DIR, context.getPackageName()), PROFILE_FILE_NAME);
+ long currentProfileSize = currentProfileFile.length();
+ boolean hasCurrentProfile =
+ currentProfileFile.exists() && currentProfileSize > 0;
+
+ // Checks package last update time that will be used to determine whether the app
+ // has been updated.
+ long packageLastUpdateTime;
+ try {
+ packageLastUpdateTime = getPackageLastUpdateTime(context);
+ } catch (PackageManager.NameNotFoundException e) {
+ return setCompilationStatus(
+ RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST,
+ hasReferenceProfile,
+ hasCurrentProfile
+ );
+ }
+
+ // Reads the current profile verification cache file
+ File cacheFile = new File(context.getFilesDir(), PROFILE_INSTALLED_CACHE_FILE_NAME);
+ Cache currentCache = null;
+ if (cacheFile.exists()) {
+ try {
+ currentCache = Cache.readFromFile(cacheFile);
+ } catch (IOException e) {
+ return setCompilationStatus(
+ RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ,
+ hasReferenceProfile,
+ hasCurrentProfile
+ );
+ }
+ }
+
+ // Here it's calculated the result code, initially set to either the latest saved value
+ // or `no profile exists`
+ int resultCode;
+
+ // There are 2 profiles: reference and current. These 2 are handled differently.
+ // The reference profile can be installed only by package manager or app Store.
+ // This can be assessed only at first app start or app updates (i.e. when the package
+ // info last update has changed). After the first install a reference profile can be
+ // created as a result of bg dex opt.
+
+ // Check if this is a first start or an update or the previous profile was awaiting
+ // compilation.
+ if (currentCache == null
+ || currentCache.mPackageLastUpdateTime != packageLastUpdateTime
+ || currentCache.mResultCode
+ == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION) {
+
+ // If so, reevaluate if the app has a reference profile and whether a current
+ // profile has been installed (since this runs after profile installer).
+ if (hasReferenceProfile) {
+ resultCode = RESULT_CODE_COMPILED_WITH_PROFILE;
+ } else if (hasCurrentProfile) {
+ resultCode = RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION;
+ } else {
+ resultCode = RESULT_CODE_NO_PROFILE;
+ }
+ } else {
+
+ // If not, utilize the cached result since the reference profile might be the result
+ // of a bg dex opt.
+ resultCode = currentCache.mResultCode;
+ }
+
+ // A current profile can be installed by the profile installer also through broadcast,
+ // therefore if this was a forced installation it can happen at anytime. the flag
+ // `forceVerifyCurrentProfile` can request a force verification for the current
+ // profile only.
+ if (forceVerifyCurrentProfile && hasCurrentProfile
+ && resultCode != RESULT_CODE_COMPILED_WITH_PROFILE) {
+ resultCode = RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION;
+ }
+
+ // If a profile has just been compiled, verify if the size matches between reference
+ // and current matches.
+ if (currentCache != null
+ && (currentCache.mResultCode == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION)
+ && resultCode == RESULT_CODE_COMPILED_WITH_PROFILE) {
+
+ // If there is an issue with the profile compilation, the reference profile size
+ // might be smaller than the current profile installed by profileinstaller. Note
+ // that this is not 100% accurate and it may return the wrong information if the
+ // portion of current profile added to the installed current profile, when the
+ // user uses the app, is larger than the installed current profile itself.
+
+ // The size of the reference profile should be at least the same in current if
+ // the compilation worked. Otherwise something went wrong. Note that on some api
+ // levels the reference profile file may not be visible to the app, so size
+ // cannot be read.
+ if (referenceProfileSize < currentCache.mInstalledCurrentProfileSize) {
+ resultCode = RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING;
+ }
+ }
+
+ // We now have a new verification result.
+ Cache newCache = new Cache(
+ /* schema = */ Cache.SCHEMA,
+ /* resultCode = */ resultCode,
+ /* packageLastUpdateTime = */ packageLastUpdateTime,
+ /* installedCurrentProfileSize = */ currentProfileSize
+ );
+
+ // At this point we can cache the result if there was no cache file or if the result has
+ // changed (for example due to a force install).
+ if (currentCache == null || !currentCache.equals(newCache)) {
+ try {
+ newCache.writeOnFile(cacheFile);
+ } catch (IOException e) {
+ resultCode =
+ RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE;
+ }
+ }
+
+ // Set and report the calculated value
+ return setCompilationStatus(resultCode, hasReferenceProfile, hasCurrentProfile);
+ }
+ }
+
+ private static CompilationStatus setCompilationStatus(
+ int resultCode,
+ boolean hasReferenceProfile,
+ boolean hasCurrentProfile
+ ) {
+ sCompilationStatus = new CompilationStatus(
+ /* resultCode = */ resultCode,
+ /* hasReferenceProfile */ hasReferenceProfile,
+ /* hasCurrentProfile */ hasCurrentProfile
+ );
+ sFuture.set(sCompilationStatus);
+ return sCompilationStatus;
+ }
+
+ @SuppressWarnings("deprecation")
+ private static long getPackageLastUpdateTime(Context context)
+ throws PackageManager.NameNotFoundException {
+
+ // PackageManager#getPackageInfo(String, int) was deprecated in API 33.
+ PackageManager packageManager = context.getApplicationContext().getPackageManager();
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ return Api33Impl.getPackageInfo(packageManager, context).lastUpdateTime;
+ } else {
+ return packageManager.getPackageInfo(context.getPackageName(), 0).lastUpdateTime;
+ }
+ }
+
+ /**
+ * Returns a future containing the {@link CompilationStatus} of the app profile. The
+ * {@link CompilationStatus} can be used to determine whether a baseline or cloud profile is
+ * installed either through app store or package manager (reference profile) or profile
+ * installer (current profile), in order to tag performance metrics versions. In the first
+ * case a reference profile is immediately installed, i.e. a the app has been compiled with a
+ * profile. In the second case the profile is awaiting compilation that will happen at some
+ * point later in background.
+ *
+ * @return A future containing the {@link CompilationStatus}.
+ */
+ @NonNull
+ public static ListenableFuture<CompilationStatus> getCompilationStatusAsync() {
+ return sFuture;
+ }
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ static class Cache {
+ private static final int SCHEMA = 1;
+ final int mSchema;
+ final int mResultCode;
+ final long mPackageLastUpdateTime;
+ final long mInstalledCurrentProfileSize;
+
+ Cache(
+ int schema,
+ int resultCode,
+ long packageLastUpdateTime,
+ long installedCurrentProfileSize
+ ) {
+ mSchema = schema;
+ mResultCode = resultCode;
+ mPackageLastUpdateTime = packageLastUpdateTime;
+ mInstalledCurrentProfileSize = installedCurrentProfileSize;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || !(o instanceof Cache)) return false;
+ Cache cacheFile = (Cache) o;
+ return mResultCode == cacheFile.mResultCode
+ && mPackageLastUpdateTime == cacheFile.mPackageLastUpdateTime
+ && mSchema == cacheFile.mSchema
+ && mInstalledCurrentProfileSize == cacheFile.mInstalledCurrentProfileSize;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ mResultCode,
+ mPackageLastUpdateTime,
+ mSchema,
+ mInstalledCurrentProfileSize
+ );
+ }
+
+ void writeOnFile(@NonNull File file) throws IOException {
+ file.delete();
+ try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) {
+ dos.writeInt(mSchema);
+ dos.writeInt(mResultCode);
+ dos.writeLong(mPackageLastUpdateTime);
+ dos.writeLong(mInstalledCurrentProfileSize);
+ }
+ }
+
+ static Cache readFromFile(@NonNull File file) throws IOException {
+ try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) {
+ return new Cache(
+ dis.readInt(),
+ dis.readInt(),
+ dis.readLong(),
+ dis.readLong()
+ );
+ }
+ }
+ }
+
+ /**
+ * {@link CompilationStatus} contains the result of a profile verification operation. It
+ * offers API to determine whether a profile was installed
+ * {@link CompilationStatus#getProfileInstallResultCode()} and to check whether the app has
+ * been compiled with a profile or a profile is enqueued for compilation. Note that the
+ * app can be compiled with a profile also as result of background dex optimization.
+ */
+ public static class CompilationStatus {
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({
+ RESULT_CODE_NO_PROFILE,
+ RESULT_CODE_COMPILED_WITH_PROFILE,
+ RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION,
+ RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING,
+ RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST,
+ RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ,
+ RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE,
+ RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
+ })
+ public @interface ResultCode {
+ }
+
+ private static final int RESULT_CODE_ERROR_CODE_BIT_SHIFT = 16;
+
+ /**
+ * Indicates that no profile was installed for this app. This means that no profile was
+ * installed when installing the app through app store or package manager and profile
+ * installer either didn't run ({@link ProfileInstallerInitializer} disabled) or the app
+ * was packaged without a compilation profile.
+ */
+ public static final int RESULT_CODE_NO_PROFILE = 0;
+
+ /**
+ * Indicates that a profile is installed and the app has been compiled with it. This is the
+ * result of installation through app store or package manager, or installation through
+ * profile installer and subsequent compilation during background dex optimization.
+ */
+ public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1;
+
+ /**
+ * Indicates that a profile is installed and the app will be compiled with it later when
+ * background dex optimization runs. This is the result of installation through profile
+ * installer. When the profile is compiled, the result code will change to
+ * {@link #RESULT_CODE_COMPILED_WITH_PROFILE}.
+ */
+ public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2;
+
+ /**
+ * Indicates that a profile is installed and the app has been compiled with it.
+ * This is the result of installation through app store or package manager. Note that
+ * this result differs from {@link #RESULT_CODE_COMPILED_WITH_PROFILE} as the profile
+ * is smaller than expected and may not include all the methods initially included in the
+ * baseline profile.
+ */
+ public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3;
+
+ /**
+ * Indicates an error during the verification process: a
+ * {@link PackageManager.NameNotFoundException} was launched when querying the
+ * {@link PackageManager} for the app package.
+ */
+ public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST =
+ 1 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+ /**
+ * Indicates that a previous verification result cache file exists but it cannot be read.
+ */
+ public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ =
+ 2 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+ /**
+ * Indicates that wasn't possible to write the verification result cache file. This can
+ * happen only because something is wrong with app folder permissions or if there is no
+ * free disk space on the device.
+ */
+ public static final int
+ RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE =
+ 3 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+ /**
+ * Indicates that ProfileVerifier runs on an unsupported api version of Android.
+ * Note that ProfileVerifier supports only {@link Build.VERSION_CODES#P} and above.
+ * Note that when this result code is returned {@link #isCompiledWithProfile()} and
+ * {@link #hasProfileEnqueuedForCompilation()} return false.
+ */
+ public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION =
+ 4 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+ final int mResultCode;
+ private final boolean mHasReferenceProfile;
+ private final boolean mHasCurrentProfile;
+
+ CompilationStatus(
+ int resultCode,
+ boolean hasReferenceProfile,
+ boolean hasCurrentProfile
+ ) {
+ this.mResultCode = resultCode;
+ this.mHasCurrentProfile = hasCurrentProfile;
+ this.mHasReferenceProfile = hasReferenceProfile;
+ }
+
+ /**
+ * @return a result code that indicates whether there is a baseline profile installed and
+ * whether the app has been compiled with it. This depends on the installation method: if it
+ * was installed through app store or package manager the app gets compiled immediately
+ * with the profile and the return code is
+ * {@link CompilationStatus#RESULT_CODE_COMPILED_WITH_PROFILE},
+ * otherwise it'll be in `awaiting compilation` state and it'll be compiled at some point
+ * later in the future, so the return code will be
+ * {@link CompilationStatus#RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION}.
+ * In the case that no profile was installed, the result code will be
+ * {@link CompilationStatus#RESULT_CODE_NO_PROFILE}.
+ *
+ * Note that even if no profile was installed it's still possible for the app to have a
+ * profile and be compiled with it, as result of background dex optimization.
+ * The result code does a simple size check to ensure the compilation process completed
+ * without errors. If the size check fails this method will return
+ * {@link CompilationStatus#RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING}. The size
+ * check is
+ * not 100% accurate as the actual compiled methods are not checked.
+ *
+ * If something fails during the verification process, this method will return one of the
+ * result codes associated with an error.
+ *
+ * Note that only api 28 {@link Build.VERSION_CODES#P} and above is supported
+ * and that {@link #RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION} is returned when calling
+ * this api on pre api 28.
+ */
+ @ResultCode
+ public int getProfileInstallResultCode() {
+ return mResultCode;
+ }
+
+ /**
+ * @return True whether this app has been compiled with a profile, false otherwise. An
+ * app can be compiled with a profile because of profile installation through app store,
+ * package manager or profileinstaller and subsequent background dex optimization. There
+ * should be a performance improvement when an app has been compiled with a profile. Note
+ * that if {@link #getProfileInstallResultCode()} returns
+ * {@link #RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION} this method always returns
+ * always false.
+ */
+ public boolean isCompiledWithProfile() {
+ return mHasReferenceProfile;
+ }
+
+ /**
+ * @return True whether this app has a profile enqueued for compilation, false otherwise. An
+ * app can have a profile enqueued for compilation because of profile installation through
+ * profileinstaller or simply when the user starts interacting with the app. Note that if
+ * {@link #getProfileInstallResultCode()} returns
+ * {@link #RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION} this method always returns false.
+ */
+ public boolean hasProfileEnqueuedForCompilation() {
+ return mHasCurrentProfile;
+ }
+ }
+
+ @RequiresApi(33)
+ private static class Api33Impl {
+ private Api33Impl() {
+ }
+
+ @DoNotInline
+ static PackageInfo getPackageInfo(
+ PackageManager packageManager,
+ Context context) throws PackageManager.NameNotFoundException {
+ return packageManager.getPackageInfo(
+ context.getPackageName(),
+ PackageManager.PackageInfoFlags.of(0)
+ );
+ }
+ }
+}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
index e98c680..783346a 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
@@ -18,9 +18,13 @@
import android.os.Build;
+import androidx.annotation.RestrictTo;
+
import java.util.Arrays;
-class ProfileVersion {
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ProfileVersion {
private ProfileVersion() {}
static final byte[] V015_S = new byte[]{'0', '1', '5', '\0'};
static final byte[] V010_P = new byte[]{'0', '1', '0', '\0'};
@@ -29,7 +33,8 @@
static final byte[] V001_N = new byte[]{'0', '0', '1', '\0'};
static final byte[] METADATA_V001_N = new byte[]{'0', '0', '1', '\0'};
static final byte[] METADATA_V002 = new byte[]{'0', '0', '2', '\0'};
- static final int MIN_SUPPORTED_SDK = Build.VERSION_CODES.N;
+ public static final int MIN_SUPPORTED_SDK = Build.VERSION_CODES.N;
+ public static final int MAX_SUPPORTED_SDK = Build.VERSION_CODES.TIRAMISU;
static String dexKeySeparator(byte[] version) {
if (Arrays.equals(version, V001_N)) {
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
index a083948..1649118 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/LinearLayoutManagerTest.java
@@ -35,15 +35,19 @@
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.StateListDrawable;
import android.os.Build;
+import android.os.Bundle;
import android.util.Log;
import android.util.StateSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.view.AccessibilityDelegateCompat;
import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
@@ -1268,7 +1272,6 @@
assertEquals("Remaining children", 0, mLayoutManager.collectChildCoordinates().size());
}
-
@Test
public void accessibilityPositions() throws Throwable {
setupByConfig(new Config(VERTICAL, false, false), true);
@@ -1288,4 +1291,186 @@
event.getToIndex(),
mLayoutManager.findLastVisibleItemPosition());
}
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_notAddedWithEmptyList()
+ throws Throwable {
+ setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(0)), false);
+ final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+
+ assertFalse(nodeInfo.getActionList().contains(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(nodeInfo);
+ }
+ });
+
+ assertFalse(nodeInfo.getActionList().contains(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ }
+
+ @SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP)
+ @Test
+ public void onInitializeAccessibilityNodeInfo_addActionScrollToPosition_addedWithNonEmptyList()
+ throws Throwable {
+ setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(1)), false);
+ final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
+
+ assertFalse(nodeInfo.getActionList().contains(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.getLayoutManager().onInitializeAccessibilityNodeInfo(nodeInfo);
+ }
+ });
+
+ assertTrue(nodeInfo.getActionList().contains(
+ AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION));
+ }
+
+ @Test
+ public void performAccessibilityAction_actionScrollToPosition_withTooLowPosition()
+ throws Throwable {
+ setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
+ assertFirstItemIsAtTop();
+
+ final boolean[] returnValue = {false};
+ Bundle arguments = new Bundle();
+ arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, -1);
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ returnValue[0] = mLayoutManager.performAccessibilityAction(
+ android.R.id.accessibilityActionScrollToPosition, arguments);
+ }
+ });
+ mLayoutManager.waitForLayout(2);
+
+ assertFalse(returnValue[0]);
+ assertFirstItemIsAtTop();
+ }
+
+ @Test
+ public void performAccessibilityAction_actionScrollToPosition_verticalWithNoRowArg()
+ throws Throwable {
+ setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
+ assertFirstItemIsAtTop();
+
+ final boolean[] returnValue = {false};
+ Bundle arguments = new Bundle();
+ arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 30);
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ returnValue[0] = mLayoutManager.performAccessibilityAction(
+ android.R.id.accessibilityActionScrollToPosition, arguments);
+ }
+ });
+ mLayoutManager.waitForLayout(2);
+
+ assertFalse(returnValue[0]);
+ assertFirstItemIsAtTop();
+ }
+
+ @Test
+ public void performAccessibilityAction_actionScrollToPosition_horizontalWithNoColumnArg()
+ throws Throwable {
+ setupByConfig(new Config(HORIZONTAL, false, false).adapter(new TestAdapter(30)), true);
+ assertFirstItemIsAtTop();
+
+ final boolean[] returnValue = {false};
+ Bundle arguments = new Bundle();
+ arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 10);
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ returnValue[0] = mLayoutManager.performAccessibilityAction(
+ android.R.id.accessibilityActionScrollToPosition, arguments);
+ }
+ });
+ mLayoutManager.waitForLayout(2);
+
+ assertFalse(returnValue[0]);
+ assertFirstItemIsAtTop();
+ }
+
+ @Test
+ public void performAccessibilityAction_actionScrollToPosition_verticalWithRowArg_scrolls()
+ throws Throwable {
+ setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
+ assertFirstItemIsAtTop();
+
+ final boolean[] returnValue = {false};
+ Bundle arguments = new Bundle();
+ arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 10);
+ // The column argument is ignored in VERTICAL orientation.
+ arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 30);
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ returnValue[0] = mLayoutManager.performAccessibilityAction(
+ android.R.id.accessibilityActionScrollToPosition, arguments);
+ }
+ });
+ mLayoutManager.waitForLayout(2);
+
+ assertTrue(returnValue[0]);
+ assertEquals(((TextView) mLayoutManager.getChildAt(0)).getText(), "Item (11)");
+ }
+
+ @Test
+ public void performAccessibilityAction_actionScrollToPosition_horizontalWithColumnArg_scrolls()
+ throws Throwable {
+ setupByConfig(new Config(HORIZONTAL, false, false).adapter(new TestAdapter(30)), true);
+ assertFirstItemIsAtTop();
+
+ final boolean[] returnValue = {false};
+ Bundle arguments = new Bundle();
+ // The row argument is ignored in HORIZONTAL orientation.
+ arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 30);
+ arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_COLUMN_INT, 10);
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ returnValue[0] = mLayoutManager.performAccessibilityAction(
+ android.R.id.accessibilityActionScrollToPosition, arguments);
+ }
+ });
+ mLayoutManager.waitForLayout(2);
+
+ assertTrue(returnValue[0]);
+ assertEquals(((TextView) mLayoutManager.getChildAt(0)).getText(), "Item (11)");
+ }
+
+
+ @Test
+ public void performAccessibilityAction_actionScrollToPosition_withTooHighPosition_scrollsToEnd()
+ throws Throwable {
+ setupByConfig(new Config(VERTICAL, false, false).adapter(new TestAdapter(30)), true);
+ assertFirstItemIsAtTop();
+
+ final boolean[] returnValue = {false};
+ Bundle arguments = new Bundle();
+ arguments.putInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, 1000);
+ mActivityRule.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ returnValue[0] = mLayoutManager.performAccessibilityAction(
+ android.R.id.accessibilityActionScrollToPosition, arguments);
+ }
+ });
+ mLayoutManager.waitForLayout(2);
+
+ assertTrue(returnValue[0]);
+ assertEquals(((TextView) mLayoutManager.getChildAt(
+ mLayoutManager.getChildCount() - 1)).getText(), "Item (30)");
+ }
+
+ private void assertFirstItemIsAtTop() {
+ assertEquals(((TextView) mLayoutManager.getChildAt(0)).getText(), "Item (1)");
+ }
}
diff --git a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
index 9cf5e62..19a9b5f 100644
--- a/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
+++ b/recyclerview/recyclerview/src/main/java/androidx/recyclerview/widget/LinearLayoutManager.java
@@ -21,6 +21,8 @@
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.PointF;
+import android.os.Build;
+import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.AttributeSet;
@@ -30,9 +32,12 @@
import android.view.accessibility.AccessibilityEvent;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.RestrictTo;
import androidx.core.os.TraceCompat;
import androidx.core.view.ViewCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
+import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat;
import java.util.List;
@@ -285,6 +290,56 @@
}
@Override
+ public void onInitializeAccessibilityNodeInfo(@NonNull RecyclerView.Recycler recycler,
+ @NonNull RecyclerView.State state, @NonNull AccessibilityNodeInfoCompat info) {
+ super.onInitializeAccessibilityNodeInfo(recycler, state, info);
+ // TODO(b/251823537)
+ if (mRecyclerView.mAdapter.getItemCount() > 0) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ info.addAction(AccessibilityActionCompat.ACTION_SCROLL_TO_POSITION);
+ }
+ }
+ }
+
+ @Override
+ boolean performAccessibilityAction(int action, @Nullable Bundle args) {
+ if (super.performAccessibilityAction(action, args)) {
+ return true;
+ }
+
+ if (action == android.R.id.accessibilityActionScrollToPosition && args != null) {
+ int position = -1;
+
+ if (mOrientation == VERTICAL) {
+ final int rowArg = args.getInt(
+ AccessibilityNodeInfoCompat.ACTION_ARGUMENT_ROW_INT, -1);
+ if (rowArg < 0) {
+ return false;
+ }
+ position = Math.min(rowArg, getRowCountForAccessibility(mRecyclerView.mRecycler,
+ mRecyclerView.mState) - 1);
+ } else { // horizontal
+ final int columnArg = args.getInt(
+ AccessibilityNodeInfoCompat.ACTION_ARGUMENT_COLUMN_INT, -1);
+ if (columnArg < 0) {
+ return false;
+ }
+ position = Math.min(columnArg,
+ getColumnCountForAccessibility(mRecyclerView.mRecycler,
+ mRecyclerView.mState) - 1);
+ }
+ if (position >= 0) {
+ // We want the target element to be the first on screen. That way, a
+ // screenreader like Talkback can directly focus on it as part of its default focus
+ // logic.
+ scrollToPositionWithOffset(position, 0);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
@SuppressLint("UnknownNullness") // b/240775049: Cannot annotate properly
public Parcelable onSaveInstanceState() {
if (mPendingSavedState != null) {
diff --git a/settings.gradle b/settings.gradle
index 014208e..dbc189b 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -571,7 +571,7 @@
includeProject(":compose:ui:ui-viewbinding:ui-viewbinding-samples", "compose/ui/ui-viewbinding/samples", [BuildType.COMPOSE])
includeProject(":compose:ui:ui:integration-tests:ui-demos", [BuildType.COMPOSE])
includeProject(":compose:ui:ui:ui-samples", "compose/ui/ui/samples", [BuildType.COMPOSE])
-includeProject(":concurrent:concurrent-futures", [BuildType.MAIN, BuildType.CAMERA])
+includeProject(":concurrent:concurrent-futures", [BuildType.MAIN, BuildType.CAMERA, BuildType.COMPOSE])
includeProject(":concurrent:concurrent-futures-ktx", [BuildType.MAIN, BuildType.CAMERA])
includeProject(":constraintlayout:constraintlayout-compose", [BuildType.COMPOSE])
includeProject(":constraintlayout:constraintlayout-compose:integration-tests:constraintlayout-compose-demos", [BuildType.COMPOSE])
@@ -782,9 +782,13 @@
includeProject(":privacysandbox:tools:tools", [BuildType.MAIN])
includeProject(":privacysandbox:tools:tools-apicompiler", [BuildType.MAIN])
includeProject(":privacysandbox:tools:tools-apigenerator", [BuildType.MAIN])
+includeProject(":privacysandbox:tools:tools-apipackager", [BuildType.MAIN])
includeProject(":privacysandbox:tools:tools-core", [BuildType.MAIN])
includeProject(":privacysandbox:tools:tools-testing", [BuildType.MAIN])
includeProject(":profileinstaller:profileinstaller", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":profileinstaller:integration-tests:profile-verification", [BuildType.MAIN])
+includeProject(":profileinstaller:integration-tests:profile-verification-sample", [BuildType.MAIN])
+includeProject(":profileinstaller:integration-tests:profile-verification-sample-no-initializer", [BuildType.MAIN])
includeProject(":profileinstaller:integration-tests:init-macrobenchmark", [BuildType.MAIN])
includeProject(":profileinstaller:integration-tests:init-macrobenchmark-target", [BuildType.MAIN])
includeProject(":profileinstaller:profileinstaller-benchmark", [BuildType.MAIN])
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Gestures.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Gestures.java
index 8addb3a..4468216 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Gestures.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/Gestures.java
@@ -16,39 +16,19 @@
package androidx.test.uiautomator;
-import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.view.ViewConfiguration;
-/**
- * The {@link Gestures} class provides factory methods for constructing common
- * {@link PointerGesture}s.
- */
+/** Factory methods for constructing {@link PointerGesture}s. */
class Gestures {
- // Singleton instance
- private static Gestures sInstance;
-
// Constants used by pinch gestures
private static final int INNER = 0;
private static final int OUTER = 1;
private static final int INNER_MARGIN = 5;
- // Keep a handle to the ViewConfiguration
- private ViewConfiguration mViewConfig;
-
- // Private constructor.
- private Gestures(ViewConfiguration config) {
- mViewConfig = config;
- }
-
- /** Returns the {@link Gestures} instance for the given {@link Context}. */
- public static Gestures getInstance(UiDevice device) {
- if (sInstance == null) {
- sInstance = new Gestures(ViewConfiguration.get(device.getUiContext()));
- }
- return sInstance;
+ private Gestures() {
}
/**
@@ -59,7 +39,7 @@
* @param displayId The ID of display where {@code point} is on.
* @return The {@link PointerGesture} representing this click.
*/
- public PointerGesture click(Point point, int displayId) {
+ public static PointerGesture click(Point point, int displayId) {
// A basic click is a touch down and touch up over the same point with no delay.
return click(point, 0, displayId);
}
@@ -73,7 +53,7 @@
* @param displayId The ID of display where {@code point} is on.
* @return The {@link PointerGesture} representing this click.
*/
- public PointerGesture click(Point point, long duration, int displayId) {
+ public static PointerGesture click(Point point, long duration, int displayId) {
// A click is a touch down and touch up over the same point with an optional delay inbetween
return new PointerGesture(point, displayId).pause(duration);
}
@@ -86,9 +66,9 @@
* @param displayId The ID of display where {@code point} is on.
* @return The {@link PointerGesture} representing this long click.
*/
- public PointerGesture longClick(Point point, int displayId) {
+ public static PointerGesture longClick(Point point, int displayId) {
// A long click is a click with a duration that exceeds a certain threshold.
- return click(point, mViewConfig.getLongPressTimeout(), displayId);
+ return click(point, ViewConfiguration.getLongPressTimeout(), displayId);
}
/**
@@ -100,7 +80,7 @@
* @param displayId The ID of display where the swipe is on.
* @return The {@link PointerGesture} representing this swipe.
*/
- public PointerGesture swipe(Point start, Point end, int speed, int displayId) {
+ public static PointerGesture swipe(Point start, Point end, int speed, int displayId) {
// A swipe is a click that moves before releasing the pointer.
return new PointerGesture(start, displayId).move(end, speed);
}
@@ -110,12 +90,12 @@
*
* @param area The area to swipe over.
* @param direction The direction in which to swipe.
- * @param float percent The size of the swipe as a percentage of the total area.
+ * @param percent The size of the swipe as a percentage of the total area.
* @param speed The speed at which to move in pixels per second.
* @param displayId The ID of display where the swipe is on.
* @return The {@link PointerGesture} representing this swipe.
*/
- public PointerGesture swipeRect(
+ public static PointerGesture swipeRect(
Rect area, Direction direction, float percent, int speed, int displayId) {
Point start, end;
// TODO: Reverse horizontal direction if locale is RTL
@@ -152,7 +132,7 @@
* @param displayId The ID of display where a click and drag are on.
* @return The {@link PointerGesture} representing this swipe.
*/
- public PointerGesture drag(Point start, Point end, int speed, int displayId) {
+ public static PointerGesture drag(Point start, Point end, int speed, int displayId) {
// A drag is a swipe that starts with a long click.
return longClick(start, displayId).move(end, speed);
}
@@ -160,13 +140,13 @@
/**
* Returns an array of {@link PointerGesture}s representing a pinch close.
*
- * @param bounds The area to pinch over.
+ * @param area The area to pinch over.
* @param percent The size of the pinch as a percentage of the total area.
* @param speed The speed at which to move in pixels per second.
* @param displayId The ID of display where a pinch close is on.
* @return An array containing the two PointerGestures representing this pinch.
*/
- public PointerGesture[] pinchClose(Rect area, float percent, int speed, int displayId) {
+ public static PointerGesture[] pinchClose(Rect area, float percent, int speed, int displayId) {
Point[] bottomLeft = new Point[2];
Point[] topRight = new Point[2];
calcPinchCoordinates(area, percent, bottomLeft, topRight);
@@ -182,13 +162,13 @@
/**
* Returns an array of {@link PointerGesture}s representing a pinch close.
*
- * @param bounds The area to pinch over.
+ * @param area The area to pinch over.
* @param percent The size of the pinch as a percentage of the total area.
* @param speed The speed at which to move in pixels per second.
* @param displayId The ID of display where a pinch open is on.
* @return An array containing the two PointerGestures representing this pinch.
*/
- public PointerGesture[] pinchOpen(Rect area, float percent, int speed, int displayId) {
+ public static PointerGesture[] pinchOpen(Rect area, float percent, int speed, int displayId) {
Point[] bottomLeft = new Point[2];
Point[] topRight = new Point[2];
calcPinchCoordinates(area, percent, bottomLeft, topRight);
@@ -202,7 +182,7 @@
}
/** Calculates the inner and outer coordinates used in a pinch gesture. */
- private void calcPinchCoordinates(Rect area, float percent,
+ private static void calcPinchCoordinates(Rect area, float percent,
Point[] bottomLeft, Point[] topRight) {
int offsetX = (int)((area.width() - 2 * INNER_MARGIN) / 2 * percent);
diff --git a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
index cd13a92..20465f6 100644
--- a/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
+++ b/test/uiautomator/uiautomator/src/main/java/androidx/test/uiautomator/UiObject2.java
@@ -51,7 +51,6 @@
private static final String TAG = UiObject2.class.getSimpleName();
private UiDevice mDevice;
- private Gestures mGestures;
private GestureController mGestureController;
private BySelector mSelector; // Hold this mainly for debugging
private AccessibilityNodeInfo mCachedNode;
@@ -83,7 +82,6 @@
mDevice = device;
mSelector = selector;
mCachedNode = cachedNode;
- mGestures = Gestures.getInstance(device);
mGestureController = GestureController.getInstance(device);
final DisplayManager dm =
(DisplayManager) mDevice.getInstrumentation().getContext().getSystemService(
@@ -429,7 +427,7 @@
/** Clicks on this object. */
public void click() {
Log.v(TAG, String.format("click(center=%s)", getVisibleCenter()));
- mGestureController.performGesture(mGestures.click(getVisibleCenter(), getDisplayId()));
+ mGestureController.performGesture(Gestures.click(getVisibleCenter(), getDisplayId()));
}
/**
@@ -441,7 +439,7 @@
public void click(@NonNull Point point) {
clipToGestureBounds(point);
Log.v(TAG, String.format("click(point=%s)", point));
- mGestureController.performGesture(mGestures.click(point, getDisplayId()));
+ mGestureController.performGesture(Gestures.click(point, getDisplayId()));
}
/** Performs a click on this object that lasts for {@code duration} milliseconds. */
@@ -449,7 +447,7 @@
Log.v(TAG, String.format("click(center=%s,duration=%d)",
getVisibleCenter(), duration));
mGestureController.performGesture(
- mGestures.click(getVisibleCenter(), duration, getDisplayId()));
+ Gestures.click(getVisibleCenter(), duration, getDisplayId()));
}
/**
@@ -463,7 +461,7 @@
public void click(@NonNull Point point, long duration) {
clipToGestureBounds(point);
Log.v(TAG, String.format("click(point=%s,duration=%d)", point, duration));
- mGestureController.performGesture(mGestures.click(point, duration, getDisplayId()));
+ mGestureController.performGesture(Gestures.click(point, duration, getDisplayId()));
}
/** Clicks on this object, and waits for the given condition to become true. */
@@ -471,7 +469,7 @@
Log.v(TAG, String.format("clickAndWait(center=%s,timeout=%d)",
getVisibleCenter(), timeout));
return mGestureController.performGestureAndWait(condition, timeout,
- mGestures.click(getVisibleCenter(), getDisplayId()));
+ Gestures.click(getVisibleCenter(), getDisplayId()));
}
/**
@@ -487,8 +485,8 @@
long timeout) {
clipToGestureBounds(point);
Log.v(TAG, String.format("clickAndWait(point=%s,timeout=%d)", point, timeout));
- return mGestureController.performGestureAndWait(condition, timeout,
- mGestures.click(point, getDisplayId()));
+ return mGestureController.performGestureAndWait(
+ condition, timeout, Gestures.click(point, getDisplayId()));
}
/**
@@ -513,14 +511,14 @@
Log.v(TAG, String.format("drag(start=%s,dest=%s,speed=%d)",
getVisibleCenter(), dest, speed));
mGestureController.performGesture(
- mGestures.drag(getVisibleCenter(), dest, speed, getDisplayId()));
+ Gestures.drag(getVisibleCenter(), dest, speed, getDisplayId()));
}
/** Performs a long click on this object. */
public void longClick() {
Log.v(TAG, String.format("longClick(center=%s)",
getVisibleCenter()));
- mGestureController.performGesture(mGestures.longClick(getVisibleCenter(), getDisplayId()));
+ mGestureController.performGesture(Gestures.longClick(getVisibleCenter(), getDisplayId()));
}
/**
@@ -548,8 +546,7 @@
Log.v(TAG, String.format("pinchClose(bounds=%s,percent=%f,speed=%d)",
getVisibleBoundsForGestures(), percent, speed));
mGestureController.performGesture(
- mGestures.pinchClose(
- getVisibleBoundsForGestures(), percent, speed, getDisplayId()));
+ Gestures.pinchClose(getVisibleBoundsForGestures(), percent, speed, getDisplayId()));
}
/**
@@ -577,8 +574,7 @@
Log.v(TAG, String.format("pinchOpen(bounds=%s,percent=%f,speed=%d)",
getVisibleBoundsForGestures(), percent, speed));
mGestureController.performGesture(
- mGestures.pinchOpen(
- getVisibleBoundsForGestures(), percent, speed, getDisplayId()));
+ Gestures.pinchOpen(getVisibleBoundsForGestures(), percent, speed, getDisplayId()));
}
/**
@@ -609,7 +605,7 @@
Log.v(TAG, String.format("swipe(bounds=%s,direction=%s,percent=%f,speed=%d)",
bounds, direction, percent, speed));
mGestureController.performGesture(
- mGestures.swipeRect(bounds, direction, percent, speed, getDisplayId()));
+ Gestures.swipeRect(bounds, direction, percent, speed, getDisplayId()));
}
/**
@@ -648,7 +644,7 @@
direction, bounds, percent, speed));
for (; percent > 0.0f; percent -= 1.0f) {
float segment = percent > 1.0f ? 1.0f : percent;
- PointerGesture swipe = mGestures.swipeRect(
+ PointerGesture swipe = Gestures.swipeRect(
bounds, swipeDirection, segment, speed, getDisplayId()).pause(250);
// Perform the gesture and return early if we reached the end
@@ -690,7 +686,7 @@
Rect bounds = getVisibleBoundsForGestures();
Log.v(TAG, String.format("fling(bounds=%s,direction=%s,speed=%d)",
bounds, direction, speed));
- PointerGesture swipe = mGestures.swipeRect(
+ PointerGesture swipe = Gestures.swipeRect(
bounds, swipeDirection, 1.0f, speed, getDisplayId());
// Perform the gesture and return true if we did not reach the end
diff --git a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt
index c3aa5d9..6207059 100644
--- a/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt
+++ b/wear/compose/compose-material/src/commonMain/kotlin/androidx/wear/compose/material/Text.kt
@@ -219,14 +219,14 @@
)
)
BasicText(
- text,
- modifier,
- mergedStyle,
- onTextLayout,
- overflow,
- softWrap,
- maxLines,
- inlineContent
+ text = text,
+ modifier = modifier,
+ style = mergedStyle,
+ onTextLayout = onTextLayout,
+ overflow = overflow,
+ softWrap = softWrap,
+ maxLines = maxLines,
+ inlineContent = inlineContent
)
}
diff --git a/wear/watchface/watchface-complications-data/api/current.txt b/wear/watchface/watchface-complications-data/api/current.txt
index d385ad9..d6cae24 100644
--- a/wear/watchface/watchface-complications-data/api/current.txt
+++ b/wear/watchface/watchface-complications-data/api/current.txt
@@ -102,9 +102,9 @@
ctor public LongTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.LongTextComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? icon);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -137,8 +137,8 @@
ctor public MonochromaticImageComplicationData.Builder(androidx.wear.watchface.complications.data.MonochromaticImage monochromaticImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
@@ -169,9 +169,9 @@
ctor public NoPermissionComplicationData.Builder();
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -195,8 +195,8 @@
ctor public PhotoImageComplicationData.Builder(android.graphics.drawable.Icon photoImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.PhotoImageComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
@@ -238,9 +238,9 @@
ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
@@ -267,9 +267,9 @@
ctor public ShortTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -304,8 +304,8 @@
ctor public SmallImageComplicationData.Builder(androidx.wear.watchface.complications.data.SmallImage smallImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.SmallImageComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
diff --git a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
index 2b7bcf9..4f035e8 100644
--- a/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
+++ b/wear/watchface/watchface-complications-data/api/public_plus_experimental_current.txt
@@ -117,9 +117,9 @@
ctor public DiscreteRangedValueComplicationData.Builder(int value, int min, int max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.DiscreteRangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
@@ -158,9 +158,9 @@
method public androidx.wear.watchface.complications.data.GoalProgressComplicationData build();
method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.GoalProgressComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
@@ -186,8 +186,8 @@
ctor public ListComplicationData.Builder(java.util.List<? extends androidx.wear.watchface.complications.data.ComplicationData> complicationList, androidx.wear.watchface.complications.data.ListComplicationData.StyleHint styleHint, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.ListComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.ListComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.ListComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
@@ -217,9 +217,9 @@
ctor public LongTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.LongTextComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? icon);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -252,8 +252,8 @@
ctor public MonochromaticImageComplicationData.Builder(androidx.wear.watchface.complications.data.MonochromaticImage monochromaticImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
@@ -284,9 +284,9 @@
ctor public NoPermissionComplicationData.Builder();
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -310,8 +310,8 @@
ctor public PhotoImageComplicationData.Builder(android.graphics.drawable.Icon photoImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.PhotoImageComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
@@ -345,8 +345,8 @@
ctor public ProtoLayoutComplicationData.Builder(androidx.wear.tiles.LayoutElementBuilders.Layout ambientLayout, androidx.wear.tiles.LayoutElementBuilders.Layout interactiveLayout, androidx.wear.tiles.ResourceBuilders.Resources resources, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.ProtoLayoutComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
@@ -379,9 +379,9 @@
method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
method @androidx.wear.watchface.complications.data.ComplicationExperimental public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setColorRamp(androidx.wear.watchface.complications.data.ColorRamp? colorRamp);
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
@@ -408,9 +408,9 @@
ctor public ShortTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -445,8 +445,8 @@
ctor public SmallImageComplicationData.Builder(androidx.wear.watchface.complications.data.SmallImage smallImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.SmallImageComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
@@ -554,9 +554,9 @@
ctor public WeightedElementsComplicationData.Builder(java.util.List<androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Element> elements, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.WeightedElementsComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
diff --git a/wear/watchface/watchface-complications-data/api/restricted_current.txt b/wear/watchface/watchface-complications-data/api/restricted_current.txt
index efb6b9a..e27cc62 100644
--- a/wear/watchface/watchface-complications-data/api/restricted_current.txt
+++ b/wear/watchface/watchface-complications-data/api/restricted_current.txt
@@ -102,9 +102,9 @@
ctor public LongTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.LongTextComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? icon);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.LongTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -137,8 +137,8 @@
ctor public MonochromaticImageComplicationData.Builder(androidx.wear.watchface.complications.data.MonochromaticImage monochromaticImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.MonochromaticImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
@@ -169,9 +169,9 @@
ctor public NoPermissionComplicationData.Builder();
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
method public androidx.wear.watchface.complications.data.NoPermissionComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -195,8 +195,8 @@
ctor public PhotoImageComplicationData.Builder(android.graphics.drawable.Icon photoImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.PhotoImageComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.PhotoImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
@@ -239,9 +239,9 @@
ctor public RangedValueComplicationData.Builder(float value, float min, float max, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.RangedValueComplicationData.Builder setText(androidx.wear.watchface.complications.data.ComplicationText? text);
@@ -268,9 +268,9 @@
ctor public ShortTextComplicationData.Builder(androidx.wear.watchface.complications.data.ComplicationText text, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setMonochromaticImage(androidx.wear.watchface.complications.data.MonochromaticImage? monochromaticImage);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setSmallImage(androidx.wear.watchface.complications.data.SmallImage? smallImage);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.ShortTextComplicationData.Builder setTitle(androidx.wear.watchface.complications.data.ComplicationText? title);
@@ -305,8 +305,8 @@
ctor public SmallImageComplicationData.Builder(androidx.wear.watchface.complications.data.SmallImage smallImage, androidx.wear.watchface.complications.data.ComplicationText contentDescription);
method public androidx.wear.watchface.complications.data.SmallImageComplicationData build();
method public final T setDataSource(android.content.ComponentName? dataSource);
- method public final T setDisplayPolicy(int displayPolicy);
- method public final T setPersistencePolicy(int persistencePolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setDisplayPolicy(int displayPolicy);
+ method @RequiresApi(android.os.Build.VERSION_CODES.TIRAMISU) public final T setPersistencePolicy(int persistencePolicy);
method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setTapAction(android.app.PendingIntent? tapAction);
method public androidx.wear.watchface.complications.data.SmallImageComplicationData.Builder setValidTimeRange(androidx.wear.watchface.complications.data.TimeRange? validTimeRange);
}
diff --git a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
index d66bdf2..a5e86a9 100644
--- a/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
+++ b/wear/watchface/watchface-complications-data/src/main/java/androidx/wear/watchface/complications/data/Data.kt
@@ -23,6 +23,7 @@
import androidx.annotation.ColorInt
import androidx.annotation.IntDef
import androidx.annotation.FloatRange
+import androidx.annotation.RequiresApi
import androidx.annotation.RestrictTo
import androidx.wear.tiles.LayoutElementBuilders
import androidx.wear.tiles.ResourceBuilders
@@ -197,6 +198,7 @@
* Sets the complication's [ComplicationPersistencePolicy].
*/
@Suppress("UNCHECKED_CAST", "SetterReturnsThis")
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public fun setPersistencePolicy(@ComplicationPersistencePolicy persistencePolicy: Int): T {
this.persistencePolicy = persistencePolicy
return this as T
@@ -206,6 +208,7 @@
* Sets the complication's [ComplicationDisplayPolicy].
*/
@Suppress("UNCHECKED_CAST", "SetterReturnsThis")
+ @RequiresApi(Build.VERSION_CODES.TIRAMISU)
public fun setDisplayPolicy(@ComplicationDisplayPolicy displayPolicy: Int): T {
this.displayPolicy = displayPolicy
return this as T
@@ -3144,6 +3147,7 @@
}
@OptIn(ComplicationExperimental::class)
+@Suppress("NewApi")
internal fun WireComplicationData.toPlaceholderComplicationData(): ComplicationData? {
// Make sure we use the correct dataSource, persistencePolicy & displayPolicy.
val dataSourceCopy = dataSource
@@ -3347,6 +3351,7 @@
*/
@OptIn(ComplicationExperimental::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+@Suppress("NewApi")
public fun WireComplicationData.toApiComplicationData(): ComplicationData {
// Make sure we use the correct dataSource, persistencePolicy & displayPolicy.
val dataSourceCopy = dataSource
diff --git a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
index 199c818..25c7f64 100644
--- a/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
+++ b/wear/watchface/watchface-complications-data/src/test/java/androidx/wear/watchface/complications/data/DataTest.kt
@@ -40,6 +40,7 @@
import org.robolectric.shadows.ShadowLog
@RunWith(SharedRobolectricTestRunner::class)
+@Suppress("NewApi")
public class AsWireComplicationDataTest {
val resources = ApplicationProvider.getApplicationContext<Context>().resources
val dataSourceA = ComponentName("com.pkg_a", "com.a")
@@ -2298,6 +2299,7 @@
}
@RunWith(SharedRobolectricTestRunner::class)
+@Suppress("NewApi")
public class FromWireComplicationDataTest {
@Test
public fun noDataComplicationData() {
@@ -3186,6 +3188,7 @@
}
@RunWith(SharedRobolectricTestRunner::class)
+@Suppress("NewApi")
public class ValidTimeRangeTest {
private val testStartInstant = Instant.ofEpochMilli(1000L)
private val testEndDateInstant = Instant.ofEpochMilli(2000L)
diff --git a/wear/watchface/watchface/src/main/AndroidManifest.xml b/wear/watchface/watchface/src/main/AndroidManifest.xml
index 546fd68..546b36f 100644
--- a/wear/watchface/watchface/src/main/AndroidManifest.xml
+++ b/wear/watchface/watchface/src/main/AndroidManifest.xml
@@ -34,7 +34,7 @@
android:enabled="@bool/watch_face_instance_service_enabled"
android:exported="true"
android:permission="com.google.android.wearable.permission.BIND_WATCH_FACE_CONTROL">
- <meta-data android:name="androidx.wear.watchface.api_version" android:value="5" />
+ <meta-data android:name="androidx.wear.watchface.api_version" android:value="6" />
<meta-data android:name="androidx.wear.watchface.xml_version"
android:value="@integer/watch_face_xml_version" />
<intent-filter>
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 417efe4..ea60837 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
@@ -2884,6 +2884,7 @@
}
@Test
+ @Suppress("NewApi")
public fun complicationCachePolicy() {
val complicationCache = HashMap<String, ByteArray>()
val instanceParams = WallpaperInteractiveWatchFaceInstanceParams(
@@ -6146,6 +6147,7 @@
}
@Test
+ @Suppress("NewApi")
public fun doNotDisplayComplicationWhenScreenLocked() {
initWallpaperInteractiveWatchFaceInstance(
WatchFaceType.ANALOG,
diff --git a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
index 14e674d..a926937 100644
--- a/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
+++ b/webkit/webkit/src/main/java/androidx/webkit/WebSettingsCompat.java
@@ -437,8 +437,8 @@
* <thead>
* <tr>
* <th>App</th>
- * <th>Web content which uses {@code prefers-color-scheme}</th>
- * <th>Web content which does not use {@code prefers-color-scheme}</th>
+ * <th>Web content which uses prefers-color-scheme</th>
+ * <th>Web content which does not use prefers-color-scheme</th>
* </tr>
* </thead>
* <tbody>