1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.statusbar.phone 16 17 import android.content.Context 18 import android.content.pm.ActivityInfo 19 import android.content.res.Configuration 20 import android.graphics.Rect 21 import android.os.LocaleList 22 import android.view.View.LAYOUT_DIRECTION_RTL 23 import com.android.systemui.dagger.SysUISingleton 24 import com.android.systemui.statusbar.policy.ConfigurationController 25 import javax.inject.Inject 26 27 @SysUISingleton 28 class ConfigurationControllerImpl @Inject constructor(context: Context) : ConfigurationController { 29 30 private val listeners: MutableList<ConfigurationController.ConfigurationListener> = ArrayList() 31 private val lastConfig = Configuration() 32 private var density: Int = 0 33 private var smallestScreenWidth: Int = 0 34 private var maxBounds = Rect() 35 private var fontScale: Float = 0.toFloat() 36 private val inCarMode: Boolean 37 private var uiMode: Int = 0 38 private var localeList: LocaleList? = null 39 private val context: Context 40 private var layoutDirection: Int 41 private var orientation = Configuration.ORIENTATION_UNDEFINED 42 43 init { 44 val currentConfig = context.resources.configuration 45 this.context = context 46 fontScale = currentConfig.fontScale 47 density = currentConfig.densityDpi 48 smallestScreenWidth = currentConfig.smallestScreenWidthDp 49 maxBounds.set(currentConfig.windowConfiguration.maxBounds) 50 inCarMode = currentConfig.uiMode and Configuration.UI_MODE_TYPE_MASK == 51 Configuration.UI_MODE_TYPE_CAR 52 uiMode = currentConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK 53 localeList = currentConfig.locales 54 layoutDirection = currentConfig.layoutDirection 55 } 56 57 override fun notifyThemeChanged() { 58 val listeners = ArrayList(listeners) 59 60 listeners.filterForEach({ this.listeners.contains(it) }) { 61 it.onThemeChanged() 62 } 63 } 64 65 override fun onConfigurationChanged(newConfig: Configuration) { 66 // Avoid concurrent modification exception 67 val listeners = ArrayList(listeners) 68 69 listeners.filterForEach({ this.listeners.contains(it) }) { 70 it.onConfigChanged(newConfig) 71 } 72 val fontScale = newConfig.fontScale 73 val density = newConfig.densityDpi 74 val uiMode = newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK 75 val uiModeChanged = uiMode != this.uiMode 76 if (density != this.density || fontScale != this.fontScale || 77 inCarMode && uiModeChanged) { 78 listeners.filterForEach({ this.listeners.contains(it) }) { 79 it.onDensityOrFontScaleChanged() 80 } 81 this.density = density 82 this.fontScale = fontScale 83 } 84 85 val smallestScreenWidth = newConfig.smallestScreenWidthDp 86 if (smallestScreenWidth != this.smallestScreenWidth) { 87 this.smallestScreenWidth = smallestScreenWidth 88 listeners.filterForEach({ this.listeners.contains(it) }) { 89 it.onSmallestScreenWidthChanged() 90 } 91 } 92 93 val maxBounds = newConfig.windowConfiguration.maxBounds 94 if (maxBounds != this.maxBounds) { 95 // Update our internal rect to have the same bounds, instead of using 96 // `this.maxBounds = maxBounds` directly. Setting it directly means that `maxBounds` 97 // would be a direct reference to windowConfiguration.maxBounds, so the if statement 98 // above would always fail. See b/245799099 for more information. 99 this.maxBounds.set(maxBounds) 100 listeners.filterForEach({ this.listeners.contains(it) }) { 101 it.onMaxBoundsChanged() 102 } 103 } 104 105 val localeList = newConfig.locales 106 if (localeList != this.localeList) { 107 this.localeList = localeList 108 listeners.filterForEach({ this.listeners.contains(it) }) { 109 it.onLocaleListChanged() 110 } 111 } 112 113 if (uiModeChanged) { 114 // We need to force the style re-evaluation to make sure that it's up to date 115 // and attrs were reloaded. 116 context.theme.applyStyle(context.themeResId, true) 117 118 this.uiMode = uiMode 119 listeners.filterForEach({ this.listeners.contains(it) }) { 120 it.onUiModeChanged() 121 } 122 } 123 124 if (layoutDirection != newConfig.layoutDirection) { 125 layoutDirection = newConfig.layoutDirection 126 listeners.filterForEach({ this.listeners.contains(it) }) { 127 it.onLayoutDirectionChanged(layoutDirection == LAYOUT_DIRECTION_RTL) 128 } 129 } 130 131 if (lastConfig.updateFrom(newConfig) and ActivityInfo.CONFIG_ASSETS_PATHS != 0) { 132 listeners.filterForEach({ this.listeners.contains(it) }) { 133 it.onThemeChanged() 134 } 135 } 136 137 val newOrientation = newConfig.orientation 138 if (orientation != newOrientation) { 139 orientation = newOrientation 140 listeners.filterForEach({ this.listeners.contains(it) }) { 141 it.onOrientationChanged(orientation) 142 } 143 } 144 } 145 146 147 148 override fun addCallback(listener: ConfigurationController.ConfigurationListener) { 149 listeners.add(listener) 150 listener.onDensityOrFontScaleChanged() 151 } 152 153 override fun removeCallback(listener: ConfigurationController.ConfigurationListener) { 154 listeners.remove(listener) 155 } 156 157 override fun isLayoutRtl(): Boolean { 158 return layoutDirection == LAYOUT_DIRECTION_RTL 159 } 160 } 161 162 // This could be done with a Collection.filter and Collection.forEach, but Collection.filter 163 // creates a new array to store them in and we really don't need that here, so this provides 164 // a little more optimized inline version. 165 inline fun <T> Collection<T>.filterForEach(f: (T) -> Boolean, execute: (T) -> Unit) { 166 forEach { 167 if (f.invoke(it)) { 168 execute.invoke(it) 169 } 170 } 171 } 172