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