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