Merge "[Media] Demo app changes." into main
diff --git a/apps/Development/src/com/android/development/OWNERS b/apps/Development/src/com/android/development/OWNERS
index 171a26e..abf4611 100644
--- a/apps/Development/src/com/android/development/OWNERS
+++ b/apps/Development/src/com/android/development/OWNERS
@@ -1 +1 @@
-per-file Connectivity.java = codewiz@google.com, jchalard@google.com, lorenzo@google.com, reminv@google.com, satk@google.com
+per-file Connectivity.java =jchalard@google.com, lorenzo@google.com, reminv@google.com, satk@google.com
diff --git a/apps/ShareTest/res/layout/activity_main.xml b/apps/ShareTest/res/layout/activity_main.xml
index 059ea30..ee4b7de 100644
--- a/apps/ShareTest/res/layout/activity_main.xml
+++ b/apps/ShareTest/res/layout/activity_main.xml
@@ -1,4 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2025 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.
+ -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
diff --git a/apps/ShareTest/src/com/android/sharetest/ChosenComponentBroadcastReceiver.kt b/apps/ShareTest/src/com/android/sharetest/ChosenComponentBroadcastReceiver.kt
index 5d08b24..c19b6b0 100644
--- a/apps/ShareTest/src/com/android/sharetest/ChosenComponentBroadcastReceiver.kt
+++ b/apps/ShareTest/src/com/android/sharetest/ChosenComponentBroadcastReceiver.kt
@@ -24,23 +24,30 @@
class ChosenComponentBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
- val result = intent?.getParcelableExtra(
- Intent.EXTRA_CHOOSER_RESULT, ChooserResult::class.java)
- val message = when (result?.type) {
- ChooserResult.CHOOSER_RESULT_SELECTED_COMPONENT -> {
- "Sent to ${result.selectedComponent?.packageName}"
- }
- ChooserResult.CHOOSER_RESULT_COPY -> {
- "Copied to clipboard"
- }
- ChooserResult.CHOOSER_RESULT_EDIT -> {
- "Opened in image editor"
- }
+ val result =
+ intent?.getParcelableExtra(Intent.EXTRA_CHOOSER_RESULT, ChooserResult::class.java)
+ val message =
+ when (result?.type) {
+ ChooserResult.CHOOSER_RESULT_SELECTED_COMPONENT -> {
+ buildString {
+ append("Sent to ${result.selectedComponent?.packageName}")
+ val id = intent.id
+ if (id >= 0) {
+ append(" (id: $id)")
+ }
+ }
+ }
+ ChooserResult.CHOOSER_RESULT_COPY -> {
+ "Copied to clipboard"
+ }
+ ChooserResult.CHOOSER_RESULT_EDIT -> {
+ "Opened in image editor"
+ }
- else -> {
- "Unknown ChooserResult"
+ else -> {
+ "Unknown ChooserResult"
+ }
}
- }
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}
diff --git a/apps/ShareTest/src/com/android/sharetest/InteractiveShareTestActivity.kt b/apps/ShareTest/src/com/android/sharetest/InteractiveShareTestActivity.kt
index 4f10dd3..88b0ad5 100644
--- a/apps/ShareTest/src/com/android/sharetest/InteractiveShareTestActivity.kt
+++ b/apps/ShareTest/src/com/android/sharetest/InteractiveShareTestActivity.kt
@@ -16,16 +16,22 @@
package com.android.sharetest
+import android.app.AlertDialog
+import android.app.Dialog
+import android.content.BroadcastReceiver
import android.content.ClipData
+import android.content.Context
import android.content.Intent
+import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER
+import android.content.IntentFilter
import android.content.res.Configuration
import android.os.Bundle
import android.provider.MediaStore
import android.service.chooser.ChooserSession
import android.service.chooser.ChooserSession.ChooserController
import android.util.Log
-import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -35,6 +41,7 @@
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.Button
+import androidx.compose.material3.Checkbox
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
@@ -43,6 +50,7 @@
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Offset
@@ -51,6 +59,8 @@
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
@@ -68,12 +78,24 @@
private const val EXTRA_CHOOSER_INTERACTIVE_CALLBACK =
"com.android.extra.EXTRA_CHOOSER_INTERACTIVE_CALLBACK"
-@AndroidEntryPoint(value = ComponentActivity::class)
+@AndroidEntryPoint(value = FragmentActivity::class)
class InteractiveShareTestActivity : Hilt_InteractiveShareTestActivity() {
private val TAG = "ShareTest/$hashId"
private var chooserWindowTopOffset = MutableStateFlow(-1)
private val isInMultiWindowMode = MutableStateFlow<Boolean>(false)
private val chooserSession = MutableStateFlow<ChooserSession?>(null)
+ private val useRefinementFlow = MutableStateFlow<Boolean>(false)
+ private val refinementReceiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent) {
+ // Need to show refinement in another activity because this one is beneath the
+ // sharesheet.
+ val activityIntent =
+ Intent(this@InteractiveShareTestActivity, RefinementActivity::class.java)
+ activityIntent.putExtras(intent)
+ startActivity(activityIntent)
+ }
+ }
private val sessionStateListener =
object : ChooserSession.ChooserSessionUpdateListener {
@@ -139,6 +161,7 @@
// .collectAsStateWithLifecycle(false)
val isChooserRunning by
chooserSession.map { it?.isActive == true }.collectAsStateWithLifecycle(false)
+ val userRefinement by useRefinementFlow.collectAsStateWithLifecycle(false)
ActivityTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column(
@@ -149,10 +172,13 @@
Button(onClick = { startCameraApp() }) { Text("Pick Camera App") }
Button(onClick = { launchActivity() }) { Text("Launch Activity") }
}
- if (showLaunchInSplitScreen) {
- Button(onClick = { launchSelfInSplitScreen() }) {
- Text("Launch Self in Split-Screen")
+ Row(horizontalArrangement = Arrangement.spacedBy(spacing)) {
+ if (showLaunchInSplitScreen) {
+ Button(onClick = { launchSelfInSplitScreen() }) {
+ Text("Launch Self in Split-Screen")
+ }
}
+ Button(onClick = { launchDialog() }) { Text("Launch Dialog") }
}
Row(
modifier = Modifier.fillMaxWidth().wrapContentHeight(),
@@ -177,6 +203,20 @@
}
}
}
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(spacing),
+ modifier = Modifier.clickable { updateRefinement() },
+ ) {
+ Checkbox(
+ checked = userRefinement,
+ onCheckedChange = {},
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ Text(
+ "Use Refinement",
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
if (isChooserRunning) {
Button(onClick = { closeChooser() }) { Text("Close Chooser") }
}
@@ -228,6 +268,9 @@
override fun onDestroy() {
Log.d(TAG, "onDestroy")
+ if (useRefinementFlow.value) {
+ unregisterReceiver(refinementReceiver)
+ }
super.onDestroy()
}
@@ -242,6 +285,21 @@
super.onConfigurationChanged(newConfig)
}
+ private fun updateRefinement() {
+ useRefinementFlow.update {
+ if (it) {
+ unregisterReceiver(refinementReceiver)
+ } else {
+ registerReceiver(
+ refinementReceiver,
+ IntentFilter(REFINEMENT_ACTION),
+ RECEIVER_EXPORTED,
+ )
+ }
+ !it
+ }
+ }
+
private fun startCameraApp() {
val targetIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
startOrUpdate(Intent.createChooser(targetIntent, null))
@@ -251,6 +309,11 @@
startActivity(Intent(this, SendTextActivity::class.java))
}
+ private fun launchDialog() {
+ val dialog = TestDialog()
+ dialog.show(supportFragmentManager, "dialog")
+ }
+
private fun launchSelfInSplitScreen() {
startActivity(
Intent(this, javaClass).apply {
@@ -304,6 +367,13 @@
private fun startOrUpdate(chooserIntent: Intent) {
val chooserController = chooserSession.value?.takeIf { it.isActive }?.chooserController
+ if (useRefinementFlow.value) {
+ chooserIntent.putExtra(
+ Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER,
+ createRefinementIntentSender(this@InteractiveShareTestActivity, true),
+ )
+ }
+ chooserIntent.putExtra(EXTRA_CHOOSER_RESULT_INTENT_SENDER, createResultIntentSender(this))
if (chooserController == null) {
val session = ChooserSession()
chooserSession.value = session
@@ -317,3 +387,12 @@
}
}
}
+
+class TestDialog : DialogFragment() {
+ override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
+ return AlertDialog.Builder(requireContext())
+ .setMessage("Just a test dialog")
+ .setPositiveButton("Close") { _, _ -> dismiss() }
+ .create()
+ }
+}
diff --git a/apps/ShareTest/src/com/android/sharetest/RefinementActivity.kt b/apps/ShareTest/src/com/android/sharetest/RefinementActivity.kt
index 9d009d9..4aeb221 100644
--- a/apps/ShareTest/src/com/android/sharetest/RefinementActivity.kt
+++ b/apps/ShareTest/src/com/android/sharetest/RefinementActivity.kt
@@ -13,30 +13,30 @@
intent.getParcelableExtra(Intent.EXTRA_RESULT_RECEIVER, ResultReceiver::class.java)
val sharedIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
val builder: AlertDialog.Builder = AlertDialog.Builder(this)
+ val message = buildString {
+ append("Refinement intent id: ${intent.id}")
+ append("\nIs modified by payload selection: ${!intent.isInitial}")
+ append("\nTarget intent action: ${sharedIntent?.action}")
+ append("\nItem count: ${sharedIntent?.extraStream?.size}")
+ append("\nTarget intent type: ${sharedIntent?.type}")
+ append("\n\nComplete the share?")
+ }
builder
- .setMessage(
- """
- |Is modified by payload selection: ${!intent.isInitial}
- |
- |Complete the share?
- """.trimMargin()
- )
- .setTitle("Refinement invoked!")
- .setPositiveButton("Yes") { _, _ ->
- val bundle = Bundle().apply {
- putParcelable(Intent.EXTRA_INTENT, sharedIntent)
- }
- resultReceiver?.send(RESULT_OK, bundle)
- finish()
- }
- .setNegativeButton("No") { _, _ ->
- resultReceiver?.send(RESULT_CANCELED, null)
- finish()
- }
- .setOnCancelListener {
- resultReceiver?.send(RESULT_CANCELED, null)
- finish()
- }
+ .setMessage(message)
+ .setTitle("Refinement invoked!")
+ .setPositiveButton("Yes") { _, _ ->
+ val bundle = Bundle().apply { putParcelable(Intent.EXTRA_INTENT, sharedIntent) }
+ resultReceiver?.send(RESULT_OK, bundle)
+ finish()
+ }
+ .setNegativeButton("No") { _, _ ->
+ resultReceiver?.send(RESULT_CANCELED, null)
+ finish()
+ }
+ .setOnCancelListener {
+ resultReceiver?.send(RESULT_CANCELED, null)
+ finish()
+ }
builder.create().show()
}
diff --git a/apps/ShareTest/src/com/android/sharetest/Utils.kt b/apps/ShareTest/src/com/android/sharetest/Utils.kt
index 58d5f94..2ed5995 100644
--- a/apps/ShareTest/src/com/android/sharetest/Utils.kt
+++ b/apps/ShareTest/src/com/android/sharetest/Utils.kt
@@ -27,10 +27,12 @@
import android.service.chooser.ChooserTarget
import android.text.TextUtils
import androidx.core.os.bundleOf
+import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.roundToLong
const val REFINEMENT_ACTION = "com.android.sharetest.REFINEMENT"
-private const val EXTRA_IS_INITIAL = "isInitial"
+private const val EXTRA_IS_INITIAL = "com.android.sharetest.IS_INITIAL"
+private const val EXTRA_ID = "com.android.sharetest.ID"
fun createAlternateIntent(intent: Intent): Intent {
val text = buildString {
@@ -116,6 +118,12 @@
}
get() = getBooleanExtra(EXTRA_IS_INITIAL, true)
+var Intent.id: Int
+ set(value) {
+ putExtra(EXTRA_ID, value)
+ }
+ get() = getIntExtra(EXTRA_ID, -1)
+
fun createCallerTarget(context: Context, text: String) =
ChooserTarget(
"Caller Target",
@@ -125,6 +133,8 @@
bundleOf(Intent.EXTRA_TEXT to text),
)
+private val refinementCounter = AtomicInteger(0)
+
fun createRefinementIntentSender(context: Context, isInitial: Boolean) =
PendingIntent.getBroadcast(
context,
@@ -132,6 +142,7 @@
Intent(REFINEMENT_ACTION).apply {
setPackage(context.packageName)
this.isInitial = isInitial
+ id = refinementCounter.incrementAndGet()
},
PendingIntent.FLAG_MUTABLE or
PendingIntent.FLAG_CANCEL_CURRENT or
@@ -139,5 +150,18 @@
)
.intentSender
+private val resultIntentSenderCounter = AtomicInteger(0)
+
+fun createResultIntentSender(context: Context) =
+ PendingIntent.getBroadcast(
+ context,
+ 0,
+ Intent(context, ChosenComponentBroadcastReceiver::class.java).apply {
+ id = resultIntentSenderCounter.incrementAndGet()
+ },
+ PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
+ )
+ .intentSender
+
val Any.hashId: String
get() = System.identityHashCode(this).toString(Character.MAX_RADIX)
diff --git a/build/sdk.atree b/build/sdk.atree
index 08ddcb8..402d34b 100644
--- a/build/sdk.atree
+++ b/build/sdk.atree
@@ -54,7 +54,7 @@
# Android Automotive OS stubs.
${OUT_DIR}/target/common/obj/JAVA_LIBRARIES/android.car-stubs_intermediates/classes.jar platforms/${PLATFORM_NAME}/optional/android.car.jar
# Wear OS platform SDK stubs.
-prebuilts/sdk/opt/wear/${PLATFORM_SDK_API_VERSION}/public/wear-sdk.jar platforms/${PLATFORM_NAME}/optional/wear-sdk.jar
+${OUT_DIR}/target/common/obj/PACKAGING/wear_sdk_jar_intermediates/wear-sdk.jar platforms/${PLATFORM_NAME}/optional/wear-sdk.jar
# Test APIs
${OUT_DIR}/target/common/obj/JAVA_LIBRARIES/android.test.mock.stubs_intermediates/classes.jar platforms/${PLATFORM_NAME}/optional/android.test.mock.jar
${OUT_DIR}/target/common/obj/JAVA_LIBRARIES/android.test.base.stubs_intermediates/classes.jar platforms/${PLATFORM_NAME}/optional/android.test.base.jar
diff --git a/build/tools/sdk-preprocess-files.mk b/build/tools/sdk-preprocess-files.mk
index 89aaf96..5af3ef3 100644
--- a/build/tools/sdk-preprocess-files.mk
+++ b/build/tools/sdk-preprocess-files.mk
@@ -147,3 +147,36 @@
# ============ SDK AIDL ============
$(eval $(call copy-one-file,$(FRAMEWORK_AIDL),$(TARGET_OUT_COMMON_INTERMEDIATES)/PACKAGING/framework.aidl))
ALL_SDK_FILES += $(TARGET_OUT_COMMON_INTERMEDIATES)/PACKAGING/framework.aidl
+
+
+# ===== Wear SDK jar file of stubs =====
+# the "current" version of the public Wear SDK (used in the even that the prebuilt API is not yet finalized)
+wear_sdk_current_stubs_target := $(if $(findstring wear-sdk.stubs.exportable,$(ALL_MODULES)), \
+ $(call intermediates-dir-for,JAVA_LIBRARIES,wear-sdk.stubs.exportable,,COMMON)/classes.jar, \
+ )
+
+# the prebuilt version of the public Wear SDK (contains only finalized API)
+wear_sdk_prebuilt_stubs_target := $(TOPDIR)prebuilts/sdk/opt/wear/$(patsubst "%",%,$(PLATFORM_SDK_VERSION_FULL))/public/wear-sdk.jar
+
+# wear-sdk.jar is what we put in the SDK package (as an optional jar).
+wear_sdk_jar_intermediates := $(call intermediates-dir-for,PACKAGING,wear_sdk_jar,,COMMON)
+wear_sdk_jar_full_target := $(wear_sdk_jar_intermediates)/wear-sdk.jar
+
+# The only explicit dependency for the optional jar is the current stub, however if the prebuilt (finalized) stub
+# exists, then we should be using it (availability of the current stub however, avoids breakage pre-finalization)
+# The one caveat here is on branches that do *NOT* contain wear projects (e.g. vendorless) - in that case we just
+# touch an empty target to ensure that we avoid breakage (we don't release from these branches, so empty is fine)
+$(wear_sdk_jar_full_target): PRIVATE_PREBUILT_STUBS_TARGET := $(wear_sdk_prebuilt_stubs_target)
+$(wear_sdk_jar_full_target): PRIVATE_CURRENT_STUBS_TARGET := $(wear_sdk_current_stubs_target)
+$(wear_sdk_jar_full_target): $(wear_sdk_current_stubs_target) $(wildcard $(PRIVATE_PREBUILT_STUBS_TARGET))
+ @echo Package Wear SDK Stubs: $@
+ $(hide) mkdir -p $(dir $@)
+ $(if $(wildcard $(PRIVATE_PREBUILT_STUBS_TARGET)), \
+ $(ACP) $(PRIVATE_PREBUILT_STUBS_TARGET) $@, \
+ $(if $(strip $(PRIVATE_CURRENT_STUBS_TARGET)), \
+ $(ACP) $(PRIVATE_CURRENT_STUBS_TARGET) $@, \
+ touch $@ \
+ ) \
+ )
+
+ALL_SDK_FILES += $(wear_sdk_jar_full_target)
diff --git a/gki/OWNERS b/gki/OWNERS
index f3b3233..e7246dc 100644
--- a/gki/OWNERS
+++ b/gki/OWNERS
@@ -1,3 +1,2 @@
howardsoc@google.com
hsinyichen@google.com
-ycchen@google.com
diff --git a/ide/intellij/codestyles/AndroidStyle.xml b/ide/intellij/codestyles/AndroidStyle.xml
index 846e69a..782988e 100644
--- a/ide/intellij/codestyles/AndroidStyle.xml
+++ b/ide/intellij/codestyles/AndroidStyle.xml
@@ -19,6 +19,8 @@
<emptyLine />
<package name="com" withSubpackages="true" static="true" />
<emptyLine />
+ <package name="dagger" withSubpackages="true" static="true" />
+ <emptyLine />
<package name="gov" withSubpackages="true" static="true" />
<emptyLine />
<package name="junit" withSubpackages="true" static="true" />
@@ -27,6 +29,8 @@
<emptyLine />
<package name="kotlin" withSubpackages="true" static="true" />
<emptyLine />
+ <package name="kotlinx" withSubpackages="true" static="true" />
+ <emptyLine />
<package name="net" withSubpackages="true" static="true" />
<emptyLine />
<package name="org" withSubpackages="true" static="true" />
@@ -59,6 +63,8 @@
<emptyLine />
<package name="kotlin" withSubpackages="true" static="false" />
<emptyLine />
+ <package name="kotlinx" withSubpackages="true" static="false" />
+ <emptyLine />
<package name="net" withSubpackages="true" static="false" />
<emptyLine />
<package name="org" withSubpackages="true" static="false" />
diff --git a/samples/MotionMechanics/src/com/android/mechanics/demo/util/ExpandableCard.kt b/samples/MotionMechanics/src/com/android/mechanics/demo/util/ExpandableCard.kt
index 413901d..bb16cfe 100644
--- a/samples/MotionMechanics/src/com/android/mechanics/demo/util/ExpandableCard.kt
+++ b/samples/MotionMechanics/src/com/android/mechanics/demo/util/ExpandableCard.kt
@@ -145,7 +145,7 @@
@Composable
private fun ContentScope.Chevron(rotate: Boolean, modifier: Modifier = Modifier) {
val key = Elements.Chevron
- Element(key, modifier) {
+ ElementWithValues(key, modifier) {
val rotation by animateElementIntAsState(if (rotate) 180 else 0, Values.ChevronRotation)
content {
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Camera.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Camera.kt
index 7a64bcd..799264a 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Camera.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Camera.kt
@@ -79,7 +79,7 @@
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
- Element(Camera.Elements.Button, modifier) {
+ ElementWithValues(Camera.Elements.Button, modifier) {
val backgroundColor by
animateElementColorAsState(backgroundColor, Camera.Values.ButtonColor)
val iconColor by
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Clock.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Clock.kt
index 502ffc8..42dfead 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Clock.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Clock.kt
@@ -40,7 +40,7 @@
@Composable
fun ContentScope.Clock(color: Color, modifier: Modifier = Modifier) {
- Element(Clock.Elements.Clock, modifier) {
+ ElementWithValues(Clock.Elements.Clock, modifier) {
val color by animateElementColorAsState(color, Clock.Values.TextColor)
content {
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/PartialShade.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/PartialShade.kt
index 5feafcb..e8fd461 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/PartialShade.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/PartialShade.kt
@@ -16,7 +16,6 @@
package com.android.compose.animation.scene.demo
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxWidth
@@ -29,7 +28,6 @@
import androidx.compose.foundation.withoutVisualEffect
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
@@ -44,6 +42,8 @@
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.compose.modifiers.thenIf
+import com.android.mechanics.behavior.EdgeContainerExpansionSpec
+import com.android.mechanics.behavior.edgeContainerExpansionBackground
object PartialShade {
object Colors {
@@ -59,6 +59,8 @@
)
val SplitBackground = RoundedCornerShape(Shade.Dimensions.ScrimCornerSize)
}
+
+ val MotionSpec = EdgeContainerExpansionSpec()
}
@Composable
@@ -69,8 +71,7 @@
) {
val isSplitShade = shouldUseSplitScenes(calculateWindowSizeClass())
- val shape =
- if (isSplitShade) PartialShade.Shapes.SplitBackground else PartialShade.Shapes.Background
+ // TODO(michschn) Add expansion for partial shade again
val contentOverscrollEffect = checkNotNull(rememberOverscrollEffect())
Box(
modifier
@@ -79,8 +80,10 @@
.overscroll(contentOverscrollEffect)
.thenIf(isSplitShade) { Modifier.padding(16.dp) }
.element(rootElement)
- .clip(shape)
- .background(PartialShade.Colors.Background)
+ .edgeContainerExpansionBackground(
+ PartialShade.Colors.Background,
+ PartialShade.MotionSpec,
+ )
.disableSwipesWhenScrolling()
.verticalScroll(
rememberScrollState(),
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Shade.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Shade.kt
index 982fb46..1b5b023 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Shade.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/Shade.kt
@@ -455,42 +455,47 @@
@Composable
fun ContentScope.ShadeTime(scale: Float, modifier: Modifier = Modifier) {
- Element(Shade.Elements.Time, modifier) {
- val measurer = rememberTextMeasurer()
- val color = LocalContentColor.current
- val style = LocalTextStyle.current
+ ElementWithValues(Shade.Elements.Time, modifier) {
val animatedScale by
animateElementFloatAsState(scale, Shade.Values.TimeScale, canOverflow = false)
- val layoutResult = remember(measurer, style) { measurer.measure("10:36", style = style) }
- val layoutDirection = LocalLayoutDirection.current
- Box {
- Spacer(
- Modifier.layout { measurable, _ ->
- // Layout this element with the *target* size/scale of the element in this
- // scene.
- val width = ceil(layoutResult.size.width * scale).roundToInt()
- val height = ceil(layoutResult.size.height * scale).roundToInt()
- measurable.measure(Constraints.fixed(width, height)).run {
- layout(width, height) { place(0, 0) }
- }
- }
- .drawBehind {
- val topLeft: Offset
- val pivot: Offset
- if (layoutDirection == LayoutDirection.Ltr) {
- topLeft = Offset.Zero
- pivot = Offset.Zero
- } else {
- topLeft = Offset(size.width - layoutResult.size.width, 0f)
- pivot = Offset(size.width, 0f)
- }
+ content {
+ val measurer = rememberTextMeasurer()
+ val color = LocalContentColor.current
+ val style = LocalTextStyle.current
+ val layoutResult =
+ remember(measurer, style) { measurer.measure("10:36", style = style) }
+ val layoutDirection = LocalLayoutDirection.current
- scale(animatedScale, pivot = pivot) {
- drawText(layoutResult, color = color, topLeft = topLeft)
+ Box {
+ Spacer(
+ Modifier.layout { measurable, _ ->
+ // Layout this element with the *target* size/scale of the element in
+ // this
+ // scene.
+ val width = ceil(layoutResult.size.width * scale).roundToInt()
+ val height = ceil(layoutResult.size.height * scale).roundToInt()
+ measurable.measure(Constraints.fixed(width, height)).run {
+ layout(width, height) { place(0, 0) }
+ }
}
- }
- )
+ .drawBehind {
+ val topLeft: Offset
+ val pivot: Offset
+ if (layoutDirection == LayoutDirection.Ltr) {
+ topLeft = Offset.Zero
+ pivot = Offset.Zero
+ } else {
+ topLeft = Offset(size.width - layoutResult.size.width, 0f)
+ pivot = Offset(size.width, 0f)
+ }
+
+ scale(animatedScale, pivot = pivot) {
+ drawText(layoutResult, color = color, topLeft = topLeft)
+ }
+ }
+ )
+ }
}
}
}
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SmartSpace.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SmartSpace.kt
index eca0d39..f1577b3 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SmartSpace.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SmartSpace.kt
@@ -50,7 +50,7 @@
@Composable
fun ContentScope.SmartSpace(textColor: Color, modifier: Modifier = Modifier) {
- Element(SmartSpace.Elements.SmartSpace, modifier) {
+ ElementWithValues(SmartSpace.Elements.SmartSpace, modifier) {
val color = animateElementColorAsState(textColor, SmartSpace.Values.TextColor)
content {
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SystemUi.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SystemUi.kt
index 4290da4..740a66d 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SystemUi.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/SystemUi.kt
@@ -541,6 +541,7 @@
} else {
DefaultEdgeDetector
},
+ implicitTestTags = true,
) {
scene(Scenes.Launcher, Launcher.userActions(shadeScene, configuration)) {
Launcher(launcherColumns)
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/notification/NotificationContent.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/notification/NotificationContent.kt
index 6fc69eb..439443e 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/notification/NotificationContent.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/notification/NotificationContent.kt
@@ -152,7 +152,7 @@
@Composable
private fun ContentScope.Chevron(rotate: Boolean, modifier: Modifier = Modifier) {
val key = NotificationContent.Elements.Chevron
- Element(key, modifier) {
+ ElementWithValues(key, modifier) {
val rotation by
animateElementIntAsState(
if (rotate) 180 else 0,
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/NotificationShadeTransitions.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/NotificationShadeTransitions.kt
index ab229cd..d155127 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/NotificationShadeTransitions.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/NotificationShadeTransitions.kt
@@ -23,6 +23,7 @@
import com.android.compose.animation.scene.demo.MediaPlayer
import com.android.compose.animation.scene.demo.NotificationShade
import com.android.compose.animation.scene.demo.Overlays
+import com.android.compose.animation.scene.demo.PartialShade
import com.android.compose.animation.scene.demo.notification.NotificationList
import com.android.compose.animation.scene.reveal.ContainerRevealHaptics
import com.android.compose.animation.scene.reveal.verticalContainerReveal
@@ -51,5 +52,5 @@
val ToNotificationShadeStartFadeProgress = 0.5f
private fun TransitionBuilder.toNotificationShade(revealHaptics: ContainerRevealHaptics) {
- verticalContainerReveal(NotificationShade.Elements.Root, revealHaptics)
+ verticalContainerReveal(NotificationShade.Elements.Root, PartialShade.MotionSpec, revealHaptics)
}
diff --git a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/QuickSettingsShadeTransitions.kt b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/QuickSettingsShadeTransitions.kt
index f509f12..af31420 100644
--- a/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/QuickSettingsShadeTransitions.kt
+++ b/samples/SceneTransitionLayoutDemo/src/com/android/compose/animation/scene/demo/transitions/QuickSettingsShadeTransitions.kt
@@ -22,6 +22,7 @@
import com.android.compose.animation.scene.demo.Clock
import com.android.compose.animation.scene.demo.MediaPlayer
import com.android.compose.animation.scene.demo.Overlays
+import com.android.compose.animation.scene.demo.PartialShade
import com.android.compose.animation.scene.demo.QuickSettings
import com.android.compose.animation.scene.demo.QuickSettingsGrid
import com.android.compose.animation.scene.demo.QuickSettingsShade
@@ -39,7 +40,11 @@
spec = tween(500)
sharedElement(MediaPlayer.Elements.MediaPlayer, elevateInContent = Overlays.QuickSettings)
- verticalContainerReveal(QuickSettingsShade.Elements.Root, revealHaptics)
+ verticalContainerReveal(
+ QuickSettingsShade.Elements.Root,
+ PartialShade.MotionSpec,
+ revealHaptics,
+ )
}
from(Overlays.QuickSettings, to = Overlays.Notifications) {
diff --git a/samples/VirtualDeviceManager/Android.bp b/samples/VirtualDeviceManager/Android.bp
index 0b6b2d4..16117a2 100644
--- a/samples/VirtualDeviceManager/Android.bp
+++ b/samples/VirtualDeviceManager/Android.bp
@@ -28,9 +28,6 @@
"guava",
"hilt_android",
],
- lint: {
- baseline_filename: "lint-baseline.xml",
- },
}
android_app {
diff --git a/samples/VirtualDeviceManager/demos/AndroidManifest.xml b/samples/VirtualDeviceManager/demos/AndroidManifest.xml
index 234a146..9a0ff2d 100644
--- a/samples/VirtualDeviceManager/demos/AndroidManifest.xml
+++ b/samples/VirtualDeviceManager/demos/AndroidManifest.xml
@@ -5,7 +5,7 @@
<uses-sdk
android:minSdkVersion="34"
- android:targetSdkVersion="35" />
+ android:targetSdkVersion="36" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
diff --git a/samples/VirtualDeviceManager/demos/res/values-v35/styles.xml b/samples/VirtualDeviceManager/demos/res/values-v35/styles.xml
deleted file mode 100644
index 68a21f4..0000000
--- a/samples/VirtualDeviceManager/demos/res/values-v35/styles.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<resources>
- <style name="AppTheme" parent="@style/Theme.AppCompat.NoActionBar">
- <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
- </style>
-</resources>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/CounterView.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/CounterView.java
index ca1ea7c..8d96fb7 100644
--- a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/CounterView.java
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/CounterView.java
@@ -65,7 +65,8 @@
@Override
protected void onDraw(Canvas canvas) {
- canvas.drawText(String.valueOf(mCounter), 0, 200, mTextPaint);
+ canvas.drawText(String.valueOf(mCounter), (float) getWidth() / 2, (float) getHeight() / 2,
+ mTextPaint);
Log.e(TAG, "Rendered counter: " + mCounter);
mCounter++;
}
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/SensorDemoActivity.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/SensorDemoActivity.java
index 17e11d7..f9a6d06 100644
--- a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/SensorDemoActivity.java
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/SensorDemoActivity.java
@@ -27,10 +27,14 @@
import android.os.Build;
import android.os.Bundle;
import android.view.View;
+import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.graphics.Insets;
import androidx.core.os.BuildCompat;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
import java.util.List;
import java.util.Objects;
@@ -54,6 +58,15 @@
setContentView(R.layout.sensor_demo_activity);
+ View deviceChangeView = (View) requireViewById(R.id.current_device).getParent();
+ ViewCompat.setOnApplyWindowInsetsListener(deviceChangeView, (v, windowInsets) -> {
+ Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams();
+ lp.topMargin = insets.top;
+ v.setLayoutParams(lp);
+ return WindowInsetsCompat.CONSUMED;
+ });
+
mBeam = requireViewById(R.id.beam);
mVirtualDeviceManager = getSystemService(VirtualDeviceManager.class);
diff --git a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/VibrationDemoActivity.java b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/VibrationDemoActivity.java
index b20bb2c..55600a6 100644
--- a/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/VibrationDemoActivity.java
+++ b/samples/VirtualDeviceManager/demos/src/com/example/android/vdmdemo/demos/VibrationDemoActivity.java
@@ -30,10 +30,14 @@
import android.os.Vibrator;
import android.view.HapticFeedbackConstants;
import android.view.View;
+import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.graphics.Insets;
import androidx.core.os.BuildCompat;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
import java.util.List;
import java.util.Objects;
@@ -56,6 +60,15 @@
setContentView(R.layout.vibration_demo_activity);
+ View deviceChangeView = (View) requireViewById(R.id.current_device).getParent();
+ ViewCompat.setOnApplyWindowInsetsListener(deviceChangeView, (v, windowInsets) -> {
+ Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams();
+ lp.topMargin = insets.top;
+ v.setLayoutParams(lp);
+ return WindowInsetsCompat.CONSUMED;
+ });
+
mVirtualDeviceManager = getSystemService(VirtualDeviceManager.class);
mVibrator = getSystemService(Vibrator.class);
diff --git a/samples/VirtualDeviceManager/host/AndroidManifest.xml b/samples/VirtualDeviceManager/host/AndroidManifest.xml
index 92e1677..0fd50fa 100644
--- a/samples/VirtualDeviceManager/host/AndroidManifest.xml
+++ b/samples/VirtualDeviceManager/host/AndroidManifest.xml
@@ -6,7 +6,7 @@
<uses-sdk
android:minSdkVersion="34"
- android:targetSdkVersion="35" />
+ android:targetSdkVersion="36" />
<uses-feature android:name="android.software.companion_device_setup" />
diff --git a/samples/VirtualDeviceManager/host/res/values-v35/styles.xml b/samples/VirtualDeviceManager/host/res/values-v35/styles.xml
deleted file mode 100644
index 6828a0b..0000000
--- a/samples/VirtualDeviceManager/host/res/values-v35/styles.xml
+++ /dev/null
@@ -1,5 +0,0 @@
-<resources>
- <style name="AppTheme" parent="@style/Theme.AppCompat.Light.NoActionBar">
- <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
- </style>
-</resources>
\ No newline at end of file
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/AudioInjector.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/AudioInjector.java
index 823adaa..2626f5f 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/AudioInjector.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/AudioInjector.java
@@ -20,6 +20,8 @@
import android.annotation.SuppressLint;
import android.content.Context;
+import android.media.AudioDeviceCallback;
+import android.media.AudioDeviceInfo;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecordingConfiguration;
@@ -64,8 +66,11 @@
PreferenceController mPreferenceController;
private final Object mLock = new Object();
+ // Use a list of always running AudioTracks that write silence when no audio data is written.
+ // This is needed to keep the Remote Submix ports open and connected so they are available
+ // when a new AudioRecord is started by an app using this VirtualAudioDevice.
@GuardedBy("mLock")
- private final List<AudioTrack> mAudioTracks = new ArrayList<>();
+ private final List<SilentAudioTrack> mAudioTracks = new ArrayList<>();
private final RemoteIo mRemoteIo;
private int mRecordingSessionId;
private boolean mIsPlaying;
@@ -84,7 +89,7 @@
public void onRecordingConfigChanged(List<AudioRecordingConfiguration> configs) {
super.onRecordingConfigChanged(configs);
- synchronized (AudioInjector.this.mLock) {
+ synchronized (mLock) {
boolean shouldStream = false;
for (AudioRecordingConfiguration config : configs) {
if (mReroutedUids.contains(config.getClientUid())
@@ -118,6 +123,30 @@
}
};
+ // Useful for logging when the audio ports are available so remote recording is done
+ // from the remote audio device
+ private final AudioDeviceCallback mAudioDeviceCallback = new AudioDeviceCallback() {
+ @Override
+ public void onAudioDevicesAdded(AudioDeviceInfo[] addedDevices) {
+ for (AudioDeviceInfo device : addedDevices) {
+ if (device.isSource()
+ && device.getType() == AudioDeviceInfo.TYPE_REMOTE_SUBMIX) {
+ Log.d(TAG, "Connected source port with address: " + device.getAddress());
+ }
+ }
+ }
+
+ @Override
+ public void onAudioDevicesRemoved(AudioDeviceInfo[] addedDevices) {
+ for (AudioDeviceInfo device : addedDevices) {
+ if (device.isSource()
+ && device.getType() == AudioDeviceInfo.TYPE_REMOTE_SUBMIX) {
+ Log.d(TAG, "Disconnected source port with address: " + device.getAddress());
+ }
+ }
+ }
+ };
+
@Inject
AudioInjector(@ApplicationContext Context context, RemoteIo remoteIo) {
mApplicationContext = context;
@@ -130,39 +159,16 @@
Log.e(TAG, "Received audio frame, but no audio track was initialized.");
}
- for (AudioTrack audioTrack : mAudioTracks) {
- playAudioFrame(audioFrame, audioTrack);
+ for (SilentAudioTrack audioTrack : mAudioTracks) {
+ audioTrack.playAudioFrame(audioFrame);
}
}
}
- private void playAudioFrame(AudioFrame audioFrame, AudioTrack audioTrack) {
- byte[] data = audioFrame.getData().toByteArray();
- int bytesToWrite = data.length;
- if (bytesToWrite == 0) {
- return;
- }
- int bytesWritten = 0;
- if (audioTrack == null) {
- Log.e(TAG, "Received audio frame, but no audio track was initialized.");
- return;
- }
-
- while (bytesToWrite > 0 && mIsPlaying) {
- int ret = audioTrack.write(data, bytesWritten, bytesToWrite);
- if (ret < 0) {
- Log.e(TAG, "AudioTrack.write returned error code " + ret);
- break;
- }
- bytesToWrite -= ret;
- bytesWritten += ret;
- }
- }
-
private void startPlayback() {
mIsPlaying = true;
synchronized (mLock) {
- for (AudioTrack audioTrack : mAudioTracks) {
+ for (SilentAudioTrack audioTrack : mAudioTracks) {
audioTrack.play();
}
@@ -173,7 +179,7 @@
private void stopPlayback() {
mIsPlaying = false;
synchronized (mLock) {
- for (AudioTrack audioTrack : mAudioTracks) {
+ for (SilentAudioTrack audioTrack : mAudioTracks) {
audioTrack.stop();
}
@@ -195,6 +201,7 @@
mAudioManager = mDeviceContext.getSystemService(AudioManager.class);
if (mAudioManager != null) {
mAudioManager.registerAudioRecordingCallback(mAudioRecordingCallback, null);
+ mAudioManager.registerAudioDeviceCallback(mAudioDeviceCallback, null);
registerAudioPolicy(ImmutableSet.of());
}
}
@@ -217,6 +224,7 @@
if (mAudioManager != null) {
mAudioManager.unregisterAudioRecordingCallback(mAudioRecordingCallback);
unregisterAudioPolicy();
+ mAudioManager.unregisterAudioDeviceCallback(mAudioDeviceCallback);
mAudioManager = null;
}
}
@@ -369,11 +377,7 @@
if (audioTrack.getState() != STATE_INITIALIZED) {
throw new IllegalStateException("Set an uninitialized AudioTrack.");
}
-
- if (mIsPlaying) {
- audioTrack.play();
- }
- mAudioTracks.add(audioTrack);
+ mAudioTracks.add(new SilentAudioTrack(audioTrack));
Log.d(TAG, "Added source audio track: " + audioTrack);
}
@@ -381,11 +385,110 @@
private void closeAndReleaseAllTracks() {
Log.i(TAG, "Close and release all source tracks.");
- for (AudioTrack audioTrack : mAudioTracks) {
- audioTrack.stop();
- audioTrack.release();
+ synchronized (mLock) {
+ for (SilentAudioTrack audioTrack : mAudioTracks) {
+ audioTrack.stop();
+ audioTrack.release();
+ }
+ mAudioTracks.clear();
}
- mAudioTracks.clear();
+ }
+
+ /**
+ * Utility class that keeps an AudioTrack "alive" and always provided with silence in absence
+ * of real audio data. Wraps around an AudioTrack and activates the silence mode when 'stop()'
+ * is called and allows for real data to be written (and stops the silence) when 'play()' is
+ * called.
+ */
+ private class SilentAudioTrack {
+ private final AudioTrack mAudioTrack;
+ private final byte[] mSilenceBuffer;
+ private Thread mSilenceAudioThread;
+ private volatile boolean mRunSilence = false;
+
+ SilentAudioTrack(AudioTrack audioTrack) {
+ mAudioTrack = audioTrack;
+ // Taken from AUDIO_FORMAT_IN channel count (1) and sample rate (2 bytesPerSample)
+ mSilenceBuffer = new byte[audioTrack.getBufferSizeInFrames() * 2];
+ if (!mIsPlaying) {
+ startSilenceThread();
+ }
+ // start playing when created
+ mAudioTrack.play();
+ }
+
+ // Switch to write "real data" mode, stop the silence
+ public void play() {
+ stopSilenceThread();
+ // empty any silence already written to the audio track
+ mAudioTrack.flush();
+ }
+
+ // Switch to write "silence" mode
+ public void stop() {
+ startSilenceThread();
+ }
+
+ // Stop and release the audio track and silence Thread
+ public void release() {
+ stopSilenceThread();
+ mAudioTrack.stop();
+ mAudioTrack.release();
+ }
+
+ private void playAudioFrame(AudioFrame audioFrame) {
+ byte[] data = audioFrame.getData().toByteArray();
+ int bytesToWrite = data.length;
+ if (bytesToWrite == 0) {
+ return;
+ }
+ int bytesWritten = 0;
+ while (bytesToWrite > 0 && mIsPlaying) {
+ int ret = mAudioTrack.write(data, bytesWritten, bytesToWrite);
+ if (ret < 0) {
+ Log.e(TAG, "AudioTrack.write returned error code " + ret);
+ break;
+ }
+ bytesToWrite -= ret;
+ bytesWritten += ret;
+ }
+ }
+
+ private void startSilenceThread() {
+ if (mSilenceAudioThread == null || !mSilenceAudioThread.isAlive()) {
+ mSilenceAudioThread = new Thread(() -> {
+ Log.d(TAG, "Silence audio thread starting for audio track: " + mAudioTrack);
+ mRunSilence = true;
+ while (mRunSilence) {
+ try {
+ int ret = mAudioTrack.write(mSilenceBuffer, 0, mSilenceBuffer.length);
+ if (ret < 0) {
+ mRunSilence = false;
+ Log.e(TAG, "Error writing silence: " + ret);
+ break;
+ }
+ } catch (Exception e) {
+ mRunSilence = false;
+ Log.e(TAG, "Exception writing silence", e);
+ }
+ }
+ Log.d(TAG, "Silence audio thread exiting for audio track: " + mAudioTrack);
+ }, "SilenceAudioThread");
+ mSilenceAudioThread.start();
+ }
+ }
+
+ private void stopSilenceThread() {
+ try {
+ mRunSilence = false;
+ if (mSilenceAudioThread != null) {
+ mSilenceAudioThread.join();
+ }
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Exception stopping the silence Thread.", e);
+ }
+ mSilenceAudioThread = null;
+ }
}
private static AudioMix getSessionIdAudioMix(int sessionId) {
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputActivity.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputActivity.java
index 6cb1f12..96917ae 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputActivity.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/InputActivity.java
@@ -21,9 +21,13 @@
import android.view.MotionEvent;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
+import android.widget.LinearLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
import androidx.fragment.app.Fragment;
import androidx.preference.PreferenceManager;
@@ -58,6 +62,15 @@
toolbar.setNavigationOnClickListener(v -> finish());
setTitle(getTitle() + " " + getString(R.string.input));
+ // Apply the insets as a margin to the toolbar.
+ ViewCompat.setOnApplyWindowInsetsListener(toolbar, (v, windowInsets) -> {
+ Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams();
+ lp.topMargin = insets.top;
+ v.setLayoutParams(lp);
+ return WindowInsetsCompat.CONSUMED;
+ });
+
mOriginalShowPointerIconPreference =
mPreferenceController.getBoolean(R.string.pref_show_pointer_icon);
@@ -115,6 +128,14 @@
v -> mInputController.sendMouseButtonEvent(MotionEvent.BUTTON_FORWARD));
requireViewById(R.id.button_home).setOnClickListener(
v -> mInputController.sendHomeToFocusedDisplay());
+
+ ViewCompat.setOnApplyWindowInsetsListener(bottomNavigationView, (v, windowInsets) -> {
+ Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams();
+ lp.bottomMargin = insets.bottom;
+ v.setLayoutParams(lp);
+ return WindowInsetsCompat.CONSUMED;
+ });
}
private void setShowPointerIcon(boolean show) {
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java
index 1ee3215..fcd8362 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/MainActivity.java
@@ -32,12 +32,16 @@
import android.view.View;
import android.widget.Button;
import android.widget.GridView;
+import android.widget.LinearLayout;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts.RequestPermission;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
import dagger.hilt.android.AndroidEntryPoint;
@@ -100,6 +104,15 @@
Toolbar toolbar = requireViewById(R.id.main_tool_bar);
setSupportActionBar(toolbar);
+ // Apply the insets as a margin to the toolbar.
+ ViewCompat.setOnApplyWindowInsetsListener(toolbar, (v, windowInsets) -> {
+ Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams();
+ lp.topMargin = insets.top;
+ v.setLayoutParams(lp);
+ return WindowInsetsCompat.CONSUMED;
+ });
+
mHomeDisplayButton = requireViewById(R.id.create_home_display);
mHomeDisplayButton.setVisibility(View.GONE);
mMirrorDisplayButton = requireViewById(R.id.create_mirror_display);
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java
index a03e981..5db419d 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/SettingsActivity.java
@@ -17,9 +17,13 @@
package com.example.android.vdmdemo.host;
import android.os.Bundle;
+import android.widget.LinearLayout;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
+import androidx.core.graphics.Insets;
+import androidx.core.view.ViewCompat;
+import androidx.core.view.WindowInsetsCompat;
import androidx.preference.PreferenceFragmentCompat;
import dagger.hilt.android.AndroidEntryPoint;
@@ -39,6 +43,15 @@
setSupportActionBar(toolbar);
toolbar.setNavigationOnClickListener(v -> finish());
setTitle(getTitle() + " " + getString(R.string.settings));
+
+ // Apply the insets as a margin to the toolbar.
+ ViewCompat.setOnApplyWindowInsetsListener(toolbar, (v, windowInsets) -> {
+ Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars());
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) v.getLayoutParams();
+ lp.topMargin = insets.top;
+ v.setLayoutParams(lp);
+ return WindowInsetsCompat.CONSUMED;
+ });
}
@AndroidEntryPoint(PreferenceFragmentCompat.class)
diff --git a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/StatusBar.java b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/StatusBar.java
index 74cab66..cfb1166 100644
--- a/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/StatusBar.java
+++ b/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/StatusBar.java
@@ -17,6 +17,7 @@
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.graphics.Insets;
@@ -96,6 +97,9 @@
windowManager.removeViewImmediate(this);
}
+ // The status bar may only be created if the relevant preference is enabled, which can only
+ // happen on B+ with the relevant flag enabled.
+ @SuppressLint("NewApi")
static StatusBar create(Context displayContext) {
final int statusBarHeight =
displayContext.getResources().getDimensionPixelSize(R.dimen.status_bar_height);
diff --git a/samples/VirtualDeviceManager/lint-baseline.xml b/samples/VirtualDeviceManager/lint-baseline.xml
deleted file mode 100644
index b323591..0000000
--- a/samples/VirtualDeviceManager/lint-baseline.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
-
- <issue
- id="NewApi"
- message="Call requires API level 36 (current min is 34): `android.view.WindowManager.LayoutParams#setInsetsParams`"
- errorLine1=" lp.setInsetsParams(List.of(new WindowManager.InsetsParams(WindowInsets.Type.statusBars())"
- errorLine2=" ~~~~~~~~~~~~~~~">
- <location
- file="development/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/StatusBar.java"
- line="117"
- column="12"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 36 (current min is 34): `new android.view.WindowManager.InsetsParams`"
- errorLine1=" lp.setInsetsParams(List.of(new WindowManager.InsetsParams(WindowInsets.Type.statusBars())"
- errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
- <location
- file="development/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/StatusBar.java"
- line="117"
- column="36"/>
- </issue>
-
- <issue
- id="NewApi"
- message="Call requires API level 36 (current min is 34): `android.view.WindowManager.InsetsParams#setInsetsSize`"
- errorLine1=" .setInsetsSize(Insets.of(0, statusBarHeight, 0, 0))));"
- errorLine2=" ~~~~~~~~~~~~~">
- <location
- file="development/samples/VirtualDeviceManager/host/src/com/example/android/vdmdemo/host/StatusBar.java"
- line="118"
- column="18"/>
- </issue>
-
-</issues>
diff --git a/scripts/Android.bp b/scripts/Android.bp
index 048dc1f..90cf944 100644
--- a/scripts/Android.bp
+++ b/scripts/Android.bp
@@ -74,9 +74,4 @@
test_options: {
unit_test: true,
},
- version: {
- py3: {
- embedded_launcher: true,
- },
- },
}
diff --git a/scripts/OWNERS b/scripts/OWNERS
index ecf242e..0f067a6 100644
--- a/scripts/OWNERS
+++ b/scripts/OWNERS
@@ -4,4 +4,3 @@
rprichard@google.com
per-file add3prf.py,add3prf_test.py,cargo2android.py,get_rust_pkg.py,update_crate_tests.py = ivanlozano@google.com,jeffv@google.com,mmaurer@google.com,srhines@google.com,tweek@google.com
per-file cargo2rulesmk.py = ivanlozano@google.com,jeffv@google.com,mmaurer@google.com,armellel@google.com,arve@android.com,oarbildo@google.com
-per-file codegen = eugenesusla@google.com
diff --git a/sys-img/OWNERS b/sys-img/OWNERS
index 5bf4e94..bbe907a 100644
--- a/sys-img/OWNERS
+++ b/sys-img/OWNERS
@@ -1,4 +1,3 @@
bohu@google.com
rajukulkarni@google.com
-lfy@google.com
jainne@google.com
diff --git a/tools/cargo_embargo/src/cargo/metadata.rs b/tools/cargo_embargo/src/cargo/metadata.rs
index b490d65..83bf50c 100644
--- a/tools/cargo_embargo/src/cargo/metadata.rs
+++ b/tools/cargo_embargo/src/cargo/metadata.rs
@@ -18,7 +18,7 @@
use crate::config::VariantConfig;
use anyhow::{bail, Context, Result};
use serde::Deserialize;
-use std::collections::BTreeMap;
+use std::collections::{BTreeMap, BTreeSet};
use std::path::{Path, PathBuf};
/// `cfg` strings for dependencies which should be considered enabled. It would be better to parse
@@ -343,7 +343,7 @@
}
}
- let mut features = Vec::new();
+ let mut features = BTreeSet::new();
if let Some(chosen_features) = chosen_features {
for feature in chosen_features {
add_feature_and_dependencies(&mut features, feature, &package_features);
@@ -352,21 +352,22 @@
// If there are no chosen features, then enable the default feature.
add_feature_and_dependencies(&mut features, "default", &package_features);
}
- features.sort();
- features.dedup();
- features
+ features.into_iter().collect()
}
/// Adds the given feature and all features it depends on to the given list of features.
///
/// Ignores features of other packages, and features which don't exist.
fn add_feature_and_dependencies(
- features: &mut Vec<String>,
+ features: &mut BTreeSet<String>,
feature: &str,
package_features: &BTreeMap<String, Vec<String>>,
) {
+ if features.contains(&feature.to_string()) {
+ return;
+ }
if package_features.contains_key(feature) || feature.starts_with("dep:") {
- features.push(feature.to_owned());
+ features.insert(feature.to_owned());
}
if let Some(dependencies) = package_features.get(feature) {
@@ -470,6 +471,21 @@
}
#[test]
+ fn resolve_dep_features_recursion() {
+ let chosen = vec!["tokio".to_string()];
+ let package_features = [
+ ("default".to_string(), vec![]),
+ ("tokio".to_string(), vec!["dep:tokio".to_string(), "tokio/net".to_string()]),
+ ]
+ .into_iter()
+ .collect();
+ assert_eq!(
+ resolve_features(&Some(chosen), &package_features, &[]),
+ vec!["dep:tokio".to_string(), "tokio".to_string(),]
+ );
+ }
+
+ #[test]
fn get_externs_cfg() {
let package = PackageMetadata {
name: "test_package".to_string(),
diff --git a/tools/cargo_embargo/src/main.rs b/tools/cargo_embargo/src/main.rs
index 7793979..5ff5437 100644
--- a/tools/cargo_embargo/src/main.rs
+++ b/tools/cargo_embargo/src/main.rs
@@ -1145,6 +1145,7 @@
.clone()
.into_iter()
.filter(|crate_cfg| !cfg.cfg_blocklist.contains(crate_cfg))
+ .map(|crate_cfg| crate_cfg.replace(r#"""#, r#"\""#))
.collect(),
);
@@ -1523,6 +1524,26 @@
}
#[test]
+ fn escape_cfgs() {
+ let c = Crate {
+ name: "name".to_string(),
+ package_name: "package_name".to_string(),
+ edition: "2021".to_string(),
+ types: vec![CrateType::Lib],
+ cfgs: vec![r#"foo="bar""#.to_string()],
+ ..Default::default()
+ };
+ let cfg = VariantConfig { ..Default::default() };
+ let package_cfg = PackageVariantConfig { ..Default::default() };
+ let modules = crate_to_bp_modules(&c, &cfg, &package_cfg, &[]).unwrap();
+
+ assert_eq!(
+ modules[0].props.map.get("cfgs"),
+ Some(&BpValue::List(vec![BpValue::String(r#"foo=\"bar\""#.to_string())]))
+ );
+ }
+
+ #[test]
fn crate_to_bp_rename() {
let c = Crate {
name: "ash".to_string(),
diff --git a/tools/external_crates/Cargo.toml b/tools/external_crates/Cargo.toml
index 4e4c644..88bc2f5 100644
--- a/tools/external_crates/Cargo.toml
+++ b/tools/external_crates/Cargo.toml
@@ -3,6 +3,7 @@
"checksum",
"crate_config",
"crate_tool",
+ "crates_io_util",
"google_metadata",
"license_checker",
"name_and_version",
diff --git a/tools/external_crates/cargo_embargo.json b/tools/external_crates/cargo_embargo.json
index 6000c7d..350518f 100644
--- a/tools/external_crates/cargo_embargo.json
+++ b/tools/external_crates/cargo_embargo.json
@@ -30,6 +30,7 @@
"tests": true,
"workspace": true,
"workspace_excludes": [
- "crate_tool"
+ "crate_tool",
+ "crates_io_util"
]
}
diff --git a/tools/external_crates/crate_tool/Cargo.toml b/tools/external_crates/crate_tool/Cargo.toml
index 5f27d90..ccb5102 100644
--- a/tools/external_crates/crate_tool/Cargo.toml
+++ b/tools/external_crates/crate_tool/Cargo.toml
@@ -14,20 +14,20 @@
crates-index = "3.2.0"
glob = "0.3"
itertools = "0.11"
-owning_ref = "0.4"
protobuf = "3"
-reqwest = { version = "0.12.5", features = ["blocking", "gzip"] }
semver = "1"
# TODO: Unpin once https://github.com/serde-rs/serde/issues/2844 is resolved.
serde = { version = "=1.0.210", features = ["derive"] }
serde_json = "1"
spdx = "0.10"
+ureq = "3"
thiserror = "1"
walkdir = "2"
checksum = { path = "../checksum" }
crate_config = { path = "../crate_config" }
+crates_io_util = { path = "../crates_io_util" }
google_metadata = { path = "../google_metadata"}
-license_checker = { path = "../license_checker", features = ["fuzzy_content_match"] }
+license_checker = { path = "../license_checker" }
name_and_version = { path = "../name_and_version" }
repo_config = { path = "../repo_config" }
rooted_path = { path = "../rooted_path" }
diff --git a/tools/external_crates/crate_tool/src/crates_io.rs b/tools/external_crates/crate_tool/src/crates_io.rs
index 6ebd02b..4f26e54 100644
--- a/tools/external_crates/crate_tool/src/crates_io.rs
+++ b/tools/external_crates/crate_tool/src/crates_io.rs
@@ -12,92 +12,13 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use anyhow::{anyhow, Result};
use cfg_expr::{
targets::{Arch, Family, Os},
Predicate, TargetPredicate,
};
-use crates_index::{http, Crate, Dependency, DependencyKind, SparseIndex, Version};
-use reqwest::blocking::Client;
+use crates_index::{Crate, Dependency, DependencyKind, Version};
use semver::VersionReq;
-use std::{
- cell::RefCell,
- collections::{HashMap, HashSet},
-};
-
-pub struct CratesIoIndex {
- fetcher: Box<dyn CratesIoFetcher>,
-}
-
-impl CratesIoIndex {
- pub fn new() -> Result<CratesIoIndex> {
- Ok(CratesIoIndex {
- fetcher: Box::new(OnlineFetcher {
- index: crates_index::SparseIndex::new_cargo_default()?,
- client: reqwest::blocking::ClientBuilder::new().gzip(true).build()?,
- fetched: RefCell::new(HashSet::new()),
- }),
- })
- }
- pub fn new_offline() -> Result<CratesIoIndex> {
- Ok(CratesIoIndex {
- fetcher: Box::new(OfflineFetcher {
- index: crates_index::SparseIndex::new_cargo_default()?,
- }),
- })
- }
- pub fn get_crate(&self, crate_name: impl AsRef<str>) -> Result<Crate> {
- self.fetcher.fetch(crate_name.as_ref())
- }
-}
-
-pub trait CratesIoFetcher {
- fn fetch(&self, crate_name: &str) -> Result<Crate>;
-}
-
-pub struct OnlineFetcher {
- index: SparseIndex,
- client: Client,
- // Keep track of crates we have fetched, to avoid fetching them multiple times.
- fetched: RefCell<HashSet<String>>,
-}
-
-pub struct OfflineFetcher {
- index: SparseIndex,
-}
-
-impl CratesIoFetcher for OnlineFetcher {
- fn fetch(&self, crate_name: &str) -> Result<Crate> {
- // Adapted from https://github.com/frewsxcv/rust-crates-index/blob/master/examples/sparse_http_reqwest.rs
-
- let mut fetched = self.fetched.borrow_mut();
- if fetched.contains(crate_name) {
- return Ok(self.index.crate_from_cache(crate_name.as_ref())?);
- }
- let req = self.index.make_cache_request(crate_name)?.body(())?;
- let req = http::Request::from_parts(req.into_parts().0, vec![]);
- let req: reqwest::blocking::Request = req.try_into()?;
- let res = self.client.execute(req)?;
- let mut builder = http::Response::builder().status(res.status()).version(res.version());
- builder
- .headers_mut()
- .ok_or(anyhow!("Failed to get headers"))?
- .extend(res.headers().iter().map(|(k, v)| (k.clone(), v.clone())));
- let body = res.bytes()?;
- let res = builder.body(body.to_vec())?;
- let res = self
- .index
- .parse_cache_response(crate_name, res, true)?
- .ok_or(anyhow!("Crate not found"))?;
- fetched.insert(crate_name.to_string());
- Ok(res)
- }
-}
-impl CratesIoFetcher for OfflineFetcher {
- fn fetch(&self, crate_name: &str) -> Result<Crate> {
- Ok(self.index.crate_from_cache(crate_name.as_ref())?)
- }
-}
+use std::collections::HashMap;
/// Filter versions by those that are "safe", meaning not yanked or pre-release.
pub trait SafeVersions {
diff --git a/tools/external_crates/crate_tool/src/license.rs b/tools/external_crates/crate_tool/src/license.rs
index 1360d71..ba18d17 100644
--- a/tools/external_crates/crate_tool/src/license.rs
+++ b/tools/external_crates/crate_tool/src/license.rs
@@ -72,6 +72,7 @@
vec![
("Apache-2.0", "MODULE_LICENSE_APACHE2"),
("MIT", "MODULE_LICENSE_MIT"),
+ ("MIT-0", "MODULE_LICENSE_MIT_0"),
("BSD-3-Clause", "MODULE_LICENSE_BSD"),
("BSD-2-Clause", "MODULE_LICENSE_BSD"),
("ISC", "MODULE_LICENSE_ISC"),
@@ -92,6 +93,7 @@
vec![
("Apache-2.0", LicenseType::NOTICE),
("MIT", LicenseType::NOTICE),
+ ("MIT-0", LicenseType::PERMISSIVE),
("BSD-3-Clause", LicenseType::NOTICE),
("BSD-2-Clause", LicenseType::NOTICE),
("ISC", LicenseType::NOTICE),
@@ -103,6 +105,7 @@
("Unicode-DFS-2016", LicenseType::NOTICE),
("NCSA", LicenseType::NOTICE),
("OpenSSL", LicenseType::NOTICE),
+ ("CC0-1.0", LicenseType::UNENCUMBERED),
]
.into_iter()
.map(|l| (Licensee::parse(l.0).unwrap().into_req(), l.1))
diff --git a/tools/external_crates/crate_tool/src/managed_crate.rs b/tools/external_crates/crate_tool/src/managed_crate.rs
index 39a726a..d151e0d 100644
--- a/tools/external_crates/crate_tool/src/managed_crate.rs
+++ b/tools/external_crates/crate_tool/src/managed_crate.rs
@@ -26,7 +26,7 @@
use glob::glob;
use google_metadata::GoogleMetadata;
use itertools::Itertools;
-use license_checker::find_licenses;
+use license_checker::{find_licenses, LicenseState};
use name_and_version::NamedAndVersioned;
use rooted_path::RootedPath;
use semver::Version;
@@ -37,7 +37,7 @@
copy_dir,
crate_type::Crate,
ensure_exists_and_empty,
- license::update_module_license_files,
+ license::{most_restrictive_type, update_module_license_files},
patch::Patch,
pseudo_crate::{CargoVendorClean, PseudoCrate},
SuccessOrError,
@@ -53,14 +53,27 @@
#[derive(Debug)]
pub struct New {}
+
+/// Crate state indicating we have run `cargo vendor` on the pseudo-crate.
#[derive(Debug)]
pub struct Vendored {
/// The vendored copy of the crate, from running `cargo vendor`
vendored_crate: Crate,
}
+
+/// Crate state indicating we have copied the vendored code to a temporary build
+/// directory, copied over Android customizations, and applied patches.
+#[derive(Debug)]
+pub struct CopiedAndPatched {
+ /// The vendored copy of the crate, from running `cargo vendor`
+ vendored_crate: Crate,
+ /// The license terms and associated license files.
+ licenses: LicenseState,
+}
pub trait ManagedCrateState {}
impl ManagedCrateState for New {}
impl ManagedCrateState for Vendored {}
+impl ManagedCrateState for CopiedAndPatched {}
static CUSTOMIZATIONS: &[&str] = &[
"*.bp",
@@ -99,6 +112,14 @@
fn patch_dir(&self) -> RootedPath {
self.android_crate_path().join("patches").unwrap()
}
+ fn temporary_build_directory(&self) -> RootedPath {
+ self.android_crate
+ .path()
+ .with_same_root("out/rust-crate-temporary-build")
+ .unwrap()
+ .join(self.name())
+ .unwrap()
+ }
pub fn patches(&self) -> Result<Vec<PathBuf>> {
let mut patches = Vec::new();
let patch_dir = self.patch_dir();
@@ -212,21 +233,22 @@
pub fn regenerate(
self,
pseudo_crate: &PseudoCrate<CargoVendorClean>,
- ) -> Result<ManagedCrate<Vendored>> {
- let vendored = self.into_vendored(pseudo_crate)?;
- vendored.regenerate()?;
- Ok(vendored)
+ ) -> Result<ManagedCrate<CopiedAndPatched>> {
+ let regenerated = self.into_vendored(pseudo_crate)?.regenerate()?;
+ Ok(regenerated)
}
}
impl ManagedCrate<Vendored> {
- fn temporary_build_directory(&self) -> RootedPath {
- self.android_crate
- .path()
- .with_same_root("out/rust-crate-temporary-build")
- .unwrap()
- .join(self.name())
- .unwrap()
+ fn into_copied_and_patched(
+ self,
+ licenses: LicenseState,
+ ) -> Result<ManagedCrate<CopiedAndPatched>> {
+ Ok(ManagedCrate {
+ android_crate: self.android_crate,
+ config: self.config,
+ extra: CopiedAndPatched { vendored_crate: self.extra.vendored_crate, licenses },
+ })
}
/// Makes a clean copy of the vendored crates in a temporary build directory.
fn copy_to_temporary_build_directory(&self) -> Result<()> {
@@ -294,31 +316,97 @@
}
Ok(())
}
- fn update_license_files(&self) -> Result<()> {
- let state = find_licenses(
+ pub fn regenerate(self) -> Result<ManagedCrate<CopiedAndPatched>> {
+ self.copy_to_temporary_build_directory()?;
+ self.copy_customizations()?;
+
+ // Delete stuff that we don't want to keep around, as specified in the
+ // android_config.toml
+ for deletion in self.config().deletions() {
+ let dir = self.temporary_build_directory().join(deletion)?;
+ if dir.abs().is_dir() {
+ remove_dir_all(dir)?;
+ } else {
+ remove_file(dir)?;
+ }
+ }
+
+ self.apply_patches()?;
+
+ let licenses = find_licenses(
self.temporary_build_directory(),
self.name(),
self.android_crate.license(),
)?;
+ let regenerated = self.into_copied_and_patched(licenses)?;
+ regenerated.regenerate()?;
+ Ok(regenerated)
+ }
+}
+impl ManagedCrate<CopiedAndPatched> {
+ pub fn regenerate(&self) -> Result<()> {
+ // License logic must happen AFTER applying patches, because we use patches
+ // to add missing license files. It must also happen BEFORE cargo_embargo,
+ // because cargo_embargo needs to put license information in the Android.bp.
+ self.update_license_files()?;
+
+ self.run_cargo_embargo()?;
+
+ self.update_metadata()?;
+ self.fix_test_mapping()?;
+ // Fails on dangling symlinks, which happens when we run on the log crate.
+ checksum::generate(self.temporary_build_directory())?;
+
+ let android_crate_dir = self.android_crate.path();
+ remove_dir_all(android_crate_dir)?;
+ rename(self.temporary_build_directory(), android_crate_dir)?;
+
+ Ok(())
+ }
+ fn update_license_files(&self) -> Result<()> {
// For every chosen license, we must be able to find an associated
// license file.
- if !state.unsatisfied.is_empty() {
- bail!("Could not find license files for some licenses: {:?}", state.unsatisfied);
+ if !self.extra.licenses.unsatisfied.is_empty() {
+ bail!(
+ "Could not find license files for some licenses: {:?}",
+ self.extra.licenses.unsatisfied
+ );
}
// SOME license must apply to the code. If none apply, that's an error.
- if state.satisfied.is_empty() {
+ if self.extra.licenses.satisfied.is_empty() {
bail!("No license terms were found for this crate");
}
- // TODO: Maybe remove unused license files.
+ // There should be no license files for terms not mentioned in Cargo.toml
+ // license expression. For example, if Cargo.toml says "Apache-2.0" and we
+ // find a file named "LICENSE-MIT", that suggests that something suspicious might
+ // be going on.
+ if !self.extra.licenses.unexpected.is_empty() {
+ bail!(
+ "Found unexpected license files that don't correspond to any terms of {}: {:?}",
+ self.android_crate.license().unwrap_or("<license terms not found in Cargo.toml>"),
+ self.extra.licenses.unexpected
+ );
+ }
+
+ // Per go/thirdparty/licenses#multiple:
+ // "Delete any LICENSE files or license texts that were not selected and are not in use."
+ for unneeded_license_file in self.extra.licenses.unneeded.values() {
+ remove_file(self.temporary_build_directory().abs().join(unneeded_license_file))?;
+ }
// Per http://go/thirdpartyreviewers#license:
// "There must be a file called LICENSE containing an allowed third party license."
- let license_files =
- state.satisfied.values().map(|path| path.as_path()).collect::<BTreeSet<_>>();
+ let license_files = self
+ .extra
+ .licenses
+ .satisfied
+ .values()
+ .map(|path| path.as_path())
+ .collect::<BTreeSet<_>>();
let canonical_license_file_name = Path::new("LICENSE");
let canonical_license_path = self.temporary_build_directory().abs().join("LICENSE");
if license_files.len() == 1 {
@@ -351,7 +439,7 @@
)?;
}
- update_module_license_files(&self.temporary_build_directory(), &state)?;
+ update_module_license_files(&self.temporary_build_directory(), &self.extra.licenses)?;
Ok(())
}
/// Runs cargo_embargo on the crate in the temporary build directory.
@@ -383,20 +471,13 @@
fn update_metadata(&self) -> Result<()> {
let mut metadata =
GoogleMetadata::try_from(self.temporary_build_directory().join("METADATA").unwrap())?;
- let mut writeback = false;
- writeback |= metadata.migrate_homepage();
- writeback |= metadata.migrate_archive();
- writeback |= metadata.remove_deprecated_url();
- let vendored_version = self.extra.vendored_crate.version();
- if self.android_crate.version() != vendored_version {
- metadata.set_date_to_today()?;
- metadata.set_version_and_urls(self.name(), vendored_version.to_string())?;
- writeback |= true;
- }
- if writeback {
- println!("Updating METADATA for {}", self.name());
- metadata.write()?;
- }
+ metadata.update(
+ self.name(),
+ self.extra.vendored_crate.version().to_string(),
+ self.extra.vendored_crate.description(),
+ most_restrictive_type(&self.extra.licenses),
+ );
+ metadata.write()?;
Ok(())
}
@@ -414,39 +495,4 @@
}
Ok(())
}
- pub fn regenerate(&self) -> Result<()> {
- self.copy_to_temporary_build_directory()?;
- self.copy_customizations()?;
-
- // Delete stuff that we don't want to keep around, as specified in the
- // android_config.toml
- for deletion in self.config().deletions() {
- let dir = self.temporary_build_directory().join(deletion)?;
- if dir.abs().is_dir() {
- remove_dir_all(dir)?;
- } else {
- remove_file(dir)?;
- }
- }
-
- self.apply_patches()?;
-
- // License logic must happen AFTER applying patches, because we use patches
- // to add missing license files. It must also happen BEFORE cargo_embargo,
- // because cargo_embargo needs to put license information in the Android.bp.
- self.update_license_files()?;
-
- self.run_cargo_embargo()?;
-
- self.update_metadata()?;
- self.fix_test_mapping()?;
- // Fails on dangling symlinks, which happens when we run on the log crate.
- checksum::generate(self.temporary_build_directory())?;
-
- let android_crate_dir = self.android_crate.path();
- remove_dir_all(android_crate_dir)?;
- rename(self.temporary_build_directory(), android_crate_dir)?;
-
- Ok(())
- }
}
diff --git a/tools/external_crates/crate_tool/src/managed_repo.rs b/tools/external_crates/crate_tool/src/managed_repo.rs
index 4648eb4..a33f5ae 100644
--- a/tools/external_crates/crate_tool/src/managed_repo.rs
+++ b/tools/external_crates/crate_tool/src/managed_repo.rs
@@ -22,6 +22,7 @@
use anyhow::{anyhow, bail, Context, Result};
use crates_index::DependencyKind;
+use crates_io_util::CratesIoIndex;
use google_metadata::GoogleMetadata;
use itertools::Itertools;
use license_checker::find_licenses;
@@ -36,7 +37,7 @@
copy_dir,
crate_collection::CrateCollection,
crate_type::Crate,
- crates_io::{AndroidDependencies, CratesIoIndex, DependencyChanges, SafeVersions},
+ crates_io::{AndroidDependencies, DependencyChanges, SafeVersions},
license::{most_restrictive_type, update_module_license_files},
managed_crate::ManagedCrate,
pseudo_crate::{CargoVendorDirty, PseudoCrate},
@@ -170,7 +171,6 @@
println!("Version {}", version.version());
let mut found_problems = false;
for (dep, req) in version.android_deps_with_version_reqs() {
- println!("Found dep {}", dep.crate_name());
let cc = if managed_crates.contains_crate(dep.crate_name()) {
&managed_crates
} else {
diff --git a/tools/external_crates/crates_io_util/Cargo.toml b/tools/external_crates/crates_io_util/Cargo.toml
index ea587fe..3bbc4c8 100644
--- a/tools/external_crates/crates_io_util/Cargo.toml
+++ b/tools/external_crates/crates_io_util/Cargo.toml
@@ -6,7 +6,7 @@
[dependencies]
cfg-expr = "0.17"
crates-index = "3.2.0"
-reqwest = { version = "0.12.5", features = ["blocking", "gzip"] }
+ureq = "3"
semver = "1"
thiserror = "1"
diff --git a/tools/external_crates/crates_io_util/src/dependency_diff.rs b/tools/external_crates/crates_io_util/src/dependency_diff.rs
index 08a65fd..7a138e8 100644
--- a/tools/external_crates/crates_io_util/src/dependency_diff.rs
+++ b/tools/external_crates/crates_io_util/src/dependency_diff.rs
@@ -20,7 +20,6 @@
// Diff dependencies between two versions of a crate.
pub struct DependencyDiffer<'a> {
- base: &'a Version,
base_deps: DepSet<'a>,
}
@@ -42,7 +41,7 @@
impl<'a> DependencyDiffer<'a> {
pub fn new(base: &'a Version) -> DependencyDiffer<'a> {
let base_deps = BTreeMap::from_iter(base.dependencies().iter().map(|d| (d.name(), d)));
- DependencyDiffer { base, base_deps }
+ DependencyDiffer { base_deps }
}
pub fn diff<'other>(&'a self, other: &'other Version) -> DependencyDiff<'a, 'other> {
let other_deps = BTreeMap::from_iter(other.dependencies().iter().map(|d| (d.name(), d)));
diff --git a/tools/external_crates/crates_io_util/src/feature_diff.rs b/tools/external_crates/crates_io_util/src/feature_diff.rs
index fc51c21..c55c286 100644
--- a/tools/external_crates/crates_io_util/src/feature_diff.rs
+++ b/tools/external_crates/crates_io_util/src/feature_diff.rs
@@ -18,7 +18,6 @@
// Diff features between two versions of a crate.
pub struct FeatureDiffer<'a> {
- base: &'a Version,
base_features: TypedFeatures<'a>,
}
@@ -31,7 +30,7 @@
impl<'a> FeatureDiffer<'a> {
pub fn new(base: &'a Version) -> FeatureDiffer<'a> {
let base_features = base.features_and_optional_deps();
- FeatureDiffer { base, base_features }
+ FeatureDiffer { base_features }
}
pub fn diff<'other>(&'a self, other: &'other Version) -> FeatureDiff<'a, 'other> {
let other_features = other.features_and_optional_deps();
diff --git a/tools/external_crates/crates_io_util/src/index.rs b/tools/external_crates/crates_io_util/src/index.rs
index 374c781..a98fff8 100644
--- a/tools/external_crates/crates_io_util/src/index.rs
+++ b/tools/external_crates/crates_io_util/src/index.rs
@@ -13,7 +13,6 @@
// limitations under the License.
use crates_index::{http, Crate, SparseIndex};
-use reqwest::blocking::Client;
use std::{cell::RefCell, collections::HashSet};
use crate::Error;
@@ -27,7 +26,7 @@
Ok(CratesIoIndex {
fetcher: Box::new(OnlineFetcher {
index: crates_index::SparseIndex::new_cargo_default()?,
- client: reqwest::blocking::ClientBuilder::new().gzip(true).build()?,
+ agent: ureq::Agent::new_with_defaults(),
fetched: RefCell::new(HashSet::new()),
}),
})
@@ -50,7 +49,7 @@
struct OnlineFetcher {
index: SparseIndex,
- client: Client,
+ agent: ureq::Agent,
// Keep track of crates we have fetched, to avoid fetching them multiple times.
fetched: RefCell<HashSet<String>>,
}
@@ -61,29 +60,27 @@
impl CratesIoFetcher for OnlineFetcher {
fn fetch(&self, crate_name: &str) -> Result<Crate, Error> {
- // Adapted from https://github.com/frewsxcv/rust-crates-index/blob/master/examples/sparse_http_reqwest.rs
-
let mut fetched = self.fetched.borrow_mut();
if fetched.contains(crate_name) {
return Ok(self.index.crate_from_cache(crate_name.as_ref())?);
}
- let req = self.index.make_cache_request(crate_name)?.body(())?;
- let req = http::Request::from_parts(req.into_parts().0, vec![]);
- let req: reqwest::blocking::Request = req.try_into()?;
- let res = self.client.execute(req)?;
- let mut builder = http::Response::builder().status(res.status()).version(res.version());
- builder
- .headers_mut()
- .ok_or(Error::HttpHeader)?
- .extend(res.headers().iter().map(|(k, v)| (k.clone(), v.clone())));
- let body = res.bytes()?;
- let res = builder.body(body.to_vec())?;
- let res = self
+
+ let request = self
.index
- .parse_cache_response(crate_name, res, true)?
+ .make_cache_request(crate_name)?
+ .version(ureq::http::Version::HTTP_11)
+ .body(())?;
+
+ let response = self.agent.run(request)?;
+ let (parts, mut body) = response.into_parts();
+ let response = http::Response::from_parts(parts, body.read_to_vec()?);
+ let response = self
+ .index
+ .parse_cache_response(crate_name, response, true)?
.ok_or(Error::CrateNotFound(crate_name.to_string()))?;
+
fetched.insert(crate_name.to_string());
- Ok(res)
+ Ok(response)
}
}
diff --git a/tools/external_crates/crates_io_util/src/lib.rs b/tools/external_crates/crates_io_util/src/lib.rs
index d34ea8e..dcf3ef9 100644
--- a/tools/external_crates/crates_io_util/src/lib.rs
+++ b/tools/external_crates/crates_io_util/src/lib.rs
@@ -64,7 +64,7 @@
HttpHeader,
/// Error fetching HTTP data
#[error(transparent)]
- Reqwest(#[from] reqwest::Error),
+ HttpFetch(#[from] ureq::Error),
/// Propagated crates_index::Error
#[error(transparent)]
CratesIndex(#[from] crates_index::Error),
diff --git a/tools/external_crates/google_metadata/src/lib.rs b/tools/external_crates/google_metadata/src/lib.rs
index 61ae2f3..60d597e 100644
--- a/tools/external_crates/google_metadata/src/lib.rs
+++ b/tools/external_crates/google_metadata/src/lib.rs
@@ -26,6 +26,7 @@
pub use google_metadata_proto::metadata::Identifier;
pub use google_metadata_proto::metadata::LicenseType;
pub use google_metadata_proto::metadata::MetaData;
+ pub use google_metadata_proto::metadata::ThirdPartyMetaData;
}
#[cfg(not(soong))]
@@ -34,6 +35,7 @@
pub use crate::metadata::Identifier;
pub use crate::metadata::LicenseType;
pub use crate::metadata::MetaData;
+ pub use crate::metadata::ThirdPartyMetaData;
}
pub use metadata_proto::*;
@@ -73,11 +75,11 @@
Ok(GoogleMetadata { path, metadata })
}
/// Initializes a new METADATA file.
- pub fn init<P: Into<PathBuf>, Q: Into<String>, R: Into<String>, S: Into<String>>(
+ pub fn init<P: Into<PathBuf>, V: Into<String>>(
path: P,
- name: Q,
- version: R,
- desc: S,
+ name: impl AsRef<str>,
+ version: V,
+ description: impl AsRef<str>,
license_type: LicenseType,
) -> Result<Self, Error> {
let path = path.into();
@@ -85,14 +87,7 @@
return Err(Error::FileExists(path));
}
let mut metadata = GoogleMetadata { path, metadata: MetaData::new() };
- let name = name.into();
- metadata.set_date_to_today()?;
- metadata.metadata.set_name(name.clone());
- metadata.set_version_and_urls(&name, version)?;
- let third_party = metadata.metadata.third_party.mut_or_insert_default();
- third_party.set_homepage(crates_io_homepage(&name));
- third_party.set_license_type(license_type);
- metadata.metadata.set_description(desc.into());
+ metadata.update(name, version, description, license_type);
Ok(metadata)
}
/// Writes to the METADATA file.
@@ -101,50 +96,55 @@
pub fn write(&self) -> Result<(), Error> {
Ok(write(&self.path, protobuf::text_format::print_to_string_pretty(&self.metadata))?)
}
+ fn third_party(&mut self) -> &mut ThirdPartyMetaData {
+ self.metadata.third_party.mut_or_insert_default()
+ }
/// Sets the date fields to today's date.
- pub fn set_date_to_today(&mut self) -> Result<(), Error> {
+ fn set_date_to_today(&mut self) {
let now = chrono::Utc::now();
- let date = self
- .metadata
- .third_party
- .mut_or_insert_default()
- .last_upgrade_date
- .mut_or_insert_default();
+ let date = self.third_party().last_upgrade_date.mut_or_insert_default();
date.set_day(now.day().try_into().unwrap());
date.set_month(now.month().try_into().unwrap());
date.set_year(now.year());
- Ok(())
}
- /// Sets the version and URL fields.
- ///
- /// Sets third_party.homepage and third_party.version, and
- /// a single "Archive" identifier with crate archive URL and version.
- pub fn set_version_and_urls<Q: Into<String>>(
+ /// Updates the METADATA based on the crate name, version, description, and license.
+ /// Also migrates and fixes up deprecated fields.
+ pub fn update<V: Into<String>>(
&mut self,
name: impl AsRef<str>,
- version: Q,
- ) -> Result<(), Error> {
- let name_in_metadata = self.metadata.name.as_ref().ok_or(Error::CrateNameMissing)?;
- if name_in_metadata != name.as_ref() {
- return Err(Error::CrateNameMismatch(
- name_in_metadata.clone(),
- name.as_ref().to_string(),
- ));
- }
- let third_party = self.metadata.third_party.mut_or_insert_default();
- third_party.set_homepage(crates_io_homepage(&name));
+ version: V,
+ description: impl AsRef<str>,
+ license_type: LicenseType,
+ ) {
+ self.migrate_homepage();
+ self.migrate_archive();
+ self.remove_deprecated_url();
+
+ let name = name.as_ref();
+ self.third_party().set_homepage(crates_io_homepage(name));
+ self.metadata.set_name(name.to_string());
+ let description = description.as_ref();
+ self.metadata.set_description(
+ description.trim().replace("\n", " ").replace("“", "\"").replace("”", "\""),
+ );
+ self.third_party().set_homepage(crates_io_homepage(name));
+ self.third_party().set_license_type(license_type);
+
let version = version.into();
- third_party.set_version(version.clone());
+ if self.third_party().version() != version {
+ self.set_date_to_today();
+ }
+ self.third_party().set_version(version.clone());
+
let mut identifier = Identifier::new();
identifier.set_type("Archive".to_string());
identifier.set_value(crate_archive_url(name, &version));
identifier.set_version(version);
- self.metadata.third_party.mut_or_insert_default().identifier.clear();
- self.metadata.third_party.mut_or_insert_default().identifier.push(identifier);
- Ok(())
+ self.third_party().identifier.clear();
+ self.third_party().identifier.push(identifier);
}
/// Migrate homepage from an identifier to its own field.
- pub fn migrate_homepage(&mut self) -> bool {
+ fn migrate_homepage(&mut self) -> bool {
let mut homepage = None;
for (idx, identifier) in self.metadata.third_party.identifier.iter().enumerate() {
if identifier.type_.as_ref().unwrap_or(&String::new()).to_lowercase() == "homepage" {
@@ -155,14 +155,14 @@
}
}
let Some(homepage) = homepage else { return false };
- self.metadata.third_party.mut_or_insert_default().identifier.remove(homepage.0);
- self.metadata.third_party.mut_or_insert_default().homepage = homepage.1.value;
+ self.third_party().identifier.remove(homepage.0);
+ self.third_party().homepage = homepage.1.value;
true
}
/// Normalize case of 'Archive' identifiers.
- pub fn migrate_archive(&mut self) -> bool {
+ fn migrate_archive(&mut self) -> bool {
let mut updated = false;
- for identifier in self.metadata.third_party.mut_or_insert_default().identifier.iter_mut() {
+ for identifier in self.third_party().identifier.iter_mut() {
if identifier.type_ == Some("ARCHIVE".to_string()) {
identifier.type_ = Some("Archive".to_string());
updated = true;
@@ -171,9 +171,9 @@
updated
}
/// Remove deprecate URL fields.
- pub fn remove_deprecated_url(&mut self) -> bool {
+ fn remove_deprecated_url(&mut self) -> bool {
let updated = !self.metadata.third_party.url.is_empty();
- self.metadata.third_party.mut_or_insert_default().url.clear();
+ self.third_party().url.clear();
updated
}
}
diff --git a/tools/external_crates/license_checker/Android.bp b/tools/external_crates/license_checker/Android.bp
index 96a6d49..abe5df8 100644
--- a/tools/external_crates/license_checker/Android.bp
+++ b/tools/external_crates/license_checker/Android.bp
@@ -17,7 +17,9 @@
edition: "2021",
rustlibs: [
"libglob",
+ "libitertools",
"libspdx",
+ "libtextdistance",
"libthiserror",
],
}
@@ -36,7 +38,9 @@
edition: "2021",
rustlibs: [
"libglob",
+ "libitertools",
"libspdx",
+ "libtextdistance",
"libthiserror",
],
}
diff --git a/tools/external_crates/license_checker/Cargo.toml b/tools/external_crates/license_checker/Cargo.toml
index dcc542e..aa9b7fd 100644
--- a/tools/external_crates/license_checker/Cargo.toml
+++ b/tools/external_crates/license_checker/Cargo.toml
@@ -5,10 +5,7 @@
[dependencies]
glob = "0.3"
-itertools = { version = "0.11", optional = true }
+itertools = "0.14"
spdx = "0.10"
-textdistance = { version = "1.1.1", optional = true }
-thiserror = "1.0"
-
-[features]
-fuzzy_content_match = ["dep:textdistance", "dep:itertools"]
\ No newline at end of file
+textdistance = "1.1.1"
+thiserror = "1.0"
\ No newline at end of file
diff --git a/tools/external_crates/license_checker/src/expression_parser.rs b/tools/external_crates/license_checker/src/expression_parser.rs
index 3a3ae80..a71e514 100644
--- a/tools/external_crates/license_checker/src/expression_parser.rs
+++ b/tools/external_crates/license_checker/src/expression_parser.rs
@@ -85,6 +85,8 @@
"Unicode-DFS-2016",
"NCSA",
"OpenSSL",
+ "MIT-0",
+ "CC0-1.0",
]
.into_iter()
.map(|l| Licensee::parse(l).unwrap())
@@ -96,7 +98,9 @@
BTreeMap::from([
("futures-channel", (Some("MIT OR Apache-2.0"), "(MIT OR Apache-2.0) AND BSD-2-Clause")),
("libfuzzer-sys", (Some("MIT/Apache-2.0/NCSA"), "(MIT OR Apache-2.0) AND NCSA")),
+ ("merge", (Some("Apache-2.0 OR MIT"), "(Apache-2.0 OR MIT) AND CC0-1.0")),
("ring", (None, "MIT AND ISC AND OpenSSL")),
+ ("tonic", (Some("MIT"), "MIT AND Apache-2.0")),
("webpki", (None, "ISC AND BSD-3-Clause")),
])
});
diff --git a/tools/external_crates/license_checker/src/file_classifier/content_classifier.rs b/tools/external_crates/license_checker/src/file_classifier/content_classifier.rs
index 2f6b700..e78eff3 100644
--- a/tools/external_crates/license_checker/src/file_classifier/content_classifier.rs
+++ b/tools/external_crates/license_checker/src/file_classifier/content_classifier.rs
@@ -12,11 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-#[cfg(feature = "fuzzy_content_match")]
use itertools::Itertools;
use spdx::{LicenseReq, Licensee};
use std::sync::LazyLock;
-#[cfg(feature = "fuzzy_content_match")]
use textdistance::str::ratcliff_obershelp;
fn strip_punctuation(text: &str) -> String {
@@ -32,7 +30,7 @@
processed.trim().to_string()
}
-pub(crate) fn classify_license_file_contents(contents: &str) -> Vec<LicenseReq> {
+pub(super) fn classify_license_file_contents(contents: &str) -> Vec<LicenseReq> {
let contents = strip_punctuation(contents);
// Exact match
@@ -42,13 +40,14 @@
matches.push(req.clone());
}
}
- if !matches.is_empty() {
- return matches;
- }
+ matches
+}
+
+pub(super) fn classify_license_file_contents_fuzzy(contents: &str) -> Option<LicenseReq> {
+ let contents = strip_punctuation(contents);
// Fuzzy match. This is expensive, so start with licenses that are closest in length to the file,
// and only return a single match at most.
- #[cfg(feature = "fuzzy_content_match")]
for (req, required_text) in LICENSE_CONTENT_CLASSIFICATION.iter().sorted_by(|a, b| {
let mut ra = a.1.len() as f32 / contents.len() as f32;
let mut rb = b.1.len() as f32 / contents.len() as f32;
@@ -62,24 +61,24 @@
}) {
let similarity = ratcliff_obershelp(contents.as_str(), required_text);
if similarity > 0.95 {
- matches.push(req.clone());
- break;
+ return Some(req.clone());
}
}
- matches
+ None
}
static LICENSE_CONTENT_CLASSIFICATION: LazyLock<Vec<(LicenseReq, String)>> = LazyLock::new(|| {
vec![
("MIT", include_str!("licenses/MIT.txt")),
+ ("MIT-0", include_str!("licenses/MIT-0.txt")),
("Apache-2.0", include_str!("licenses/Apache-2.0.txt")),
("ISC", include_str!("licenses/ISC.txt")),
("MPL-2.0", include_str!("licenses/MPL-2.0.txt")),
- ("MPL-2.0", include_str!("licenses/MPL-2.0-source-code-form.txt")),
("BSD-2-Clause", include_str!("licenses/BSD-2-Clause.txt")),
("BSD-3-Clause", include_str!("licenses/BSD-3-Clause.txt")),
("Unicode-3.0", include_str!("licenses/Unicode-3.0.txt")),
+ ("Unicode-DFS-2016", include_str!("licenses/Unicode-DFS-2016.txt")),
("Unlicense", include_str!("licenses/Unlicense.txt")),
("Zlib", include_str!("licenses/Zlib.txt")),
("OpenSSL", include_str!("licenses/OpenSSL.txt")),
@@ -124,12 +123,13 @@
);
}
- #[cfg(feature = "fuzzy_content_match")]
#[test]
fn test_classify_fuzzy() {
+ assert!(classify_license_file_contents(include_str!("testdata/BSD-3-Clause-bindgen.txt"))
+ .is_empty());
assert_eq!(
- classify_license_file_contents(include_str!("testdata/BSD-3-Clause-bindgen.txt")),
- vec![Licensee::parse("BSD-3-Clause").unwrap().into_req()]
+ classify_license_file_contents_fuzzy(include_str!("testdata/BSD-3-Clause-bindgen.txt")),
+ Some(Licensee::parse("BSD-3-Clause").unwrap().into_req())
);
}
diff --git a/tools/external_crates/license_checker/src/file_classifier/inexact_name_classifier.rs b/tools/external_crates/license_checker/src/file_classifier/inexact_name_classifier.rs
new file mode 100644
index 0000000..55165f2
--- /dev/null
+++ b/tools/external_crates/license_checker/src/file_classifier/inexact_name_classifier.rs
@@ -0,0 +1,64 @@
+// Copyright (C) 2025 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.
+
+use std::{collections::BTreeMap, ffi::OsString, path::Path, sync::LazyLock};
+
+use super::normalize_filename;
+
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) enum InexactLicenseType {
+ BSD,
+ UNICODE,
+ LGPL,
+}
+
+pub(super) fn classify_inexact_license_file_name(
+ file: impl AsRef<Path>,
+) -> Option<InexactLicenseType> {
+ if let Some(inexact_type) = INEXACT_LICENSE_FILE_NAMES.get(&normalize_filename(file)) {
+ return Some(inexact_type.clone());
+ }
+ None
+}
+
+static INEXACT_LICENSE_FILE_NAMES: LazyLock<BTreeMap<OsString, InexactLicenseType>> =
+ LazyLock::new(|| {
+ vec![
+ ("LICENSE-BSD", InexactLicenseType::BSD),
+ ("LICENSE-UNICODE", InexactLicenseType::UNICODE),
+ ("LICENSE-LGPL", InexactLicenseType::LGPL),
+ ("LICENSE.LGPL-2.1", InexactLicenseType::LGPL),
+ ]
+ .into_iter()
+ .map(|(file, inexact_type)| (OsString::from(file.to_uppercase()), inexact_type))
+ .collect()
+ });
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_classify() {
+ assert!(
+ classify_inexact_license_file_name(Path::new("LICENSE-BSD-2-Clause")).is_none(),
+ "Specific license file name"
+ );
+ assert_eq!(
+ classify_inexact_license_file_name(Path::new("LICENSE-BSD")),
+ Some(InexactLicenseType::BSD),
+ "Some kind of BSD license file"
+ );
+ }
+}
diff --git a/tools/external_crates/license_checker/src/file_classifier/licenses/MIT-0.txt b/tools/external_crates/license_checker/src/file_classifier/licenses/MIT-0.txt
new file mode 100644
index 0000000..1d54927
--- /dev/null
+++ b/tools/external_crates/license_checker/src/file_classifier/licenses/MIT-0.txt
@@ -0,0 +1,12 @@
+Permission is hereby granted, free of charge, to any person obtaining a copy of this
+software and associated documentation files (the "Software"), to deal in the Software
+without restriction, including without limitation the rights to use, copy, modify,
+merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/tools/external_crates/license_checker/src/file_classifier/licenses/MPL-2.0-source-code-form.txt b/tools/external_crates/license_checker/src/file_classifier/licenses/MPL-2.0-source-code-form.txt
deleted file mode 100644
index 2b2a624..0000000
--- a/tools/external_crates/license_checker/src/file_classifier/licenses/MPL-2.0-source-code-form.txt
+++ /dev/null
@@ -1 +0,0 @@
-This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.
diff --git a/tools/external_crates/license_checker/src/file_classifier/licenses/Unicode-DFS-2016.txt b/tools/external_crates/license_checker/src/file_classifier/licenses/Unicode-DFS-2016.txt
new file mode 100644
index 0000000..e92e9b5
--- /dev/null
+++ b/tools/external_crates/license_checker/src/file_classifier/licenses/Unicode-DFS-2016.txt
@@ -0,0 +1,14 @@
+NOTICE TO USER: Carefully read the following legal agreement. BY DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING UNICODE INC.'S DATA FILES ("DATA FILES"), AND/OR SOFTWARE ("SOFTWARE"), YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE.
+
+COPYRIGHT AND PERMISSION NOTICE
+
+Copyright © 1991-2016 Unicode, Inc. All rights reserved. Distributed under the Terms of Use in http://www.unicode.org/copyright.html.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of the Unicode data files and any associated documentation (the "Data Files") or Unicode software and any associated documentation (the "Software") to deal in the Data Files or Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, and/or sell copies of the Data Files or Software, and to permit persons to whom the Data Files or Software are furnished to do so, provided that either
+
+ (a) this copyright and permission notice appear with all copies of the Data Files or Software, or
+ (b) this copyright and permission notice appear in associated Documentation.
+
+THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF THIRD PARTY RIGHTS. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA FILES OR SOFTWARE.
+
+Except as contained in this notice, the name of a copyright holder shall not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder.
\ No newline at end of file
diff --git a/tools/external_crates/license_checker/src/file_classifier/mod.rs b/tools/external_crates/license_checker/src/file_classifier/mod.rs
index 36105b7..868a6a8 100644
--- a/tools/external_crates/license_checker/src/file_classifier/mod.rs
+++ b/tools/external_crates/license_checker/src/file_classifier/mod.rs
@@ -13,39 +13,125 @@
// limitations under the License.
mod content_classifier;
+mod inexact_name_classifier;
mod name_classifier;
use content_classifier::classify_license_file_contents;
+use inexact_name_classifier::{classify_inexact_license_file_name, InexactLicenseType};
use name_classifier::classify_license_file_name;
use spdx::LicenseReq;
-use std::{cell::OnceCell, fs::read_to_string, path::PathBuf};
+use std::{
+ collections::BTreeSet,
+ ffi::{OsStr, OsString},
+ fs::read_to_string,
+ path::{Path, PathBuf},
+ sync::OnceLock,
+};
+
+use crate::Error;
+
+fn normalize_filename(file: impl AsRef<Path>) -> OsString {
+ // File should be relative
+ let file = file.as_ref();
+ let mut basename = if file.extension() == Some(OsStr::new("txt"))
+ || file.extension() == Some(OsStr::new("md"))
+ {
+ file.with_extension("")
+ } else {
+ file.to_owned()
+ };
+ let uppercase_name = basename.as_mut_os_string();
+ uppercase_name.make_ascii_uppercase();
+ uppercase_name.to_owned()
+}
#[derive(Debug)]
pub(crate) struct Classifier {
- crate_path: PathBuf,
file_path: PathBuf,
+ contents: String,
by_name: Option<LicenseReq>,
- by_content: OnceCell<Vec<LicenseReq>>,
+ #[allow(dead_code)]
+ by_inexact_name: Option<InexactLicenseType>,
+ by_content: OnceLock<Vec<LicenseReq>>,
+ by_content_fuzzy: OnceLock<Option<LicenseReq>>,
}
impl Classifier {
- pub fn new<CP: Into<PathBuf>, FP: Into<PathBuf>>(crate_path: CP, file_path: FP) -> Classifier {
+ pub fn new<FP: Into<PathBuf>>(file_path: FP, contents: String) -> Classifier {
let file_path = file_path.into();
let by_name = classify_license_file_name(&file_path);
+ let by_inexact_name = classify_inexact_license_file_name(&file_path);
Classifier {
- crate_path: crate_path.into(),
file_path,
+ contents,
by_name,
- by_content: OnceCell::new(),
+ by_inexact_name,
+ by_content: OnceLock::new(),
+ by_content_fuzzy: OnceLock::new(),
}
}
+ pub fn new_vec<CP: Into<PathBuf>>(
+ crate_path: CP,
+ possible_license_files: BTreeSet<PathBuf>,
+ ) -> Result<Vec<Classifier>, Error> {
+ let crate_path = crate_path.into();
+ let mut classifiers = Vec::new();
+ for file_path in possible_license_files {
+ let full_path = crate_path.join(&file_path);
+ let contents =
+ read_to_string(&full_path).map_err(|e| Error::FileReadError(full_path, e))?;
+ classifiers.push(Classifier::new(file_path, contents));
+ }
+ Ok(classifiers)
+ }
+ pub fn file_path(&self) -> &Path {
+ self.file_path.as_path()
+ }
pub fn by_name(&self) -> Option<&LicenseReq> {
self.by_name.as_ref()
}
+ #[allow(dead_code)]
+ pub fn by_inexact_name(&self) -> Option<InexactLicenseType> {
+ self.by_inexact_name.clone()
+ }
pub fn by_content(&self) -> &Vec<LicenseReq> {
- self.by_content.get_or_init(|| {
- let contents = read_to_string(self.crate_path.join(&self.file_path)).unwrap();
- classify_license_file_contents(&contents)
- })
+ self.by_content.get_or_init(|| classify_license_file_contents(&self.contents))
+ }
+ pub fn by_content_fuzzy(&self) -> Option<&LicenseReq> {
+ self.by_content_fuzzy
+ .get_or_init(|| {
+ content_classifier::classify_license_file_contents_fuzzy(&self.contents)
+ })
+ .as_ref()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_normalize_filename() {
+ assert_eq!(normalize_filename(Path::new("LICENSE")), OsString::from("LICENSE"));
+ assert_eq!(
+ normalize_filename(Path::new("LICENSE.txt")),
+ OsString::from("LICENSE"),
+ ".txt extension removed"
+ );
+ assert_eq!(
+ normalize_filename(Path::new("LICENSE.md")),
+ OsString::from("LICENSE"),
+ ".md extension removed"
+ );
+ assert_eq!(
+ normalize_filename(Path::new("LICENSE.jpg")),
+ OsString::from("LICENSE.JPG"),
+ "Other extensions preserved"
+ );
+ assert_eq!(
+ normalize_filename(Path::new("license")),
+ OsString::from("LICENSE"),
+ "Converted to uppercase"
+ );
}
}
diff --git a/tools/external_crates/license_checker/src/file_classifier/name_classifier.rs b/tools/external_crates/license_checker/src/file_classifier/name_classifier.rs
index 60e89bf..a73178f 100644
--- a/tools/external_crates/license_checker/src/file_classifier/name_classifier.rs
+++ b/tools/external_crates/license_checker/src/file_classifier/name_classifier.rs
@@ -12,28 +12,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-use std::{
- collections::BTreeMap,
- ffi::{OsStr, OsString},
- path::Path,
- sync::LazyLock,
-};
+use std::{collections::BTreeMap, ffi::OsString, path::Path, sync::LazyLock};
use spdx::{LicenseReq, Licensee};
-pub(crate) fn classify_license_file_name(file: impl AsRef<Path>) -> Option<LicenseReq> {
- // File should be relative
- let file = file.as_ref();
- let mut basename = if file.extension() == Some(OsStr::new("txt"))
- || file.extension() == Some(OsStr::new("md"))
- {
- file.with_extension("")
- } else {
- file.to_owned()
- };
- let uppercase_name = basename.as_mut_os_string();
- uppercase_name.make_ascii_uppercase();
- if let Some(req) = LICENSE_FILE_NAME_CLASSIFICATION.get(uppercase_name) {
+use super::normalize_filename;
+
+pub(super) fn classify_license_file_name(file: impl AsRef<Path>) -> Option<LicenseReq> {
+ if let Some(req) = LICENSE_FILE_NAME_CLASSIFICATION.get(&normalize_filename(file)) {
return Some(req.clone());
}
None
@@ -53,20 +39,16 @@
("LICENSE-Apache-2.0_WITH_LLVM-exception", "Apache-2.0 WITH LLVM-exception"),
("docs/LICENSE-APACHE", "Apache-2.0"),
- ("LICENSE-BSD", "BSD-2-Clause"),
("LICENSE-BSD-2-Clause", "BSD-2-Clause"),
("LICENSE.BSD-2-Clause", "BSD-2-Clause"),
("LICENSE-BSD-3-Clause", "BSD-3-Clause"),
- ("LICENSE-UNICODE", "Unicode-DFS-2016"),
-
("LICENSE-0BSD", "0BSD"),
("LICENSE-ZLIB", "Zlib"),
("UNLICENSE", "Unlicense"),
("LICENSE-BOOST", "BSL-1.0"),
("LICENSES/CC0-1.0", "CC0-1.0"),
- ("LICENSE-LGPL", "LGPL-3.0"),
]
.into_iter()
.map(|(file, req)| (OsString::from(file.to_uppercase()), Licensee::parse(req).unwrap().into_req()))
diff --git a/tools/external_crates/license_checker/src/lib.rs b/tools/external_crates/license_checker/src/lib.rs
index 6d00e5b..156dd90 100644
--- a/tools/external_crates/license_checker/src/lib.rs
+++ b/tools/external_crates/license_checker/src/lib.rs
@@ -19,6 +19,7 @@
path::{Path, PathBuf},
};
+use expression_parser::LicenseTerms;
use file_classifier::Classifier;
use spdx::LicenseReq;
@@ -60,6 +61,9 @@
/// Error minimizing SPDX expression
#[error(transparent)]
MinimizeError(#[from] spdx::expression::MinimizeError),
+ /// Failed to read file
+ #[error("Failed to read {0}: {1}")]
+ FileReadError(PathBuf, std::io::Error),
}
/// The result of license file verification, containing a set of acceptable licenses, and the
@@ -71,6 +75,74 @@
pub unsatisfied: BTreeSet<LicenseReq>,
/// Licenses for which a license file file was found, and the path to that file.
pub satisfied: BTreeMap<LicenseReq, PathBuf>,
+ /// License files which are unneeded. That is, they are for license terms we are not
+ /// required to follow, such as LICENSE-MIT in the case of "Apache-2.0 OR MIT".
+ pub unneeded: BTreeMap<LicenseReq, PathBuf>,
+ /// Unexpected license files. They don't correspond to any terms in Cargo.toml, and
+ /// indicate that the stated license terms may be incorrect.
+ pub unexpected: BTreeMap<LicenseReq, PathBuf>,
+}
+
+impl LicenseState {
+ fn from(terms: LicenseTerms, file_classifiers: &Vec<Classifier>) -> LicenseState {
+ let mut state = LicenseState {
+ unsatisfied: terms.required,
+ satisfied: BTreeMap::new(),
+ unneeded: BTreeMap::new(),
+ unexpected: BTreeMap::new(),
+ };
+ let not_required = terms.not_required;
+
+ for classifier in file_classifiers {
+ if let Some(req) = classifier.by_name() {
+ if state.unsatisfied.remove(req) {
+ state.satisfied.insert(req.clone(), classifier.file_path().to_owned());
+ } else if !state.satisfied.contains_key(req) {
+ if not_required.contains(req) {
+ state.unneeded.insert(req.clone(), classifier.file_path().to_owned());
+ } else {
+ state.unexpected.insert(req.clone(), classifier.file_path().to_owned());
+ }
+ }
+ }
+ }
+
+ if !state.unsatisfied.is_empty() {
+ for classifier in file_classifiers {
+ for req in classifier.by_content() {
+ if state.unsatisfied.remove(req) {
+ state.satisfied.insert(req.clone(), classifier.file_path().to_owned());
+ } else if !state.satisfied.contains_key(req) && !not_required.contains(req) {
+ state.unexpected.insert(req.clone(), classifier.file_path().to_owned());
+ }
+ }
+ if classifier.by_content().len() == 1 {
+ let req = &classifier.by_content()[0];
+ if !state.satisfied.contains_key(req) && not_required.contains(req) {
+ state.unneeded.insert(req.clone(), classifier.file_path().to_owned());
+ }
+ }
+ }
+ }
+
+ if !state.unsatisfied.is_empty() {
+ for classifier in file_classifiers {
+ if classifier.by_name().is_some() || !classifier.by_content().is_empty() {
+ continue;
+ }
+ if let Some(req) = classifier.by_content_fuzzy() {
+ if state.unsatisfied.remove(req) {
+ state.satisfied.insert(req.clone(), classifier.file_path().to_owned());
+ if state.unsatisfied.is_empty() {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ state
+ }
}
/// Evaluates the license expression for a crate at a given path and returns a minimal set of
@@ -83,26 +155,226 @@
cargo_toml_license: Option<&str>,
) -> Result<LicenseState, Error> {
let crate_path = crate_path.as_ref();
- let mut state = LicenseState { unsatisfied: BTreeSet::new(), satisfied: BTreeMap::new() };
- state.unsatisfied =
- expression_parser::evaluate_license_expr(crate_name, cargo_toml_license)?.required;
- let possible_license_files = license_file_finder::find_license_files(crate_path)?;
+ let terms = expression_parser::evaluate_license_expr(crate_name, cargo_toml_license)?;
+ Ok(LicenseState::from(
+ terms,
+ &Classifier::new_vec(
+ crate_path.to_owned(),
+ license_file_finder::find_license_files(crate_path)?,
+ )?,
+ ))
+}
- for file in &possible_license_files {
- let classifier = Classifier::new(crate_path, file.clone());
- if let Some(req) = classifier.by_name() {
- if state.unsatisfied.remove(req) {
- state.satisfied.insert(req.clone(), file.clone());
- }
- } else {
- for req in classifier.by_content() {
- if state.unsatisfied.remove(req) {
- state.satisfied.insert(req.clone(), file.clone());
- }
- }
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ mod license_req {
+ use spdx::{LicenseReq, Licensee};
+
+ pub(super) fn apache() -> LicenseReq {
+ Licensee::parse("Apache-2.0").unwrap().into_req()
+ }
+ pub(super) fn mit() -> LicenseReq {
+ Licensee::parse("MIT").unwrap().into_req()
}
}
- Ok(state)
+ mod license_terms {
+ use crate::{expression_parser::evaluate_license_expr, LicenseTerms};
+
+ pub(super) fn apache() -> LicenseTerms {
+ evaluate_license_expr("foo", Some("Apache-2.0")).unwrap()
+ }
+ pub(super) fn apache_or_mit() -> LicenseTerms {
+ evaluate_license_expr("foo", Some("Apache-2.0 OR MIT")).unwrap()
+ }
+ pub(super) fn apache_or_bsd() -> LicenseTerms {
+ evaluate_license_expr("foo", Some("Apache-2.0 OR BSD-3-Clause")).unwrap()
+ }
+ }
+
+ mod classifiers {
+ use itertools::Itertools;
+
+ use crate::Classifier;
+
+ pub(super) fn apache_by_name() -> Classifier {
+ Classifier::new("LICENSE-APACHE", "".to_string())
+ }
+ pub(super) fn apache_by_content() -> Classifier {
+ Classifier::new(
+ "LICENSE",
+ include_str!("file_classifier/licenses/Apache-2.0.txt").to_string(),
+ )
+ }
+ pub(super) fn unknown() -> Classifier {
+ Classifier::new("LICENSE", "".to_string())
+ }
+ pub(super) fn mit_by_name() -> Classifier {
+ Classifier::new("LICENSE-MIT", "".to_string())
+ }
+ pub(super) fn apache_and_mit_concatenated() -> Classifier {
+ Classifier::new(
+ "LICENSE",
+ [
+ include_str!("file_classifier/licenses/Apache-2.0.txt"),
+ include_str!("file_classifier/licenses/MIT.txt"),
+ ]
+ .iter()
+ .join("\n\n\n-----\n\n\n"),
+ )
+ }
+ pub(super) fn bsd_fuzzy() -> Classifier {
+ Classifier::new(
+ "LICENSE-BSD",
+ include_str!("file_classifier/testdata/BSD-3-Clause-bindgen.txt").to_string(),
+ )
+ }
+ }
+
+ #[test]
+ fn basic() {
+ let state = LicenseState::from(
+ license_terms::apache_or_mit(),
+ &vec![classifiers::apache_by_name(), classifiers::mit_by_name()],
+ );
+ assert_eq!(
+ state.satisfied,
+ BTreeMap::from([(
+ license_req::apache(),
+ classifiers::apache_by_name().file_path().to_owned()
+ )])
+ );
+ assert!(state.unsatisfied.is_empty());
+ assert_eq!(
+ state.unneeded,
+ BTreeMap::from([(
+ license_req::mit(),
+ classifiers::mit_by_name().file_path().to_owned()
+ )])
+ );
+ assert!(state.unexpected.is_empty());
+ }
+
+ #[test]
+ fn unsatisfied() {
+ let state = LicenseState::from(license_terms::apache_or_mit(), &vec![]);
+ assert!(state.satisfied.is_empty());
+ assert_eq!(state.unsatisfied, BTreeSet::from([license_req::apache()]));
+ assert!(state.unneeded.is_empty());
+ assert!(state.unexpected.is_empty());
+ }
+
+ #[test]
+ fn unexpected() {
+ let state = LicenseState::from(
+ license_terms::apache(),
+ &vec![classifiers::apache_by_name(), classifiers::mit_by_name()],
+ );
+ assert_eq!(
+ state.satisfied,
+ BTreeMap::from([(
+ license_req::apache(),
+ classifiers::apache_by_name().file_path().to_owned()
+ )])
+ );
+ assert!(state.unsatisfied.is_empty());
+ assert!(state.unneeded.is_empty());
+ assert_eq!(
+ state.unexpected,
+ BTreeMap::from([(
+ license_req::mit(),
+ classifiers::mit_by_name().file_path().to_owned()
+ )])
+ );
+ }
+
+ #[test]
+ fn name_preferred_to_content() {
+ let state = LicenseState::from(
+ license_terms::apache(),
+ &vec![classifiers::apache_by_content(), classifiers::apache_by_name()],
+ );
+ assert_eq!(
+ state.satisfied,
+ BTreeMap::from([(
+ license_req::apache(),
+ classifiers::apache_by_name().file_path().to_owned()
+ )])
+ );
+ assert!(state.unsatisfied.is_empty());
+ assert!(state.unneeded.is_empty());
+ assert!(state.unexpected.is_empty());
+ }
+
+ #[test]
+ fn unknown_files_not_reported() {
+ let state = LicenseState::from(
+ license_terms::apache(),
+ &vec![classifiers::apache_by_name(), classifiers::unknown()],
+ );
+ assert_eq!(
+ state.satisfied,
+ BTreeMap::from([(
+ license_req::apache(),
+ classifiers::apache_by_name().file_path().to_owned()
+ )])
+ );
+ assert!(state.unsatisfied.is_empty());
+ assert!(state.unneeded.is_empty());
+ assert!(state.unexpected.is_empty());
+ }
+
+ #[test]
+ fn concatenated_licenses_not_reported_as_unexpected() {
+ let state = LicenseState::from(
+ license_terms::apache_or_mit(),
+ &vec![classifiers::apache_and_mit_concatenated()],
+ );
+ assert_eq!(
+ state.satisfied,
+ BTreeMap::from([(
+ license_req::apache(),
+ classifiers::apache_and_mit_concatenated().file_path().to_owned()
+ )])
+ );
+ assert!(state.unsatisfied.is_empty());
+ assert!(state.unneeded.is_empty());
+ assert!(state.unexpected.is_empty());
+ }
+
+ #[test]
+ fn fuzzy_classifications_not_reported_as_unneeded_or_unexpected() {
+ let state = LicenseState::from(
+ license_terms::apache_or_bsd(),
+ &vec![classifiers::apache_by_name(), classifiers::bsd_fuzzy()],
+ );
+ assert_eq!(
+ state.satisfied,
+ BTreeMap::from([(
+ license_req::apache(),
+ classifiers::apache_by_name().file_path().to_owned()
+ )])
+ );
+ assert!(state.unsatisfied.is_empty());
+ assert!(state.unneeded.is_empty());
+ assert!(state.unexpected.is_empty());
+
+ let state = LicenseState::from(
+ license_terms::apache(),
+ &vec![classifiers::apache_by_name(), classifiers::bsd_fuzzy()],
+ );
+ assert_eq!(
+ state.satisfied,
+ BTreeMap::from([(
+ license_req::apache(),
+ classifiers::apache_by_name().file_path().to_owned()
+ )])
+ );
+ assert!(state.unsatisfied.is_empty());
+ assert!(state.unneeded.is_empty());
+ assert!(state.unexpected.is_empty());
+ }
}
diff --git a/tools/ninja_dependency_analysis/Android.bp b/tools/ninja_dependency_analysis/Android.bp
index db3cd8a..c8e111c 100644
--- a/tools/ninja_dependency_analysis/Android.bp
+++ b/tools/ninja_dependency_analysis/Android.bp
@@ -15,9 +15,4 @@
proto: {
canonical_path_from_root: false,
},
- version: {
- py3: {
- embedded_launcher: true,
- },
- }
}
diff --git a/tools/privapp_permissions/OWNERS b/tools/privapp_permissions/OWNERS
index fe7bdf0..e69de29 100644
--- a/tools/privapp_permissions/OWNERS
+++ b/tools/privapp_permissions/OWNERS
@@ -1 +0,0 @@
-fkupolov@google.com
diff --git a/tools/winscope/src/app/mediator.ts b/tools/winscope/src/app/mediator.ts
index 0397664..e64966c 100644
--- a/tools/winscope/src/app/mediator.ts
+++ b/tools/winscope/src/app/mediator.ts
@@ -210,14 +210,14 @@
'Downloading files...',
undefined,
);
- console.log("App reset for remote tool download.");
+ console.log('App reset for remote tool download.');
},
);
await event.visit(
WinscopeEventType.REMOTE_TOOL_FILES_RECEIVED,
async (event) => {
- console.log("Remote tool files received.");
+ console.log('Remote tool files received.');
await this.processRemoteFilesReceived(
event.files,
FilesSource.REMOTE_TOOL,
@@ -640,6 +640,8 @@
}
private findViewerByType(type: TraceType): Viewer | undefined {
- return this.viewers.find((viewer) => viewer.getTraces()[0].type === type);
+ return this.viewers.find(
+ (viewer) => viewer.getTraces().at(0)?.type === type,
+ );
}
}
diff --git a/tools/winscope/src/parsers/window_manager/operations/operation_lists.ts b/tools/winscope/src/parsers/window_manager/operations/operation_lists.ts
index 63ebe86..40aaec7 100644
--- a/tools/winscope/src/parsers/window_manager/operations/operation_lists.ts
+++ b/tools/winscope/src/parsers/window_manager/operations/operation_lists.ts
@@ -22,7 +22,7 @@
import {EAGER_PROPERTIES} from 'parsers/window_manager/eager_properties';
import {ProtoType} from 'parsers/window_manager/proto_type';
import {TamperedProtos} from 'parsers/window_manager/tampered_protos';
-import {RECT_FORMATTER} from 'trace/tree_node/formatters';
+import {HEX_FORMATTER, RECT_FORMATTER} from 'trace/tree_node/formatters';
import {Operation} from 'trace/tree_node/operations/operation';
import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
import {AddIsVisible} from './add_is_visible';
@@ -34,13 +34,18 @@
lazy: Array<Operation<PropertyTreeNode>>;
}
+const commonFormatters = new Map([['hashCode', HEX_FORMATTER]]);
+
export class WmOperationLists {
private readonly LISTS = new Map<ProtoType, OperationLists>([
[
ProtoType.WindowManagerService,
{
common: [
- new SetFormatters(this.tamperedProtos.windowManagerServiceField),
+ new SetFormatters(
+ this.tamperedProtos.windowManagerServiceField,
+ commonFormatters,
+ ),
new TranslateIntDef(this.tamperedProtos.windowManagerServiceField),
],
eager: [
@@ -63,7 +68,10 @@
ProtoType.RootWindowContainer,
{
common: [
- new SetFormatters(this.tamperedProtos.rootWindowContainerField),
+ new SetFormatters(
+ this.tamperedProtos.rootWindowContainerField,
+ commonFormatters,
+ ),
new TranslateIntDef(this.tamperedProtos.rootWindowContainerField),
],
eager: [
@@ -86,7 +94,10 @@
ProtoType.WindowContainer,
{
common: [
- new SetFormatters(this.tamperedProtos.windowContainerField),
+ new SetFormatters(
+ this.tamperedProtos.windowContainerField,
+ commonFormatters,
+ ),
new TranslateIntDef(this.tamperedProtos.windowContainerField),
],
eager: [
@@ -110,7 +121,10 @@
ProtoType.DisplayContent,
{
common: [
- new SetFormatters(this.tamperedProtos.displayContentField),
+ new SetFormatters(
+ this.tamperedProtos.displayContentField,
+ commonFormatters,
+ ),
new TranslateIntDef(this.tamperedProtos.displayContentField),
],
eager: [
@@ -133,7 +147,10 @@
ProtoType.DisplayArea,
{
common: [
- new SetFormatters(this.tamperedProtos.displayAreaField),
+ new SetFormatters(
+ this.tamperedProtos.displayAreaField,
+ commonFormatters,
+ ),
new TranslateIntDef(this.tamperedProtos.displayAreaField),
],
eager: [
@@ -156,7 +173,7 @@
ProtoType.Task,
{
common: [
- new SetFormatters(this.tamperedProtos.taskField),
+ new SetFormatters(this.tamperedProtos.taskField, commonFormatters),
new TranslateIntDef(this.tamperedProtos.taskField),
],
eager: [
@@ -179,7 +196,10 @@
ProtoType.Activity,
{
common: [
- new SetFormatters(this.tamperedProtos.activityField),
+ new SetFormatters(
+ this.tamperedProtos.activityField,
+ commonFormatters,
+ ),
new TranslateIntDef(this.tamperedProtos.activityField),
],
eager: [
@@ -203,7 +223,10 @@
ProtoType.WindowToken,
{
common: [
- new SetFormatters(this.tamperedProtos.windowTokenField),
+ new SetFormatters(
+ this.tamperedProtos.windowTokenField,
+ commonFormatters,
+ ),
new TranslateIntDef(this.tamperedProtos.windowTokenField),
],
eager: [
@@ -231,6 +254,7 @@
new Map([
['containingFrame', RECT_FORMATTER],
['parentFrame', RECT_FORMATTER],
+ ...Array.from(commonFormatters.entries()),
]),
),
new TranslateIntDef(this.tamperedProtos.windowStateField),
@@ -257,7 +281,10 @@
ProtoType.TaskFragment,
{
common: [
- new SetFormatters(this.tamperedProtos.taskFragmentField),
+ new SetFormatters(
+ this.tamperedProtos.taskFragmentField,
+ commonFormatters,
+ ),
new TranslateIntDef(this.tamperedProtos.taskFragmentField),
],
eager: [
diff --git a/tools/winscope/src/trace/tree_node/formatters.ts b/tools/winscope/src/trace/tree_node/formatters.ts
index f09d78e..2e5945e 100644
--- a/tools/winscope/src/trace/tree_node/formatters.ts
+++ b/tools/winscope/src/trace/tree_node/formatters.ts
@@ -253,6 +253,13 @@
}
const CUJ_TYPE_FORMATTER = new CujTypeFormatter();
+class HexFormatter implements PropertyFormatter {
+ format(node: PropertyTreeNode): string {
+ return formatAsHex(node.getValue() ?? 0);
+ }
+}
+const HEX_FORMATTER = new HexFormatter();
+
export {
EMPTY_OBJ_STRING,
EMPTY_ARRAY_STRING,
@@ -272,4 +279,5 @@
TIMESTAMP_NODE_FORMATTER,
MATRIX_FORMATTER,
CUJ_TYPE_FORMATTER,
+ HEX_FORMATTER,
};
diff --git a/tools/winscope/src/trace/tree_node/formatters_test.ts b/tools/winscope/src/trace/tree_node/formatters_test.ts
index 4448b8b..9e3b5b1 100644
--- a/tools/winscope/src/trace/tree_node/formatters_test.ts
+++ b/tools/winscope/src/trace/tree_node/formatters_test.ts
@@ -27,6 +27,7 @@
EMPTY_ARRAY_STRING,
EMPTY_OBJ_STRING,
formatAsHex,
+ HEX_FORMATTER,
LAYER_ID_FORMATTER,
MATRIX_FORMATTER,
POSITION_FORMATTER,
@@ -312,10 +313,21 @@
});
});
- it('formatAsHex()', () => {
- expect(formatAsHex(0)).toEqual('0x0');
- expect(formatAsHex(1024)).toEqual('0x400');
- expect(formatAsHex(-1024)).toEqual('0xfffffc00');
- expect(formatAsHex(-1024, true)).toEqual('0xFFFFFC00');
+ describe('hex formatting', () => {
+ it('formatAsHex()', () => {
+ expect(formatAsHex(0)).toEqual('0x0');
+ expect(formatAsHex(1024)).toEqual('0x400');
+ expect(formatAsHex(-1024)).toEqual('0xfffffc00');
+ expect(formatAsHex(-1024, true)).toEqual('0xFFFFFC00');
+ });
+
+ it('HexFormatter', () => {
+ const hashcode = new PropertyTreeBuilder()
+ .setRootId('test node')
+ .setName('hashcode')
+ .setValue(1024)
+ .build();
+ expect(HEX_FORMATTER.format(hashcode)).toEqual('0x400');
+ });
});
});
diff --git a/tools/winscope/src/viewers/common/ui_property_tree_node.ts b/tools/winscope/src/viewers/common/ui_property_tree_node.ts
index 7bd7600..30fa52a 100644
--- a/tools/winscope/src/viewers/common/ui_property_tree_node.ts
+++ b/tools/winscope/src/viewers/common/ui_property_tree_node.ts
@@ -23,6 +23,7 @@
private diff: DiffType = DiffType.NONE;
private displayName: string = this.name;
private oldValue = 'null';
+ private propagate = false;
static from(node: PropertyTreeNode): UiPropertyTreeNode {
const displayNode = new UiPropertyTreeNode(
@@ -72,4 +73,12 @@
getOldValue(): string {
return this.oldValue;
}
+
+ canPropagate(): boolean {
+ return this.propagate;
+ }
+
+ setCanPropagate(value: boolean) {
+ this.propagate = value;
+ }
}
diff --git a/tools/winscope/src/viewers/common/viewer_events.ts b/tools/winscope/src/viewers/common/viewer_events.ts
index 2cf4e8b..8a6d55c 100644
--- a/tools/winscope/src/viewers/common/viewer_events.ts
+++ b/tools/winscope/src/viewers/common/viewer_events.ts
@@ -42,6 +42,7 @@
OverlayDblClick = 'OverlayDblClick',
AdditionalPropertySelected = 'AdditionalPropertySelected',
+ PropagatePropertyClick = 'PropagatePropertyClick',
TimestampClick = 'TimestampClick',
LogEntryClick = 'LogEntryClick',
diff --git a/tools/winscope/src/viewers/components/property_tree_node_data_view_component.ts b/tools/winscope/src/viewers/components/property_tree_node_data_view_component.ts
index 64d9402..647f73c 100644
--- a/tools/winscope/src/viewers/components/property_tree_node_data_view_component.ts
+++ b/tools/winscope/src/viewers/components/property_tree_node_data_view_component.ts
@@ -20,7 +20,10 @@
import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
import {TimestampClickDetail, ViewerEvents} from 'viewers/common/viewer_events';
import {propertyTreeNodeDataViewStyles} from 'viewers/components/styles/tree_node_data_view.styles';
-import {timeButtonStyle} from './styles/clickable_property.styles';
+import {
+ inlineButtonStyle,
+ timeButtonStyle,
+} from './styles/clickable_property.styles';
@Component({
selector: 'property-tree-node-data-view',
@@ -36,7 +39,15 @@
(click)="onTimestampClicked(node)">
{{ node.formattedValue() }}
</button>
- <a *ngIf="!isTimestamp()" [class]="[valueClass()]" class="mat-body-2 value new">{{ node.formattedValue() }}</a>
+ <div *ngIf="!isTimestamp() && node?.canPropagate()" class="inline">
+ <button
+ mat-button
+ color="primary"
+ (click)="onPropagateButtonClicked(node)">
+ {{ node.formattedValue() }}
+ </button>
+ </div>
+ <a *ngIf="!isTimestamp() && !node?.canPropagate()" [class]="[valueClass()]" class="mat-body-2 value new">{{ node.formattedValue() }}</a>
<s *ngIf="isModified()" class="mat-body-2 old-value">{{ node.getOldValue() }}</s>
</div>
</div>
@@ -49,6 +60,7 @@
`,
propertyTreeNodeDataViewStyles,
timeButtonStyle,
+ inlineButtonStyle,
],
})
export class PropertyTreeNodeDataViewComponent {
@@ -76,6 +88,14 @@
this.elementRef.nativeElement.dispatchEvent(customEvent);
}
+ onPropagateButtonClicked(node: UiPropertyTreeNode) {
+ const event = new CustomEvent(ViewerEvents.PropagatePropertyClick, {
+ bubbles: true,
+ detail: node,
+ });
+ this.elementRef.nativeElement.dispatchEvent(event);
+ }
+
valueClass() {
const property = assertDefined(this.node).formattedValue();
if (!property) {
diff --git a/tools/winscope/src/viewers/components/property_tree_node_data_view_component_test.ts b/tools/winscope/src/viewers/components/property_tree_node_data_view_component_test.ts
index 554644c..87e0c3c 100644
--- a/tools/winscope/src/viewers/components/property_tree_node_data_view_component_test.ts
+++ b/tools/winscope/src/viewers/components/property_tree_node_data_view_component_test.ts
@@ -24,7 +24,10 @@
import {TimestampConverterUtils} from 'common/time/test_utils';
import {Timestamp} from 'common/time/time';
import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
-import {TIMESTAMP_NODE_FORMATTER} from 'trace/tree_node/formatters';
+import {
+ HEX_FORMATTER,
+ TIMESTAMP_NODE_FORMATTER,
+} from 'trace/tree_node/formatters';
import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
import {ViewerEvents} from 'viewers/common/viewer_events';
import {PropertyTreeNodeDataViewComponent} from './property_tree_node_data_view_component';
@@ -67,14 +70,42 @@
component.node = node;
fixture.detectChanges();
- const timestampButton = assertDefined(
- htmlElement.querySelector('.time-button'),
- ) as HTMLButtonElement;
- timestampButton.click();
+ assertDefined(
+ htmlElement.querySelector<HTMLElement>('.time-button'),
+ ).click();
fixture.detectChanges();
expect(assertDefined(timestamp).format()).toEqual(
'2022-07-29, 20:34:49.102',
);
});
+
+ it('can emit propagatable node', () => {
+ let clickedNode: UiPropertyTreeNode | undefined;
+ htmlElement.addEventListener(
+ ViewerEvents.PropagatePropertyClick,
+ (event) => {
+ clickedNode = (event as CustomEvent).detail;
+ },
+ );
+ const node = UiPropertyTreeNode.from(
+ new PropertyTreeBuilder()
+ .setRootId('test node')
+ .setName('property')
+ .setValue(12345)
+ .setFormatter(HEX_FORMATTER)
+ .build(),
+ );
+ node.setCanPropagate(true);
+ component.node = node;
+ fixture.detectChanges();
+
+ const button = assertDefined(
+ htmlElement.querySelector<HTMLElement>('.inline button'),
+ );
+ expect(button.textContent?.trim()).toEqual('0x3039');
+ button.click();
+ fixture.detectChanges();
+ expect(clickedNode).toEqual(node);
+ });
});
diff --git a/tools/winscope/src/viewers/viewer_window_manager/operations/propagate_hash_codes.ts b/tools/winscope/src/viewers/viewer_window_manager/operations/propagate_hash_codes.ts
new file mode 100644
index 0000000..3ca755f
--- /dev/null
+++ b/tools/winscope/src/viewers/viewer_window_manager/operations/propagate_hash_codes.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2025 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 {Operation} from 'trace/tree_node/operations/operation';
+import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
+
+export class PropagateHashCodes implements Operation<UiPropertyTreeNode> {
+ private readonly layerFields = [
+ 'surfaceControl',
+ 'leash',
+ 'capturedLeash',
+ 'startLeash',
+ ];
+
+ apply(tree: UiPropertyTreeNode): void {
+ tree.forEachNodeDfs((node) => {
+ if (this.layerFields.includes(node.name)) {
+ return;
+ }
+ node.getAllChildren().forEach((child) => {
+ if (child.name !== 'hashCode') {
+ return;
+ }
+ const hex = (child.getValue() ?? 0).toString(16);
+ if (child.id.split(' ').at(1) === hex) {
+ return;
+ }
+ child.setCanPropagate(true);
+ });
+ });
+ }
+}
diff --git a/tools/winscope/src/viewers/viewer_window_manager/operations/propagate_hash_codes_test.ts b/tools/winscope/src/viewers/viewer_window_manager/operations/propagate_hash_codes_test.ts
new file mode 100644
index 0000000..68251aa
--- /dev/null
+++ b/tools/winscope/src/viewers/viewer_window_manager/operations/propagate_hash_codes_test.ts
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2025 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 {assertDefined} from 'common/assert_utils';
+import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
+import {PropagateHashCodes} from './propagate_hash_codes';
+
+describe('PropagateHashCodes', () => {
+ let operation: PropagateHashCodes;
+
+ beforeEach(() => {
+ operation = new PropagateHashCodes();
+ });
+
+ it('allows hashCode propagation', () => {
+ const root = UiPropertyTreeNode.from(
+ new PropertyTreeBuilder()
+ .setRootId('test 12345')
+ .setName('root')
+ .setChildren([{name: 'hashCode', value: 67890}])
+ .build(),
+ );
+ operation.apply(root);
+ const hashCode = assertDefined(root.getChildByName('hashCode'));
+ expect(hashCode.canPropagate()).toEqual(true);
+ });
+
+ it('does not allow hashCode propagation for layer fields', () => {
+ const root = UiPropertyTreeNode.from(
+ new PropertyTreeBuilder()
+ .setRootId('test 12345')
+ .setName('root')
+ .setChildren([
+ {
+ name: 'surfaceControl',
+ children: [{name: 'hashCode', value: 67890}],
+ },
+ {name: 'leash', children: [{name: 'hashCode', value: 67890}]},
+ {name: 'capturedLeash', children: [{name: 'hashCode', value: 67890}]},
+ {name: 'startLeash', children: [{name: 'hashCode', value: 67890}]},
+ ])
+ .build(),
+ );
+
+ operation.apply(root);
+ root.getAllChildren().forEach((child) => {
+ const hashCode = assertDefined(child.getChildByName('hashCode'));
+ expect(hashCode.canPropagate()).toEqual(false);
+ });
+ });
+
+ it('does not allow hashCode propagation if value is for current node', () => {
+ const root = UiPropertyTreeNode.from(
+ new PropertyTreeBuilder()
+ .setRootId('test ' + (12345).toString(16) + ' title')
+ .setName('root')
+ .setChildren([{name: 'hashCode', value: 12345}])
+ .build(),
+ );
+ operation.apply(root);
+ const hashCode = assertDefined(root.getChildByName('hashCode'));
+ expect(hashCode.canPropagate()).toEqual(false);
+ });
+
+ it('does not allow propagation for non hashCode fields', () => {
+ const root = UiPropertyTreeNode.from(
+ new PropertyTreeBuilder()
+ .setRootId('test 12345')
+ .setName('root')
+ .setChildren([{name: 'hash', value: 67890}])
+ .build(),
+ );
+ operation.apply(root);
+ const hash = assertDefined(root.getChildByName('hash'));
+ expect(hash.canPropagate()).toEqual(false);
+ });
+});
diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
index 77718ab..71e6960 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/presenter.ts
@@ -35,13 +35,16 @@
import {RectsPresenter} from 'viewers/common/rects_presenter';
import {TextFilter} from 'viewers/common/text_filter';
import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
+import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
import {UI_RECT_FACTORY} from 'viewers/common/ui_rect_factory';
import {UserOptions} from 'viewers/common/user_options';
+import {ViewerEvents} from 'viewers/common/viewer_events';
import {
RectLegendFactory,
TraceRectType,
} from 'viewers/components/rects/rect_spec';
import {UiRect} from 'viewers/components/rects/ui_rect';
+import {PropagateHashCodes} from './operations/propagate_hash_codes';
import {UpdateDisplayNames} from './operations/update_display_names';
import {UiData} from './ui_data';
@@ -127,6 +130,7 @@
),
new TextFilter(),
Presenter.DENYLIST_PROPERTY_NAMES,
+ [new PropagateHashCodes()],
);
protected override multiTraceType = undefined;
@@ -145,6 +149,29 @@
super(trace, traces, storage, notifyViewCallback, uiData);
}
+ async onPropagatePropertyClick(node: UiPropertyTreeNode) {
+ if (node.name !== 'hashCode') {
+ return;
+ }
+ const token = (node.getValue() ?? 0).toString(16);
+ const target = this.uiData.hierarchyTrees
+ ?.at(0)
+ ?.findDfs((node) => node.id.includes(token));
+ if (target) {
+ await this.onHighlightedNodeChange(target);
+ }
+ }
+
+ protected override addViewerSpecificListeners(htmlElement: HTMLElement) {
+ htmlElement.addEventListener(
+ ViewerEvents.PropagatePropertyClick,
+ async (event) => {
+ const node = (event as CustomEvent).detail;
+ await this.onPropagatePropertyClick(node);
+ },
+ );
+ }
+
override async onHighlightedNodeChange(item: UiHierarchyTreeNode) {
await this.applyHighlightedNodeChange(item);
this.refreshUIData();
diff --git a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
index 7b330b6..c4ea5ce 100644
--- a/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_window_manager/presenter_test.ts
@@ -19,6 +19,7 @@
import {Store} from 'common/store/store';
import {TracePositionUpdate} from 'messaging/winscope_event';
import {TraceBuilder} from 'test/unit/trace_builder';
+import {TreeNodeUtils} from 'test/unit/tree_node_utils';
import {UnitTestUtils} from 'test/unit/utils';
import {Trace} from 'trace/trace';
import {Traces} from 'trace/traces';
@@ -30,7 +31,9 @@
import {VISIBLE_CHIP} from 'viewers/common/chip';
import {TextFilter} from 'viewers/common/text_filter';
import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
+import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
import {UiTreeUtils} from 'viewers/common/ui_tree_utils';
+import {ViewerEvents} from 'viewers/common/viewer_events';
import {TraceRectType} from 'viewers/components/rects/rect_spec';
import {Presenter} from './presenter';
import {UiData} from './ui_data';
@@ -200,6 +203,11 @@
expect(
assertDefined(propertiesTree.getChildByName('state')).formattedValue(),
).toEqual('STOPPED');
+ expect(
+ assertDefined(
+ propertiesTree.findDfs((node) => node.name === 'hashCode'),
+ ).formattedValue(),
+ ).toEqual('0xf7092ed');
expect(uiData.displays).toEqual([
{
displayId: 'DisplayContent 1f3454e Built-in Screen',
@@ -221,6 +229,69 @@
assertDefined(propertiesTree.getChildByName('state')).formattedValue(),
).toEqual('RESUMED');
}
+
+ override executeSpecializedTests(): void {
+ const invalidNode = UiPropertyTreeNode.from(
+ TreeNodeUtils.makeUiPropertyNode('', '', 0),
+ );
+
+ describe('Specialized tests', () => {
+ let presenter: Presenter;
+ let uiData: UiData;
+
+ beforeAll(async () => {
+ await this.setUpTestEnvironment();
+ });
+
+ beforeEach(() => {
+ const notifyViewCallback = (newData: UiData) => {
+ uiData = newData;
+ };
+ presenter = this.createPresenter(
+ notifyViewCallback as NotifyHierarchyViewCallbackType<UiData>,
+ new InMemoryStorage(),
+ );
+ });
+
+ it('adds event listeners', async () => {
+ const el = document.createElement('div');
+ presenter.addEventListeners(el);
+
+ const spy: jasmine.Spy = spyOn(presenter, 'onPropagatePropertyClick');
+ el.dispatchEvent(
+ new CustomEvent(ViewerEvents.PropagatePropertyClick, {
+ detail: invalidNode,
+ }),
+ );
+ expect(spy).toHaveBeenCalledWith(invalidNode);
+ });
+
+ it('does not propagate hashcode if name does not match', async () => {
+ await presenter.onPropagatePropertyClick(invalidNode);
+ expect(uiData.highlightedItem).toEqual('');
+ });
+
+ it('does not propagate hashcode if matching node not found', async () => {
+ const missingHashcode = UiPropertyTreeNode.from(
+ TreeNodeUtils.makeUiPropertyNode('', 'hashCode', 0),
+ );
+ await presenter.onPropagatePropertyClick(missingHashcode);
+ expect(uiData.highlightedItem).toEqual('');
+ });
+
+ it('propagates node with matching hashcode', async () => {
+ const validHashcode = UiPropertyTreeNode.from(
+ TreeNodeUtils.makeUiPropertyNode('', 'hashCode', 32720206),
+ );
+ await presenter.onAppEvent(this.getPositionUpdate());
+ console.log(uiData.hierarchyTrees?.at(0)?.getAllChildren()[0].id);
+ await presenter.onPropagatePropertyClick(validHashcode);
+ expect(uiData.highlightedItem).toEqual(
+ 'DisplayContent 1f3454e Built-in Screen',
+ );
+ });
+ });
+ }
}
describe('PresenterWindowManager', () => {
diff --git a/vndk/tools/elfcheck/Android.bp b/vndk/tools/elfcheck/Android.bp
index e7e4877..c3f016b 100644
--- a/vndk/tools/elfcheck/Android.bp
+++ b/vndk/tools/elfcheck/Android.bp
@@ -37,9 +37,4 @@
"elfcheck/*.py",
"fix_android_mk_prebuilt.py",
],
- version: {
- py3: {
- embedded_launcher: true,
- },
- },
}
diff --git a/vndk/tools/header-checker/utils/Android.bp b/vndk/tools/header-checker/utils/Android.bp
index 88c6dfa..45a39db 100644
--- a/vndk/tools/header-checker/utils/Android.bp
+++ b/vndk/tools/header-checker/utils/Android.bp
@@ -25,10 +25,4 @@
"create_reference_dumps.py",
"utils.py",
],
- version: {
- py3: {
- enabled: true,
- embedded_launcher: true,
- },
- },
}
diff --git a/vndk/tools/sourcedr/OWNERS b/vndk/tools/sourcedr/OWNERS
index 402c928..389b55b 100644
--- a/vndk/tools/sourcedr/OWNERS
+++ b/vndk/tools/sourcedr/OWNERS
@@ -1,2 +1 @@
andrewhsieh@google.com
-loganchien@google.com