blob: 2a4022cfcb2e580ede629696134de38bbd5bf54d [file] [log] [blame]
/*
* 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.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.RequiresApi;
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;
@RequiresApi(14)
class AppCompatDelegateImplV14 extends AppCompatDelegateImplV11 {
private static final String KEY_LOCAL_NIGHT_MODE = "appcompat:local_night_mode";
@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();
// Update the UI Mode to reflect the new night mode
config.uiMode = newNightMode | (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);
res.updateConfiguration(config, metrics);
// We may need to flush the Resources' drawable cache due to framework bugs.
if (!(Build.VERSION.SDK_INT >= 26)) {
ResourcesFlusher.flush(res);
}
}
return true;
} else {
if (DEBUG) {
Log.d(TAG, "applyNightMode() | Skipping. Night mode has not changed: " + mode);
}
}
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() {
mIsNight = mTwilightManager.isNight();
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 sends 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;
}
}
}
}