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.wm.shell.common.pip
18 
19 import android.content.Context
20 import android.content.res.Resources
21 import android.util.Size
22 import com.android.wm.shell.R
23 import java.io.PrintWriter
24 
25 class PhoneSizeSpecSource(
26         private val context: Context,
27         private val pipDisplayLayoutState: PipDisplayLayoutState
28 ) : SizeSpecSource {
29     private var DEFAULT_OPTIMIZED_ASPECT_RATIO = 9f / 16
30 
31     private var mDefaultMinSize = 0
32     /** The absolute minimum an overridden size's edge can be */
33     private var mOverridableMinSize = 0
34     /** The preferred minimum (and default minimum) size specified by apps.  */
35     private var mOverrideMinSize: Size? = null
36 
37 
38     /**
39      * Default percentages for the PIP size logic.
40      * 1. Determine max widths
41      * Subtract width of system UI and default padding from the shortest edge of the device.
42      * This is the max width.
43      * 2. Calculate Default and Mins
44      * Default is mSystemPreferredDefaultSizePercent of max-width/height.
45      * Min is mSystemPreferredMinimumSizePercent of it.
46      *
47      * NOTE: Do not use this directly, use the mPreferredDefaultSizePercent getter instead.
48      */
49     private var mSystemPreferredDefaultSizePercent = 0.6f
50     /** Minimum percentages for the PIP size logic. */
51     private var mSystemPreferredMinimumSizePercent = 0.5f
52 
53     /** Threshold to categorize the Display as square, calculated as min(w, h) / max(w, h). */
54     private var mSquareDisplayThresholdForSystemPreferredSize = 0.95f
55     /**
56      * Default percentages for the PIP size logic when the Display is square-ish.
57      * This is used instead when the display is square-ish, like fold-ables when unfolded,
58      * to make sure that default PiP does not cover the hinge (halfway of the display).
59      * 1. Determine max widths
60      * Subtract width of system UI and default padding from the shortest edge of the device.
61      * This is the max width.
62      * 2. Calculate Default and Mins
63      * Default is mSystemPreferredDefaultSizePercent of max-width/height.
64      * Min is mSystemPreferredMinimumSizePercent of it.
65      *
66      * NOTE: Do not use this directly, use the mPreferredDefaultSizePercent getter instead.
67      */
68     private var mSystemPreferredDefaultSizePercentForSquareDisplay = 0.5f
69     /** Minimum percentages for the PIP size logic. */
70     private var mSystemPreferredMinimumSizePercentForSquareDisplay = 0.4f
71 
72     private val mIsSquareDisplay
73         get() = minOf(pipDisplayLayoutState.displayLayout.width(),
74                         pipDisplayLayoutState.displayLayout.height()).toFloat() /
75                 maxOf(pipDisplayLayoutState.displayLayout.width(),
76                         pipDisplayLayoutState.displayLayout.height()) >
77                 mSquareDisplayThresholdForSystemPreferredSize
78     private val mPreferredDefaultSizePercent
79         get() = if (mIsSquareDisplay) mSystemPreferredDefaultSizePercentForSquareDisplay else
80             mSystemPreferredDefaultSizePercent
81 
82     private val mPreferredMinimumSizePercent
83         get() = if (mIsSquareDisplay) mSystemPreferredMinimumSizePercentForSquareDisplay else
84             mSystemPreferredMinimumSizePercent
85 
86     /** Aspect ratio that the PIP size spec logic optimizes for.  */
87     private var mOptimizedAspectRatio = 0f
88 
89     init {
90         reloadResources()
91     }
92 
reloadResourcesnull93     private fun reloadResources() {
94         val res: Resources = context.resources
95 
96         mDefaultMinSize = res.getDimensionPixelSize(
97                 R.dimen.default_minimal_size_pip_resizable_task)
98         mOverridableMinSize = res.getDimensionPixelSize(
99                 R.dimen.overridable_minimal_size_pip_resizable_task)
100 
101         mSystemPreferredDefaultSizePercent = res.getFloat(
102                 R.dimen.config_pipSystemPreferredDefaultSizePercent)
103         mSystemPreferredMinimumSizePercent = res.getFloat(
104                 R.dimen.config_pipSystemPreferredMinimumSizePercent)
105 
106         mSquareDisplayThresholdForSystemPreferredSize = res.getFloat(
107                 R.dimen.config_pipSquareDisplayThresholdForSystemPreferredSize)
108         mSystemPreferredDefaultSizePercentForSquareDisplay = res.getFloat(
109                 R.dimen.config_pipSystemPreferredDefaultSizePercentForSquareDisplay)
110         mSystemPreferredMinimumSizePercentForSquareDisplay = res.getFloat(
111                 R.dimen.config_pipSystemPreferredMinimumSizePercentForSquareDisplay)
112 
113         val requestedOptAspRatio = res.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio)
114         // make sure the optimized aspect ratio is valid with a default value to fall back to
115         mOptimizedAspectRatio = if (requestedOptAspRatio > 1) {
116             DEFAULT_OPTIMIZED_ASPECT_RATIO
117         } else {
118             requestedOptAspRatio
119         }
120     }
121 
onConfigurationChangednull122     override fun onConfigurationChanged() {
123         reloadResources()
124     }
125 
126     /**
127      * Calculates the max size of PIP.
128      *
129      * Optimizes for 16:9 aspect ratios, making them take full length of shortest display edge.
130      * As aspect ratio approaches values close to 1:1, the logic does not let PIP occupy the
131      * whole screen. A linear function is used to calculate these sizes.
132      *
133      * @param aspectRatio aspect ratio of the PIP window
134      * @return dimensions of the max size of the PIP
135      */
getMaxSizenull136     override fun getMaxSize(aspectRatio: Float): Size {
137         val insetBounds = pipDisplayLayoutState.insetBounds
138         val displayBounds = pipDisplayLayoutState.displayBounds
139 
140         val totalHorizontalPadding: Int = (insetBounds.left +
141                 (displayBounds.width() - insetBounds.right))
142         val totalVerticalPadding: Int = (insetBounds.top +
143                 (displayBounds.height() - insetBounds.bottom))
144         val shorterLength: Int = Math.min(displayBounds.width() - totalHorizontalPadding,
145                 displayBounds.height() - totalVerticalPadding)
146         var maxWidth: Int
147         val maxHeight: Int
148 
149         // use the optimized max sizing logic only within a certain aspect ratio range
150         if (aspectRatio >= mOptimizedAspectRatio && aspectRatio <= 1 / mOptimizedAspectRatio) {
151             // this formula and its derivation is explained in b/198643358#comment16
152             maxWidth = Math.round(mOptimizedAspectRatio * shorterLength +
153                     shorterLength * (aspectRatio - mOptimizedAspectRatio) / (1 + aspectRatio))
154             // make sure the max width doesn't go beyond shorter screen length after rounding
155             maxWidth = Math.min(maxWidth, shorterLength)
156             maxHeight = Math.round(maxWidth / aspectRatio)
157         } else {
158             if (aspectRatio > 1f) {
159                 maxWidth = shorterLength
160                 maxHeight = Math.round(maxWidth / aspectRatio)
161             } else {
162                 maxHeight = shorterLength
163                 maxWidth = Math.round(maxHeight * aspectRatio)
164             }
165         }
166         return Size(maxWidth, maxHeight)
167     }
168 
169     /**
170      * Decreases the dimensions by a percentage relative to max size to get default size.
171      *
172      * @param aspectRatio aspect ratio of the PIP window
173      * @return dimensions of the default size of the PIP
174      */
getDefaultSizenull175     override fun getDefaultSize(aspectRatio: Float): Size {
176         val minSize = getMinSize(aspectRatio)
177         if (mOverrideMinSize != null) {
178             return minSize
179         }
180         val maxSize = getMaxSize(aspectRatio)
181         val defaultWidth = Math.max(Math.round(maxSize.width * mPreferredDefaultSizePercent),
182                 minSize.width)
183         val defaultHeight = Math.round(defaultWidth / aspectRatio)
184         return Size(defaultWidth, defaultHeight)
185     }
186 
187     /**
188      * Decreases the dimensions by a certain percentage relative to max size to get min size.
189      *
190      * @param aspectRatio aspect ratio of the PIP window
191      * @return dimensions of the min size of the PIP
192      */
getMinSizenull193     override fun getMinSize(aspectRatio: Float): Size {
194         // if there is an overridden min size provided, return that
195         if (mOverrideMinSize != null) {
196             return adjustOverrideMinSizeToAspectRatio(aspectRatio)!!
197         }
198         val maxSize = getMaxSize(aspectRatio)
199         var minWidth = Math.round(maxSize.width * mPreferredMinimumSizePercent)
200         var minHeight = Math.round(maxSize.height * mPreferredMinimumSizePercent)
201 
202         // make sure the calculated min size is not smaller than the allowed default min size
203         if (aspectRatio > 1f) {
204             minHeight = Math.max(minHeight, mDefaultMinSize)
205             minWidth = Math.round(minHeight * aspectRatio)
206         } else {
207             minWidth = Math.max(minWidth, mDefaultMinSize)
208             minHeight = Math.round(minWidth / aspectRatio)
209         }
210         return Size(minWidth, minHeight)
211     }
212 
213     /**
214      * Returns the size for target aspect ratio making sure new size conforms with the rules.
215      *
216      *
217      * Recalculates the dimensions such that the target aspect ratio is achieved, while
218      * maintaining the same maximum size to current size ratio.
219      *
220      * @param size current size
221      * @param aspectRatio target aspect ratio
222      */
getSizeForAspectRationull223     override fun getSizeForAspectRatio(size: Size, aspectRatio: Float): Size {
224         if (size == mOverrideMinSize) {
225             return adjustOverrideMinSizeToAspectRatio(aspectRatio)!!
226         }
227 
228         val currAspectRatio = size.width.toFloat() / size.height
229 
230         // getting the percentage of the max size that current size takes
231         val currentMaxSize = getMaxSize(currAspectRatio)
232         val currentPercent = size.width.toFloat() / currentMaxSize.width
233 
234         // getting the max size for the target aspect ratio
235         val updatedMaxSize = getMaxSize(aspectRatio)
236         var width = Math.round(updatedMaxSize.width * currentPercent)
237         var height = Math.round(updatedMaxSize.height * currentPercent)
238 
239         // adjust the dimensions if below allowed min edge size
240         val minEdgeSize =
241                 if (mOverrideMinSize == null) mDefaultMinSize else getOverrideMinEdgeSize()
242 
243         if (width < minEdgeSize && aspectRatio <= 1) {
244             width = minEdgeSize
245             height = Math.round(width / aspectRatio)
246         } else if (height < minEdgeSize && aspectRatio > 1) {
247             height = minEdgeSize
248             width = Math.round(height * aspectRatio)
249         }
250 
251         // reduce the dimensions of the updated size to the calculated percentage
252         return Size(width, height)
253     }
254 
255     /** Sets the preferred size of PIP as specified by the activity in PIP mode.  */
setOverrideMinSizenull256     override fun setOverrideMinSize(overrideMinSize: Size?) {
257         mOverrideMinSize = overrideMinSize
258     }
259 
260     /** Returns the preferred minimal size specified by the activity in PIP.  */
getOverrideMinSizenull261     override fun getOverrideMinSize(): Size? {
262         val overrideMinSize = mOverrideMinSize ?: return null
263         return if (overrideMinSize.width < mOverridableMinSize ||
264                 overrideMinSize.height < mOverridableMinSize) {
265             Size(mOverridableMinSize, mOverridableMinSize)
266         } else {
267             overrideMinSize
268         }
269     }
270 
271     /**
272      * Returns the adjusted overridden min size if it is set; otherwise, returns null.
273      *
274      *
275      * Overridden min size needs to be adjusted in its own way while making sure that the target
276      * aspect ratio is maintained
277      *
278      * @param aspectRatio target aspect ratio
279      */
adjustOverrideMinSizeToAspectRationull280     private fun adjustOverrideMinSizeToAspectRatio(aspectRatio: Float): Size? {
281         val size = getOverrideMinSize() ?: return null
282         val sizeAspectRatio = size.width / size.height.toFloat()
283         return if (sizeAspectRatio > aspectRatio) {
284             // Size is wider, fix the width and increase the height
285             Size(size.width, (size.width / aspectRatio).toInt())
286         } else {
287             // Size is taller, fix the height and adjust the width.
288             Size((size.height * aspectRatio).toInt(), size.height)
289         }
290     }
291 
dumpnull292     override fun dump(pw: PrintWriter, prefix: String) {
293         val innerPrefix = "$prefix  "
294         pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize)
295         pw.println(innerPrefix + "mOverridableMinSize=" + mOverridableMinSize)
296         pw.println(innerPrefix + "mDefaultMinSize=" + mDefaultMinSize)
297         pw.println(innerPrefix + "mDefaultSizePercent=" + mPreferredDefaultSizePercent)
298         pw.println(innerPrefix + "mMinimumSizePercent=" + mPreferredMinimumSizePercent)
299         pw.println(innerPrefix + "mOptimizedAspectRatio=" + mOptimizedAspectRatio)
300     }
301 }