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