Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c0737306 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Boilerplate TransactionFlinger" into main

parents addba1b8 3722b6bf
Loading
Loading
Loading
Loading
+42 −0
Original line number Diff line number Diff line
//
// Copyright 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.
//

package {
    default_team: "trendy_team_android_core_graphics_stack",
    // See: http://go/android-license-faq
    // A large-scale-change added 'default_applicable_licenses' to import
    // all of the 'license_kinds' from "frameworks_base_license"
    // to get the below license kinds:
    //   SPDX-license-identifier-Apache-2.0
    default_applicable_licenses: ["frameworks_base_license"],
}

android_test {
    name: "TransactionFlinger",
    srcs: [
        "**/*.kt",
    ],
    platform_apis: true,
    certificate: "platform",
    static_libs: [
        "androidx.activity_activity-compose",
        "androidx.appcompat_appcompat",
        "androidx.compose.foundation_foundation-layout",
        "androidx.compose.runtime_runtime",
        "androidx.compose.ui_ui",
        "androidx.core_core",
    ],
}
+39 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 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.
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
     package="com.android.test.transactionflinger">

    <uses-sdk android:minSdkVersion="35"/>

    <application android:label="TransactionFlinger"
         android:theme="@android:style/Theme.Material">

        <activity android:name=".Main"
             android:label="TransactionFlinger"
             android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.LAUNCHER"/>
                <category android:name="android.intent.category.LEANBACK_LAUNCHER"/>
            </intent-filter>
        </activity>

        <activity android:name=".activities.TrivialActivity" />

    </application>
</manifest>
+124 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.
 */
package com.android.test.transactionflinger

import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseExpandableListAdapter
import android.widget.ExpandableListView
import android.widget.TextView
import androidx.activity.ComponentActivity
import com.android.test.transactionflinger.activities.TrivialActivity
import kotlin.reflect.KClass

class Demo(val name: String, val makeIntent: (Context) -> Intent) {
    constructor(name: String, activity: KClass<out Activity>) : this(name, { context ->
        Intent(context, activity.java)
    })
}

data class DemoGroup(val groupName: String, val demos: List<Demo>)

private val AllDemos = listOf(
    DemoGroup(
        "Workloads", listOf(
            Demo("TrivialActivity", TrivialActivity::class)
        )
    )
)

/**
 * Main entry point when manually opening the app
 */
class Main : ComponentActivity() {
    public override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val list = ExpandableListView(this)
        list.setFitsSystemWindows(true)

        setContentView(list)

        val inflater = LayoutInflater.from(this)
        list.setAdapter(object : BaseExpandableListAdapter() {
            override fun getGroup(groupPosition: Int): DemoGroup {
                return AllDemos[groupPosition]
            }

            override fun isChildSelectable(groupPosition: Int, childPosition: Int): Boolean = true

            override fun hasStableIds(): Boolean = true

            override fun getGroupView(
                groupPosition: Int,
                isExpanded: Boolean,
                convertView: View?,
                parent: ViewGroup?
            ): View {
                val view = (convertView ?: inflater.inflate(
                    android.R.layout.simple_expandable_list_item_1, parent, false
                )) as TextView
                view.text = AllDemos[groupPosition].groupName
                return view
            }

            override fun getChildrenCount(groupPosition: Int): Int {
                return AllDemos[groupPosition].demos.size
            }

            override fun getChild(groupPosition: Int, childPosition: Int): Demo {
                return AllDemos[groupPosition].demos[childPosition]
            }

            override fun getGroupId(groupPosition: Int): Long = groupPosition.toLong()

            override fun getChildView(
                groupPosition: Int,
                childPosition: Int,
                isLastChild: Boolean,
                convertView: View?,
                parent: ViewGroup?
            ): View {
                val view = (convertView ?: inflater.inflate(
                    android.R.layout.simple_expandable_list_item_1, parent, false
                )) as TextView
                view.text = AllDemos[groupPosition].demos[childPosition].name
                return view
            }

            override fun getChildId(groupPosition: Int, childPosition: Int): Long {
                return (groupPosition.toLong() shl 32) or childPosition.toLong()
            }

            override fun getGroupCount(): Int {
                return AllDemos.size
            }
        })

        list.setOnChildClickListener { _, _, groupPosition, childPosition, _ ->
            val demo = AllDemos[groupPosition].demos[childPosition]
            startActivity(demo.makeIntent(this))
            return@setOnChildClickListener true
        }

        AllDemos.forEachIndexed { index, _ -> list.expandGroup(index) }
    }
}
+129 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.
 */

package com.android.test.transactionflinger.activities

import android.graphics.Color
import android.graphics.ColorSpace
import android.graphics.HardwareBufferRenderer
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RenderNode
import android.hardware.HardwareBuffer
import android.os.Bundle
import android.view.Choreographer
import android.view.Choreographer.VsyncCallback
import android.view.SurfaceControl
import android.view.SurfaceHolder
import android.view.SurfaceView
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.ui.Modifier
import androidx.core.view.WindowCompat
import android.view.WindowInsets
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.Duration.Companion.seconds

/**
 * Trivial activity. Not very interesting.
 */
class TrivialActivity : ComponentActivity(), SurfaceHolder.Callback, VsyncCallback {
    private lateinit var surfaceView: SurfaceView
    private lateinit var sceneSurfaceControl: SurfaceControl
    private lateinit var choroegrapher: Choreographer
    private var startTime = 0L
    private var width = 0
    private var height = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Hide the system bars. Ain't dealing with this when we actually start setting up a scene
        val windowInsetsController = WindowCompat.getInsetsController(window, window.decorView)
        windowInsetsController.hide(WindowInsets.Type.systemBars())
        actionBar?.hide()

        choroegrapher = Choreographer.getInstance()
        setContent {
            AndroidView(
                modifier = Modifier.fillMaxSize(),
                factory = { context ->
                    surfaceView = SurfaceView(context).apply {
                        holder.addCallback(this@TrivialActivity)
                    }
                    surfaceView
                }
            )
        }
    }

    override fun surfaceCreated(holder: SurfaceHolder) {}

    override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
        this.width = width
        this.height = height
        sceneSurfaceControl = SurfaceControl.Builder().setBufferSize(width, height).setHidden(true)
            .setParent(surfaceView.surfaceControl).setName("cogsapp").build()
        choroegrapher.postVsyncCallback(this)
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {}

    override fun onVsync(data: Choreographer.FrameData) {
        if (startTime == 0L) {
            startTime = data.preferredFrameTimeline.deadlineNanos
        }

        val animationTime =
            ((data.preferredFrameTimeline.deadlineNanos - startTime) % 2.seconds.inWholeNanoseconds).nanoseconds

        val red = if (animationTime < 1.seconds) {
            (animationTime.inWholeMilliseconds * 255.0 / 1.seconds.inWholeMilliseconds).toInt()
        } else {
            ((2.seconds - animationTime).inWholeMilliseconds * 255.0 / 1.seconds.inWholeMilliseconds).toInt()
        }

        val renderNode = RenderNode("cogsapp")
        renderNode.setPosition(Rect(0, 0, width, height))
        val paint = Paint()
        paint.color = Color.argb(255, red, 0, 0)
        renderNode.beginRecording(width, height).drawPaint(paint)
        renderNode.endRecording()

        // TODO: use a pool of buffers
        val buffer = HardwareBuffer.create(
            width, height, HardwareBuffer.RGBA_8888, 1,
            HardwareBuffer.USAGE_COMPOSER_OVERLAY or HardwareBuffer.USAGE_GPU_COLOR_OUTPUT
                    or HardwareBuffer.USAGE_GPU_SAMPLED_IMAGE
        )

        val renderer = HardwareBufferRenderer(buffer)
        renderer.setContentRoot(renderNode)
        renderer.obtainRenderRequest().setColorSpace(ColorSpace.get(ColorSpace.Named.SRGB)).draw(
            Runnable::run
        ) {
            SurfaceControl.Transaction()
                .setBuffer(sceneSurfaceControl, buffer, it.fence)
                .setVisibility(
                    sceneSurfaceControl, true
                )
                .apply()
            choroegrapher.postVsyncCallback(this@TrivialActivity)
        }
    }
}
 No newline at end of file