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