/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package android.support.v7.app; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.res.Configuration; import android.content.res.Resources; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import android.support.v7.view.SupportActionModeWrapper; import android.util.DisplayMetrics; import android.util.Log; import android.view.ActionMode; import android.view.Window; class AppCompatDelegateImplV14 extends AppCompatDelegateImplV11 { private static final String KEY_LOCAL_NIGHT_MODE = "appcompat:local_night_mode"; private static final boolean FLUSH_RESOURCE_CACHES_ON_NIGHT_CHANGE = true; @NightMode private int mLocalNightMode = MODE_NIGHT_UNSPECIFIED; private boolean mApplyDayNightCalled; private boolean mHandleNativeActionModes = true; // defaults to true private AutoNightModeManager mAutoNightModeManager; AppCompatDelegateImplV14(Context context, Window window, AppCompatCallback callback) { super(context, window, callback); } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null && mLocalNightMode == MODE_NIGHT_UNSPECIFIED) { // If we have a icicle and we haven't had a local night mode set yet, try and read // it from the icicle mLocalNightMode = savedInstanceState.getInt(KEY_LOCAL_NIGHT_MODE, MODE_NIGHT_UNSPECIFIED); } } @Override Window.Callback wrapWindowCallback(Window.Callback callback) { // Override the window callback so that we can intercept onWindowStartingActionMode() // calls return new AppCompatWindowCallbackV14(callback); } @Override public void setHandleNativeActionModesEnabled(boolean enabled) { mHandleNativeActionModes = enabled; } @Override public boolean isHandleNativeActionModesEnabled() { return mHandleNativeActionModes; } @Override public boolean applyDayNight() { boolean applied = false; @NightMode final int nightMode = getNightMode(); @ApplyableNightMode final int modeToApply = mapNightMode(nightMode); if (modeToApply != MODE_NIGHT_FOLLOW_SYSTEM) { applied = updateForNightMode(modeToApply); } if (nightMode == MODE_NIGHT_AUTO) { // If we're already been started, we may need to setup auto mode again ensureAutoNightModeManager(); mAutoNightModeManager.setup(); } mApplyDayNightCalled = true; return applied; } @Override public void onStart() { super.onStart(); // This will apply day/night if the time has changed, it will also call through to // setupAutoNightModeIfNeeded() applyDayNight(); } @Override public void onStop() { super.onStop(); // Make sure we clean up any receivers setup for AUTO mode if (mAutoNightModeManager != null) { mAutoNightModeManager.cleanup(); } } @Override public void setLocalNightMode(@NightMode final int mode) { switch (mode) { case MODE_NIGHT_AUTO: case MODE_NIGHT_NO: case MODE_NIGHT_YES: case MODE_NIGHT_FOLLOW_SYSTEM: if (mLocalNightMode != mode) { mLocalNightMode = mode; if (mApplyDayNightCalled) { // If we've already applied day night, re-apply since we won't be // called again applyDayNight(); } } break; default: Log.i(TAG, "setLocalNightMode() called with an unknown mode"); break; } } @ApplyableNightMode int mapNightMode(@NightMode final int mode) { switch (mode) { case MODE_NIGHT_AUTO: ensureAutoNightModeManager(); return mAutoNightModeManager.getApplyableNightMode(); case MODE_NIGHT_UNSPECIFIED: // If we don't have a mode specified, just let the system handle it return MODE_NIGHT_FOLLOW_SYSTEM; default: return mode; } } @NightMode private int getNightMode() { return mLocalNightMode != MODE_NIGHT_UNSPECIFIED ? mLocalNightMode : getDefaultNightMode(); } @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); if (mLocalNightMode != MODE_NIGHT_UNSPECIFIED) { // If we have a local night mode set, save it outState.putInt(KEY_LOCAL_NIGHT_MODE, mLocalNightMode); } } @Override public void onDestroy() { super.onDestroy(); // Make sure we clean up any receivers setup for AUTO mode if (mAutoNightModeManager != null) { mAutoNightModeManager.cleanup(); } } /** * Updates the {@link Resources} configuration {@code uiMode} with the * chosen {@code UI_MODE_NIGHT} value. */ private boolean updateForNightMode(@ApplyableNightMode final int mode) { final Resources res = mContext.getResources(); final Configuration conf = res.getConfiguration(); final int currentNightMode = conf.uiMode & Configuration.UI_MODE_NIGHT_MASK; final int newNightMode = (mode == MODE_NIGHT_YES) ? Configuration.UI_MODE_NIGHT_YES : Configuration.UI_MODE_NIGHT_NO; if (currentNightMode != newNightMode) { if (shouldRecreateOnNightModeChange()) { if (DEBUG) { Log.d(TAG, "applyNightMode() | Night mode changed, recreating Activity"); } // If we've already been created, we need to recreate the Activity for the // mode to be applied final Activity activity = (Activity) mContext; activity.recreate(); } else { if (DEBUG) { Log.d(TAG, "applyNightMode() | Night mode changed, updating configuration"); } final Configuration config = new Configuration(conf); final DisplayMetrics metrics = res.getDisplayMetrics(); final float originalFontScale = config.fontScale; // Update the UI Mode to reflect the new night mode config.uiMode = newNightMode | (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK); if (FLUSH_RESOURCE_CACHES_ON_NIGHT_CHANGE) { // Set a fake font scale value to flush any resource caches config.fontScale = originalFontScale * 2; } // Now update the configuration res.updateConfiguration(config, metrics); if (FLUSH_RESOURCE_CACHES_ON_NIGHT_CHANGE) { // If we're flushing the resources cache, revert back to the original // font scale value config.fontScale = originalFontScale; res.updateConfiguration(config, metrics); } } return true; } else { if (DEBUG) { Log.d(TAG, "applyNightMode() | Night mode has not changed. Skipping"); } } return false; } private void ensureAutoNightModeManager() { if (mAutoNightModeManager == null) { mAutoNightModeManager = new AutoNightModeManager(TwilightManager.getInstance(mContext)); } } @VisibleForTesting final AutoNightModeManager getAutoNightModeManager() { ensureAutoNightModeManager(); return mAutoNightModeManager; } private boolean shouldRecreateOnNightModeChange() { if (mApplyDayNightCalled && mContext instanceof Activity) { // If we've already applyDayNight() (via setTheme), we need to check if the // Activity has configChanges set to handle uiMode changes final PackageManager pm = mContext.getPackageManager(); try { final ActivityInfo info = pm.getActivityInfo( new ComponentName(mContext, mContext.getClass()), 0); // We should return true (to recreate) if configChanges does not want to // handle uiMode return (info.configChanges & ActivityInfo.CONFIG_UI_MODE) == 0; } catch (PackageManager.NameNotFoundException e) { // This shouldn't happen but let's not crash because of it, we'll just log and // return true (since most apps will do that anyway) Log.d(TAG, "Exception while getting ActivityInfo", e); return true; } } return false; } class AppCompatWindowCallbackV14 extends AppCompatWindowCallbackBase { AppCompatWindowCallbackV14(Window.Callback callback) { super(callback); } @Override public ActionMode onWindowStartingActionMode(ActionMode.Callback callback) { // We wrap in a support action mode on v14+ if enabled if (isHandleNativeActionModesEnabled()) { return startAsSupportActionMode(callback); } // Else, let the call fall through to the wrapped callback return super.onWindowStartingActionMode(callback); } /** * Wrap the framework {@link ActionMode.Callback} in a support action mode and * let AppCompat display it. */ final ActionMode startAsSupportActionMode(ActionMode.Callback callback) { // Wrap the callback as a v7 ActionMode.Callback final SupportActionModeWrapper.CallbackWrapper callbackWrapper = new SupportActionModeWrapper.CallbackWrapper(mContext, callback); // Try and start a support action mode using the wrapped callback final android.support.v7.view.ActionMode supportActionMode = startSupportActionMode(callbackWrapper); if (supportActionMode != null) { // If we received a support action mode, wrap and return it return callbackWrapper.getActionModeWrapper(supportActionMode); } return null; } } @VisibleForTesting final class AutoNightModeManager { private TwilightManager mTwilightManager; private boolean mIsNight; private BroadcastReceiver mAutoTimeChangeReceiver; private IntentFilter mAutoTimeChangeReceiverFilter; AutoNightModeManager(@NonNull TwilightManager twilightManager) { mTwilightManager = twilightManager; mIsNight = twilightManager.isNight(); } @ApplyableNightMode final int getApplyableNightMode() { return mIsNight ? MODE_NIGHT_YES : MODE_NIGHT_NO; } final void dispatchTimeChanged() { final boolean isNight = mTwilightManager.isNight(); if (isNight != mIsNight) { mIsNight = isNight; applyDayNight(); } } final void setup() { cleanup(); // If we're set to AUTO, we register a receiver to be notified on time changes. The // system only send the tick out every minute, but that's enough fidelity for our use // case if (mAutoTimeChangeReceiver == null) { mAutoTimeChangeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (DEBUG) { Log.d("AutoTimeChangeReceiver", "onReceive | Intent: " + intent); } dispatchTimeChanged(); } }; } if (mAutoTimeChangeReceiverFilter == null) { mAutoTimeChangeReceiverFilter = new IntentFilter(); mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIME_CHANGED); mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); mAutoTimeChangeReceiverFilter.addAction(Intent.ACTION_TIME_TICK); } mContext.registerReceiver(mAutoTimeChangeReceiver, mAutoTimeChangeReceiverFilter); } final void cleanup() { if (mAutoTimeChangeReceiver != null) { mContext.unregisterReceiver(mAutoTimeChangeReceiver); mAutoTimeChangeReceiver = null; } } } }