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