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

Commit a38f25de authored by Jordan Demeulenaere's avatar Jordan Demeulenaere
Browse files

Add support for device emulation in the Gallery app

This CL adds support for device emulation in the Gallery app so that we
can easily emulate any device on another device, e.g. emulate a phone on
a tablet or a emulate tablet on a phone.

The emulation is done at the device level, so that might even be useful
outside of the Gallery app, e.g. when working on SystemUI.

See b/231131244#comment11 for a quick video.

Bug: 231131244
Test: Manual
Change-Id: I64094369c32b796ad41a274d23e0910b7c61a3b0
parent 8a3b78ac
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -17,5 +17,6 @@

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.systemui.compose.gallery">

    <!-- To emulate a display size and density. -->
    <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
</manifest>
+2 −2
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@
-->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.android.systemui.compose.gallery">
    package="com.android.systemui.compose.gallery.app">
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
@@ -25,7 +25,7 @@
        android:supportsRtl="true"
        android:theme="@style/Theme.SystemUIGallery">
        <activity
            android:name=".GalleryActivity"
            android:name="com.android.systemui.compose.gallery.GalleryActivity"
            android:exported="true"
            android:label="@string/app_name">
            <intent-filter>
+114 −0
Original line number Diff line number Diff line
package com.android.systemui.compose.gallery

import android.graphics.Point
import android.os.UserHandle
import android.view.Display
import android.view.WindowManagerGlobal
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
@@ -9,14 +14,25 @@ import androidx.compose.material.icons.filled.BrightnessLow
import androidx.compose.material.icons.filled.FormatSize
import androidx.compose.material.icons.filled.FormatTextdirectionLToR
import androidx.compose.material.icons.filled.FormatTextdirectionRToL
import androidx.compose.material.icons.filled.Smartphone
import androidx.compose.material.icons.filled.Tablet
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import kotlin.math.max
import kotlin.math.min

enum class FontScale(val scale: Float) {
    Small(0.85f),
@@ -35,6 +51,39 @@ fun ConfigurationControls(
    onChangeLayoutDirection: () -> Unit,
    onChangeFontScale: () -> Unit,
) {
    // The display we are emulating, if any.
    var emulatedDisplayName by rememberSaveable { mutableStateOf<String?>(null) }
    val emulatedDisplay =
        emulatedDisplayName?.let { name -> EmulatedDisplays.firstOrNull { it.name == name } }

    LaunchedEffect(emulatedDisplay) {
        val wm = WindowManagerGlobal.getWindowManagerService()

        val defaultDisplayId = Display.DEFAULT_DISPLAY
        if (emulatedDisplay == null) {
            wm.clearForcedDisplayDensityForUser(defaultDisplayId, UserHandle.myUserId())
            wm.clearForcedDisplaySize(defaultDisplayId)
        } else {
            val density = emulatedDisplay.densityDpi

            // Emulate the display and make sure that we use the maximum available space possible.
            val initialSize = Point()
            wm.getInitialDisplaySize(defaultDisplayId, initialSize)
            val width = emulatedDisplay.width
            val height = emulatedDisplay.height
            val minOfSize = min(width, height)
            val maxOfSize = max(width, height)
            if (initialSize.x < initialSize.y) {
                wm.setForcedDisplaySize(defaultDisplayId, minOfSize, maxOfSize)
            } else {
                wm.setForcedDisplaySize(defaultDisplayId, maxOfSize, minOfSize)
            }
            wm.setForcedDisplayDensityForUser(defaultDisplayId, density, UserHandle.myUserId())
        }
    }

    // TODO(b/231131244): Fork FlowRow from Accompanist and use that instead to make sure that users
    // don't miss any available configuration.
    LazyRow {
        // Dark/light theme.
        item {
@@ -82,5 +131,70 @@ fun ConfigurationControls(
                }
            }
        }

        // Display emulation.
        EmulatedDisplays.forEach { display ->
            item {
                DisplayButton(
                    display,
                    emulatedDisplay == display,
                    { emulatedDisplayName = it?.name },
                )
            }
        }
    }
}

@Composable
private fun DisplayButton(
    display: EmulatedDisplay,
    selected: Boolean,
    onChangeEmulatedDisplay: (EmulatedDisplay?) -> Unit,
) {
    val onClick = {
        if (selected) {
            onChangeEmulatedDisplay(null)
        } else {
            onChangeEmulatedDisplay(display)
        }
    }

    val content: @Composable RowScope.() -> Unit = {
        Icon(display.icon, null)
        Spacer(Modifier.width(8.dp))
        Text(display.name)
    }

    if (selected) {
        Button(onClick, contentPadding = ButtonDefaults.TextButtonContentPadding, content = content)
    } else {
        TextButton(onClick, content = content)
    }
}

/** The displays that can be emulated from this Gallery app. */
private val EmulatedDisplays =
    listOf(
        EmulatedDisplay(
            "Phone",
            Icons.Default.Smartphone,
            width = 1440,
            height = 3120,
            densityDpi = 560,
        ),
        EmulatedDisplay(
            "Tablet",
            Icons.Default.Tablet,
            width = 2560,
            height = 1600,
            densityDpi = 320,
        ),
    )

private data class EmulatedDisplay(
    val name: String,
    val icon: ImageVector,
    val width: Int,
    val height: Int,
    val densityDpi: Int,
)