1 /*
2  * Copyright (C) 2024 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.communal.widgets
18 
19 import android.annotation.IdRes
20 import android.annotation.Nullable
21 import android.content.Context
22 import android.content.res.Resources
23 import android.graphics.Rect
24 import android.view.View
25 import android.view.ViewGroup
26 import androidx.core.os.BuildCompat.isAtLeastS
27 import com.android.systemui.res.R
28 import kotlin.math.min
29 
30 /**
31  * Utilities to compute the enforced use of rounded corners on App Widgets. This is a fork of the
32  * Launcher3 source code to enforce the same visual treatment on communal hub.
33  */
34 internal object RoundedCornerEnforcement {
35     /**
36      * Find the background view for a widget.
37      *
38      * @param appWidget the view containing the App Widget (typically the instance of
39      *   [CommunalAppWidgetHostView]).
40      */
findBackgroundnull41     fun findBackground(appWidget: View): View? {
42         val backgrounds = findViewsWithId(appWidget, R.id.background)
43         if (backgrounds.size == 1) {
44             return backgrounds[0]
45         }
46         // Really, the argument should contain the widget, so it cannot be the background.
47         if (appWidget is ViewGroup) {
48             val vg = appWidget
49             if (vg.childCount > 0) {
50                 return findUndefinedBackground(vg.getChildAt(0))
51             }
52         }
53         return appWidget
54     }
55 
56     /** Check whether the app widget has opted out of the enforcement. */
hasAppWidgetOptedOutnull57     fun hasAppWidgetOptedOut(appWidget: View?, background: View): Boolean {
58         return background.id == R.id.background && background.clipToOutline
59     }
60 
61     /**
62      * Computes the rounded rectangle needed for this app widget.
63      *
64      * @param appWidget View onto which the rounded rectangle will be applied.
65      * @param background Background view. This must be either `appWidget` or a descendant of
66      *   `appWidget`.
67      * @param outRect Rectangle set to the rounded rectangle coordinates, in the reference frame of
68      *   `appWidget`.
69      */
computeRoundedRectanglenull70     fun computeRoundedRectangle(appWidget: View, background: View, outRect: Rect) {
71         var background = background
72         outRect.left = 0
73         outRect.right = background.width
74         outRect.top = 0
75         outRect.bottom = background.height
76         while (background !== appWidget) {
77             outRect.offset(background.left, background.top)
78             background = background.parent as View
79         }
80     }
81 
82     /** Get the radius of the rounded rectangle defined in the host's resource. */
getOwnedEnforcedRadiusnull83     private fun getOwnedEnforcedRadius(context: Context): Float {
84         val res: Resources = context.resources
85         return res.getDimension(R.dimen.communal_enforced_rounded_corner_max_radius)
86     }
87 
88     /**
89      * Computes the radius of the rounded rectangle that should be applied to a widget expanded in
90      * the given context.
91      */
computeEnforcedRadiusnull92     fun computeEnforcedRadius(context: Context): Float {
93         if (!isAtLeastS()) {
94             return 0f
95         }
96         val res: Resources = context.resources
97         val systemRadius: Float =
98             res.getDimension(android.R.dimen.system_app_widget_background_radius)
99         val defaultRadius = getOwnedEnforcedRadius(context)
100         return min(defaultRadius, systemRadius)
101     }
102 
findViewsWithIdnull103     private fun findViewsWithId(view: View, @IdRes viewId: Int): List<View> {
104         val output: MutableList<View> = ArrayList()
105         accumulateViewsWithId(view, viewId, output)
106         return output
107     }
108 
109     // Traverse views. If the predicate returns true, continue on the children, otherwise, don't.
accumulateViewsWithIdnull110     private fun accumulateViewsWithId(view: View, @IdRes viewId: Int, output: MutableList<View>) {
111         if (view.id == viewId) {
112             output.add(view)
113             return
114         }
115         if (view is ViewGroup) {
116             val vg = view
117             for (i in 0 until vg.childCount) {
118                 accumulateViewsWithId(vg.getChildAt(i), viewId, output)
119             }
120         }
121     }
122 
isViewVisiblenull123     private fun isViewVisible(view: View): Boolean {
124         return if (view.visibility != View.VISIBLE) {
125             false
126         } else !view.willNotDraw() || view.foreground != null || view.background != null
127     }
128 
129     @Nullable
findUndefinedBackgroundnull130     private fun findUndefinedBackground(current: View): View? {
131         if (current.visibility != View.VISIBLE) {
132             return null
133         }
134         if (isViewVisible(current)) {
135             return current
136         }
137         var lastVisibleView: View? = null
138         // Find the first view that is either not a ViewGroup, or a ViewGroup which will draw
139         // something, or a ViewGroup that contains more than one view.
140         if (current is ViewGroup) {
141             val vg = current
142             for (i in 0 until vg.childCount) {
143                 val visibleView = findUndefinedBackground(vg.getChildAt(i))
144                 if (visibleView != null) {
145                     if (lastVisibleView != null) {
146                         return current // At least two visible children
147                     }
148                     lastVisibleView = visibleView
149                 }
150             }
151         }
152         return lastVisibleView
153     }
154 }
155