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.graphics.PointF
22 import android.util.Size
23 import com.android.wm.shell.R
24 
25 class LegacySizeSpecSource(
26         private val context: Context,
27         private val pipDisplayLayoutState: PipDisplayLayoutState
28 ) : SizeSpecSource {
29 
30     private var mDefaultMinSize = 0
31     /** The absolute minimum an overridden size's edge can be */
32     private var mOverridableMinSize = 0
33     /** The preferred minimum (and default minimum) size specified by apps.  */
34     private var mOverrideMinSize: Size? = null
35 
36     private var mDefaultSizePercent = 0f
37     private var mMinimumSizePercent = 0f
38     private var mMaxAspectRatioForMinSize = 0f
39     private var mMinAspectRatioForMinSize = 0f
40 
41     init {
42         reloadResources()
43     }
44 
45     private fun reloadResources() {
46         val res: Resources = context.getResources()
47 
48         mDefaultMinSize = res.getDimensionPixelSize(
49                 R.dimen.default_minimal_size_pip_resizable_task)
50         mOverridableMinSize = res.getDimensionPixelSize(
51                 R.dimen.overridable_minimal_size_pip_resizable_task)
52 
53         mDefaultSizePercent = res.getFloat(R.dimen.config_pictureInPictureDefaultSizePercent)
54         mMinimumSizePercent = res.getFraction(R.fraction.config_pipShortestEdgePercent, 1, 1)
55 
56         mMaxAspectRatioForMinSize = res.getFloat(
57                 R.dimen.config_pictureInPictureAspectRatioLimitForMinSize)
58         mMinAspectRatioForMinSize = 1f / mMaxAspectRatioForMinSize
59     }
60 
61     override fun onConfigurationChanged() {
62         reloadResources()
63     }
64 
65     override fun getMaxSize(aspectRatio: Float): Size {
66         val insetBounds = pipDisplayLayoutState.insetBounds
67 
68         val shorterLength: Int = Math.min(getDisplayBounds().width(),
69                 getDisplayBounds().height())
70         val totalHorizontalPadding: Int = (insetBounds.left +
71                 (getDisplayBounds().width() - insetBounds.right))
72         val totalVerticalPadding: Int = (insetBounds.top +
73                 (getDisplayBounds().height() - insetBounds.bottom))
74 
75         return if (aspectRatio > 1f) {
76             val maxWidth = Math.max(getDefaultSize(aspectRatio).width,
77                     shorterLength - totalHorizontalPadding)
78             val maxHeight = (maxWidth / aspectRatio).toInt()
79             Size(maxWidth, maxHeight)
80         } else {
81             val maxHeight = Math.max(getDefaultSize(aspectRatio).height,
82                     shorterLength - totalVerticalPadding)
83             val maxWidth = (maxHeight * aspectRatio).toInt()
84             Size(maxWidth, maxHeight)
85         }
86     }
87 
88     override fun getDefaultSize(aspectRatio: Float): Size {
89         if (mOverrideMinSize != null) {
90             return getMinSize(aspectRatio)
91         }
92         val smallestDisplaySize: Int = Math.min(getDisplayBounds().width(),
93                 getDisplayBounds().height())
94         val minSize = Math.max(getMinEdgeSize().toFloat(),
95                 smallestDisplaySize * mDefaultSizePercent).toInt()
96         val width: Int
97         val height: Int
98         if (aspectRatio <= mMinAspectRatioForMinSize ||
99                 aspectRatio > mMaxAspectRatioForMinSize) {
100             // Beyond these points, we can just use the min size as the shorter edge
101             if (aspectRatio <= 1) {
102                 // Portrait, width is the minimum size
103                 width = minSize
104                 height = Math.round(width / aspectRatio)
105             } else {
106                 // Landscape, height is the minimum size
107                 height = minSize
108                 width = Math.round(height * aspectRatio)
109             }
110         } else {
111             // Within these points, ensure that the bounds fit within the radius of the limits
112             // at the points
113             val widthAtMaxAspectRatioForMinSize: Float = mMaxAspectRatioForMinSize * minSize
114             val radius = PointF.length(widthAtMaxAspectRatioForMinSize, minSize.toFloat())
115             height = Math.round(Math.sqrt((radius * radius /
116                     (aspectRatio * aspectRatio + 1)).toDouble())).toInt()
117             width = Math.round(height * aspectRatio)
118         }
119         return Size(width, height)
120     }
121 
122     override fun getMinSize(aspectRatio: Float): Size {
123         if (mOverrideMinSize != null) {
124             return adjustOverrideMinSizeToAspectRatio(aspectRatio)!!
125         }
126         val shorterLength: Int = Math.min(getDisplayBounds().width(),
127                 getDisplayBounds().height())
128         val minWidth: Int
129         val minHeight: Int
130         if (aspectRatio > 1f) {
131             minWidth = Math.min(getDefaultSize(aspectRatio).width.toFloat(),
132                     shorterLength * mMinimumSizePercent).toInt()
133             minHeight = (minWidth / aspectRatio).toInt()
134         } else {
135             minHeight = Math.min(getDefaultSize(aspectRatio).height.toFloat(),
136                     shorterLength * mMinimumSizePercent).toInt()
137             minWidth = (minHeight * aspectRatio).toInt()
138         }
139         return Size(minWidth, minHeight)
140     }
141 
142     override fun getSizeForAspectRatio(size: Size, aspectRatio: Float): Size {
143         val smallestSize = Math.min(size.width, size.height)
144         val minSize = Math.max(getMinEdgeSize(), smallestSize)
145         val width: Int
146         val height: Int
147         if (aspectRatio <= 1) {
148             // Portrait, width is the minimum size.
149             width = minSize
150             height = Math.round(width / aspectRatio)
151         } else {
152             // Landscape, height is the minimum size
153             height = minSize
154             width = Math.round(height * aspectRatio)
155         }
156         return Size(width, height)
157     }
158 
159     private fun getDisplayBounds() = pipDisplayLayoutState.displayBounds
160 
161     /** Sets the preferred size of PIP as specified by the activity in PIP mode.  */
162     override fun setOverrideMinSize(overrideMinSize: Size?) {
163         mOverrideMinSize = overrideMinSize
164     }
165 
166     /** Returns the preferred minimal size specified by the activity in PIP.  */
167     override fun getOverrideMinSize(): Size? {
168         val overrideMinSize = mOverrideMinSize ?: return null
169         return if (overrideMinSize.width < mOverridableMinSize ||
170                 overrideMinSize.height < mOverridableMinSize) {
171             Size(mOverridableMinSize, mOverridableMinSize)
172         } else {
173             overrideMinSize
174         }
175     }
176 
177     private fun getMinEdgeSize(): Int {
178         return if (mOverrideMinSize == null) mDefaultMinSize else getOverrideMinEdgeSize()
179     }
180 
181     /**
182      * Returns the adjusted overridden min size if it is set; otherwise, returns null.
183      *
184      *
185      * Overridden min size needs to be adjusted in its own way while making sure that the target
186      * aspect ratio is maintained
187      *
188      * @param aspectRatio target aspect ratio
189      */
190     private fun adjustOverrideMinSizeToAspectRatio(aspectRatio: Float): Size? {
191         val size = getOverrideMinSize() ?: return null
192         val sizeAspectRatio = size.width / size.height.toFloat()
193         return if (sizeAspectRatio > aspectRatio) {
194             // Size is wider, fix the width and increase the height
195             Size(size.width, (size.width / aspectRatio).toInt())
196         } else {
197             // Size is taller, fix the height and adjust the width.
198             Size((size.height * aspectRatio).toInt(), size.height)
199         }
200     }
201 }