1/*
2 * Copyright (C) 2010 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.providers.media;
18
19import android.app.ActivityManager;
20import android.app.Service;
21import android.content.Intent;
22import android.hardware.usb.UsbManager;
23import android.mtp.MtpDatabase;
24import android.mtp.MtpServer;
25import android.mtp.MtpStorage;
26import android.os.Environment;
27import android.os.IBinder;
28import android.os.UserHandle;
29import android.os.storage.StorageEventListener;
30import android.os.storage.StorageManager;
31import android.os.storage.StorageVolume;
32import android.util.Log;
33
34import java.io.File;
35import java.util.HashMap;
36
37public class MtpService extends Service {
38    private static final String TAG = "MtpService";
39    private static final boolean LOGD = false;
40
41    // We restrict PTP to these subdirectories
42    private static final String[] PTP_DIRECTORIES = new String[] {
43        Environment.DIRECTORY_DCIM,
44        Environment.DIRECTORY_PICTURES,
45    };
46
47    private void addStorageDevicesLocked() {
48        if (mPtpMode) {
49            // In PTP mode we support only primary storage
50            final StorageVolume primary = StorageManager.getPrimaryVolume(mVolumes);
51            final String path = primary.getPath();
52            if (path != null) {
53                String state = mStorageManager.getVolumeState(path);
54                if (Environment.MEDIA_MOUNTED.equals(state)) {
55                    addStorageLocked(mVolumeMap.get(path));
56                }
57            }
58        } else {
59            for (StorageVolume volume : mVolumeMap.values()) {
60                addStorageLocked(volume);
61            }
62        }
63    }
64
65    private final StorageEventListener mStorageEventListener = new StorageEventListener() {
66        @Override
67        public void onStorageStateChanged(String path, String oldState, String newState) {
68            synchronized (mBinder) {
69                Log.d(TAG, "onStorageStateChanged " + path + " " + oldState + " -> " + newState);
70                if (Environment.MEDIA_MOUNTED.equals(newState)) {
71                    volumeMountedLocked(path);
72                } else if (Environment.MEDIA_MOUNTED.equals(oldState)) {
73                    StorageVolume volume = mVolumeMap.remove(path);
74                    if (volume != null) {
75                        removeStorageLocked(volume);
76                    }
77                }
78            }
79        }
80    };
81
82    private MtpDatabase mDatabase;
83    private MtpServer mServer;
84    private StorageManager mStorageManager;
85    /** Flag indicating if MTP is disabled due to keyguard */
86    private boolean mMtpDisabled;
87    private boolean mUnlocked;
88    private boolean mPtpMode;
89    private final HashMap<String, StorageVolume> mVolumeMap = new HashMap<String, StorageVolume>();
90    private final HashMap<String, MtpStorage> mStorageMap = new HashMap<String, MtpStorage>();
91    private StorageVolume[] mVolumes;
92
93    @Override
94    public void onCreate() {
95        mStorageManager = StorageManager.from(this);
96        synchronized (mBinder) {
97            updateDisabledStateLocked();
98            mStorageManager.registerListener(mStorageEventListener);
99            StorageVolume[] volumes = mStorageManager.getVolumeList();
100            mVolumes = volumes;
101            for (int i = 0; i < volumes.length; i++) {
102                String path = volumes[i].getPath();
103                String state = mStorageManager.getVolumeState(path);
104                if (Environment.MEDIA_MOUNTED.equals(state)) {
105                    volumeMountedLocked(path);
106                }
107            }
108        }
109    }
110
111    @Override
112    public int onStartCommand(Intent intent, int flags, int startId) {
113        mUnlocked = intent.getBooleanExtra(UsbManager.USB_DATA_UNLOCKED, false);
114        if (LOGD) { Log.d(TAG, "onStartCommand intent=" + intent + " mUnlocked=" + mUnlocked); }
115        synchronized (mBinder) {
116            updateDisabledStateLocked();
117            mPtpMode = (intent == null ? false
118                    : intent.getBooleanExtra(UsbManager.USB_FUNCTION_PTP, false));
119            String[] subdirs = null;
120            if (mPtpMode) {
121                int count = PTP_DIRECTORIES.length;
122                subdirs = new String[count];
123                for (int i = 0; i < count; i++) {
124                    File file =
125                            Environment.getExternalStoragePublicDirectory(PTP_DIRECTORIES[i]);
126                    // make sure this directory exists
127                    file.mkdirs();
128                    subdirs[i] = file.getPath();
129                }
130            }
131            final StorageVolume primary = StorageManager.getPrimaryVolume(mVolumes);
132            if (mDatabase != null) {
133                mDatabase.setServer(null);
134            }
135            manageServiceLocked(primary, subdirs);
136        }
137
138        return START_REDELIVER_INTENT;
139    }
140
141    private void updateDisabledStateLocked() {
142        final boolean isCurrentUser = UserHandle.myUserId() == ActivityManager.getCurrentUser();
143        mMtpDisabled = !mUnlocked || !isCurrentUser;
144        if (LOGD) {
145            Log.d(TAG, "updating state; isCurrentUser=" + isCurrentUser + ", mMtpLocked="
146                    + mMtpDisabled);
147        }
148    }
149
150    /**
151     * Manage {@link #mServer}, creating only when running as the current user.
152     */
153    private void manageServiceLocked(StorageVolume primary, String[] subdirs) {
154        final boolean isCurrentUser = UserHandle.myUserId() == ActivityManager.getCurrentUser();
155        if (mServer == null && isCurrentUser) {
156            Log.d(TAG, "starting MTP server in " + (mPtpMode ? "PTP mode" : "MTP mode"));
157            mDatabase = new MtpDatabase(this, MediaProvider.EXTERNAL_VOLUME,
158                    primary.getPath(), subdirs);
159            mServer = new MtpServer(mDatabase, mPtpMode);
160            mDatabase.setServer(mServer);
161            if (!mMtpDisabled) {
162                addStorageDevicesLocked();
163            }
164            mServer.start();
165        } else if (mServer != null && !isCurrentUser) {
166            Log.d(TAG, "no longer current user; shutting down MTP server");
167            // Internally, kernel will close our FD, and server thread will
168            // handle cleanup.
169            mServer = null;
170            mDatabase.setServer(null);
171        }
172    }
173
174    @Override
175    public void onDestroy() {
176        mStorageManager.unregisterListener(mStorageEventListener);
177        if (mDatabase != null) {
178            mDatabase.setServer(null);
179        }
180    }
181
182    private final IMtpService.Stub mBinder =
183            new IMtpService.Stub() {
184        public void sendObjectAdded(int objectHandle) {
185            synchronized (mBinder) {
186                if (mServer != null) {
187                    mServer.sendObjectAdded(objectHandle);
188                }
189            }
190        }
191
192        public void sendObjectRemoved(int objectHandle) {
193            synchronized (mBinder) {
194                if (mServer != null) {
195                    mServer.sendObjectRemoved(objectHandle);
196                }
197            }
198        }
199    };
200
201    @Override
202    public IBinder onBind(Intent intent) {
203        return mBinder;
204    }
205
206    private void volumeMountedLocked(String path) {
207        for (int i = 0; i < mVolumes.length; i++) {
208            StorageVolume volume = mVolumes[i];
209            if (volume.getPath().equals(path)) {
210                mVolumeMap.put(path, volume);
211                if (!mMtpDisabled) {
212                    // In PTP mode we support only primary storage
213                    if (volume.isPrimary() || !mPtpMode) {
214                        addStorageLocked(volume);
215                    }
216                }
217                break;
218            }
219        }
220    }
221
222    private void addStorageLocked(StorageVolume volume) {
223        MtpStorage storage = new MtpStorage(volume, getApplicationContext());
224        mStorageMap.put(storage.getPath(), storage);
225
226        if (storage.getStorageId() == StorageVolume.STORAGE_ID_INVALID) {
227            Log.w(TAG, "Ignoring volume with invalid MTP storage ID: " + storage);
228            return;
229        } else {
230            Log.d(TAG, "Adding MTP storage 0x" + Integer.toHexString(storage.getStorageId())
231                    + " at " + storage.getPath());
232        }
233
234        if (mDatabase != null) {
235            mDatabase.addStorage(storage);
236        }
237        if (mServer != null) {
238            mServer.addStorage(storage);
239        }
240    }
241
242    private void removeStorageLocked(StorageVolume volume) {
243        MtpStorage storage = mStorageMap.remove(volume.getPath());
244        if (storage == null) {
245            Log.e(TAG, "Missing MtpStorage for " + volume.getPath());
246            return;
247        }
248
249        Log.d(TAG, "Removing MTP storage " + Integer.toHexString(storage.getStorageId()) + " at "
250                + storage.getPath());
251        if (mDatabase != null) {
252            mDatabase.removeStorage(storage);
253        }
254        if (mServer != null) {
255            mServer.removeStorage(storage);
256        }
257    }
258}
259