1 /* 2 * Copyright (C) 2022 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.statusbar.phone 18 19 import android.annotation.ColorInt 20 import android.graphics.Rect 21 import android.view.InsetsFlags 22 import android.view.ViewDebug 23 import android.view.WindowInsetsController 24 import android.view.WindowInsetsController.APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS 25 import android.view.WindowInsetsController.Appearance 26 import com.android.internal.statusbar.LetterboxDetails 27 import com.android.internal.util.ContrastColorUtil 28 import com.android.internal.view.AppearanceRegion 29 import com.android.systemui.Dumpable 30 import com.android.systemui.dagger.SysUISingleton 31 import com.android.systemui.dump.DumpManager 32 import com.android.systemui.statusbar.core.StatusBarInitializer.OnStatusBarViewInitializedListener 33 import com.android.systemui.statusbar.phone.fragment.dagger.StatusBarFragmentComponent 34 import java.io.PrintWriter 35 import java.util.Arrays 36 import javax.inject.Inject 37 38 class LetterboxAppearance( 39 @Appearance val appearance: Int, 40 val appearanceRegions: Array<AppearanceRegion> 41 ) { 42 override fun toString(): String { 43 val appearanceString = 44 ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", appearance) 45 return "LetterboxAppearance{$appearanceString, ${appearanceRegions.contentToString()}}" 46 } 47 } 48 49 /** 50 * Responsible for calculating the [Appearance] and [AppearanceRegion] for the status bar when apps 51 * are letterboxed. 52 */ 53 @SysUISingleton 54 class LetterboxAppearanceCalculator 55 @Inject 56 constructor( 57 private val lightBarController: LightBarController, 58 dumpManager: DumpManager, 59 private val letterboxBackgroundProvider: LetterboxBackgroundProvider, 60 ) : OnStatusBarViewInitializedListener, Dumpable { 61 62 init { 63 dumpManager.registerCriticalDumpable(this) 64 } 65 66 private var statusBarBoundsProvider: StatusBarBoundsProvider? = null 67 68 private var lastAppearance: Int? = null 69 private var lastAppearanceRegions: Array<AppearanceRegion>? = null 70 private var lastLetterboxes: Array<LetterboxDetails>? = null 71 private var lastLetterboxAppearance: LetterboxAppearance? = null 72 73 fun getLetterboxAppearance( 74 @Appearance originalAppearance: Int, 75 originalAppearanceRegions: Array<AppearanceRegion>, 76 letterboxes: Array<LetterboxDetails> 77 ): LetterboxAppearance { 78 lastAppearance = originalAppearance 79 lastAppearanceRegions = originalAppearanceRegions 80 lastLetterboxes = letterboxes 81 return getLetterboxAppearanceInternal( 82 letterboxes, originalAppearance, originalAppearanceRegions) 83 .also { lastLetterboxAppearance = it } 84 } 85 86 private fun getLetterboxAppearanceInternal( 87 letterboxes: Array<LetterboxDetails>, 88 originalAppearance: Int, 89 originalAppearanceRegions: Array<AppearanceRegion> 90 ): LetterboxAppearance { 91 if (isScrimNeeded(letterboxes)) { 92 return originalAppearanceWithScrim(originalAppearance, originalAppearanceRegions) 93 } 94 val appearance = appearanceWithoutScrim(originalAppearance) 95 val appearanceRegions = getAppearanceRegions(originalAppearanceRegions, letterboxes) 96 return LetterboxAppearance(appearance, appearanceRegions.toTypedArray()) 97 } 98 99 private fun isScrimNeeded(letterboxes: Array<LetterboxDetails>): Boolean { 100 if (isOuterLetterboxMultiColored()) { 101 return true 102 } 103 return letterboxes.any { letterbox -> 104 letterbox.letterboxInnerBounds.overlapsWith(getStartSideIconBounds()) || 105 letterbox.letterboxInnerBounds.overlapsWith(getEndSideIconsBounds()) 106 } 107 } 108 109 private fun getAppearanceRegions( 110 originalAppearanceRegions: Array<AppearanceRegion>, 111 letterboxes: Array<LetterboxDetails> 112 ): List<AppearanceRegion> { 113 return sanitizeAppearanceRegions(originalAppearanceRegions, letterboxes) + 114 getAllOuterAppearanceRegions(letterboxes) 115 } 116 117 private fun sanitizeAppearanceRegions( 118 originalAppearanceRegions: Array<AppearanceRegion>, 119 letterboxes: Array<LetterboxDetails> 120 ): List<AppearanceRegion> = 121 originalAppearanceRegions.map { appearanceRegion -> 122 val matchingLetterbox = 123 letterboxes.find { it.letterboxFullBounds == appearanceRegion.bounds } 124 if (matchingLetterbox == null) { 125 appearanceRegion 126 } else { 127 // When WindowManager sends appearance regions for an app, it sends them for the 128 // full bounds of its window. 129 // Here we want the bounds to be only for the inner bounds of the letterboxed app. 130 AppearanceRegion( 131 appearanceRegion.appearance, matchingLetterbox.letterboxInnerBounds) 132 } 133 } 134 135 private fun originalAppearanceWithScrim( 136 @Appearance originalAppearance: Int, 137 originalAppearanceRegions: Array<AppearanceRegion> 138 ): LetterboxAppearance { 139 return LetterboxAppearance( 140 originalAppearance or APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS, 141 originalAppearanceRegions) 142 } 143 144 @Appearance 145 private fun appearanceWithoutScrim(@Appearance originalAppearance: Int): Int = 146 originalAppearance and APPEARANCE_SEMI_TRANSPARENT_STATUS_BARS.inv() 147 148 private fun getAllOuterAppearanceRegions( 149 letterboxes: Array<LetterboxDetails> 150 ): List<AppearanceRegion> = letterboxes.map(this::getOuterAppearanceRegions).flatten() 151 152 private fun getOuterAppearanceRegions( 153 letterboxDetails: LetterboxDetails 154 ): List<AppearanceRegion> { 155 @Appearance val outerAppearance = getOuterAppearance() 156 return getVisibleOuterBounds(letterboxDetails).map { bounds -> 157 AppearanceRegion(outerAppearance, bounds) 158 } 159 } 160 161 private fun getVisibleOuterBounds(letterboxDetails: LetterboxDetails): List<Rect> { 162 val inner = letterboxDetails.letterboxInnerBounds 163 val outer = letterboxDetails.letterboxFullBounds 164 val top = Rect(outer.left, outer.top, outer.right, inner.top) 165 val left = Rect(outer.left, outer.top, inner.left, outer.bottom) 166 val right = Rect(inner.right, outer.top, outer.right, outer.bottom) 167 val bottom = Rect(outer.left, inner.bottom, outer.right, outer.bottom) 168 return listOf(left, top, right, bottom).filter { !it.isEmpty } 169 } 170 171 @Appearance 172 private fun getOuterAppearance(): Int { 173 val backgroundColor = outerLetterboxBackgroundColor() 174 val darkAppearanceContrast = 175 ContrastColorUtil.calculateContrast( 176 lightBarController.darkAppearanceIconColor, backgroundColor) 177 val lightAppearanceContrast = 178 ContrastColorUtil.calculateContrast( 179 lightBarController.lightAppearanceIconColor, backgroundColor) 180 return if (lightAppearanceContrast > darkAppearanceContrast) { 181 WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS 182 } else { 183 0 // APPEARANCE_DEFAULT 184 } 185 } 186 187 @ColorInt 188 private fun outerLetterboxBackgroundColor(): Int { 189 return letterboxBackgroundProvider.letterboxBackgroundColor 190 } 191 192 private fun isOuterLetterboxMultiColored(): Boolean { 193 return letterboxBackgroundProvider.isLetterboxBackgroundMultiColored 194 } 195 196 private fun getEndSideIconsBounds(): Rect { 197 return statusBarBoundsProvider?.visibleEndSideBounds ?: Rect() 198 } 199 200 private fun getStartSideIconBounds(): Rect { 201 return statusBarBoundsProvider?.visibleStartSideBounds ?: Rect() 202 } 203 204 override fun onStatusBarViewInitialized(component: StatusBarFragmentComponent) { 205 statusBarBoundsProvider = component.boundsProvider 206 } 207 208 private fun Rect.overlapsWith(other: Rect): Boolean { 209 if (this.contains(other) || other.contains(this)) { 210 return false 211 } 212 return this.intersects(other.left, other.top, other.right, other.bottom) 213 } 214 215 override fun dump(pw: PrintWriter, args: Array<out String>) { 216 pw.println( 217 """ 218 lastAppearance: ${lastAppearance?.toAppearanceString()} 219 lastAppearanceRegion: ${Arrays.toString(lastAppearanceRegions)}, 220 lastLetterboxes: ${Arrays.toString(lastLetterboxes)}, 221 lastLetterboxAppearance: $lastLetterboxAppearance 222 """.trimIndent()) 223 } 224 } 225 226 private fun Int.toAppearanceString(): String = 227 ViewDebug.flagsToString(InsetsFlags::class.java, "appearance", this) 228