1/* 2 * Copyright (C) 2015 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 17package android.support.v7.app; 18 19import android.app.Activity; 20import android.content.BroadcastReceiver; 21import android.content.ComponentName; 22import android.content.Context; 23import android.content.Intent; 24import android.content.IntentFilter; 25import android.content.pm.ActivityInfo; 26import android.content.pm.PackageManager; 27import android.content.res.Configuration; 28import android.content.res.Resources; 29import android.os.Bundle; 30import android.support.annotation.NonNull; 31import android.support.annotation.VisibleForTesting; 32import android.support.v7.view.SupportActionModeWrapper; 33import android.util.DisplayMetrics; 34import android.util.Log; 35import android.view.ActionMode; 36import android.view.Window; 37 38class AppCompatDelegateImplV14 extends AppCompatDelegateImplV11 { 39 40 private static final String KEY_LOCAL_NIGHT_MODE = "appcompat:local_night_mode"; 41 42 private static final boolean FLUSH_RESOURCE_CACHES_ON_NIGHT_CHANGE = true; 43 44 @NightMode 45 private int mLocalNightMode = MODE_NIGHT_UNSPECIFIED; 46 private boolean mApplyDayNightCalled; 47 48 private boolean mHandleNativeActionModes = true; // defaults to true 49 50 private AutoNightModeManager mAutoNightModeManager; 51 52 AppCompatDelegateImplV14(Context context, Window window, AppCompatCallback callback) { 53 super(context, window, callback); 54 } 55 56 @Override 57 public void onCreate(Bundle savedInstanceState) { 58 super.onCreate(savedInstanceState); 59 60 if (savedInstanceState != null && mLocalNightMode == MODE_NIGHT_UNSPECIFIED) { 61 // If we have a icicle and we haven't had a local night mode set yet, try and read 62 // it from the icicle 63 mLocalNightMode = savedInstanceState.getInt(KEY_LOCAL_NIGHT_MODE, 64 MODE_NIGHT_UNSPECIFIED); 65 } 66 } 67 68 @Override 69 Window.Callback wrapWindowCallback(Window.Callback callback) { 70 // Override the window callback so that we can intercept onWindowStartingActionMode() 71 // calls 72 return new AppCompatWindowCallbackV14(callback); 73 } 74 75 @Override 76 public void setHandleNativeActionModesEnabled(boolean enabled) { 77 mHandleNativeActionModes = enabled; 78 } 79 80 @Override 81 public boolean isHandleNativeActionModesEnabled() { 82 return mHandleNativeActionModes; 83 } 84 85 @Override 86 public boolean applyDayNight() { 87 boolean applied = false; 88 89 @NightMode final int nightMode = getNightMode(); 90 @ApplyableNightMode final int modeToApply = mapNightMode(nightMode); 91 if (modeToApply != MODE_NIGHT_FOLLOW_SYSTEM) { 92 applied = updateForNightMode(modeToApply); 93 } 94 95 if (nightMode == MODE_NIGHT_AUTO) { 96 // If we're already been started, we may need to setup auto mode again 97 ensureAutoNightModeManager(); 98 mAutoNightModeManager.setup(); 99 } 100 101 mApplyDayNightCalled = true; 102 return applied; 103 } 104 105 @Override 106 public void onStart() { 107 super.onStart(); 108 109 // This will apply day/night if the time has changed, it will also call through to 110 // setupAutoNightModeIfNeeded() 111 applyDayNight(); 112 } 113 114 @Override 115 public void onStop() { 116 super.onStop(); 117 118 // Make sure we clean up any receivers setup for AUTO mode 119 if (mAutoNightModeManager != null) { 120 mAutoNightModeManager.cleanup(); 121 } 122 } 123 124 @Override 125 public void setLocalNightMode(@NightMode final int mode) { 126 switch (mode) { 127 case MODE_NIGHT_AUTO: 128 case MODE_NIGHT_NO: 129 case MODE_NIGHT_YES: 130 case MODE_NIGHT_FOLLOW_SYSTEM: 131 if (mLocalNightMode != mode) { 132 mLocalNightMode = mode; 133 if (mApplyDayNightCalled) { 134 // If we've already applied day night, re-apply since we won't be 135 // called again 136 applyDayNight(); 137 } 138 } 139 break; 140 default: 141 Log.i(TAG, "setLocalNightMode() called with an unknown mode"); 142 break; 143 } 144 } 145 146 @ApplyableNightMode 147 int mapNightMode(@NightMode final int mode) { 148 switch (mode) { 149 case MODE_NIGHT_AUTO: 150 ensureAutoNightModeManager(); 151 return mAutoNightModeManager.getApplyableNightMode(); 152 case MODE_NIGHT_UNSPECIFIED: 153 // If we don't have a mode specified, just let the system handle it 154 return MODE_NIGHT_FOLLOW_SYSTEM; 155 default: 156 return mode; 157 } 158 } 159 160 @NightMode 161 private int getNightMode() { 162 return mLocalNightMode != MODE_NIGHT_UNSPECIFIED ? mLocalNightMode : getDefaultNightMode(); 163 } 164 165 @Override 166 public void onSaveInstanceState(Bundle outState) { 167 super.onSaveInstanceState(outState); 168 169 if (mLocalNightMode != MODE_NIGHT_UNSPECIFIED) { 170 // If we have a local night mode set, save it 171 outState.putInt(KEY_LOCAL_NIGHT_MODE, mLocalNightMode); 172 } 173 } 174 175 @Override 176 public void onDestroy() { 177 super.onDestroy(); 178 179 // Make sure we clean up any receivers setup for AUTO mode 180 if (mAutoNightModeManager != null) { 181 mAutoNightModeManager.cleanup(); 182 } 183 } 184 185 /** 186 * Updates the {@link Resources} configuration {@code uiMode} with the 187 * chosen {@code UI_MODE_NIGHT} value. 188 */ 189 private boolean updateForNightMode(@ApplyableNightMode final int mode) { 190 final Resources res = mContext.getResources(); 191 final Configuration conf = res.getConfiguration(); 192 final int currentNightMode = conf.uiMode & Configuration.UI_MODE_NIGHT_MASK; 193 194 final int newNightMode = (mode == MODE_NIGHT_YES) 195 ? Configuration.UI_MODE_NIGHT_YES 196 : Configuration.UI_MODE_NIGHT_NO; 197 198 if (currentNightMode != newNightMode) { 199 if (shouldRecreateOnNightModeChange()) { 200 if (DEBUG) { 201 Log.d(TAG, "applyNightMode() | Night mode changed, recreating Activity"); 202 } 203 // If we've already been created, we need to recreate the Activity for the 204 // mode to be applied 205 final Activity activity = (Activity) mContext; 206 activity.recreate(); 207 } else { 208 if (DEBUG) { 209 Log.d(TAG, "applyNightMode() | Night mode changed, updating configuration"); 210 } 211 final Configuration config = new Configuration(conf); 212 final DisplayMetrics metrics = res.getDisplayMetrics(); 213 final float originalFontScale = config.fontScale; 214 215 // Update the UI Mode to reflect the new night mode 216 config.uiMode = newNightMode | (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK); 217 if (FLUSH_RESOURCE_CACHES_ON_NIGHT_CHANGE) { 218 // Set a fake font scale value to flush any resource caches 219 config.fontScale = originalFontScale * 2; 220 } 221 // Now update the configuration 222 res.updateConfiguration(config, metrics); 223 224 if (FLUSH_RESOURCE_CACHES_ON_NIGHT_CHANGE) { 225 // If we're flushing the resources cache, revert back to the original 226 // font scale value 227 config.fontScale = originalFontScale; 228 res.updateConfiguration(config, metrics); 229 } 230 } 231 return true; 232 } else { 233 if (DEBUG) { 234 Log.d(TAG, "applyNightMode() | Night mode has not changed. Skipping"); 235 } 236 } 237 return false; 238 } 239 240 private void ensureAutoNightModeManager() { 241 if (mAutoNightModeManager == null) { 242 mAutoNightModeManager = new AutoNightModeManager(TwilightManager.getInstance(mContext)); 243 } 244 } 245 246 @VisibleForTesting 247 final AutoNightModeManager getAutoNightModeManager() { 248 ensureAutoNightModeManager(); 249 return mAutoNightModeManager; 250 } 251 252 private boolean shouldRecreateOnNightModeChange() { 253 if (mApplyDayNightCalled && mContext instanceof Activity) { 254 // If we've already applyDayNight() (via setTheme), we need to check if the 255 // Activity has configChanges set to handle uiMode changes 256 final PackageManager pm = mContext.getPackageManager(); 257 try { 258 final ActivityInfo info = pm.getActivityInfo( 259 new ComponentName(mContext, mContext.getClass()), 0); 260 // We should return true (to recreate) if configChanges does not want to 261 // handle uiMode 262 return (info.configChanges & ActivityInfo.CONFIG_UI_MODE) == 0; 263 } catch (PackageManager.NameNotFoundException e) { 264 // This shouldn't happen but let's not crash because of it, we'll just log and 265 // return true (since most apps will do that anyway) 266 Log.d(TAG, "Exception while getting ActivityInfo", e); 267 return true; 268 } 269 } 270 return false; 271 } 272 273 class AppCompatWindowCallbackV14 extends AppCompatWindowCallbackBase { 274 AppCompatWindowCallbackV14(Window.Callback callback) { 275 super(callback); 276 } 277 278 @Override 279 public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { 280 // We wrap in a support action mode on v14+ if enabled 281 if (isHandleNativeActionModesEnabled()) { 282 return startAsSupportActionMode(callback); 283 } 284 // Else, let the call fall through to the wrapped callback 285 return super.onWindowStartingActionMode(callback); 286 } 287 288 /** 289 * Wrap the framework {@link ActionMode.Callback} in a support action mode and 290 * let AppCompat display it. 291 */ 292 final ActionMode startAsSupportActionMode(ActionMode.Callback callback) { 293 // Wrap the callback as a v7 ActionMode.Callback 294 final SupportActionModeWrapper.CallbackWrapper callbackWrapper 295 = new SupportActionModeWrapper.CallbackWrapper(mContext, callback); 296 297 // Try and start a support action mode using the wrapped callback 298 final android.support.v7.view.ActionMode supportActionMode 299 = startSupportActionMode(callbackWrapper); 300 301 if (supportActionMode != null) { 302 // If we received a support action mode, wrap and return it 303 return callbackWrapper.getActionModeWrapper(supportActionMode); 304 } 305 return null; 306 } 307 } 308 309 @VisibleForTesting 310 final class AutoNightModeManager { 311 private TwilightManager mTwilightManager; 312 private boolean mIsNight; 313 314 private BroadcastReceiver mAutoTimeChangeReceiver; 315 private IntentFilter mAutoTimeChangeReceiverFilter; 316 317 AutoNightModeManager(@NonNull TwilightManager twilightManager) { 318 mTwilightManager = twilightManager; 319 mIsNight = twilightManager.isNight(); 320 } 321 322 @ApplyableNightMode 323 final int getApplyableNightMode() { 324 return mIsNight ? MODE_NIGHT_YES : MODE_NIGHT_NO; 325 } 326 327 final void dispatchTimeChanged() { 328 final boolean isNight = mTwilightManager.isNight(); 329 if (isNight != mIsNight) { 330 mIsNight = isNight; 331 applyDayNight(); 332 } 333 } 334 335 final void setup() { 336 cleanup(); 337 338 // If we're set to AUTO, we register a receiver to be notified on time changes. The 339 // system only send the tick out every minute, but that's enough fidelity for our use 340 // case 341 if (mAutoTimeChangeReceiver == null) { 342 mAutoTimeChangeReceiver = new BroadcastReceiver() { 343 @Override 344 public void onReceive(Context context, Intent intent) { 345 if (DEBUG) { 346 Log.d("AutoTimeChangeReceiver", "onReceive | Intent: " + intent); 347 } 348 dispatchTimeChanged(); 349 } 350 }; 351 } 352 if (mAutoTimeChangeReceiverFilter == null) { 353 mAutoTimeChangeReceiverFilter = new IntentFilter(); 354 mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIME_CHANGED); 355 mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); 356 mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIME_TICK); 357 } 358 mContext.registerReceiver(mAutoTimeChangeReceiver, mAutoTimeChangeReceiverFilter); 359 } 360 361 final void cleanup() { 362 if (mAutoTimeChangeReceiver != null) { 363 mContext.unregisterReceiver(mAutoTimeChangeReceiver); 364 mAutoTimeChangeReceiver = null; 365 } 366 } 367 } 368} 369