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