1/*
2 * Copyright (C) 2008 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 com.android.server;
18
19import android.app.Activity;
20import android.app.ActivityManager;
21import android.app.ActivityManagerNative;
22import android.app.IUiModeManager;
23import android.app.Notification;
24import android.app.NotificationManager;
25import android.app.PendingIntent;
26import android.app.StatusBarManager;
27import android.app.UiModeManager;
28import android.content.BroadcastReceiver;
29import android.content.Context;
30import android.content.Intent;
31import android.content.IntentFilter;
32import android.content.pm.PackageManager;
33import android.content.res.Configuration;
34import android.os.BatteryManager;
35import android.os.Binder;
36import android.os.Handler;
37import android.os.PowerManager;
38import android.os.RemoteException;
39import android.os.ServiceManager;
40import android.os.UserHandle;
41import android.provider.Settings;
42import android.service.dreams.Sandman;
43import android.util.Slog;
44
45import java.io.FileDescriptor;
46import java.io.PrintWriter;
47
48import com.android.internal.R;
49import com.android.internal.app.DisableCarModeActivity;
50import com.android.server.TwilightService.TwilightState;
51
52final class UiModeManagerService extends IUiModeManager.Stub {
53    private static final String TAG = UiModeManager.class.getSimpleName();
54    private static final boolean LOG = false;
55
56    // Enable launching of applications when entering the dock.
57    private static final boolean ENABLE_LAUNCH_CAR_DOCK_APP = true;
58    private static final boolean ENABLE_LAUNCH_DESK_DOCK_APP = true;
59
60    private final Context mContext;
61    private final TwilightService mTwilightService;
62    private final Handler mHandler = new Handler();
63
64    final Object mLock = new Object();
65
66    private int mDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
67    private int mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
68
69    private int mNightMode = UiModeManager.MODE_NIGHT_NO;
70    private boolean mCarModeEnabled = false;
71    private boolean mCharging = false;
72    private final int mDefaultUiModeType;
73    private final boolean mCarModeKeepsScreenOn;
74    private final boolean mDeskModeKeepsScreenOn;
75    private final boolean mTelevision;
76
77    private boolean mComputedNightMode;
78    private int mCurUiMode = 0;
79    private int mSetUiMode = 0;
80
81    private boolean mHoldingConfiguration = false;
82    private Configuration mConfiguration = new Configuration();
83
84    private boolean mSystemReady;
85
86    private NotificationManager mNotificationManager;
87
88    private StatusBarManager mStatusBarManager;
89
90    private final PowerManager mPowerManager;
91    private final PowerManager.WakeLock mWakeLock;
92
93    static Intent buildHomeIntent(String category) {
94        Intent intent = new Intent(Intent.ACTION_MAIN);
95        intent.addCategory(category);
96        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
97                | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
98        return intent;
99    }
100
101    // The broadcast receiver which receives the result of the ordered broadcast sent when
102    // the dock state changes. The original ordered broadcast is sent with an initial result
103    // code of RESULT_OK. If any of the registered broadcast receivers changes this value, e.g.,
104    // to RESULT_CANCELED, then the intent to start a dock app will not be sent.
105    private final BroadcastReceiver mResultReceiver = new BroadcastReceiver() {
106        @Override
107        public void onReceive(Context context, Intent intent) {
108            if (getResultCode() != Activity.RESULT_OK) {
109                if (LOG) {
110                    Slog.v(TAG, "Handling broadcast result for action " + intent.getAction()
111                            + ": canceled: " + getResultCode());
112                }
113                return;
114            }
115
116            final int enableFlags = intent.getIntExtra("enableFlags", 0);
117            final int disableFlags = intent.getIntExtra("disableFlags", 0);
118            synchronized (mLock) {
119                updateAfterBroadcastLocked(intent.getAction(), enableFlags, disableFlags);
120            }
121        }
122    };
123
124    private final BroadcastReceiver mDockModeReceiver = new BroadcastReceiver() {
125        @Override
126        public void onReceive(Context context, Intent intent) {
127            int state = intent.getIntExtra(Intent.EXTRA_DOCK_STATE,
128                    Intent.EXTRA_DOCK_STATE_UNDOCKED);
129            updateDockState(state);
130        }
131    };
132
133    private final BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
134        @Override
135        public void onReceive(Context context, Intent intent) {
136            mCharging = (intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0);
137            synchronized (mLock) {
138                if (mSystemReady) {
139                    updateLocked(0, 0);
140                }
141            }
142        }
143    };
144
145    private final TwilightService.TwilightListener mTwilightListener =
146            new TwilightService.TwilightListener() {
147        @Override
148        public void onTwilightStateChanged() {
149            updateTwilight();
150        }
151    };
152
153    public UiModeManagerService(Context context, TwilightService twilight) {
154        mContext = context;
155        mTwilightService = twilight;
156
157        ServiceManager.addService(Context.UI_MODE_SERVICE, this);
158
159        mContext.registerReceiver(mDockModeReceiver,
160                new IntentFilter(Intent.ACTION_DOCK_EVENT));
161        mContext.registerReceiver(mBatteryReceiver,
162                new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
163
164        mPowerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
165        mWakeLock = mPowerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, TAG);
166
167        mConfiguration.setToDefaults();
168
169        mDefaultUiModeType = context.getResources().getInteger(
170                com.android.internal.R.integer.config_defaultUiModeType);
171        mCarModeKeepsScreenOn = (context.getResources().getInteger(
172                com.android.internal.R.integer.config_carDockKeepsScreenOn) == 1);
173        mDeskModeKeepsScreenOn = (context.getResources().getInteger(
174                com.android.internal.R.integer.config_deskDockKeepsScreenOn) == 1);
175        mTelevision = context.getPackageManager().hasSystemFeature(
176                PackageManager.FEATURE_TELEVISION);
177
178        mNightMode = Settings.Secure.getInt(mContext.getContentResolver(),
179                Settings.Secure.UI_NIGHT_MODE, UiModeManager.MODE_NIGHT_AUTO);
180
181        mTwilightService.registerListener(mTwilightListener, mHandler);
182    }
183
184    @Override // Binder call
185    public void disableCarMode(int flags) {
186        final long ident = Binder.clearCallingIdentity();
187        try {
188            synchronized (mLock) {
189                setCarModeLocked(false);
190                if (mSystemReady) {
191                    updateLocked(0, flags);
192                }
193            }
194        } finally {
195            Binder.restoreCallingIdentity(ident);
196        }
197    }
198
199    @Override // Binder call
200    public void enableCarMode(int flags) {
201        final long ident = Binder.clearCallingIdentity();
202        try {
203            synchronized (mLock) {
204                setCarModeLocked(true);
205                if (mSystemReady) {
206                    updateLocked(flags, 0);
207                }
208            }
209        } finally {
210            Binder.restoreCallingIdentity(ident);
211        }
212    }
213
214    @Override // Binder call
215    public int getCurrentModeType() {
216        final long ident = Binder.clearCallingIdentity();
217        try {
218            synchronized (mLock) {
219                return mCurUiMode & Configuration.UI_MODE_TYPE_MASK;
220            }
221        } finally {
222            Binder.restoreCallingIdentity(ident);
223        }
224    }
225
226    @Override // Binder call
227    public void setNightMode(int mode) {
228        switch (mode) {
229            case UiModeManager.MODE_NIGHT_NO:
230            case UiModeManager.MODE_NIGHT_YES:
231            case UiModeManager.MODE_NIGHT_AUTO:
232                break;
233            default:
234                throw new IllegalArgumentException("Unknown mode: " + mode);
235        }
236
237        final long ident = Binder.clearCallingIdentity();
238        try {
239            synchronized (mLock) {
240                if (isDoingNightModeLocked() && mNightMode != mode) {
241                    Settings.Secure.putInt(mContext.getContentResolver(),
242                            Settings.Secure.UI_NIGHT_MODE, mode);
243                    mNightMode = mode;
244                    updateLocked(0, 0);
245                }
246            }
247        } finally {
248            Binder.restoreCallingIdentity(ident);
249        }
250    }
251
252    @Override // Binder call
253    public int getNightMode() {
254        synchronized (mLock) {
255            return mNightMode;
256        }
257    }
258
259    void systemReady() {
260        synchronized (mLock) {
261            mSystemReady = true;
262            mCarModeEnabled = mDockState == Intent.EXTRA_DOCK_STATE_CAR;
263            updateComputedNightModeLocked();
264            updateLocked(0, 0);
265        }
266    }
267
268    private boolean isDoingNightModeLocked() {
269        return mCarModeEnabled || mDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED;
270    }
271
272    private void setCarModeLocked(boolean enabled) {
273        if (mCarModeEnabled != enabled) {
274            mCarModeEnabled = enabled;
275        }
276    }
277
278    private void updateDockState(int newState) {
279        synchronized (mLock) {
280            if (newState != mDockState) {
281                mDockState = newState;
282                setCarModeLocked(mDockState == Intent.EXTRA_DOCK_STATE_CAR);
283                if (mSystemReady) {
284                    updateLocked(UiModeManager.ENABLE_CAR_MODE_GO_CAR_HOME, 0);
285                }
286            }
287        }
288    }
289
290    private static boolean isDeskDockState(int state) {
291        switch (state) {
292            case Intent.EXTRA_DOCK_STATE_DESK:
293            case Intent.EXTRA_DOCK_STATE_LE_DESK:
294            case Intent.EXTRA_DOCK_STATE_HE_DESK:
295                return true;
296            default:
297                return false;
298        }
299    }
300
301    private void updateConfigurationLocked() {
302        int uiMode = mTelevision ? Configuration.UI_MODE_TYPE_TELEVISION : mDefaultUiModeType;
303        if (mCarModeEnabled) {
304            uiMode = Configuration.UI_MODE_TYPE_CAR;
305        } else if (isDeskDockState(mDockState)) {
306            uiMode = Configuration.UI_MODE_TYPE_DESK;
307        }
308        if (mCarModeEnabled) {
309            if (mNightMode == UiModeManager.MODE_NIGHT_AUTO) {
310                updateComputedNightModeLocked();
311                uiMode |= mComputedNightMode ? Configuration.UI_MODE_NIGHT_YES
312                        : Configuration.UI_MODE_NIGHT_NO;
313            } else {
314                uiMode |= mNightMode << 4;
315            }
316        } else {
317            // Disabling the car mode clears the night mode.
318            uiMode = (uiMode & ~Configuration.UI_MODE_NIGHT_MASK) | Configuration.UI_MODE_NIGHT_NO;
319        }
320
321        if (LOG) {
322            Slog.d(TAG,
323                "updateConfigurationLocked: mDockState=" + mDockState
324                + "; mCarMode=" + mCarModeEnabled
325                + "; mNightMode=" + mNightMode
326                + "; uiMode=" + uiMode);
327        }
328
329        mCurUiMode = uiMode;
330        if (!mHoldingConfiguration) {
331            mConfiguration.uiMode = uiMode;
332        }
333    }
334
335    private void sendConfigurationLocked() {
336        if (mSetUiMode != mConfiguration.uiMode) {
337            mSetUiMode = mConfiguration.uiMode;
338
339            try {
340                ActivityManagerNative.getDefault().updateConfiguration(mConfiguration);
341            } catch (RemoteException e) {
342                Slog.w(TAG, "Failure communicating with activity manager", e);
343            }
344        }
345    }
346
347    private void updateLocked(int enableFlags, int disableFlags) {
348        String action = null;
349        String oldAction = null;
350        if (mLastBroadcastState == Intent.EXTRA_DOCK_STATE_CAR) {
351            adjustStatusBarCarModeLocked();
352            oldAction = UiModeManager.ACTION_EXIT_CAR_MODE;
353        } else if (isDeskDockState(mLastBroadcastState)) {
354            oldAction = UiModeManager.ACTION_EXIT_DESK_MODE;
355        }
356
357        if (mCarModeEnabled) {
358            if (mLastBroadcastState != Intent.EXTRA_DOCK_STATE_CAR) {
359                adjustStatusBarCarModeLocked();
360
361                if (oldAction != null) {
362                    mContext.sendBroadcastAsUser(new Intent(oldAction), UserHandle.ALL);
363                }
364                mLastBroadcastState = Intent.EXTRA_DOCK_STATE_CAR;
365                action = UiModeManager.ACTION_ENTER_CAR_MODE;
366            }
367        } else if (isDeskDockState(mDockState)) {
368            if (!isDeskDockState(mLastBroadcastState)) {
369                if (oldAction != null) {
370                    mContext.sendBroadcastAsUser(new Intent(oldAction), UserHandle.ALL);
371                }
372                mLastBroadcastState = mDockState;
373                action = UiModeManager.ACTION_ENTER_DESK_MODE;
374            }
375        } else {
376            mLastBroadcastState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
377            action = oldAction;
378        }
379
380        if (action != null) {
381            if (LOG) {
382                Slog.v(TAG, String.format(
383                    "updateLocked: preparing broadcast: action=%s enable=0x%08x disable=0x%08x",
384                    action, enableFlags, disableFlags));
385            }
386
387            // Send the ordered broadcast; the result receiver will receive after all
388            // broadcasts have been sent. If any broadcast receiver changes the result
389            // code from the initial value of RESULT_OK, then the result receiver will
390            // not launch the corresponding dock application. This gives apps a chance
391            // to override the behavior and stay in their app even when the device is
392            // placed into a dock.
393            Intent intent = new Intent(action);
394            intent.putExtra("enableFlags", enableFlags);
395            intent.putExtra("disableFlags", disableFlags);
396            mContext.sendOrderedBroadcastAsUser(intent, UserHandle.CURRENT, null,
397                    mResultReceiver, null, Activity.RESULT_OK, null, null);
398
399            // Attempting to make this transition a little more clean, we are going
400            // to hold off on doing a configuration change until we have finished
401            // the broadcast and started the home activity.
402            mHoldingConfiguration = true;
403            updateConfigurationLocked();
404        } else {
405            String category = null;
406            if (mCarModeEnabled) {
407                if (ENABLE_LAUNCH_CAR_DOCK_APP
408                        && (enableFlags & UiModeManager.ENABLE_CAR_MODE_GO_CAR_HOME) != 0) {
409                    category = Intent.CATEGORY_CAR_DOCK;
410                }
411            } else if (isDeskDockState(mDockState)) {
412                if (ENABLE_LAUNCH_DESK_DOCK_APP
413                        && (enableFlags & UiModeManager.ENABLE_CAR_MODE_GO_CAR_HOME) != 0) {
414                    category = Intent.CATEGORY_DESK_DOCK;
415                }
416            } else {
417                if ((disableFlags & UiModeManager.DISABLE_CAR_MODE_GO_HOME) != 0) {
418                    category = Intent.CATEGORY_HOME;
419                }
420            }
421
422            if (LOG) {
423                Slog.v(TAG, "updateLocked: null action, mDockState="
424                        + mDockState +", category=" + category);
425            }
426
427            sendConfigurationAndStartDreamOrDockAppLocked(category);
428        }
429
430        // keep screen on when charging and in car mode
431        boolean keepScreenOn = mCharging &&
432                ((mCarModeEnabled && mCarModeKeepsScreenOn) ||
433                 (mCurUiMode == Configuration.UI_MODE_TYPE_DESK && mDeskModeKeepsScreenOn));
434        if (keepScreenOn != mWakeLock.isHeld()) {
435            if (keepScreenOn) {
436                mWakeLock.acquire();
437            } else {
438                mWakeLock.release();
439            }
440        }
441    }
442
443    private void updateAfterBroadcastLocked(String action, int enableFlags, int disableFlags) {
444        // Launch a dock activity
445        String category = null;
446        if (UiModeManager.ACTION_ENTER_CAR_MODE.equals(action)) {
447            // Only launch car home when car mode is enabled and the caller
448            // has asked us to switch to it.
449            if (ENABLE_LAUNCH_CAR_DOCK_APP
450                    && (enableFlags & UiModeManager.ENABLE_CAR_MODE_GO_CAR_HOME) != 0) {
451                category = Intent.CATEGORY_CAR_DOCK;
452            }
453        } else if (UiModeManager.ACTION_ENTER_DESK_MODE.equals(action)) {
454            // Only launch car home when desk mode is enabled and the caller
455            // has asked us to switch to it.  Currently re-using the car
456            // mode flag since we don't have a formal API for "desk mode".
457            if (ENABLE_LAUNCH_DESK_DOCK_APP
458                    && (enableFlags & UiModeManager.ENABLE_CAR_MODE_GO_CAR_HOME) != 0) {
459                category = Intent.CATEGORY_DESK_DOCK;
460            }
461        } else {
462            // Launch the standard home app if requested.
463            if ((disableFlags & UiModeManager.DISABLE_CAR_MODE_GO_HOME) != 0) {
464                category = Intent.CATEGORY_HOME;
465            }
466        }
467
468        if (LOG) {
469            Slog.v(TAG, String.format(
470                "Handling broadcast result for action %s: enable=0x%08x, disable=0x%08x, "
471                    + "category=%s",
472                action, enableFlags, disableFlags, category));
473        }
474
475        sendConfigurationAndStartDreamOrDockAppLocked(category);
476    }
477
478    private void sendConfigurationAndStartDreamOrDockAppLocked(String category) {
479        // Update the configuration but don't send it yet.
480        mHoldingConfiguration = false;
481        updateConfigurationLocked();
482
483        // Start the dock app, if there is one.
484        boolean dockAppStarted = false;
485        if (category != null) {
486            // Now we are going to be careful about switching the
487            // configuration and starting the activity -- we need to
488            // do this in a specific order under control of the
489            // activity manager, to do it cleanly.  So compute the
490            // new config, but don't set it yet, and let the
491            // activity manager take care of both the start and config
492            // change.
493            Intent homeIntent = buildHomeIntent(category);
494            if (Sandman.shouldStartDockApp(mContext, homeIntent)) {
495                try {
496                    int result = ActivityManagerNative.getDefault().startActivityWithConfig(
497                            null, homeIntent, null, null, null, 0, 0,
498                            mConfiguration, null, UserHandle.USER_CURRENT);
499                    if (result >= ActivityManager.START_SUCCESS) {
500                        dockAppStarted = true;
501                    } else if (result != ActivityManager.START_INTENT_NOT_RESOLVED) {
502                        Slog.e(TAG, "Could not start dock app: " + homeIntent
503                                + ", startActivityWithConfig result " + result);
504                    }
505                } catch (RemoteException ex) {
506                    Slog.e(TAG, "Could not start dock app: " + homeIntent, ex);
507                }
508            }
509        }
510
511        // Send the new configuration.
512        sendConfigurationLocked();
513
514        // If we did not start a dock app, then start dreaming if supported.
515        if (category != null && !dockAppStarted) {
516            Sandman.startDreamWhenDockedIfAppropriate(mContext);
517        }
518    }
519
520    private void adjustStatusBarCarModeLocked() {
521        if (mStatusBarManager == null) {
522            mStatusBarManager = (StatusBarManager)
523                    mContext.getSystemService(Context.STATUS_BAR_SERVICE);
524        }
525
526        // Fear not: StatusBarManagerService manages a list of requests to disable
527        // features of the status bar; these are ORed together to form the
528        // active disabled list. So if (for example) the device is locked and
529        // the status bar should be totally disabled, the calls below will
530        // have no effect until the device is unlocked.
531        if (mStatusBarManager != null) {
532            mStatusBarManager.disable(mCarModeEnabled
533                ? StatusBarManager.DISABLE_NOTIFICATION_TICKER
534                : StatusBarManager.DISABLE_NONE);
535        }
536
537        if (mNotificationManager == null) {
538            mNotificationManager = (NotificationManager)
539                    mContext.getSystemService(Context.NOTIFICATION_SERVICE);
540        }
541
542        if (mNotificationManager != null) {
543            if (mCarModeEnabled) {
544                Intent carModeOffIntent = new Intent(mContext, DisableCarModeActivity.class);
545
546                Notification n = new Notification();
547                n.icon = R.drawable.stat_notify_car_mode;
548                n.defaults = Notification.DEFAULT_LIGHTS;
549                n.flags = Notification.FLAG_ONGOING_EVENT;
550                n.when = 0;
551                n.setLatestEventInfo(
552                        mContext,
553                        mContext.getString(R.string.car_mode_disable_notification_title),
554                        mContext.getString(R.string.car_mode_disable_notification_message),
555                        PendingIntent.getActivityAsUser(mContext, 0, carModeOffIntent, 0,
556                                null, UserHandle.CURRENT));
557                mNotificationManager.notifyAsUser(null,
558                        R.string.car_mode_disable_notification_title, n, UserHandle.ALL);
559            } else {
560                mNotificationManager.cancelAsUser(null,
561                        R.string.car_mode_disable_notification_title, UserHandle.ALL);
562            }
563        }
564    }
565
566    private void updateTwilight() {
567        synchronized (mLock) {
568            if (isDoingNightModeLocked() && mNightMode == UiModeManager.MODE_NIGHT_AUTO) {
569                updateComputedNightModeLocked();
570                updateLocked(0, 0);
571            }
572        }
573    }
574
575    private void updateComputedNightModeLocked() {
576        TwilightState state = mTwilightService.getCurrentState();
577        if (state != null) {
578            mComputedNightMode = state.isNight();
579        }
580    }
581
582    @Override
583    protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
584        if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
585                != PackageManager.PERMISSION_GRANTED) {
586
587            pw.println("Permission Denial: can't dump uimode service from from pid="
588                    + Binder.getCallingPid()
589                    + ", uid=" + Binder.getCallingUid());
590            return;
591        }
592
593        synchronized (mLock) {
594            pw.println("Current UI Mode Service state:");
595            pw.print("  mDockState="); pw.print(mDockState);
596                    pw.print(" mLastBroadcastState="); pw.println(mLastBroadcastState);
597            pw.print("  mNightMode="); pw.print(mNightMode);
598                    pw.print(" mCarModeEnabled="); pw.print(mCarModeEnabled);
599                    pw.print(" mComputedNightMode="); pw.println(mComputedNightMode);
600            pw.print("  mCurUiMode=0x"); pw.print(Integer.toHexString(mCurUiMode));
601                    pw.print(" mSetUiMode=0x"); pw.println(Integer.toHexString(mSetUiMode));
602            pw.print("  mHoldingConfiguration="); pw.print(mHoldingConfiguration);
603                    pw.print(" mSystemReady="); pw.println(mSystemReady);
604            pw.print("  mTwilightService.getCurrentState()=");
605                    pw.println(mTwilightService.getCurrentState());
606        }
607    }
608}
609