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