Merge "Improve UX of grouping buttons." into main
diff --git a/apps/ShareTest/AndroidManifest.xml b/apps/ShareTest/AndroidManifest.xml
index 7e1cb3c..08b3402 100644
--- a/apps/ShareTest/AndroidManifest.xml
+++ b/apps/ShareTest/AndroidManifest.xml
@@ -35,6 +35,13 @@
android:name=".ImageContentProvider"
android:grantUriPermissions="true" />
+ <provider
+ android:authorities="com.android.sharetest.additionalcontent"
+ android:name=".AdditionalContentProvider"
+ android:exported="false"
+ android:enabled="true"
+ android:grantUriPermissions="true" />
+
<receiver android:name=".ChosenComponentBroadcastReceiver" />
</application>
</manifest>
diff --git a/apps/ShareTest/res/layout/activity_main.xml b/apps/ShareTest/res/layout/activity_main.xml
index 3077ea9..5716bbd 100644
--- a/apps/ShareTest/res/layout/activity_main.xml
+++ b/apps/ShareTest/res/layout/activity_main.xml
@@ -49,11 +49,19 @@
/>
</RadioGroup>
- <TextView
+ <CheckBox
+ android:id="@+id/shareousel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- style="@style/title"
- android:text="Represent Media As"
+ android:text="Enable Shareousel"
+ />
+
+ <TextView
+ android:id="@+id/media_type_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/subtitle"
+ android:text="Media Type"
/>
<Spinner
@@ -190,6 +198,19 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/title"
+ android:text="Metadata"
+ />
+
+ <EditText
+ android:id="@+id/metadata"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ />
+
+ <TextView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ style="@style/title"
android:text="Advanced Options"
/>
diff --git a/apps/ShareTest/src/com/android/sharetest/AdditionalContentProvider.kt b/apps/ShareTest/src/com/android/sharetest/AdditionalContentProvider.kt
new file mode 100644
index 0000000..b0a79a6
--- /dev/null
+++ b/apps/ShareTest/src/com/android/sharetest/AdditionalContentProvider.kt
@@ -0,0 +1,99 @@
+package com.android.sharetest
+
+import android.content.ContentProvider
+import android.content.ContentValues
+import android.content.Intent
+import android.database.Cursor
+import android.database.MatrixCursor
+import android.net.Uri
+import android.os.Bundle
+import android.os.CancellationSignal
+import android.service.chooser.AdditionalContentContract
+import kotlin.random.Random
+
+class AdditionalContentProvider : ContentProvider() {
+ override fun onCreate(): Boolean {
+ return true
+ }
+
+ override fun query(
+ uri: Uri,
+ projection: Array<String>?,
+ queryArgs: Bundle?,
+ cancellationSignal: CancellationSignal?
+ ): Cursor? {
+ val context = context ?: return null
+ val cursor = MatrixCursor(arrayOf(AdditionalContentContract.Columns.URI))
+ val chooserIntent =
+ queryArgs?.getParcelable(Intent.EXTRA_INTENT, Intent::class.java) ?: return cursor
+ // Images are img1 ... img8
+ var uris = Array(ImageContentProvider.IMAGE_COUNT) { idx ->
+ ImageContentProvider.makeItemUri(idx + 1, "image/jpeg")
+ }
+ val callingPackage = getCallingPackage()
+ for (u in uris) {
+ cursor.addRow(arrayOf(u.toString()))
+ context.grantUriPermission(callingPackage, u, Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }
+ val startPos = chooserIntent.getIntExtra(CURSOR_START_POSITION, -1)
+ if (startPos >= 0) {
+ var cursorExtras = cursor.extras
+ cursorExtras = if (cursorExtras == null) {
+ Bundle()
+ } else {
+ Bundle(cursorExtras)
+ }
+ cursorExtras.putInt(AdditionalContentContract.CursorExtraKeys.POSITION, startPos)
+ cursor.extras = cursorExtras
+ }
+ return cursor
+ }
+
+ override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
+ val context = context ?: return null
+ val result = Bundle()
+ val customActionFactory = CustomActionFactory(context)
+
+ // Make a random number of custom actions each time they change something.
+ result.putParcelableArray(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS,
+ customActionFactory.getCustomActions(Random.nextInt(5)))
+ return result
+ }
+
+ override fun query(
+ uri: Uri,
+ projection: Array<String>?,
+ selection: String?,
+ selectionArgs: Array<String>?,
+ sortOrder: String?
+ ): Cursor? {
+ return null
+ }
+
+ override fun getType(uri: Uri): String? {
+ return null
+ }
+
+ override fun insert(uri: Uri, values: ContentValues?): Uri? {
+ return null
+ }
+
+ override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
+ return 0
+ }
+
+ override fun update(
+ uri: Uri,
+ values: ContentValues?,
+ selection: String?,
+ selectionArgs: Array<String>?
+ ): Int {
+ return 0
+ }
+
+ companion object {
+ val ADDITIONAL_CONTENT_URI = Uri.parse("content://com.android.sharetest.additionalcontent")
+ val CURSOR_START_POSITION = "com.android.sharetest.CURSOR_START_POS"
+ }
+}
+
diff --git a/apps/ShareTest/src/com/android/sharetest/CustomActionFactory.kt b/apps/ShareTest/src/com/android/sharetest/CustomActionFactory.kt
new file mode 100644
index 0000000..78b839c
--- /dev/null
+++ b/apps/ShareTest/src/com/android/sharetest/CustomActionFactory.kt
@@ -0,0 +1,31 @@
+package com.android.sharetest
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.service.chooser.ChooserAction
+
+class CustomActionFactory(private val context: Context) {
+ fun getCustomActions(count: Int): Array<ChooserAction> {
+ val actions = Array(count) { idx ->
+ val customAction = PendingIntent.getBroadcast(
+ context,
+ idx,
+ Intent(BROADCAST_ACTION),
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
+ )
+ ChooserAction.Builder(
+ Icon.createWithResource(context, R.drawable.testicon),
+ "Action ${idx + 1}",
+ customAction
+ ).build()
+ }
+
+ return actions
+ }
+
+ companion object {
+ const val BROADCAST_ACTION = "broadcast-action"
+ }
+}
diff --git a/apps/ShareTest/src/com/android/sharetest/ImageContentProvider.kt b/apps/ShareTest/src/com/android/sharetest/ImageContentProvider.kt
index 586b4d3..6118560 100644
--- a/apps/ShareTest/src/com/android/sharetest/ImageContentProvider.kt
+++ b/apps/ShareTest/src/com/android/sharetest/ImageContentProvider.kt
@@ -73,6 +73,7 @@
if (shouldFailOpen()) {
return null
}
+
return uri.lastPathSegment?.let{ context?.assets?.openFd(it) }
}
@@ -89,6 +90,14 @@
}
companion object {
+ fun makeItemUri(idx: Int, mimeType: String): Uri =
+ Uri.parse("${URI_PREFIX}img$idx.jpg")
+ .buildUpon()
+ .appendQueryParameter(PARAM_TYPE, mimeType)
+ .build()
+
+ const val IMAGE_COUNT = 8
+
const val URI_PREFIX = "content://com.android.sharetest.provider/"
const val PARAM_TYPE = "type"
val ICON_URI: Uri = Uri.parse("${URI_PREFIX}letter_a.png")
diff --git a/apps/ShareTest/src/com/android/sharetest/ShareTestActivity.kt b/apps/ShareTest/src/com/android/sharetest/ShareTestActivity.kt
index 11edd16..5e85962 100644
--- a/apps/ShareTest/src/com/android/sharetest/ShareTestActivity.kt
+++ b/apps/ShareTest/src/com/android/sharetest/ShareTestActivity.kt
@@ -26,7 +26,6 @@
import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.Icon
-import android.net.Uri
import android.os.Bundle
import android.service.chooser.ChooserAction
import android.text.Spannable
@@ -41,14 +40,14 @@
import android.widget.ArrayAdapter
import android.widget.Button
import android.widget.CheckBox
+import android.widget.EditText
import android.widget.RadioButton
import android.widget.RadioGroup
import android.widget.Spinner
import android.widget.Toast
import androidx.annotation.RequiresApi
+import kotlin.random.Random
-private const val BROADCAST_ACTION = "broadcast-action"
-private const val IMAGE_COUNT = 8
private const val TYPE_IMAGE = "Image"
private const val TYPE_VIDEO = "Video"
private const val TYPE_PDF = "PDF Doc"
@@ -63,8 +62,12 @@
private lateinit var mediaSelection: RadioGroup
private lateinit var textSelection: RadioGroup
private lateinit var mediaTypeSelection: Spinner
+ private lateinit var mediaTypeHeader: View
private lateinit var richText: CheckBox
private lateinit var albumCheck: CheckBox
+ private lateinit var metadata: EditText
+ private lateinit var shareouselCheck: CheckBox
+ private val customActionFactory = CustomActionFactory(this)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -79,17 +82,20 @@
registerReceiver(
customActionReceiver,
- IntentFilter(BROADCAST_ACTION),
+ IntentFilter(CustomActionFactory.BROADCAST_ACTION),
Context.RECEIVER_EXPORTED
)
richText = requireViewById(R.id.use_rich_text)
albumCheck = requireViewById(R.id.album_text)
+ shareouselCheck = requireViewById(R.id.shareousel)
mediaTypeSelection = requireViewById(R.id.media_type_selection)
+ mediaTypeHeader = requireViewById(R.id.media_type_header)
mediaSelection = requireViewById<RadioGroup>(R.id.media_selection).apply {
setOnCheckedChangeListener { _, id -> updateMediaTypesList(id) }
check(R.id.no_media)
}
+ metadata = requireViewById<EditText>(R.id.metadata)
textSelection = requireViewById<RadioGroup>(R.id.text_selection).apply {
check(R.id.short_text)
@@ -152,7 +158,7 @@
).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
- mediaTypeSelection.isEnabled = false
+ setMediaTypeVisibility(false)
}
private fun setSingleMediaTypeOptions() {
@@ -163,7 +169,7 @@
).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
- mediaTypeSelection.isEnabled = true
+ setMediaTypeVisibility(true)
}
private fun setAllMediaTypeOptions() {
@@ -182,7 +188,14 @@
).apply {
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
}
- mediaTypeSelection.isEnabled = true
+ setMediaTypeVisibility(true)
+ }
+
+ private fun setMediaTypeVisibility(visible: Boolean) {
+ val visibility = if (visible) View.VISIBLE else View.GONE
+ mediaTypeHeader.visibility = visibility
+ mediaTypeSelection.visibility = visibility
+ shareouselCheck.visibility = visibility
}
private fun share(view: View) {
@@ -192,20 +205,22 @@
val mimeTypes = getSelectedContentTypes()
val imageUris = ArrayList(
- (1..IMAGE_COUNT).map{ idx ->
- makeItemUri(idx, mimeTypes[idx % mimeTypes.size])
- }.shuffled())
+ (1..ImageContentProvider.IMAGE_COUNT).map{ idx ->
+ ImageContentProvider.makeItemUri(idx, mimeTypes[idx % mimeTypes.size])
+ })
+
+ val imageIndex = Random.nextInt(ImageContentProvider.IMAGE_COUNT)
when (mediaSelection.checkedRadioButtonId) {
R.id.one_image -> share.apply {
- putExtra(Intent.EXTRA_STREAM, imageUris[0])
+ putExtra(Intent.EXTRA_STREAM, imageUris[imageIndex])
clipData = ClipData("", arrayOf("image/jpg"), ClipData.Item(imageUris[0]))
type = if (mimeTypes.size == 1) mimeTypes[0] else "*/*"
}
R.id.many_images -> share.apply {
action = Intent.ACTION_SEND_MULTIPLE
clipData = ClipData("", arrayOf("image/jpg"), ClipData.Item(imageUris[0])).apply {
- for (i in 1 until IMAGE_COUNT) {
+ for (i in 1 until ImageContentProvider.IMAGE_COUNT) {
addItem(ClipData.Item(imageUris[i]))
}
}
@@ -253,7 +268,7 @@
val pendingIntent = PendingIntent.getBroadcast(
this,
1,
- Intent(BROADCAST_ACTION),
+ Intent(CustomActionFactory.BROADCAST_ACTION),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
)
val modifyShareAction = ChooserAction.Builder(
@@ -267,13 +282,25 @@
when (requireViewById<RadioGroup>(R.id.action_selection).checkedRadioButtonId) {
R.id.one_action -> chooserIntent.putExtra(
- Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, getCustomActions(1)
+ Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, customActionFactory.getCustomActions(1)
)
R.id.five_actions -> chooserIntent.putExtra(
- Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, getCustomActions(5)
+ Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, customActionFactory.getCustomActions(5)
)
}
+ if (metadata.text.isNotEmpty()) {
+ chooserIntent.putExtra(Intent.EXTRA_METADATA_TEXT, metadata.text)
+ }
+ if (shareouselCheck.isChecked) {
+ chooserIntent.putExtra(Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI,
+ AdditionalContentProvider.ADDITIONAL_CONTENT_URI)
+ chooserIntent.putExtra(Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, 0)
+ chooserIntent.clipData?.addItem(
+ ClipData.Item(AdditionalContentProvider.ADDITIONAL_CONTENT_URI))
+ chooserIntent.putExtra(AdditionalContentProvider.CURSOR_START_POSITION, 0)
+ }
+
startActivity(chooserIntent)
}
@@ -290,12 +317,6 @@
}
} ?: arrayOf("image/jpeg")
- private fun makeItemUri(idx: Int, mimeType: String): Uri =
- Uri.parse("${ImageContentProvider.URI_PREFIX}img$idx.jpg")
- .buildUpon()
- .appendQueryParameter(ImageContentProvider.PARAM_TYPE, mimeType)
- .build()
-
private fun setIntentText(intent: Intent, text: CharSequence) {
if (TextUtils.isEmpty(intent.type)) {
intent.type = "text/plain"
@@ -349,26 +370,6 @@
if (richText.isChecked) it else it.toString()
}
- private fun getCustomActions(count: Int): Array<ChooserAction?> {
- val actions = arrayOfNulls<ChooserAction>(count)
-
- for (i in 0 until count) {
- val customAction = PendingIntent.getBroadcast(
- this,
- i,
- Intent(BROADCAST_ACTION),
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_CANCEL_CURRENT
- )
- actions[i] = ChooserAction.Builder(
- Icon.createWithResource(this, R.drawable.testicon),
- "Action ${i + 1}",
- customAction
- ).build()
- }
-
- return actions
- }
-
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(customActionReceiver)
diff --git a/samples/ApiDemos/tests/Android.bp b/samples/ApiDemos/tests/Android.bp
index abb7fd7..b2fbf80 100644
--- a/samples/ApiDemos/tests/Android.bp
+++ b/samples/ApiDemos/tests/Android.bp
@@ -10,7 +10,10 @@
"android.test.runner.stubs",
"android.test.base.stubs",
],
- static_libs: ["junit"],
+ static_libs: [
+ "junit",
+ "androidx.test.rules",
+ ],
// Include all test java files.
srcs: ["src/**/*.java"],
// Notice that we don't have to include the src files of ApiDemos because, by
diff --git a/samples/ApiDemos/tests/src/com/example/android/apis/ApiDemosApplicationTests.java b/samples/ApiDemos/tests/src/com/example/android/apis/ApiDemosApplicationTests.java
index 3074ca6..0b63f6c 100644
--- a/samples/ApiDemos/tests/src/com/example/android/apis/ApiDemosApplicationTests.java
+++ b/samples/ApiDemos/tests/src/com/example/android/apis/ApiDemosApplicationTests.java
@@ -17,8 +17,9 @@
package com.example.android.apis;
import android.test.ApplicationTestCase;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
/**
* This is a simple framework for a test of an Application. See
diff --git a/samples/ApiDemos/tests/src/com/example/android/apis/app/ForwardingTest.java b/samples/ApiDemos/tests/src/com/example/android/apis/app/ForwardingTest.java
index 340bedb..19677c0 100644
--- a/samples/ApiDemos/tests/src/com/example/android/apis/app/ForwardingTest.java
+++ b/samples/ApiDemos/tests/src/com/example/android/apis/app/ForwardingTest.java
@@ -16,15 +16,16 @@
package com.example.android.apis.app;
-import com.example.android.apis.R;
-import com.example.android.apis.view.Focus2ActivityTest;
-
import android.content.Context;
import android.content.Intent;
import android.test.ActivityUnitTestCase;
-import android.test.suitebuilder.annotation.MediumTest;
import android.widget.Button;
+import androidx.test.filters.MediumTest;
+
+import com.example.android.apis.R;
+import com.example.android.apis.view.Focus2ActivityTest;
+
/**
* This demonstrates completely isolated "unit test" of an Activity class.
*
diff --git a/samples/ApiDemos/tests/src/com/example/android/apis/app/LocalServiceTest.java b/samples/ApiDemos/tests/src/com/example/android/apis/app/LocalServiceTest.java
index 78fee41..99d7426 100644
--- a/samples/ApiDemos/tests/src/com/example/android/apis/app/LocalServiceTest.java
+++ b/samples/ApiDemos/tests/src/com/example/android/apis/app/LocalServiceTest.java
@@ -16,16 +16,12 @@
package com.example.android.apis.app;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.content.Context;
import android.content.Intent;
-import android.os.Handler;
import android.os.IBinder;
-import android.test.MoreAsserts;
import android.test.ServiceTestCase;
-import android.test.suitebuilder.annotation.MediumTest;
-import android.test.suitebuilder.annotation.SmallTest;
+
+import androidx.test.filters.MediumTest;
+import androidx.test.filters.SmallTest;
/**
* This is a simple framework for a test of a Service. See {@link android.test.ServiceTestCase
diff --git a/samples/ApiDemos/tests/src/com/example/android/apis/os/MorseCodeConverterTest.java b/samples/ApiDemos/tests/src/com/example/android/apis/os/MorseCodeConverterTest.java
index 7cf0395..7e18a1b 100644
--- a/samples/ApiDemos/tests/src/com/example/android/apis/os/MorseCodeConverterTest.java
+++ b/samples/ApiDemos/tests/src/com/example/android/apis/os/MorseCodeConverterTest.java
@@ -16,8 +16,9 @@
package com.example.android.apis.os;
+import androidx.test.filters.SmallTest;
+
import junit.framework.TestCase;
-import android.test.suitebuilder.annotation.SmallTest;
/**
* An example of a true unit test that tests the utility class {@link MorseCodeConverter}.
diff --git a/samples/ApiDemos/tests/src/com/example/android/apis/view/Focus2ActivityTest.java b/samples/ApiDemos/tests/src/com/example/android/apis/view/Focus2ActivityTest.java
index b555913..7cc595a 100644
--- a/samples/ApiDemos/tests/src/com/example/android/apis/view/Focus2ActivityTest.java
+++ b/samples/ApiDemos/tests/src/com/example/android/apis/view/Focus2ActivityTest.java
@@ -16,13 +16,14 @@
package com.example.android.apis.view;
-import com.example.android.apis.R;
-
import android.test.ActivityInstrumentationTestCase2;
-import android.test.suitebuilder.annotation.MediumTest;
import android.view.KeyEvent;
import android.widget.Button;
+import androidx.test.filters.MediumTest;
+
+import com.example.android.apis.R;
+
/**
* An example of an {@link ActivityInstrumentationTestCase} of a specific activity {@link Focus2}.
* By virtue of extending {@link ActivityInstrumentationTestCase}, the target activity is automatically
diff --git a/samples/ApiDemos/tests/src/com/example/android/apis/view/Focus2AndroidTest.java b/samples/ApiDemos/tests/src/com/example/android/apis/view/Focus2AndroidTest.java
index b52e4b8..db164fa 100644
--- a/samples/ApiDemos/tests/src/com/example/android/apis/view/Focus2AndroidTest.java
+++ b/samples/ApiDemos/tests/src/com/example/android/apis/view/Focus2AndroidTest.java
@@ -16,17 +16,18 @@
package com.example.android.apis.view;
-import com.example.android.apis.R;
-
import android.content.Context;
import android.test.AndroidTestCase;
-import android.test.suitebuilder.annotation.SmallTest;
import android.view.FocusFinder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
+import androidx.test.filters.SmallTest;
+
+import com.example.android.apis.R;
+
/**
* This exercises the same logic as {@link Focus2ActivityTest} but in a lighter
* weight manner; it doesn't need to launch the activity, and it can test the
diff --git a/tools/winscope/google.tslint.json b/tools/winscope/google.tslint.json
index 68500bd..fd318dc 100644
--- a/tools/winscope/google.tslint.json
+++ b/tools/winscope/google.tslint.json
@@ -2,14 +2,12 @@
// Enable non-strict mode to allow comments.
{
"rules": {
- // The disabled rules below are google3 custom rules that need to be built
- // and added to the project (see google3/javascript/typescript/tslint/rules/).
- // If needed, it should be possible to build and run them in the Winscope npm
- // environment as well. It is probably simpler and quicker to wait till
- // Winscope is ported to google3 though.
-
- //"angular-use-component-harnesses": true,
- //"angular-output-is-readonly": true,
+ "angular-use-component-harnesses": true,
+ "angular-output-is-readonly": true,
+ "angular-no-entry-components": true,
+ "angular-no-manual-lifecycle-hook-method-calls": true,
+ "angular-no-test-overrides-for-framework-features": true,
+ "angular-non-null-asserted-input-is-required": true,
"array-type": [true, "array-simple"],
"arrow-return-shorthand": true,
"ban": [true,
@@ -39,100 +37,106 @@
{"name": ["pdescribe", "only"]},
{"name": ["describeWithDate", "skip"]},
{"name": ["describeWithDate", "only"]},
+ {"name": ["describeBooleanFlag", "only"]},
+ {"name": ["describeEnabledBooleanFlag", "only"]},
{"name": "parseInt", "message": "See http://go/tsstyle#type-coercion"},
{"name": "parseFloat", "message": "See http://go/tsstyle#type-coercion"},
{"name": "Array", "message": "See http://go/tsstyle#array-constructor"},
{"name": ["*", "innerText"], "message": "Use .textContent instead. http://go/typescript/patterns#browser-oddities"},
{"name": ["goog", "setTestOnly"], "message": "See http://go/tsstyle#tests"}
],
- //"ban-as-never": true,
- //"ban-implicit-undefined-default-parameters": true,
- //"ban-jsdoc-enum-tag": true,
- //"ban-malformed-import-paths": true,
- //"ban-passing-async-function-to-describe": true,
- //"ban-spy-returning-rejected-promise": true,
- //"ban-strict-prop-init-comment": true,
+ "ban-as-never": true,
+ "ban-const-enum": true,
+ "ban-implicit-undefined-default-parameters": true,
+ "ban-jsdoc-enum-tag": true,
+ "ban-malformed-import-paths": true,
+ "ban-passing-async-function-to-describe": true,
+ "ban-spy-returning-rejected-promise": true,
+ "ban-strict-prop-init-comment": true,
// allowedSuppressions is a list of strings with no whitespace which, when
- // found wrapped in parentheses immediately after the suppresion, will
- // prevent this rule from triggering.
- // For example: `// @ts-ignore(go/ts99upgrade) Some explanation.`
+ // found anywhere in the suppression comment, will prevent this rule from
+ // triggering.
+ // For example: `// TODO: go/ts99upgrade - Fix the suppressed error`
// Prefer using b/ bug links or go/ go links.
// To check if your suppression string is available in prod, use:
// cl-status/#/summary/tricorder.go-worker/[[SUBMITTED_CL_NUM]]
// Or, for CLs using the suppression, a go/startblock directive of:
// cl-status tricorder.go-worker contains cl/[[SUBMITTED_CL_NUM]] in prod
- //"ban-ts-suppressions": [true, {
- // "allowedSuppressions": [
- // "b/249999919", // Node 18.x typings update
- // "go/tsjs-aatm",
- // "go/ts49upgrade",
- // "go/jspb-ts-enums-fix",
- // "KEEP_ME_LAST_TO_AVOID_NEEDING_TO_ADD_A_COMMA_TO_THE_LAST_ENTRY"
- // ]
- //}],
- //"ban-tslint-disable": true,
+ "ban-ts-suppressions": [true, {
+ "allowedSuppressions": [
+ "b/317387267",
+ "go/tsjs-aatm",
+ "go/ts54upgrade",
+ "go/closure-fail-never",
+ "go/clutz-ts-ignore",
+ "KEEP_ME_LAST_TO_AVOID_NEEDING_TO_ADD_A_COMMA_TO_THE_LAST_ENTRY"
+ ]
+ }],
+ "ban-tslint-disable": true,
"ban-types": [true,
["Object", "Use {} or 'object' instead. See http://go/ts-style#wrapper-types"],
["String", "Use 'string' instead."],
["Number", "Use 'number' instead."],
["Boolean", "Use 'boolean' instead."],
// Add tests in google3/javascript/typescript/tslint/test/googleConfig/ban_types.ts.lint
- ["AnyDuring(?!((ICentral|CelloJs|AngularIvy|Drive|1TF|AllAsUnknown|GoogPromiseThen|Search|DWE|JasmineApril2021|Assisted)Migration)).*",
+ ["AnyDuring(?!((CelloJs|AngularIvy|Drive|1TF|AllAsUnknown|GoogPromiseThen|Search|DWE|Assisted)Migration)).*",
"AnyDuringMigration is a quick-fix used during TypeScript migrations, and should be removed as soon as possible. See http://go/any_during_migration."]
],
// go/keep-sorted start
- //"class-as-namespace": true,
+ "class-as-namespace": true,
"class-name": true,
"curly": [true, "ignore-same-line"],
- //"decorator-placement": true,
- //"discourage-angular-material-subpackage-imports": true,
- //"enforce-comments-on-exported-symbols": true,
- //"enforce-name-casing": true,
- //"file-comment": true,
- //"fix-trailing-comma-import-export": true,
+ "decorator-placement": true,
+ "discourage-angular-material-subpackage-imports": true,
+ "enforce-comments-on-exported-symbols": true,
+ "file-comment": true,
"forin": true,
"interface-name": [true, "never-prefix"],
"interface-over-type-literal": true,
"jsdoc-format": true,
- //"jsdoc-tags": true,
+ "jsdoc-tags": true,
"label-position": true,
"member-access": [true, "no-public"],
"new-parens": true,
"no-angle-bracket-type-assertion": true,
- //TODO (b/264508345): enable rule below after removeing 'any' types
- //"no-any": true,
+ "no-any": true,
"no-conditional-assignment": [true, "allow-within-parenthesis"],
"no-construct": true,
"no-debugger": true,
"no-default-export": true,
"no-duplicate-switch-case": true,
- //"no-inferrable-new-expression": true,
+ "no-inferrable-new-expression": true,
+ "no-inferrable-primitive-types": [true, "ignore-readonly-properties"],
"no-namespace": [true, "allow-declarations"],
- //"no-new-decorators": true,
- //"no-quoted-property-signatures": true,
+ "no-new-decorators": true,
+ "no-quoted-property-signatures": true,
"no-reference": true,
"no-require-imports": true,
- //"no-return-only-generics": true,
+ "no-return-only-generics": true,
"no-string-throw": true,
- //"no-undefined-type-alias": true,
- //"no-unnecessary-escapes": true,
+ "no-undefined-type-alias": true,
+ "no-unnecessary-escapes": true,
"no-unsafe-finally": true,
"no-unused-expression": [true, "allow-fast-null-checks"],
"no-unused-variable": true,
- //"no-unused-wiz-injections": true,
+ "no-unused-wiz-injections": true,
"no-var-keyword": true,
"object-literal-shorthand": true,
+ "one-variable-per-declaration": [true, "ignore-for-loop"],
"only-arrow-functions": [true, "allow-declarations", "allow-named-functions"],
"prefer-const": [true, {"destructuring": "all"}],
- //"prefer-function-declaration": true,
- //"prefer-type-annotation": true,
+ "prefer-function-declaration": true,
+ "prefer-type-annotation": true,
"radix": true,
"semicolon": [true, "always", "strict-bound-class-methods"],
"static-this": true,
"switch-default": true,
"triple-equals": [true, "allow-null-check"],
- "unnecessary-constructor": true
- //"well-formed-closure-message": true
+ "unnecessary-constructor": true,
+ "well-formed-closure-message": true
// go/keep-sorted end
- }
-}
+ },
+ "rulesDirectory": [
+ "rules"
+ ]
+}
\ No newline at end of file
diff --git a/tools/winscope/prettier.config.js b/tools/winscope/prettier.config.js
index 72fd720..dabbe2b 100644
--- a/tools/winscope/prettier.config.js
+++ b/tools/winscope/prettier.config.js
@@ -1,29 +1,54 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-module.exports = {
- arrowParens: 'always',
- bracketSameLine: true,
- bracketSpacing: false,
- printWidth: 100,
- singleQuote: true,
- semi: true,
+const shared = {
+ printWidth: 80,
tabWidth: 2,
useTabs: false,
- trailingComma: 'es5',
- //enable this setting in case prettier-plugin-organize-imports starts breaking stuff
- //organizeImportsSkipDestructiveCodeActions: true,
+ semi: true,
+ singleQuote: true,
+ quoteProps: 'preserve',
+ bracketSpacing: false,
+ trailingComma: 'all',
+ arrowParens: 'always',
+ embeddedLanguageFormatting: 'off',
+ bracketSameLine: true,
+ singleAttributePerLine: false,
+ jsxSingleQuote: false,
+ htmlWhitespaceSensitivity: 'strict',
+};
+
+module.exports = {
+ overrides: [
+ {
+ /** TSX/TS/JS-specific configuration. */
+ files: '*.tsx',
+ options: shared,
+ },
+ {
+ files: '*.ts',
+ options: shared,
+ },
+ {
+ files: '*.js',
+ options: shared,
+ },
+ {
+ /** Sass-specific configuration. */
+ files: '*.scss',
+ options: {
+ singleQuote: true,
+ },
+ },
+ {
+ files: '*.html',
+ options: {
+ printWidth: 100,
+ },
+ },
+ {
+ files: '*.acx.html',
+ options: {
+ parser: 'angular',
+ singleQuote: true,
+ },
+ },
+ ],
};
diff --git a/tools/winscope/src/app/mediator.ts b/tools/winscope/src/app/mediator.ts
index 3796c62..72a206e 100644
--- a/tools/winscope/src/app/mediator.ts
+++ b/tools/winscope/src/app/mediator.ts
@@ -149,6 +149,9 @@
});
await event.visit(WinscopeEventType.TRACE_POSITION_UPDATE, async (event) => {
+ if (event.updateTimeline) {
+ this.timelineData.setPosition(event.position);
+ }
await this.propagateTracePosition(event.position, false);
});
diff --git a/tools/winscope/src/app/mediator_test.ts b/tools/winscope/src/app/mediator_test.ts
index 58a59bf..be21df4 100644
--- a/tools/winscope/src/app/mediator_test.ts
+++ b/tools/winscope/src/app/mediator_test.ts
@@ -14,6 +14,7 @@
* limitations under the License.
*/
+import {assertDefined} from 'common/assert_utils';
import {FunctionUtils} from 'common/function_utils';
import {NO_TIMEZONE_OFFSET_FACTORY, TimestampFactory} from 'common/timestamp_factory';
import {ProgressListener} from 'messaging/progress_listener';
@@ -245,6 +246,26 @@
);
});
+ it('propagates trace position update and updates timeline data', async () => {
+ await loadFiles();
+ await loadTraceView();
+
+ // notify position
+ resetSpyCalls();
+ const finalTimestampNs = timelineData.getFullTimeRange().to.getValueNs();
+ const timestamp = NO_TIMEZONE_OFFSET_FACTORY.makeRealTimestamp(finalTimestampNs);
+ const position = TracePosition.fromTimestamp(timestamp);
+
+ await mediator.onWinscopeEvent(new TracePositionUpdate(position, true));
+ checkTracePositionUpdateEvents(
+ [viewerStub0, viewerOverlay, timelineComponent, crossToolProtocol],
+ position
+ );
+ expect(assertDefined(timelineData.getCurrentPosition()).timestamp.getValueNs()).toEqual(
+ finalTimestampNs
+ );
+ });
+
it("initializes viewers' trace position also when loaded traces have no valid timestamps", async () => {
const dumpFile = await UnitTestUtils.getFixtureFile('traces/dump_WindowManager.pb');
await mediator.onWinscopeEvent(new AppFilesUploaded([dumpFile]));
diff --git a/tools/winscope/src/common/time_utils.ts b/tools/winscope/src/common/time_utils.ts
index 5350c52..aef660a 100644
--- a/tools/winscope/src/common/time_utils.ts
+++ b/tools/winscope/src/common/time_utils.ts
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import {Timestamp, TimestampType, TimezoneInfo} from 'common/time';
+import {Timestamp, TimestampType} from 'common/time';
import {NO_TIMEZONE_OFFSET_FACTORY} from './timestamp_factory';
export class TimeUtils {
@@ -133,10 +133,10 @@
return ts1;
}
- static addTimezoneOffset(timezoneInfo: TimezoneInfo, timestampNs: bigint): bigint {
+ static addTimezoneOffset(timezone: string, timestampNs: bigint): bigint {
const utcDate = new Date(Number(timestampNs / 1000000n));
- const timezoneDateFormatted = utcDate.toLocaleString(timezoneInfo.locale, {
- timeZone: timezoneInfo.timezone,
+ const timezoneDateFormatted = utcDate.toLocaleString('en-US', {
+ timeZone: timezone,
});
const timezoneDate = new Date(timezoneDateFormatted);
const hoursDiff = timezoneDate.getHours() - utcDate.getHours();
diff --git a/tools/winscope/src/common/time_utils_test.ts b/tools/winscope/src/common/time_utils_test.ts
index 38ea474..3f34054 100644
--- a/tools/winscope/src/common/time_utils_test.ts
+++ b/tools/winscope/src/common/time_utils_test.ts
@@ -341,19 +341,39 @@
expect(TimeUtils.format(timestamp)).toEqual('100ms0ns');
});
- it('addTimezoneOffset', () => {
- const timestampNs = 1000000000000n;
- expect(
- TimeUtils.addTimezoneOffset({timezone: 'Europe/London', locale: 'en-GB'}, timestampNs)
- ).toEqual(4600000000000n);
- expect(
- TimeUtils.addTimezoneOffset({timezone: 'Europe/Zurich', locale: 'en-US'}, timestampNs)
- ).toEqual(4600000000000n);
- expect(
- TimeUtils.addTimezoneOffset({timezone: 'America/Los_Angeles', locale: 'en-US'}, timestampNs)
- ).toEqual(58600000000000n);
- expect(
- TimeUtils.addTimezoneOffset({timezone: 'Asia/Kolkata', locale: 'en-US'}, timestampNs)
- ).toEqual(20800000000000n);
+ it('addTimezoneOffset for elapsed timestamps', () => {
+ const elapsedTimestampNs = 1000000000000n;
+ expect(TimeUtils.addTimezoneOffset('Europe/London', elapsedTimestampNs)).toEqual(
+ 4600000000000n
+ );
+ expect(TimeUtils.addTimezoneOffset('Europe/Zurich', elapsedTimestampNs)).toEqual(
+ 4600000000000n
+ );
+ expect(TimeUtils.addTimezoneOffset('America/Los_Angeles', elapsedTimestampNs)).toEqual(
+ 58600000000000n
+ );
+ expect(TimeUtils.addTimezoneOffset('Asia/Kolkata', elapsedTimestampNs)).toEqual(
+ 20800000000000n
+ );
+ });
+
+ it('addTimezoneOffset for real timestamps', () => {
+ const realTimestampNs = 1706094750112797658n;
+ expect(TimeUtils.addTimezoneOffset('Europe/London', realTimestampNs)).toEqual(
+ 1706094750112797658n
+ );
+ expect(TimeUtils.addTimezoneOffset('Europe/Zurich', realTimestampNs)).toEqual(
+ 1706098350112797658n
+ );
+ expect(TimeUtils.addTimezoneOffset('America/Los_Angeles', realTimestampNs)).toEqual(
+ 1706065950112797658n
+ );
+ expect(TimeUtils.addTimezoneOffset('Asia/Kolkata', realTimestampNs)).toEqual(
+ 1706114550112797658n
+ );
+ });
+
+ it('addTimezoneOffset throws for invalid timezone', () => {
+ expect(() => TimeUtils.addTimezoneOffset('Invalid/Timezone', 10n)).toThrow();
});
});
diff --git a/tools/winscope/src/common/timestamp_factory.ts b/tools/winscope/src/common/timestamp_factory.ts
index 00872a9..d89fcff 100644
--- a/tools/winscope/src/common/timestamp_factory.ts
+++ b/tools/winscope/src/common/timestamp_factory.ts
@@ -24,7 +24,7 @@
const valueWithRealtimeOffset = valueNs + (realToElapsedTimeOffsetNs ?? 0n);
const localNs =
this.timezoneInfo.timezone !== 'UTC'
- ? TimeUtils.addTimezoneOffset(this.timezoneInfo, valueWithRealtimeOffset)
+ ? TimeUtils.addTimezoneOffset(this.timezoneInfo.timezone, valueWithRealtimeOffset)
: valueWithRealtimeOffset;
return new Timestamp(TimestampType.REAL, localNs, localNs - valueWithRealtimeOffset);
}
diff --git a/tools/winscope/src/messaging/winscope_event.ts b/tools/winscope/src/messaging/winscope_event.ts
index 86a52a6..2284048 100644
--- a/tools/winscope/src/messaging/winscope_event.ts
+++ b/tools/winscope/src/messaging/winscope_event.ts
@@ -151,20 +151,22 @@
export class TracePositionUpdate extends WinscopeEvent {
override readonly type = WinscopeEventType.TRACE_POSITION_UPDATE;
readonly position: TracePosition;
+ readonly updateTimeline: boolean;
- constructor(position: TracePosition) {
+ constructor(position: TracePosition, updateTimeline = false) {
super();
this.position = position;
+ this.updateTimeline = updateTimeline;
}
- static fromTimestamp(timestamp: Timestamp): TracePositionUpdate {
+ static fromTimestamp(timestamp: Timestamp, updateTimeline = false): TracePositionUpdate {
const position = TracePosition.fromTimestamp(timestamp);
- return new TracePositionUpdate(position);
+ return new TracePositionUpdate(position, updateTimeline);
}
- static fromTraceEntry(entry: TraceEntry<object>): TracePositionUpdate {
+ static fromTraceEntry(entry: TraceEntry<object>, updateTimeline = false): TracePositionUpdate {
const position = TracePosition.fromTraceEntry(entry);
- return new TracePositionUpdate(position);
+ return new TracePositionUpdate(position, updateTimeline);
}
}
diff --git a/tools/winscope/src/trace/transition.ts b/tools/winscope/src/trace/transition.ts
index f35f552..1e2ebec 100644
--- a/tools/winscope/src/trace/transition.ts
+++ b/tools/winscope/src/trace/transition.ts
@@ -19,8 +19,8 @@
export interface Transition {
id: number;
type: string;
- sendTime?: string;
- finishTime?: string;
+ sendTime?: PropertyTreeNode;
+ dispatchTime?: PropertyTreeNode;
duration?: string;
merged: boolean;
aborted: boolean;
diff --git a/tools/winscope/src/viewers/components/ime_additional_properties_component.ts b/tools/winscope/src/viewers/components/ime_additional_properties_component.ts
index 0fc78e0..1eafa7e 100644
--- a/tools/winscope/src/viewers/components/ime_additional_properties_component.ts
+++ b/tools/winscope/src/viewers/components/ime_additional_properties_component.ts
@@ -22,6 +22,7 @@
import {ImeAdditionalProperties} from 'viewers/common/ime_additional_properties';
import {ImeContainerProperties, InputMethodSurfaceProperties} from 'viewers/common/ime_utils';
import {ViewerEvents} from 'viewers/common/viewer_events';
+import {selectedElementStyle} from './styles/selected_element.styles';
@Component({
selector: 'ime-additional-properties',
@@ -320,10 +321,10 @@
}
.selected {
- background-color: #87acec;
color: black;
}
`,
+ selectedElementStyle,
],
})
export class ImeAdditionalPropertiesComponent {
diff --git a/tools/winscope/src/viewers/components/styles/current_element.styles.ts b/tools/winscope/src/viewers/components/styles/current_element.styles.ts
new file mode 100644
index 0000000..ac96b93
--- /dev/null
+++ b/tools/winscope/src/viewers/components/styles/current_element.styles.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+export const currentElementStyle = `
+ .current {
+ color: white;
+ background-color: #365179;
+ }
+`;
diff --git a/tools/winscope/src/viewers/components/styles/node.styles.ts b/tools/winscope/src/viewers/components/styles/node.styles.ts
index 0777e9c..c9bd563 100644
--- a/tools/winscope/src/viewers/components/styles/node.styles.ts
+++ b/tools/winscope/src/viewers/components/styles/node.styles.ts
@@ -13,7 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-export const nodeStyles = `
+
+import {selectedElementStyle} from './selected_element.styles';
+
+export const nodeStyles =
+ `
.node {
position: relative;
display: inline-flex;
@@ -52,11 +56,7 @@
padding: 3px;
color: white;
}
-
- .selected {
- background-color: #87ACEC;
- }
-`;
+` + selectedElementStyle;
// FIXME: child-hover selector is not working.
export const treeNodeDataViewStyles = `
diff --git a/tools/winscope/src/viewers/components/styles/selected_element.styles.ts b/tools/winscope/src/viewers/components/styles/selected_element.styles.ts
new file mode 100644
index 0000000..9b2d70e
--- /dev/null
+++ b/tools/winscope/src/viewers/components/styles/selected_element.styles.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+export const selectedElementStyle = `
+ .selected {
+ background-color: #87ACEC;
+ }
+`;
diff --git a/tools/winscope/src/viewers/components/styles/timestamp_button.styles.ts b/tools/winscope/src/viewers/components/styles/timestamp_button.styles.ts
new file mode 100644
index 0000000..d3694c2
--- /dev/null
+++ b/tools/winscope/src/viewers/components/styles/timestamp_button.styles.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+
+export const timeButtonStyle = `
+ .time button {
+ padding: 0px;
+ line-height: normal;
+ text-align: left;
+ white-space: normal;
+ }
+`;
diff --git a/tools/winscope/src/viewers/viewer_protolog/events.ts b/tools/winscope/src/viewers/viewer_protolog/events.ts
index 4cbe3a7..8f3f8b4 100644
--- a/tools/winscope/src/viewers/viewer_protolog/events.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/events.ts
@@ -18,6 +18,7 @@
static TagsFilterChanged = 'ViewerProtoLogEvent_TagsFilterChanged';
static SourceFilesFilterChanged = 'ViewerProtoLogEvent_SourceFilesFilterChanged';
static SearchStringFilterChanged = 'ViewerProtoLogEvent_SearchStringFilterChanged';
+ static TimestampSelected = 'ViewerProtoLogEvent_TimestampSelected';
}
export {Events};
diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter.ts b/tools/winscope/src/viewers/viewer_protolog/presenter.ts
index 8f3f6b7..f6ee918 100644
--- a/tools/winscope/src/viewers/viewer_protolog/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/presenter.ts
@@ -123,7 +123,7 @@
return {
originalIndex: index,
text: assertDefined(messageNode.getChildByName('text')).formattedValue(),
- time: assertDefined(messageNode.getChildByName('timestamp')).formattedValue(),
+ time: assertDefined(messageNode.getChildByName('timestamp')),
tag: assertDefined(messageNode.getChildByName('tag')).formattedValue(),
level: assertDefined(messageNode.getChildByName('level')).formattedValue(),
at: assertDefined(messageNode.getChildByName('at')).formattedValue(),
diff --git a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
index 4f583e8..b422a42 100644
--- a/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/presenter_test.ts
@@ -45,33 +45,6 @@
const elapsedTime20 = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(20n);
const elapsedTime30 = NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(30n);
- inputMessages = [
- {
- originalIndex: 0,
- text: 'text0',
- time: '10ns',
- tag: 'tag0',
- level: 'level0',
- at: 'sourcefile0',
- },
- {
- originalIndex: 1,
- text: 'text1',
- time: '20ns',
- tag: 'tag1',
- level: 'level1',
- at: 'sourcefile1',
- },
- {
- originalIndex: 2,
- text: 'text2',
- time: '30ns',
- tag: 'tag2',
- level: 'level2',
- at: 'sourcefile2',
- },
- ];
-
const entries = [
new PropertyTreeBuilder()
.setRootId('ProtologTrace')
@@ -110,6 +83,33 @@
.build(),
];
+ inputMessages = [
+ {
+ originalIndex: 0,
+ text: 'text0',
+ time: assertDefined(entries[0].getChildByName('timestamp')),
+ tag: 'tag0',
+ level: 'level0',
+ at: 'sourcefile0',
+ },
+ {
+ originalIndex: 1,
+ text: 'text1',
+ time: assertDefined(entries[1].getChildByName('timestamp')),
+ tag: 'tag1',
+ level: 'level1',
+ at: 'sourcefile1',
+ },
+ {
+ originalIndex: 2,
+ text: 'text2',
+ time: assertDefined(entries[2].getChildByName('timestamp')),
+ tag: 'tag2',
+ level: 'level2',
+ at: 'sourcefile2',
+ },
+ ];
+
trace = new TraceBuilder<PropertyTreeNode>()
.setEntries(entries)
.setTimestamps([time10, time11, time12])
diff --git a/tools/winscope/src/viewers/viewer_protolog/scroll_strategy/protolog_scroll_strategy.ts b/tools/winscope/src/viewers/viewer_protolog/scroll_strategy/protolog_scroll_strategy.ts
index fa91c3f..d4afe7d 100644
--- a/tools/winscope/src/viewers/viewer_protolog/scroll_strategy/protolog_scroll_strategy.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/scroll_strategy/protolog_scroll_strategy.ts
@@ -25,7 +25,10 @@
protected override predictScrollItemHeight(message: UiDataMessage): number {
const textHeight = this.subItemHeight(message.text, this.textCharsPerRow);
- const timestampHeight = this.subItemHeight(message.time, this.timestampCharsPerRow);
+ const timestampHeight = this.subItemHeight(
+ message.time.formattedValue(),
+ this.timestampCharsPerRow
+ );
const sourceFileHeight = this.subItemHeight(message.at, this.sourceFileCharsPerRow);
return Math.max(textHeight, timestampHeight, sourceFileHeight);
}
diff --git a/tools/winscope/src/viewers/viewer_protolog/ui_data.ts b/tools/winscope/src/viewers/viewer_protolog/ui_data.ts
index 0977b62..2032fc2 100644
--- a/tools/winscope/src/viewers/viewer_protolog/ui_data.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/ui_data.ts
@@ -14,10 +14,12 @@
* limitations under the License.
*/
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+
export interface UiDataMessage {
readonly originalIndex: number;
readonly text: string;
- readonly time: string;
+ readonly time: PropertyTreeNode;
readonly tag: string;
readonly level: string;
readonly at: string;
diff --git a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts
index 882129e..925a8bd 100644
--- a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog.ts
@@ -14,9 +14,13 @@
* limitations under the License.
*/
-import {WinscopeEvent} from 'messaging/winscope_event';
+import {FunctionUtils} from 'common/function_utils';
+import {Timestamp} from 'common/time';
+import {TracePositionUpdate, WinscopeEvent} from 'messaging/winscope_event';
+import {EmitEvent} from 'messaging/winscope_event_emitter';
import {Traces} from 'trace/traces';
import {TraceType} from 'trace/trace_type';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
import {View, Viewer, ViewType} from 'viewers/viewer';
import {Events} from './events';
import {Presenter} from './presenter';
@@ -28,6 +32,7 @@
private readonly htmlElement: HTMLElement;
private readonly presenter: Presenter;
private readonly view: View;
+ private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
constructor(traces: Traces) {
this.htmlElement = document.createElement('viewer-protolog');
@@ -48,6 +53,9 @@
this.htmlElement.addEventListener(Events.SearchStringFilterChanged, (event) => {
this.presenter.onSearchStringFilterChanged((event as CustomEvent).detail);
});
+ this.htmlElement.addEventListener(Events.TimestampSelected, (event) => {
+ this.propagateTimestamp((event as CustomEvent).detail);
+ });
this.view = new View(
ViewType.TAB,
@@ -62,8 +70,13 @@
await this.presenter.onAppEvent(event);
}
- setEmitEvent() {
- // do nothing
+ setEmitEvent(callback: EmitEvent) {
+ this.emitAppEvent = callback;
+ }
+
+ async propagateTimestamp(timestampNode: PropertyTreeNode) {
+ const timestamp: Timestamp = timestampNode.getValue();
+ await this.emitAppEvent(TracePositionUpdate.fromTimestamp(timestamp, true));
}
getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts
index 6c76d4d..3acbca8 100644
--- a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component.ts
@@ -16,6 +16,9 @@
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
import {Component, ElementRef, Inject, Input, ViewChild} from '@angular/core';
import {MatSelectChange} from '@angular/material/select';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {currentElementStyle} from 'viewers/components/styles/current_element.styles';
+import {timeButtonStyle} from 'viewers/components/styles/timestamp_button.styles';
import {Events} from './events';
import {UiData} from './ui_data';
@@ -76,9 +79,14 @@
*cdkVirtualFor="let message of uiData.messages; let i = index"
class="message"
[attr.item-id]="i"
- [class.current-message]="isCurrentMessage(i)">
+ [class.current]="isCurrentMessage(i)">
<div class="time">
- <span class="mat-body-1">{{ message.time }}</span>
+ <button
+ mat-button
+ [color]="isCurrentMessage(i) ? 'secondary' : 'primary'"
+ (click)="onTimestampClicked(message.time)">
+ {{ message.time.formattedValue() }}
+ </button>
</div>
<div class="log-level">
<span class="mat-body-1">{{ message.level }}</span>
@@ -122,11 +130,6 @@
overflow-wrap: anywhere;
}
- .message.current-message {
- background-color: #365179;
- color: white;
- }
-
.time {
flex: 2;
}
@@ -175,12 +178,15 @@
font-size: 12px;
}
`,
+ currentElementStyle,
+ timeButtonStyle,
],
})
export class ViewerProtologComponent {
uiData: UiData = UiData.EMPTY;
private searchString = '';
+ private lastClicked = '';
@ViewChild(CdkVirtualScrollViewport) scrollComponent?: CdkVirtualScrollViewport;
@@ -189,7 +195,12 @@
@Input()
set inputData(data: UiData) {
this.uiData = data;
- if (this.uiData.currentMessageIndex !== undefined && this.scrollComponent) {
+ if (
+ this.uiData.currentMessageIndex !== undefined &&
+ this.scrollComponent &&
+ this.lastClicked !==
+ this.uiData.messages[this.uiData.currentMessageIndex].time.formattedValue()
+ ) {
this.scrollComponent.scrollToIndex(this.uiData.currentMessageIndex);
}
}
@@ -216,6 +227,11 @@
}
}
+ onTimestampClicked(timestamp: PropertyTreeNode) {
+ this.lastClicked = timestamp.formattedValue();
+ this.emitEvent(Events.TimestampSelected, timestamp);
+ }
+
isCurrentMessage(index: number): boolean {
return index === this.uiData.currentMessageIndex;
}
diff --git a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts
index 297b86b..1f8ae22 100644
--- a/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_protolog/viewer_protolog_component_test.ts
@@ -22,6 +22,9 @@
import {MatSelectModule} from '@angular/material/select';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {assertDefined} from 'common/assert_utils';
+import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
+import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
import {executeScrollComponentTests} from 'viewers/common/scroll_component_test_utils';
import {Events} from './events';
import {ProtologScrollDirective} from './scroll_strategy/protolog_scroll_directive';
@@ -96,6 +99,21 @@
goToCurrentTimeButton.click();
expect(spy).toHaveBeenCalledWith(150);
});
+
+ it('propagates timestamp on click', () => {
+ component.inputData = makeUiData();
+ fixture.detectChanges();
+ let timestamp = '';
+ htmlElement.addEventListener(Events.TimestampSelected, (event) => {
+ timestamp = (event as CustomEvent).detail.formattedValue();
+ });
+ const logTimestampButton = assertDefined(
+ htmlElement.querySelector('.time button')
+ ) as HTMLButtonElement;
+ logTimestampButton.click();
+
+ expect(timestamp).toEqual('10ns');
+ });
});
describe('Scroll component', () => {
@@ -123,6 +141,13 @@
const allTags = ['WindowManager', 'INVALID'];
const allSourceFiles = ['test_source_file.java', 'other_test_source_file.java'];
+ const time = new PropertyTreeBuilder()
+ .setRootId('ProtologMessage')
+ .setName('timestamp')
+ .setValue(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(10n))
+ .setFormatter(TIMESTAMP_FORMATTER)
+ .build();
+
const messages = [];
const shortMessage = 'test information about message';
const longMessage = shortMessage.repeat(10) + 'keep';
@@ -130,7 +155,7 @@
const uiDataMessage: UiDataMessage = {
originalIndex: i,
text: i % 2 === 0 ? shortMessage : longMessage,
- time: '2022-11-21T18:05:09.777144978',
+ time,
tag: i % 2 === 0 ? allTags[0] : allTags[1],
level: i % 2 === 0 ? allLogLevels[0] : allLogLevels[1],
at: i % 2 === 0 ? allSourceFiles[0] : allSourceFiles[1],
diff --git a/tools/winscope/src/viewers/viewer_transactions/events.ts b/tools/winscope/src/viewers/viewer_transactions/events.ts
index 7af5bab..8a1302d 100644
--- a/tools/winscope/src/viewers/viewer_transactions/events.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/events.ts
@@ -23,6 +23,7 @@
static WhatFilterChanged = 'ViewerTransactionsEvent_WhatFilterChanged';
static EntryClicked = 'ViewerTransactionsEvent_EntryClicked';
static TransactionIdFilterChanged = 'ViewerTransactionsEvent_TransactionIdFilterChanged';
+ static TimestampSelected = 'ViewerTransactionsEvent_TimestampSelected';
}
export {Events};
diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter.ts b/tools/winscope/src/viewers/viewer_transactions/presenter.ts
index d8b39c8..1bb09d8 100644
--- a/tools/winscope/src/viewers/viewer_transactions/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/presenter.ts
@@ -17,13 +17,14 @@
import {ArrayUtils} from 'common/array_utils';
import {assertDefined} from 'common/assert_utils';
import {PersistentStoreProxy} from 'common/persistent_store_proxy';
-import {TimeUtils} from 'common/time_utils';
import {WinscopeEvent, WinscopeEventType} from 'messaging/winscope_event';
import {Trace, TraceEntry} from 'trace/trace';
import {Traces} from 'trace/traces';
import {TraceEntryFinder} from 'trace/trace_entry_finder';
import {TraceType} from 'trace/trace_type';
+import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {DEFAULT_PROPERTY_TREE_NODE_FACTORY} from 'trace/tree_node/property_tree_node_factory';
import {Filter} from 'viewers/common/operations/filter';
import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
import {UiTreeFormatter} from 'viewers/common/ui_tree_formatter';
@@ -348,7 +349,13 @@
const entry = this.trace.getEntry(originalIndex);
const entryNode = entryProtos[originalIndex];
const vsyncId = Number(assertDefined(entryNode.getChildByName('vsyncId')).getValue());
- const entryTimestamp = TimeUtils.format(entry.getTimestamp());
+
+ const entryTimestamp = DEFAULT_PROPERTY_TREE_NODE_FACTORY.makeCalculatedProperty(
+ 'TransactionsTraceEntry',
+ 'timestamp',
+ entry.getTimestamp()
+ );
+ entryTimestamp.setFormatter(TIMESTAMP_FORMATTER);
for (const transactionState of assertDefined(
entryNode.getChildByName('transactions')
diff --git a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
index ac06b3a..253ccae 100644
--- a/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/presenter_test.ts
@@ -320,12 +320,14 @@
it('formats real time', async () => {
await setUpTestEnvironment(TimestampType.REAL);
- expect(assertDefined(outputUiData).entries[0].time).toEqual('2022-08-03T06:19:01.051480997');
+ expect(assertDefined(outputUiData).entries[0].time.formattedValue()).toEqual(
+ '2022-08-03T06:19:01.051480997'
+ );
});
it('formats elapsed time', async () => {
await setUpTestEnvironment(TimestampType.ELAPSED);
- expect(assertDefined(outputUiData).entries[0].time).toEqual('2s450ms981445ns');
+ expect(assertDefined(outputUiData).entries[0].time.formattedValue()).toEqual('2s450ms981445ns');
});
const setUpTestEnvironment = async (timestampType: TimestampType) => {
diff --git a/tools/winscope/src/viewers/viewer_transactions/scroll_strategy/transactions_scroll_strategy.ts b/tools/winscope/src/viewers/viewer_transactions/scroll_strategy/transactions_scroll_strategy.ts
index 20b1447..2535661 100644
--- a/tools/winscope/src/viewers/viewer_transactions/scroll_strategy/transactions_scroll_strategy.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/scroll_strategy/transactions_scroll_strategy.ts
@@ -24,7 +24,10 @@
protected override predictScrollItemHeight(entry: UiDataEntry): number {
const whatHeight = this.subItemHeight(entry.what, this.whatCharsPerRow);
- const timestampHeight = this.subItemHeight(entry.time, this.timestampCharsPerRow);
+ const timestampHeight = this.subItemHeight(
+ entry.time.formattedValue(),
+ this.timestampCharsPerRow
+ );
return Math.max(whatHeight, timestampHeight);
}
}
diff --git a/tools/winscope/src/viewers/viewer_transactions/ui_data.ts b/tools/winscope/src/viewers/viewer_transactions/ui_data.ts
index 9af0c9f..c494224 100644
--- a/tools/winscope/src/viewers/viewer_transactions/ui_data.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/ui_data.ts
@@ -55,7 +55,7 @@
class UiDataEntry {
constructor(
public originalIndexInTraceEntry: number,
- public time: string,
+ public time: PropertyTreeNode,
public vsyncId: number,
public pid: string,
public uid: string,
diff --git a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts
index 2ff5415..cc52c8f 100644
--- a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions.ts
@@ -14,9 +14,13 @@
* limitations under the License.
*/
-import {WinscopeEvent} from 'messaging/winscope_event';
+import {FunctionUtils} from 'common/function_utils';
+import {Timestamp} from 'common/time';
+import {TracePositionUpdate, WinscopeEvent} from 'messaging/winscope_event';
+import {EmitEvent} from 'messaging/winscope_event_emitter';
import {Traces} from 'trace/traces';
import {TraceType} from 'trace/trace_type';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
import {ViewerEvents} from 'viewers/common/viewer_events';
import {View, Viewer, ViewType} from 'viewers/viewer';
import {Events} from './events';
@@ -29,6 +33,7 @@
private readonly htmlElement: HTMLElement;
private readonly presenter: Presenter;
private readonly view: View;
+ private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
constructor(traces: Traces, storage: Storage) {
this.htmlElement = document.createElement('viewer-transactions');
@@ -68,13 +73,12 @@
this.htmlElement.addEventListener(Events.EntryClicked, (event) => {
this.presenter.onEntryClicked((event as CustomEvent).detail);
});
+ this.htmlElement.addEventListener(Events.TimestampSelected, (event) => {
+ this.propagateTimestamp((event as CustomEvent).detail);
+ });
- this.htmlElement.addEventListener(
- ViewerEvents.PropertiesUserOptionsChange,
- async (event) =>
- await this.presenter.onPropertiesUserOptionsChange(
- (event as CustomEvent).detail.userOptions
- )
+ this.htmlElement.addEventListener(ViewerEvents.PropertiesUserOptionsChange, (event) =>
+ this.presenter.onPropertiesUserOptionsChange((event as CustomEvent).detail.userOptions)
);
this.view = new View(
@@ -90,8 +94,13 @@
await this.presenter.onAppEvent(event);
}
- setEmitEvent() {
- // do nothing
+ setEmitEvent(callback: EmitEvent) {
+ this.emitAppEvent = callback;
+ }
+
+ async propagateTimestamp(timestampNode: PropertyTreeNode) {
+ const timestamp: Timestamp = timestampNode.getValue();
+ await this.emitAppEvent(TracePositionUpdate.fromTimestamp(timestamp, true));
}
getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts
index eeda175..fb72c90 100644
--- a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component.ts
@@ -16,7 +16,11 @@
import {CdkVirtualScrollViewport} from '@angular/cdk/scrolling';
import {Component, ElementRef, Inject, Input, ViewChild} from '@angular/core';
import {MatSelectChange} from '@angular/material/select';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
import {ViewerEvents} from 'viewers/common/viewer_events';
+import {currentElementStyle} from 'viewers/components/styles/current_element.styles';
+import {selectedElementStyle} from 'viewers/components/styles/selected_element.styles';
+import {timeButtonStyle} from 'viewers/components/styles/timestamp_button.styles';
import {Events} from './events';
import {UiData} from './ui_data';
@@ -104,11 +108,16 @@
*cdkVirtualFor="let entry of uiData.entries; let i = index"
class="entry"
[attr.item-id]="i"
- [class.current-entry]="isCurrentEntry(i)"
- [class.selected-entry]="isSelectedEntry(i)"
+ [class.current]="isCurrentEntry(i)"
+ [class.selected]="isSelectedEntry(i)"
(click)="onEntryClicked(i)">
<div class="time">
- <span class="mat-body-1">{{ entry.time }}</span>
+ <button
+ mat-button
+ [color]="isCurrentEntry(i) ? 'secondary' : 'primary'"
+ (click)="onTimestampClicked(entry.time)">
+ {{ entry.time.formattedValue() }}
+ </button>
</div>
<div class="id">
<span class="mat-body-1">{{ entry.transactionId }}</span>
@@ -227,16 +236,6 @@
margin-right: 16px;
}
- .entry.current-entry {
- color: white;
- background-color: #365179;
- }
-
- .entry.selected-entry {
- color: white;
- background-color: #98aecd;
- }
-
.go-to-current-time {
flex: none;
margin-top: 4px;
@@ -251,11 +250,15 @@
max-height: 75vh;
}
`,
+ selectedElementStyle,
+ currentElementStyle,
+ timeButtonStyle,
],
})
class ViewerTransactionsComponent {
objectKeys = Object.keys;
uiData: UiData = UiData.EMPTY;
+ private lastClicked = '';
@ViewChild(CdkVirtualScrollViewport) scrollComponent?: CdkVirtualScrollViewport;
@@ -264,7 +267,11 @@
@Input()
set inputData(data: UiData) {
this.uiData = data;
- if (this.uiData.scrollToIndex !== undefined && this.scrollComponent) {
+ if (
+ this.uiData.scrollToIndex !== undefined &&
+ this.scrollComponent &&
+ this.lastClicked !== this.uiData.entries[this.uiData.scrollToIndex].time.formattedValue()
+ ) {
this.scrollComponent.scrollToIndex(this.uiData.scrollToIndex);
}
}
@@ -315,6 +322,11 @@
}
}
+ onTimestampClicked(timestamp: PropertyTreeNode) {
+ this.lastClicked = timestamp.formattedValue();
+ this.emitEvent(Events.TimestampSelected, timestamp);
+ }
+
isCurrentEntry(index: number): boolean {
return index === this.uiData.currentEntryIndex;
}
diff --git a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts
index f8e3cd3..258d294 100644
--- a/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_transactions/viewer_transactions_component_test.ts
@@ -18,9 +18,12 @@
import {ComponentFixture, ComponentFixtureAutoDetect, TestBed} from '@angular/core/testing';
import {MatDividerModule} from '@angular/material/divider';
import {assertDefined} from 'common/assert_utils';
+import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
+import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
import {executeScrollComponentTests} from 'viewers/common/scroll_component_test_utils';
import {UiPropertyTreeNode} from 'viewers/common/ui_property_tree_node';
+import {Events} from './events';
import {TransactionsScrollDirective} from './scroll_strategy/transactions_scroll_directive';
import {UiData, UiDataEntry} from './ui_data';
import {ViewerTransactionsComponent} from './viewer_transactions_component';
@@ -62,7 +65,7 @@
expect(htmlElement.querySelector('.scroll')).toBeTruthy();
const entry = assertDefined(htmlElement.querySelector('.scroll .entry'));
- expect(entry.innerHTML).toContain('TIME_VALUE');
+ expect(entry.innerHTML).toContain('1ns');
expect(entry.innerHTML).toContain('-111');
expect(entry.innerHTML).toContain('PID_VALUE');
expect(entry.innerHTML).toContain('UID_VALUE');
@@ -84,6 +87,21 @@
expect(spy).toHaveBeenCalledWith(1);
});
+ it('propagates timestamp on click', () => {
+ component.inputData = makeUiData();
+ fixture.detectChanges();
+ let timestamp = '';
+ htmlElement.addEventListener(Events.TimestampSelected, (event) => {
+ timestamp = (event as CustomEvent).detail.formattedValue();
+ });
+ const logTimestampButton = assertDefined(
+ htmlElement.querySelector('.time button')
+ ) as HTMLButtonElement;
+ logTimestampButton.click();
+
+ expect(timestamp).toEqual('1ns');
+ });
+
function makeUiData(): UiData {
const propertiesTree = new PropertyTreeBuilder()
.setRootId('Transactions')
@@ -91,9 +109,16 @@
.setValue(null)
.build();
+ const time = new PropertyTreeBuilder()
+ .setRootId(propertiesTree.id)
+ .setName('timestamp')
+ .setValue(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1n))
+ .setFormatter(TIMESTAMP_FORMATTER)
+ .build();
+
const entry = new UiDataEntry(
0,
- 'TIME_VALUE',
+ time,
-111,
'PID_VALUE',
'UID_VALUE',
@@ -106,7 +131,7 @@
const entry2 = new UiDataEntry(
1,
- 'TIME_VALUE',
+ time,
-222,
'PID_VALUE_2',
'UID_VALUE_2',
@@ -144,6 +169,14 @@
.setName('tree')
.setValue(null)
.build();
+
+ const time = new PropertyTreeBuilder()
+ .setRootId(propertiesTree.id)
+ .setName('timestamp')
+ .setValue(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(1n))
+ .setFormatter(TIMESTAMP_FORMATTER)
+ .build();
+
const uiData = new UiData(
[],
[],
@@ -164,7 +197,7 @@
for (let i = 0; i < 200; i++) {
const entry = new UiDataEntry(
0,
- 'TIME_VALUE',
+ time,
-111,
'PID_VALUE',
'UID_VALUE',
diff --git a/tools/winscope/src/viewers/viewer_transitions/events.ts b/tools/winscope/src/viewers/viewer_transitions/events.ts
index 95f9665..aa19964 100644
--- a/tools/winscope/src/viewers/viewer_transitions/events.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/events.ts
@@ -16,6 +16,7 @@
class Events {
static TransitionSelected = 'ViewerTransitionsEvent_TransitionSelected';
+ static TimestampSelected = 'ViewerTransitionsEvent_TimestampSelected';
}
export {Events};
diff --git a/tools/winscope/src/viewers/viewer_transitions/presenter.ts b/tools/winscope/src/viewers/viewer_transitions/presenter.ts
index b8a165b..b9a08ca 100644
--- a/tools/winscope/src/viewers/viewer_transitions/presenter.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/presenter.ts
@@ -112,12 +112,13 @@
private makeTransitions(entries: PropertyTreeNode[]): Transition[] {
return entries.map((transitionNode) => {
const wmDataNode = assertDefined(transitionNode.getChildByName('wmData'));
+ const shellDataNode = assertDefined(transitionNode.getChildByName('shellData'));
const transition: Transition = {
id: assertDefined(transitionNode.getChildByName('id')).getValue(),
type: wmDataNode.getChildByName('type')?.formattedValue() ?? 'NONE',
- sendTime: wmDataNode.getChildByName('sendTimeNs')?.formattedValue(),
- finishTime: wmDataNode.getChildByName('finishTimeNs')?.formattedValue(),
+ sendTime: wmDataNode.getChildByName('sendTimeNs'),
+ dispatchTime: shellDataNode.getChildByName('dispatchTimeNs'),
duration: transitionNode.getChildByName('duration')?.formattedValue(),
merged: assertDefined(transitionNode.getChildByName('merged')).getValue(),
aborted: assertDefined(transitionNode.getChildByName('aborted')).getValue(),
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts
index 495e825..f5e53a1 100644
--- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions.ts
@@ -13,9 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import {WinscopeEvent} from 'messaging/winscope_event';
+import {FunctionUtils} from 'common/function_utils';
+import {Timestamp} from 'common/time';
+import {TracePositionUpdate, WinscopeEvent} from 'messaging/winscope_event';
+import {EmitEvent} from 'messaging/winscope_event_emitter';
import {Traces} from 'trace/traces';
import {TraceType} from 'trace/trace_type';
+import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
import {View, Viewer, ViewType} from 'viewers/viewer';
import {Events} from './events';
import {Presenter} from './presenter';
@@ -27,6 +31,7 @@
private readonly htmlElement: HTMLElement;
private readonly presenter: Presenter;
private readonly view: View;
+ private emitAppEvent: EmitEvent = FunctionUtils.DO_NOTHING_ASYNC;
constructor(traces: Traces) {
this.htmlElement = document.createElement('viewer-transitions');
@@ -39,6 +44,10 @@
this.presenter.onTransitionSelected((event as CustomEvent).detail);
});
+ this.htmlElement.addEventListener(Events.TimestampSelected, (event) => {
+ this.propagateTimestamp((event as CustomEvent).detail);
+ });
+
this.view = new View(
ViewType.TAB,
this.getDependencies(),
@@ -52,8 +61,13 @@
await this.presenter.onAppEvent(event);
}
- setEmitEvent() {
- // do nothing
+ setEmitEvent(callback: EmitEvent) {
+ this.emitAppEvent = callback;
+ }
+
+ async propagateTimestamp(timestampNode: PropertyTreeNode) {
+ const timestamp: Timestamp = timestampNode.getValue();
+ await this.emitAppEvent(TracePositionUpdate.fromTimestamp(timestamp, true));
}
getViews(): View[] {
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts
index 8e8a066..ca78276 100644
--- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component.ts
@@ -17,6 +17,8 @@
import {Component, ElementRef, Inject, Input} from '@angular/core';
import {Transition} from 'trace/transition';
import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
+import {selectedElementStyle} from 'viewers/components/styles/selected_element.styles';
+import {timeButtonStyle} from 'viewers/components/styles/timestamp_button.styles';
import {Events} from './events';
import {UiData} from './ui_data';
@@ -29,6 +31,7 @@
<div class="id mat-body-2">Id</div>
<div class="type mat-body-2">Type</div>
<div class="send-time mat-body-2">Send Time</div>
+ <div class="dispatch-time mat-body-2">Dispatch Time</div>
<div class="duration mat-body-2">Duration</div>
<div class="status mat-body-2">Status</div>
</div>
@@ -36,7 +39,7 @@
<div
*cdkVirtualFor="let transition of uiData.entries; let i = index"
class="entry table-row"
- [class.current]="isCurrentTransition(transition)"
+ [class.selected]="isSelectedTransition(transition)"
(click)="onTransitionClicked(transition)">
<div class="id">
<span class="mat-body-1">{{ transition.id }}</span>
@@ -44,10 +47,26 @@
<div class="type">
<span class="mat-body-1">{{ transition.type }}</span>
</div>
- <div class="send-time">
- <span *ngIf="transition.sendTime" class="mat-body-1">{{ transition.sendTime }}</span>
+ <div class="send-time time">
+ <button
+ mat-button
+ color="primary"
+ *ngIf="transition.sendTime"
+ (click)="onTimestampClicked(transition.sendTime)">
+ {{ transition.sendTime.formattedValue() }}
+ </button>
<span *ngIf="!transition.sendTime" class="mat-body-1"> n/a </span>
</div>
+ <div class="dispatch-time time">
+ <button
+ mat-button
+ color="primary"
+ *ngIf="transition.dispatchTime"
+ (click)="onTimestampClicked(transition.dispatchTime)">
+ {{ transition.dispatchTime.formattedValue() }}
+ </button>
+ <span *ngIf="!transition.dispatchTime" class="mat-body-1"> n/a </span>
+ </div>
<div class="duration">
<span *ngIf="transition.duration" class="mat-body-1">{{ transition.duration }}</span>
<span *ngIf="!transition.duration" class="mat-body-1"> n/a </span>
@@ -133,11 +152,6 @@
border-bottom: solid 1px rgba(0, 0, 0, 0.5);
}
- .scroll .entry.current {
- color: white;
- background-color: #365179;
- }
-
.table-row > div {
padding: 16px;
}
@@ -150,6 +164,10 @@
flex: 2;
}
+ .table-row .dispatch-time {
+ flex: 4;
+ }
+
.table-row .send-time {
flex: 4;
}
@@ -169,7 +187,7 @@
gap: 5px;
}
- .current .status mat-icon {
+ .selected .status mat-icon {
color: white !important;
}
@@ -186,13 +204,9 @@
flex-grow: 1;
padding: 0.5rem;
}
-
- .selected-transition {
- padding: 1rem;
- border-bottom: solid 1px rgba(0, 0, 0, 0.12);
- flex-grow: 1;
- }
`,
+ selectedElementStyle,
+ timeButtonStyle,
],
})
export class ViewerTransitionsComponent {
@@ -207,7 +221,7 @@
this.emitEvent(Events.TransitionSelected, transition.propertiesTree);
}
- isCurrentTransition(transition: Transition): boolean {
+ isSelectedTransition(transition: Transition): boolean {
return (
transition.id ===
this.uiData.selectedTransition
@@ -222,6 +236,10 @@
);
}
+ onTimestampClicked(timestamp: PropertyTreeNode) {
+ this.emitEvent(Events.TimestampSelected, timestamp);
+ }
+
emitEvent(event: string, propertiesTree: PropertyTreeNode) {
const customEvent = new CustomEvent(event, {
bubbles: true,
diff --git a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
index 835ba06..f8cc62f 100644
--- a/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
+++ b/tools/winscope/src/viewers/viewer_transitions/viewer_transitions_component_test.ts
@@ -20,6 +20,7 @@
import {MatDividerModule} from '@angular/material/divider';
import {assertDefined} from 'common/assert_utils';
import {TimestampType} from 'common/time';
+import {NO_TIMEZONE_OFFSET_FACTORY} from 'common/timestamp_factory';
import {TracePositionUpdate} from 'messaging/winscope_event';
import {PropertyTreeBuilder} from 'test/unit/property_tree_builder';
import {UnitTestUtils} from 'test/unit/utils';
@@ -29,6 +30,7 @@
import {TracePosition} from 'trace/trace_position';
import {TraceType} from 'trace/trace_type';
import {Transition} from 'trace/transition';
+import {TIMESTAMP_FORMATTER} from 'trace/tree_node/formatters';
import {PropertyTreeNode} from 'trace/tree_node/property_tree_node';
import {TreeComponent} from 'viewers/components/tree_component';
import {TreeNodeComponent} from 'viewers/components/tree_node_component';
@@ -154,6 +156,19 @@
const textContentWithoutWhitespaces = treeView.textContent?.replace(/(\s|\t|\n)*/g, '');
expect(textContentWithoutWhitespaces).toContain(`id:${selectedTransitionId}`);
});
+
+ it('propagates timestamp on click', () => {
+ let timestamp = '';
+ htmlElement.addEventListener(Events.TimestampSelected, (event) => {
+ timestamp = (event as CustomEvent).detail.formattedValue();
+ });
+ const logTimestampButton = assertDefined(
+ htmlElement.querySelector('.time button')
+ ) as HTMLButtonElement;
+ logTimestampButton.click();
+
+ expect(timestamp).toEqual('20ns');
+ });
});
function makeUiData(): UiData {
@@ -181,11 +196,18 @@
.setChildren([{name: 'id', value: id}])
.build();
+ const sendTimeNode = new PropertyTreeBuilder()
+ .setRootId(transitionTree.id)
+ .setName('sendTimeNs')
+ .setValue(NO_TIMEZONE_OFFSET_FACTORY.makeElapsedTimestamp(BigInt(sendTimeNanos)))
+ .setFormatter(TIMESTAMP_FORMATTER)
+ .build();
+
return {
id,
type: 'TO_FRONT',
- sendTime: sendTimeNanos.toString() + 'ns',
- finishTime: finishTimeNanos.toString() + 'ns',
+ sendTime: sendTimeNode,
+ dispatchTime: undefined,
duration: (finishTimeNanos - sendTimeNanos).toString() + 'ns',
merged: false,
aborted: false,
diff --git a/vndk/tools/header-checker/utils/create_reference_dumps.py b/vndk/tools/header-checker/utils/create_reference_dumps.py
index 40ceba7..901970e 100755
--- a/vndk/tools/header-checker/utils/create_reference_dumps.py
+++ b/vndk/tools/header-checker/utils/create_reference_dumps.py
@@ -13,7 +13,7 @@
PRODUCTS_DEFAULT = ['aosp_arm', 'aosp_arm64', 'aosp_x86', 'aosp_x86_64']
PREBUILTS_ABI_DUMPS_DIR = os.path.join(AOSP_DIR, 'prebuilts', 'abi-dumps')
-PREBUILTS_ABI_DUMPS_SUBDIRS = ('ndk', 'platform')
+PREBUILTS_ABI_DUMPS_SUBDIRS = ('ndk', 'platform', 'vndk')
NON_AOSP_TAGS = {'VENDOR', 'PRODUCT'}
SOONG_DIR = os.path.join(AOSP_DIR, 'out', 'soong', '.intermediates')
@@ -28,8 +28,9 @@
class GetVersionedRefDumpDirStem:
- def __init__(self, chosen_platform_version,
+ def __init__(self, board_api_level, chosen_platform_version,
binder_bitness):
+ self.board_api_level = board_api_level
self.chosen_platform_version = chosen_platform_version
self.binder_bitness = binder_bitness
@@ -37,8 +38,9 @@
if subdir not in PREBUILTS_ABI_DUMPS_SUBDIRS:
raise ValueError(f'"{subdir}" is not a valid dump directory under '
f'{PREBUILTS_ABI_DUMPS_DIR}.')
- return os.path.join(PREBUILTS_ABI_DUMPS_DIR, subdir,
- self.chosen_platform_version,
+ version_stem = (self.board_api_level if subdir == 'vndk' else
+ self.chosen_platform_version)
+ return os.path.join(PREBUILTS_ABI_DUMPS_DIR, subdir, version_stem,
self.binder_bitness, arch_str)
@@ -56,8 +58,10 @@
return ''
if tag == 'NDK':
return 'ndk'
- if tag in ('PLATFORM', 'LLNDK'):
+ if tag == 'PLATFORM':
return 'platform'
+ if tag == 'LLNDK':
+ return 'vndk'
raise ValueError(tag + ' is not a known tag.')
@@ -94,10 +98,10 @@
for product in args.products:
build_target = BuildTarget(product, args.release, args.build_variant)
(
- platform_vndk_version, binder_32_bit,
- platform_version_codename, platform_sdk_version
- ) = build_vars = get_build_vars(
- ['PLATFORM_VNDK_VERSION', 'BINDER32BIT',
+ platform_vndk_version, release_board_api_level, binder_32_bit,
+ platform_version_codename, platform_sdk_version,
+ ) = get_build_vars(
+ ['PLATFORM_VNDK_VERSION', 'RELEASE_BOARD_API_LEVEL', 'BINDER32BIT',
'PLATFORM_VERSION_CODENAME', 'PLATFORM_SDK_VERSION'],
build_target
)
@@ -121,7 +125,7 @@
exclude_tags = ()
else:
get_ref_dump_dir_stem = GetVersionedRefDumpDirStem(
- chosen_platform_version,
+ release_board_api_level, chosen_platform_version,
binder_bitness)
exclude_tags = NON_AOSP_TAGS