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.content.ContentResolver; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.res.Resources; 23 import android.database.ContentObserver; 24 import android.media.AudioManager; 25 import android.media.Ringtone; 26 import android.media.RingtoneManager; 27 import android.net.Uri; 28 import android.os.Binder; 29 import android.os.Handler; 30 import android.os.PowerManager; 31 import android.os.SystemClock; 32 import android.os.UEventObserver; 33 import android.os.UserHandle; 34 import android.provider.Settings; 35 import android.util.Pair; 36 import android.util.Slog; 37 38 import com.android.internal.R; 39 import com.android.internal.annotations.GuardedBy; 40 import com.android.internal.annotations.VisibleForTesting; 41 import com.android.internal.util.DumpUtils; 42 import com.android.internal.util.FrameworkStatsLog; 43 import com.android.server.ExtconUEventObserver.ExtconInfo; 44 45 import java.io.FileDescriptor; 46 import java.io.FileNotFoundException; 47 import java.io.FileReader; 48 import java.io.PrintWriter; 49 import java.util.ArrayList; 50 import java.util.HashMap; 51 import java.util.List; 52 import java.util.Map; 53 54 /** 55 * DockObserver monitors for a docking station. 56 */ 57 final class DockObserver extends SystemService { 58 private static final String TAG = "DockObserver"; 59 60 private final PowerManager mPowerManager; 61 private final PowerManager.WakeLock mWakeLock; 62 63 private final Object mLock = new Object(); 64 65 private boolean mSystemReady; 66 67 @GuardedBy("mLock") 68 private int mActualDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; 69 70 @GuardedBy("mLock") 71 private int mReportedDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; 72 73 @GuardedBy("mLock") 74 private int mPreviousDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED; 75 76 @GuardedBy("mLock") 77 private boolean mUpdatesStopped; 78 79 private final boolean mKeepDreamingWhenUnplugging; 80 private final boolean mAllowTheaterModeWakeFromDock; 81 82 private final List<ExtconStateConfig> mExtconStateConfigs; 83 private DeviceProvisionedObserver mDeviceProvisionedObserver; 84 85 private final DockObserverLocalService mDockObserverLocalService; 86 87 static final class ExtconStateProvider { 88 private final Map<String, String> mState; 89 ExtconStateProvider(Map<String, String> state)90 ExtconStateProvider(Map<String, String> state) { 91 mState = state; 92 } 93 getValue(String key)94 String getValue(String key) { 95 return mState.get(key); 96 } 97 98 fromString(String stateString)99 static ExtconStateProvider fromString(String stateString) { 100 Map<String, String> states = new HashMap<>(); 101 String[] lines = stateString.split("\n"); 102 for (String line : lines) { 103 String[] fields = line.split("="); 104 if (fields.length == 2) { 105 states.put(fields[0], fields[1]); 106 } else { 107 Slog.e(TAG, "Invalid line: " + line); 108 } 109 } 110 return new ExtconStateProvider(states); 111 } 112 fromFile(String stateFilePath)113 static ExtconStateProvider fromFile(String stateFilePath) { 114 char[] buffer = new char[1024]; 115 try (FileReader file = new FileReader(stateFilePath)) { 116 int len = file.read(buffer, 0, 1024); 117 String stateString = (new String(buffer, 0, len)).trim(); 118 return ExtconStateProvider.fromString(stateString); 119 } catch (FileNotFoundException e) { 120 Slog.w(TAG, "No state file found at: " + stateFilePath); 121 return new ExtconStateProvider(new HashMap<>()); 122 } catch (Exception e) { 123 Slog.e(TAG, "", e); 124 return new ExtconStateProvider(new HashMap<>()); 125 } 126 } 127 } 128 129 /** 130 * Represents a mapping from extcon state to EXTRA_DOCK_STATE value. Each 131 * instance corresponds to an entry in config_dockExtconStateMapping. 132 */ 133 private static final class ExtconStateConfig { 134 135 // The EXTRA_DOCK_STATE that will be used if the extcon key-value pairs match 136 public final int extraStateValue; 137 138 // A list of key-value pairs that must be present in the extcon state for a match 139 // to be considered. An empty list is considered a matching wildcard. 140 public final List<Pair<String, String>> keyValuePairs = new ArrayList<>(); 141 ExtconStateConfig(int extraStateValue)142 ExtconStateConfig(int extraStateValue) { 143 this.extraStateValue = extraStateValue; 144 } 145 } 146 loadExtconStateConfigs(Context context)147 private static List<ExtconStateConfig> loadExtconStateConfigs(Context context) { 148 String[] rows = context.getResources().getStringArray( 149 com.android.internal.R.array.config_dockExtconStateMapping); 150 try { 151 ArrayList<ExtconStateConfig> configs = new ArrayList<>(); 152 for (String row : rows) { 153 String[] rowFields = row.split(","); 154 ExtconStateConfig config = new ExtconStateConfig(Integer.parseInt(rowFields[0])); 155 for (int i = 1; i < rowFields.length; i++) { 156 String[] keyValueFields = rowFields[i].split("="); 157 if (keyValueFields.length != 2) { 158 throw new IllegalArgumentException("Invalid key-value: " + rowFields[i]); 159 } 160 config.keyValuePairs.add(Pair.create(keyValueFields[0], keyValueFields[1])); 161 } 162 configs.add(config); 163 } 164 return configs; 165 } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException e) { 166 Slog.e(TAG, "Could not parse extcon state config", e); 167 return new ArrayList<>(); 168 } 169 } 170 DockObserver(Context context)171 public DockObserver(Context context) { 172 super(context); 173 174 mPowerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE); 175 mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG); 176 mAllowTheaterModeWakeFromDock = context.getResources().getBoolean( 177 com.android.internal.R.bool.config_allowTheaterModeWakeFromDock); 178 mKeepDreamingWhenUnplugging = context.getResources().getBoolean( 179 com.android.internal.R.bool.config_keepDreamingWhenUnplugging); 180 mDeviceProvisionedObserver = new DeviceProvisionedObserver(mHandler); 181 182 mExtconStateConfigs = loadExtconStateConfigs(context); 183 184 List<ExtconInfo> infos = ExtconInfo.getExtconInfoForTypes(new String[] { 185 ExtconInfo.EXTCON_DOCK 186 }); 187 188 synchronized (mLock) { 189 if (!infos.isEmpty()) { 190 ExtconInfo info = infos.get(0); 191 Slog.i( 192 TAG, 193 "Found extcon info devPath: " 194 + info.getDevicePath() 195 + ", statePath: " 196 + info.getStatePath()); 197 198 // set initial status 199 setDockStateFromProviderLocked(ExtconStateProvider.fromFile(info.getStatePath())); 200 mPreviousDockState = mActualDockState; 201 202 mExtconUEventObserver.startObserving(info); 203 } else { 204 Slog.i(TAG, "No extcon dock device found in this kernel."); 205 } 206 } 207 208 mDockObserverLocalService = new DockObserverLocalService(); 209 LocalServices.addService(DockObserverInternal.class, mDockObserverLocalService); 210 } 211 212 public class DockObserverLocalService extends DockObserverInternal { 213 @Override getActualDockState()214 public int getActualDockState() { 215 return mActualDockState; 216 } 217 } 218 219 @Override onStart()220 public void onStart() { 221 publishBinderService(TAG, new BinderService()); 222 } 223 224 @Override onBootPhase(int phase)225 public void onBootPhase(int phase) { 226 if (phase == PHASE_ACTIVITY_MANAGER_READY) { 227 synchronized (mLock) { 228 mSystemReady = true; 229 mDeviceProvisionedObserver.onSystemReady(); 230 updateIfDockedLocked(); 231 } 232 } 233 } 234 235 @GuardedBy("mLock") updateIfDockedLocked()236 private void updateIfDockedLocked() { 237 // don't bother broadcasting undocked here 238 if (mReportedDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) { 239 postWakefulDockStateChange(); 240 } 241 } 242 243 @GuardedBy("mLock") setActualDockStateLocked(int newState)244 private void setActualDockStateLocked(int newState) { 245 mActualDockState = newState; 246 if (!mUpdatesStopped) { 247 setDockStateLocked(newState); 248 } 249 } 250 251 @GuardedBy("mLock") setDockStateLocked(int newState)252 private void setDockStateLocked(int newState) { 253 if (newState != mReportedDockState) { 254 mReportedDockState = newState; 255 // Here is the place mReportedDockState is updated. Logs dock state for 256 // mReportedDockState here so we can report the dock state. 257 FrameworkStatsLog.write(FrameworkStatsLog.DOCK_STATE_CHANGED, mReportedDockState); 258 if (mSystemReady) { 259 // Wake up immediately when docked or undocked unless prohibited from doing so. 260 if (allowWakeFromDock()) { 261 mPowerManager.wakeUp( 262 SystemClock.uptimeMillis(), 263 PowerManager.WAKE_REASON_DOCK, 264 "android.server:DOCK"); 265 } 266 postWakefulDockStateChange(); 267 } 268 } 269 } 270 allowWakeFromDock()271 private boolean allowWakeFromDock() { 272 if (mKeepDreamingWhenUnplugging) { 273 return false; 274 } 275 return (mAllowTheaterModeWakeFromDock 276 || Settings.Global.getInt(getContext().getContentResolver(), 277 Settings.Global.THEATER_MODE_ON, 0) == 0); 278 } 279 postWakefulDockStateChange()280 private void postWakefulDockStateChange() { 281 mHandler.post(mWakeLock.wrap(this::handleDockStateChange)); 282 } 283 handleDockStateChange()284 private void handleDockStateChange() { 285 synchronized (mLock) { 286 Slog.i(TAG, "Dock state changed from " + mPreviousDockState + " to " 287 + mReportedDockState); 288 final int previousDockState = mPreviousDockState; 289 mPreviousDockState = mReportedDockState; 290 291 final ContentResolver cr = getContext().getContentResolver(); 292 293 /// If the allow dock rotation before provision is enabled then we allow rotation. 294 final Resources r = getContext().getResources(); 295 final boolean allowDockBeforeProvision = 296 r.getBoolean(R.bool.config_allowDockBeforeProvision); 297 if (!allowDockBeforeProvision) { 298 // Skip the dock intent if not yet provisioned. 299 if (!mDeviceProvisionedObserver.isDeviceProvisioned()) { 300 Slog.i(TAG, "Device not provisioned, skipping dock broadcast"); 301 return; 302 } 303 } 304 305 // Pack up the values and broadcast them to everyone 306 Intent intent = new Intent(Intent.ACTION_DOCK_EVENT); 307 intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); 308 intent.putExtra(Intent.EXTRA_DOCK_STATE, mReportedDockState); 309 310 boolean dockSoundsEnabled = Settings.Global.getInt(cr, 311 Settings.Global.DOCK_SOUNDS_ENABLED, 1) == 1; 312 boolean dockSoundsEnabledWhenAccessibility = Settings.Global.getInt(cr, 313 Settings.Global.DOCK_SOUNDS_ENABLED_WHEN_ACCESSIBILITY, 1) == 1; 314 boolean accessibilityEnabled = Settings.Secure.getInt(cr, 315 Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1; 316 317 // Play a sound to provide feedback to confirm dock connection. 318 // Particularly useful for flaky contact pins... 319 if ((dockSoundsEnabled) || 320 (accessibilityEnabled && dockSoundsEnabledWhenAccessibility)) { 321 String whichSound = null; 322 if (mReportedDockState == Intent.EXTRA_DOCK_STATE_UNDOCKED) { 323 if ((previousDockState == Intent.EXTRA_DOCK_STATE_DESK) || 324 (previousDockState == Intent.EXTRA_DOCK_STATE_LE_DESK) || 325 (previousDockState == Intent.EXTRA_DOCK_STATE_HE_DESK)) { 326 whichSound = Settings.Global.DESK_UNDOCK_SOUND; 327 } else if (previousDockState == Intent.EXTRA_DOCK_STATE_CAR) { 328 whichSound = Settings.Global.CAR_UNDOCK_SOUND; 329 } 330 } else { 331 if ((mReportedDockState == Intent.EXTRA_DOCK_STATE_DESK) || 332 (mReportedDockState == Intent.EXTRA_DOCK_STATE_LE_DESK) || 333 (mReportedDockState == Intent.EXTRA_DOCK_STATE_HE_DESK)) { 334 whichSound = Settings.Global.DESK_DOCK_SOUND; 335 } else if (mReportedDockState == Intent.EXTRA_DOCK_STATE_CAR) { 336 whichSound = Settings.Global.CAR_DOCK_SOUND; 337 } 338 } 339 340 if (whichSound != null) { 341 final String soundPath = Settings.Global.getString(cr, whichSound); 342 if (soundPath != null) { 343 final Uri soundUri = Uri.parse("file://" + soundPath); 344 if (soundUri != null) { 345 final Ringtone sfx = RingtoneManager.getRingtone( 346 getContext(), soundUri); 347 if (sfx != null) { 348 sfx.setStreamType(AudioManager.STREAM_SYSTEM); 349 sfx.preferBuiltinDevice(true); 350 sfx.play(); 351 } 352 } 353 } 354 } 355 } 356 357 // Send the dock event intent. 358 // There are many components in the system watching for this so as to 359 // adjust audio routing, screen orientation, etc. 360 getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL); 361 } 362 } 363 364 private final Handler mHandler = new Handler(true /*async*/); 365 getDockedStateExtraValue(ExtconStateProvider state)366 private int getDockedStateExtraValue(ExtconStateProvider state) { 367 for (ExtconStateConfig config : mExtconStateConfigs) { 368 boolean match = true; 369 for (Pair<String, String> keyValue : config.keyValuePairs) { 370 String stateValue = state.getValue(keyValue.first); 371 match = match && keyValue.second.equals(stateValue); 372 if (!match) { 373 break; 374 } 375 } 376 377 if (match) { 378 return config.extraStateValue; 379 } 380 } 381 382 return Intent.EXTRA_DOCK_STATE_DESK; 383 } 384 385 @VisibleForTesting setDockStateFromProviderForTesting(ExtconStateProvider provider)386 void setDockStateFromProviderForTesting(ExtconStateProvider provider) { 387 synchronized (mLock) { 388 setDockStateFromProviderLocked(provider); 389 } 390 } 391 392 @GuardedBy("mLock") setDockStateFromProviderLocked(ExtconStateProvider provider)393 private void setDockStateFromProviderLocked(ExtconStateProvider provider) { 394 int state = Intent.EXTRA_DOCK_STATE_UNDOCKED; 395 if ("1".equals(provider.getValue("DOCK"))) { 396 state = getDockedStateExtraValue(provider); 397 } 398 setActualDockStateLocked(state); 399 } 400 401 private final ExtconUEventObserver mExtconUEventObserver = new ExtconUEventObserver() { 402 @Override 403 public void onUEvent(ExtconInfo extconInfo, UEventObserver.UEvent event) { 404 synchronized (mLock) { 405 String stateString = event.get("STATE"); 406 if (stateString != null) { 407 setDockStateFromProviderLocked(ExtconStateProvider.fromString(stateString)); 408 } else { 409 Slog.e(TAG, "Extcon event missing STATE: " + event); 410 } 411 } 412 } 413 }; 414 415 private final class BinderService extends Binder { 416 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)417 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 418 if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return; 419 final long ident = Binder.clearCallingIdentity(); 420 try { 421 synchronized (mLock) { 422 if (args == null || args.length == 0 || "-a".equals(args[0])) { 423 pw.println("Current Dock Observer Service state:"); 424 if (mUpdatesStopped) { 425 pw.println(" (UPDATES STOPPED -- use 'reset' to restart)"); 426 } 427 pw.println(" reported state: " + mReportedDockState); 428 pw.println(" previous state: " + mPreviousDockState); 429 pw.println(" actual state: " + mActualDockState); 430 } else if (args.length == 3 && "set".equals(args[0])) { 431 String key = args[1]; 432 String value = args[2]; 433 try { 434 if ("state".equals(key)) { 435 mUpdatesStopped = true; 436 setDockStateLocked(Integer.parseInt(value)); 437 } else { 438 pw.println("Unknown set option: " + key); 439 } 440 } catch (NumberFormatException ex) { 441 pw.println("Bad value: " + value); 442 } 443 } else if (args.length == 1 && "reset".equals(args[0])) { 444 mUpdatesStopped = false; 445 setDockStateLocked(mActualDockState); 446 } else { 447 pw.println("Dump current dock state, or:"); 448 pw.println(" set state <value>"); 449 pw.println(" reset"); 450 } 451 } 452 } finally { 453 Binder.restoreCallingIdentity(ident); 454 } 455 } 456 } 457 458 private final class DeviceProvisionedObserver extends ContentObserver { 459 private boolean mRegistered; 460 DeviceProvisionedObserver(Handler handler)461 public DeviceProvisionedObserver(Handler handler) { 462 super(handler); 463 } 464 465 @Override onChange(boolean selfChange, Uri uri)466 public void onChange(boolean selfChange, Uri uri) { 467 synchronized (mLock) { 468 updateRegistration(); 469 if (isDeviceProvisioned()) { 470 // Send the dock broadcast if device is docked after provisioning. 471 updateIfDockedLocked(); 472 } 473 } 474 } 475 onSystemReady()476 void onSystemReady() { 477 updateRegistration(); 478 } 479 updateRegistration()480 private void updateRegistration() { 481 boolean register = !isDeviceProvisioned(); 482 if (register == mRegistered) { 483 return; 484 } 485 final ContentResolver resolver = getContext().getContentResolver(); 486 if (register) { 487 resolver.registerContentObserver( 488 Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED), 489 false, this); 490 } else { 491 resolver.unregisterContentObserver(this); 492 } 493 mRegistered = register; 494 } 495 isDeviceProvisioned()496 boolean isDeviceProvisioned() { 497 return Settings.Global.getInt(getContext().getContentResolver(), 498 Settings.Global.DEVICE_PROVISIONED, 0) != 0; 499 } 500 } 501 } 502