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 }