1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemui.decor
18 
19 import android.graphics.Color
20 import android.graphics.Path
21 import android.util.PathParser
22 import com.android.systemui.statusbar.commandline.ParseableCommand
23 import com.android.systemui.statusbar.commandline.Type
24 import com.android.systemui.statusbar.commandline.map
25 import java.io.PrintWriter
26 
27 /** Debug screen-decor command to be handled by the SystemUI command line interface */
28 class ScreenDecorCommand(
29     private val callback: Callback,
30 ) : ParseableCommand(SCREEN_DECOR_CMD_NAME) {
31     val debug: Boolean? by
32         param(
33             longName = "debug",
34             description =
35                 "Enter or exits debug mode. Effectively makes the corners visible and allows " +
36                     "for overriding the path data for the anti-aliasing corner paths and display " +
37                     "cutout.",
38             valueParser = Type.Boolean,
39         )
40 
41     val color: Int? by
42         param(
43             longName = "color",
44             shortName = "c",
45             description =
46                 "Set a specific color for the debug assets. See Color#parseString() for " +
47                     "accepted inputs.",
48             valueParser = Type.String.map { it.toColorIntOrNull() }
49         )
50 
51     val roundedTop: RoundedCornerSubCommand? by subCommand(RoundedCornerSubCommand("rounded-top"))
52 
53     val roundedBottom: RoundedCornerSubCommand? by
54         subCommand(RoundedCornerSubCommand("rounded-bottom"))
55 
56     override fun execute(pw: PrintWriter) {
57         callback.onExecute(this, pw)
58     }
59 
60     override fun toString(): String {
61         return "ScreenDecorCommand(" +
62             "debug=$debug, " +
63             "color=$color, " +
64             "roundedTop=$roundedTop, " +
65             "roundedBottom=$roundedBottom)"
66     }
67 
68     /** For use in ScreenDecorations.java, define a Callback */
69     interface Callback {
70         fun onExecute(cmd: ScreenDecorCommand, pw: PrintWriter)
71     }
72 
73     companion object {
74         const val SCREEN_DECOR_CMD_NAME = "screen-decor"
75     }
76 }
77 
78 /**
79  * Defines a subcommand suitable for `rounded-top` and `rounded-bottom`. They both have the same
80  * API.
81  */
82 class RoundedCornerSubCommand(name: String) : ParseableCommand(name) {
83     val height by
84         param(
85                 longName = "height",
86                 description = "The height of a corner, in pixels.",
87                 valueParser = Type.Int,
88             )
89             .required()
90 
91     val width by
92         param(
93                 longName = "width",
94                 description =
95                     "The width of the corner, in pixels. Likely should be equal to the height.",
96                 valueParser = Type.Int,
97             )
98             .required()
99 
100     val pathData by
101         param(
102                 longName = "path-data",
103                 shortName = "d",
104                 description =
105                     "PathParser-compatible path string to be rendered as the corner drawable. " +
106                         "This path should be a closed arc oriented as the top-left corner " +
107                         "of the device",
108                 valueParser = Type.String.map { it.toPathOrNull() }
109             )
110             .required()
111 
112     val viewportHeight: Float? by
113         param(
114             longName = "viewport-height",
115             description =
116                 "The height of the viewport for the given path string. " +
117                     "If null, the corner height will be used.",
118             valueParser = Type.Float,
119         )
120 
121     val scaleY: Float
122         get() = viewportHeight?.let { height.toFloat() / it } ?: 1.0f
123 
124     val viewportWidth: Float? by
125         param(
126             longName = "viewport-width",
127             description =
128                 "The width of the viewport for the given path string. " +
129                     "If null, the corner width will be used.",
130             valueParser = Type.Float,
131         )
132 
133     val scaleX: Float
134         get() = viewportWidth?.let { width.toFloat() / it } ?: 1.0f
135 
136     override fun execute(pw: PrintWriter) {
137         // Not needed for a subcommand
138     }
139 
140     override fun toString(): String {
141         return "RoundedCornerSubCommand(" +
142             "height=$height," +
143             " width=$width," +
144             " pathData='$pathData'," +
145             " viewportHeight=$viewportHeight," +
146             " viewportWidth=$viewportWidth)"
147     }
148 
149     fun toRoundedCornerDebugModel(): DebugRoundedCornerModel =
150         DebugRoundedCornerModel(
151             path = pathData,
152             width = width,
153             height = height,
154             scaleX = scaleX,
155             scaleY = scaleY,
156         )
157 }
158 
159 fun String.toPathOrNull(): Path? =
160     try {
161         PathParser.createPathFromPathData(this)
162     } catch (e: Exception) {
163         null
164     }
165 
166 fun String.toColorIntOrNull(): Int? =
167     try {
168         Color.parseColor(this)
169     } catch (e: Exception) {
170         null
171     }
172