/* * Copyright (C) 2010 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.mtp; import android.app.ActivityManager; import android.app.Service; import android.content.Context; import android.content.Intent; import android.hardware.usb.IUsbManager; import android.hardware.usb.UsbManager; import android.mtp.MtpDatabase; import android.mtp.MtpServer; import android.os.Binder; import android.os.Build; import android.os.Environment; import android.os.IBinder; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; import android.os.storage.StorageEventListener; import android.os.storage.StorageManager; import android.os.storage.StorageVolume; import android.util.Log; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; import com.android.internal.util.Preconditions; import java.io.File; import java.io.FileDescriptor; import java.util.HashMap; /** * The singleton service backing instances of MtpServer that are started for the foreground user. * The service has the responsibility of retrieving user storage information and managing server * lifetime. */ public class MtpService extends Service { private static final String TAG = "MtpService"; private static final boolean LOGD = false; // We restrict PTP to these subdirectories private static final String[] PTP_DIRECTORIES = new String[] { Environment.DIRECTORY_DCIM, Environment.DIRECTORY_PICTURES, }; private final StorageEventListener mStorageEventListener = new StorageEventListener() { @Override public void onStorageStateChanged(String path, String oldState, String newState) { synchronized (MtpService.this) { Log.d(TAG, "onStorageStateChanged " + path + " " + oldState + " -> " + newState); if (Environment.MEDIA_MOUNTED.equals(newState)) { // Updating mVolumes variable because new storage could be inserted mVolumes = StorageManager.getVolumeList(getUserId(), 0); for (int i = 0; i < mVolumes.length; i++) { StorageVolume volume = mVolumes[i]; if (volume.getPath().equals(path)) { mVolumeMap.put(path, volume); if (mUnlocked && (volume.isPrimary() || !mPtpMode)) { addStorage(volume); } break; } } } else if (Environment.MEDIA_MOUNTED.equals(oldState)) { if (mVolumeMap.containsKey(path)) { removeStorage(mVolumeMap.remove(path)); } } } } }; /** * Static state of MtpServer. MtpServer opens FD for MTP driver internally and we cannot open * multiple MtpServer at the same time. The static field used to handle the case where MtpServer * lives beyond the lifetime of MtpService. * * Lock MtpService.this before locking MtpService.class if needed. Otherwise it goes to * deadlock. */ @GuardedBy("MtpService.class") private static ServerHolder sServerHolder; private StorageManager mStorageManager; @GuardedBy("this") private boolean mUnlocked; @GuardedBy("this") private boolean mPtpMode; // A map of user volumes that are currently mounted. @GuardedBy("this") private HashMap mVolumeMap; // All user volumes in existence, in any state. @GuardedBy("this") private StorageVolume[] mVolumes; @Override public void onCreate() { mVolumes = StorageManager.getVolumeList(getUserId(), 0); mVolumeMap = new HashMap<>(); mStorageManager = this.getSystemService(StorageManager.class); mStorageManager.registerListener(mStorageEventListener); } @Override public void onDestroy() { mStorageManager.unregisterListener(mStorageEventListener); synchronized (MtpService.class) { if (sServerHolder != null) { sServerHolder.database.setServer(null); } } } @Override public synchronized int onStartCommand(Intent intent, int flags, int startId) { mUnlocked = intent.getBooleanExtra(UsbManager.USB_DATA_UNLOCKED, false); mPtpMode = intent.getBooleanExtra(UsbManager.USB_FUNCTION_PTP, false); for (StorageVolume v : mVolumes) { if (v.getState().equals(Environment.MEDIA_MOUNTED)) { mVolumeMap.put(v.getPath(), v); } } String[] subdirs = null; if (mPtpMode) { Environment.UserEnvironment env = new Environment.UserEnvironment(getUserId()); int count = PTP_DIRECTORIES.length; subdirs = new String[count]; for (int i = 0; i < count; i++) { File file = env.buildExternalStoragePublicDirs(PTP_DIRECTORIES[i])[0]; // make sure this directory exists file.mkdirs(); subdirs[i] = file.getName(); } } final StorageVolume primary = StorageManager.getPrimaryVolume(mVolumes); startServer(primary, subdirs); return START_REDELIVER_INTENT; } private synchronized void startServer(StorageVolume primary, String[] subdirs) { if (!(UserHandle.myUserId() == ActivityManager.getCurrentUser())) { return; } synchronized (MtpService.class) { if (sServerHolder != null) { if (LOGD) { Log.d(TAG, "Cannot launch second MTP server."); } // Previously executed MtpServer is still running. It will be terminated // because MTP device FD will become invalid soon. Also MtpService will get new // intent after that when UsbDeviceManager configures USB with new state. return; } Log.d(TAG, "starting MTP server in " + (mPtpMode ? "PTP mode" : "MTP mode") + " with storage " + primary.getPath() + (mUnlocked ? " unlocked" : "") + " as user " + UserHandle.myUserId()); final MtpDatabase database = new MtpDatabase(this, subdirs); IUsbManager usbMgr = IUsbManager.Stub.asInterface(ServiceManager.getService( Context.USB_SERVICE)); ParcelFileDescriptor controlFd = null; try { controlFd = usbMgr.getControlFd( mPtpMode ? UsbManager.FUNCTION_PTP : UsbManager.FUNCTION_MTP); } catch (RemoteException e) { Log.e(TAG, "Error communicating with UsbManager: " + e); } FileDescriptor fd = null; if (controlFd == null) { Log.i(TAG, "Couldn't get control FD!"); } else { fd = controlFd.getFileDescriptor(); } final MtpServer server = new MtpServer(database, fd, mPtpMode, new OnServerTerminated(), Build.MANUFACTURER, Build.MODEL, "1.0"); database.setServer(server); sServerHolder = new ServerHolder(server, database); // Add currently mounted and enabled storages to the server if (mUnlocked) { for (StorageVolume v : mVolumeMap.values()) { addStorage(v); } } server.start(); } } @Override public IBinder onBind(Intent intent) { return new Binder(); } private void addStorage(StorageVolume volume) { Log.v(TAG, "Adding MTP storage:" + volume.getPath()); synchronized (MtpService.class) { if (sServerHolder != null) { sServerHolder.database.addStorage(volume); } } } private void removeStorage(StorageVolume volume) { synchronized (MtpService.class) { if (sServerHolder != null) { sServerHolder.database.removeStorage(volume); } } } private static class ServerHolder { @NonNull final MtpServer server; @NonNull final MtpDatabase database; ServerHolder(@NonNull MtpServer server, @NonNull MtpDatabase database) { Preconditions.checkNotNull(server); Preconditions.checkNotNull(database); this.server = server; this.database = database; } void close() { this.database.setServer(null); } } private class OnServerTerminated implements Runnable { @Override public void run() { synchronized (MtpService.class) { if (sServerHolder == null) { Log.e(TAG, "sServerHolder is unexpectedly null."); return; } sServerHolder.close(); sServerHolder = null; } } } }