• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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