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