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.os.SystemProperties 22 import android.util.Size 23 import com.android.wm.shell.R 24 import java.io.PrintWriter 25 26 class PhoneSizeSpecSource( 27 private val context: Context, 28 private val pipDisplayLayoutState: PipDisplayLayoutState 29 ) : SizeSpecSource { 30 private var DEFAULT_OPTIMIZED_ASPECT_RATIO = 9f / 16 31 32 private var mDefaultMinSize = 0 33 /** The absolute minimum an overridden size's edge can be */ 34 private var mOverridableMinSize = 0 35 /** The preferred minimum (and default minimum) size specified by apps. */ 36 private var mOverrideMinSize: Size? = null 37 38 39 /** Default and minimum percentages for the PIP size logic. */ 40 private val mDefaultSizePercent: Float 41 private val mMinimumSizePercent: Float 42 43 /** Aspect ratio that the PIP size spec logic optimizes for. */ 44 private var mOptimizedAspectRatio = 0f 45 46 init { 47 mDefaultSizePercent = SystemProperties 48 .get("com.android.wm.shell.pip.phone.def_percentage", "0.6").toFloat() 49 mMinimumSizePercent = SystemProperties 50 .get("com.android.wm.shell.pip.phone.min_percentage", "0.5").toFloat() 51 52 reloadResources() 53 } 54 reloadResourcesnull55 private fun reloadResources() { 56 val res: Resources = context.getResources() 57 58 mDefaultMinSize = res.getDimensionPixelSize( 59 R.dimen.default_minimal_size_pip_resizable_task) 60 mOverridableMinSize = res.getDimensionPixelSize( 61 R.dimen.overridable_minimal_size_pip_resizable_task) 62 63 val requestedOptAspRatio = res.getFloat(R.dimen.config_pipLargeScreenOptimizedAspectRatio) 64 // make sure the optimized aspect ratio is valid with a default value to fall back to 65 mOptimizedAspectRatio = if (requestedOptAspRatio > 1) { 66 DEFAULT_OPTIMIZED_ASPECT_RATIO 67 } else { 68 requestedOptAspRatio 69 } 70 } 71 onConfigurationChangednull72 override fun onConfigurationChanged() { 73 reloadResources() 74 } 75 76 /** 77 * Calculates the max size of PIP. 78 * 79 * Optimizes for 16:9 aspect ratios, making them take full length of shortest display edge. 80 * As aspect ratio approaches values close to 1:1, the logic does not let PIP occupy the 81 * whole screen. A linear function is used to calculate these sizes. 82 * 83 * @param aspectRatio aspect ratio of the PIP window 84 * @return dimensions of the max size of the PIP 85 */ getMaxSizenull86 override fun getMaxSize(aspectRatio: Float): Size { 87 val insetBounds = pipDisplayLayoutState.insetBounds 88 val displayBounds = pipDisplayLayoutState.displayBounds 89 90 val totalHorizontalPadding: Int = (insetBounds.left + 91 (displayBounds.width() - insetBounds.right)) 92 val totalVerticalPadding: Int = (insetBounds.top + 93 (displayBounds.height() - insetBounds.bottom)) 94 val shorterLength: Int = Math.min(displayBounds.width() - totalHorizontalPadding, 95 displayBounds.height() - totalVerticalPadding) 96 var maxWidth: Int 97 val maxHeight: Int 98 99 // use the optimized max sizing logic only within a certain aspect ratio range 100 if (aspectRatio >= mOptimizedAspectRatio && aspectRatio <= 1 / mOptimizedAspectRatio) { 101 // this formula and its derivation is explained in b/198643358#comment16 102 maxWidth = Math.round(mOptimizedAspectRatio * shorterLength + 103 shorterLength * (aspectRatio - mOptimizedAspectRatio) / (1 + aspectRatio)) 104 // make sure the max width doesn't go beyond shorter screen length after rounding 105 maxWidth = Math.min(maxWidth, shorterLength) 106 maxHeight = Math.round(maxWidth / aspectRatio) 107 } else { 108 if (aspectRatio > 1f) { 109 maxWidth = shorterLength 110 maxHeight = Math.round(maxWidth / aspectRatio) 111 } else { 112 maxHeight = shorterLength 113 maxWidth = Math.round(maxHeight * aspectRatio) 114 } 115 } 116 return Size(maxWidth, maxHeight) 117 } 118 119 /** 120 * Decreases the dimensions by a percentage relative to max size to get default size. 121 * 122 * @param aspectRatio aspect ratio of the PIP window 123 * @return dimensions of the default size of the PIP 124 */ getDefaultSizenull125 override fun getDefaultSize(aspectRatio: Float): Size { 126 val minSize = getMinSize(aspectRatio) 127 if (mOverrideMinSize != null) { 128 return minSize 129 } 130 val maxSize = getMaxSize(aspectRatio) 131 val defaultWidth = Math.max(Math.round(maxSize.width * mDefaultSizePercent), 132 minSize.width) 133 val defaultHeight = Math.round(defaultWidth / aspectRatio) 134 return Size(defaultWidth, defaultHeight) 135 } 136 137 /** 138 * Decreases the dimensions by a certain percentage relative to max size to get min size. 139 * 140 * @param aspectRatio aspect ratio of the PIP window 141 * @return dimensions of the min size of the PIP 142 */ getMinSizenull143 override fun getMinSize(aspectRatio: Float): Size { 144 // if there is an overridden min size provided, return that 145 if (mOverrideMinSize != null) { 146 return adjustOverrideMinSizeToAspectRatio(aspectRatio)!! 147 } 148 val maxSize = getMaxSize(aspectRatio) 149 var minWidth = Math.round(maxSize.width * mMinimumSizePercent) 150 var minHeight = Math.round(maxSize.height * mMinimumSizePercent) 151 152 // make sure the calculated min size is not smaller than the allowed default min size 153 if (aspectRatio > 1f) { 154 minHeight = Math.max(minHeight, mDefaultMinSize) 155 minWidth = Math.round(minHeight * aspectRatio) 156 } else { 157 minWidth = Math.max(minWidth, mDefaultMinSize) 158 minHeight = Math.round(minWidth / aspectRatio) 159 } 160 return Size(minWidth, minHeight) 161 } 162 163 /** 164 * Returns the size for target aspect ratio making sure new size conforms with the rules. 165 * 166 * 167 * Recalculates the dimensions such that the target aspect ratio is achieved, while 168 * maintaining the same maximum size to current size ratio. 169 * 170 * @param size current size 171 * @param aspectRatio target aspect ratio 172 */ getSizeForAspectRationull173 override fun getSizeForAspectRatio(size: Size, aspectRatio: Float): Size { 174 if (size == mOverrideMinSize) { 175 return adjustOverrideMinSizeToAspectRatio(aspectRatio)!! 176 } 177 178 val currAspectRatio = size.width.toFloat() / size.height 179 180 // getting the percentage of the max size that current size takes 181 val currentMaxSize = getMaxSize(currAspectRatio) 182 val currentPercent = size.width.toFloat() / currentMaxSize.width 183 184 // getting the max size for the target aspect ratio 185 val updatedMaxSize = getMaxSize(aspectRatio) 186 var width = Math.round(updatedMaxSize.width * currentPercent) 187 var height = Math.round(updatedMaxSize.height * currentPercent) 188 189 // adjust the dimensions if below allowed min edge size 190 val minEdgeSize = 191 if (mOverrideMinSize == null) mDefaultMinSize else getOverrideMinEdgeSize() 192 193 if (width < minEdgeSize && aspectRatio <= 1) { 194 width = minEdgeSize 195 height = Math.round(width / aspectRatio) 196 } else if (height < minEdgeSize && aspectRatio > 1) { 197 height = minEdgeSize 198 width = Math.round(height * aspectRatio) 199 } 200 201 // reduce the dimensions of the updated size to the calculated percentage 202 return Size(width, height) 203 } 204 205 /** Sets the preferred size of PIP as specified by the activity in PIP mode. */ setOverrideMinSizenull206 override fun setOverrideMinSize(overrideMinSize: Size?) { 207 mOverrideMinSize = overrideMinSize 208 } 209 210 /** Returns the preferred minimal size specified by the activity in PIP. */ getOverrideMinSizenull211 override fun getOverrideMinSize(): Size? { 212 val overrideMinSize = mOverrideMinSize ?: return null 213 return if (overrideMinSize.width < mOverridableMinSize || 214 overrideMinSize.height < mOverridableMinSize) { 215 Size(mOverridableMinSize, mOverridableMinSize) 216 } else { 217 overrideMinSize 218 } 219 } 220 221 /** 222 * Returns the adjusted overridden min size if it is set; otherwise, returns null. 223 * 224 * 225 * Overridden min size needs to be adjusted in its own way while making sure that the target 226 * aspect ratio is maintained 227 * 228 * @param aspectRatio target aspect ratio 229 */ adjustOverrideMinSizeToAspectRationull230 private fun adjustOverrideMinSizeToAspectRatio(aspectRatio: Float): Size? { 231 val size = getOverrideMinSize() ?: return null 232 val sizeAspectRatio = size.width / size.height.toFloat() 233 return if (sizeAspectRatio > aspectRatio) { 234 // Size is wider, fix the width and increase the height 235 Size(size.width, (size.width / aspectRatio).toInt()) 236 } else { 237 // Size is taller, fix the height and adjust the width. 238 Size((size.height * aspectRatio).toInt(), size.height) 239 } 240 } 241 dumpnull242 override fun dump(pw: PrintWriter, prefix: String) { 243 val innerPrefix = "$prefix " 244 pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize) 245 pw.println(innerPrefix + "mOverridableMinSize=" + mOverridableMinSize) 246 pw.println(innerPrefix + "mDefaultMinSize=" + mDefaultMinSize) 247 pw.println(innerPrefix + "mDefaultSizePercent=" + mDefaultSizePercent) 248 pw.println(innerPrefix + "mMinimumSizePercent=" + mMinimumSizePercent) 249 pw.println(innerPrefix + "mOptimizedAspectRatio=" + mOptimizedAspectRatio) 250 } 251 }