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