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
17package com.android.server;
18
19import android.content.ContentResolver;
20import android.content.Context;
21import android.content.Intent;
22import android.content.pm.PackageManager;
23import android.media.AudioManager;
24import android.media.Ringtone;
25import android.media.RingtoneManager;
26import android.net.Uri;
27import android.os.Binder;
28import android.os.Handler;
29import android.os.Message;
30import android.os.PowerManager;
31import android.os.SystemClock;
32import android.os.UEventObserver;
33import android.os.UserHandle;
34import android.provider.Settings;
35import android.util.Log;
36import android.util.Slog;
37
38import com.android.internal.util.DumpUtils;
39
40import java.io.FileDescriptor;
41import java.io.FileNotFoundException;
42import java.io.FileReader;
43import java.io.PrintWriter;
44
45/**
46 * DockObserver monitors for a docking station.
47 */
48final class DockObserver extends SystemService {
49    private static final String TAG = "DockObserver";
50
51    private static final String DOCK_UEVENT_MATCH = "DEVPATH=/devices/virtual/switch/dock";
52    private static final String DOCK_STATE_PATH = "/sys/class/switch/dock/state";
53
54    private static final int MSG_DOCK_STATE_CHANGED = 0;
55
56    private final PowerManager mPowerManager;
57    private final PowerManager.WakeLock mWakeLock;
58
59    private final Object mLock = new Object();
60
61    private boolean mSystemReady;
62
63    private int mActualDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
64
65    private int mReportedDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
66    private int mPreviousDockState = Intent.EXTRA_DOCK_STATE_UNDOCKED;
67
68    private boolean mUpdatesStopped;
69
70    private final boolean mAllowTheaterModeWakeFromDock;
71
72    public DockObserver(Context context) {
73        super(context);
74
75        mPowerManager = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
76        mWakeLock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
77        mAllowTheaterModeWakeFromDock = context.getResources().getBoolean(
78                com.android.internal.R.bool.config_allowTheaterModeWakeFromDock);
79
80        init();  // set initial status
81
82        mObserver.startObserving(DOCK_UEVENT_MATCH);
83    }
84
85    @Override
86    public void onStart() {
87        publishBinderService(TAG, new BinderService());
88    }
89
90    @Override
91    public void onBootPhase(int phase) {
92        if (phase == PHASE_ACTIVITY_MANAGER_READY) {
93            synchronized (mLock) {
94                mSystemReady = true;
95
96                // don't bother broadcasting undocked here
97                if (mReportedDockState != Intent.EXTRA_DOCK_STATE_UNDOCKED) {
98                    updateLocked();
99                }
100            }
101        }
102    }
103
104    private void init() {
105        synchronized (mLock) {
106            try {
107                char[] buffer = new char[1024];
108                FileReader file = new FileReader(DOCK_STATE_PATH);
109                try {
110                    int len = file.read(buffer, 0, 1024);
111                    setActualDockStateLocked(Integer.parseInt((new String(buffer, 0, len)).trim()));
112                    mPreviousDockState = mActualDockState;
113                } finally {
114                    file.close();
115                }
116            } catch (FileNotFoundException e) {
117                Slog.w(TAG, "This kernel does not have dock station support");
118            } catch (Exception e) {
119                Slog.e(TAG, "" , e);
120            }
121        }
122    }
123
124    private void setActualDockStateLocked(int newState) {
125        mActualDockState = newState;
126        if (!mUpdatesStopped) {
127            setDockStateLocked(newState);
128        }
129    }
130
131    private void setDockStateLocked(int newState) {
132        if (newState != mReportedDockState) {
133            mReportedDockState = newState;
134            if (mSystemReady) {
135                // Wake up immediately when docked or undocked except in theater mode.
136                if (mAllowTheaterModeWakeFromDock
137                        || Settings.Global.getInt(getContext().getContentResolver(),
138                            Settings.Global.THEATER_MODE_ON, 0) == 0) {
139                    mPowerManager.wakeUp(SystemClock.uptimeMillis(),
140                            "android.server:DOCK");
141                }
142                updateLocked();
143            }
144        }
145    }
146
147    private void updateLocked() {
148        mWakeLock.acquire();
149        mHandler.sendEmptyMessage(MSG_DOCK_STATE_CHANGED);
150    }
151
152    private void handleDockStateChange() {
153        synchronized (mLock) {
154            Slog.i(TAG, "Dock state changed from " + mPreviousDockState + " to "
155                    + mReportedDockState);
156            final int previousDockState = mPreviousDockState;
157            mPreviousDockState = mReportedDockState;
158
159            // Skip the dock intent if not yet provisioned.
160            final ContentResolver cr = getContext().getContentResolver();
161            if (Settings.Global.getInt(cr,
162                    Settings.Global.DEVICE_PROVISIONED, 0) == 0) {
163                Slog.i(TAG, "Device not provisioned, skipping dock broadcast");
164                return;
165            }
166
167            // Pack up the values and broadcast them to everyone
168            Intent intent = new Intent(Intent.ACTION_DOCK_EVENT);
169            intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
170            intent.putExtra(Intent.EXTRA_DOCK_STATE, mReportedDockState);
171
172            boolean dockSoundsEnabled = Settings.Global.getInt(cr,
173                    Settings.Global.DOCK_SOUNDS_ENABLED, 1) == 1;
174            boolean dockSoundsEnabledWhenAccessibility = Settings.Global.getInt(cr,
175                    Settings.Global.DOCK_SOUNDS_ENABLED_WHEN_ACCESSIBILITY, 1) == 1;
176            boolean accessibilityEnabled = Settings.Secure.getInt(cr,
177                    Settings.Secure.ACCESSIBILITY_ENABLED, 0) == 1;
178
179            // Play a sound to provide feedback to confirm dock connection.
180            // Particularly useful for flaky contact pins...
181            if ((dockSoundsEnabled) ||
182                   (accessibilityEnabled && dockSoundsEnabledWhenAccessibility)) {
183                String whichSound = null;
184                if (mReportedDockState == Intent.EXTRA_DOCK_STATE_UNDOCKED) {
185                    if ((previousDockState == Intent.EXTRA_DOCK_STATE_DESK) ||
186                        (previousDockState == Intent.EXTRA_DOCK_STATE_LE_DESK) ||
187                        (previousDockState == Intent.EXTRA_DOCK_STATE_HE_DESK)) {
188                        whichSound = Settings.Global.DESK_UNDOCK_SOUND;
189                    } else if (previousDockState == Intent.EXTRA_DOCK_STATE_CAR) {
190                        whichSound = Settings.Global.CAR_UNDOCK_SOUND;
191                    }
192                } else {
193                    if ((mReportedDockState == Intent.EXTRA_DOCK_STATE_DESK) ||
194                        (mReportedDockState == Intent.EXTRA_DOCK_STATE_LE_DESK) ||
195                        (mReportedDockState == Intent.EXTRA_DOCK_STATE_HE_DESK)) {
196                        whichSound = Settings.Global.DESK_DOCK_SOUND;
197                    } else if (mReportedDockState == Intent.EXTRA_DOCK_STATE_CAR) {
198                        whichSound = Settings.Global.CAR_DOCK_SOUND;
199                    }
200                }
201
202                if (whichSound != null) {
203                    final String soundPath = Settings.Global.getString(cr, whichSound);
204                    if (soundPath != null) {
205                        final Uri soundUri = Uri.parse("file://" + soundPath);
206                        if (soundUri != null) {
207                            final Ringtone sfx = RingtoneManager.getRingtone(
208                                    getContext(), soundUri);
209                            if (sfx != null) {
210                                sfx.setStreamType(AudioManager.STREAM_SYSTEM);
211                                sfx.play();
212                            }
213                        }
214                    }
215                }
216            }
217
218            // Send the dock event intent.
219            // There are many components in the system watching for this so as to
220            // adjust audio routing, screen orientation, etc.
221            getContext().sendStickyBroadcastAsUser(intent, UserHandle.ALL);
222        }
223    }
224
225    private final Handler mHandler = new Handler(true /*async*/) {
226        @Override
227        public void handleMessage(Message msg) {
228            switch (msg.what) {
229                case MSG_DOCK_STATE_CHANGED:
230                    handleDockStateChange();
231                    mWakeLock.release();
232                    break;
233            }
234        }
235    };
236
237    private final UEventObserver mObserver = new UEventObserver() {
238        @Override
239        public void onUEvent(UEventObserver.UEvent event) {
240            if (Log.isLoggable(TAG, Log.VERBOSE)) {
241                Slog.v(TAG, "Dock UEVENT: " + event.toString());
242            }
243
244            try {
245                synchronized (mLock) {
246                    setActualDockStateLocked(Integer.parseInt(event.get("SWITCH_STATE")));
247                }
248            } catch (NumberFormatException e) {
249                Slog.e(TAG, "Could not parse switch state from event " + event);
250            }
251        }
252    };
253
254    private final class BinderService extends Binder {
255        @Override
256        protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
257            if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return;
258            final long ident = Binder.clearCallingIdentity();
259            try {
260                synchronized (mLock) {
261                    if (args == null || args.length == 0 || "-a".equals(args[0])) {
262                        pw.println("Current Dock Observer Service state:");
263                        if (mUpdatesStopped) {
264                            pw.println("  (UPDATES STOPPED -- use 'reset' to restart)");
265                        }
266                        pw.println("  reported state: " + mReportedDockState);
267                        pw.println("  previous state: " + mPreviousDockState);
268                        pw.println("  actual state: " + mActualDockState);
269                    } else if (args.length == 3 && "set".equals(args[0])) {
270                        String key = args[1];
271                        String value = args[2];
272                        try {
273                            if ("state".equals(key)) {
274                                mUpdatesStopped = true;
275                                setDockStateLocked(Integer.parseInt(value));
276                            } else {
277                                pw.println("Unknown set option: " + key);
278                            }
279                        } catch (NumberFormatException ex) {
280                            pw.println("Bad value: " + value);
281                        }
282                    } else if (args.length == 1 && "reset".equals(args[0])) {
283                        mUpdatesStopped = false;
284                        setDockStateLocked(mActualDockState);
285                    } else {
286                        pw.println("Dump current dock state, or:");
287                        pw.println("  set state <value>");
288                        pw.println("  reset");
289                    }
290                }
291            } finally {
292                Binder.restoreCallingIdentity(ident);
293            }
294        }
295    }
296}
297