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