Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 2efe4cac authored by Daichi Hirono's avatar Daichi Hirono
Browse files

Add MtpDocumentsService.

BUG=20274999

Change-Id: I1d60ebcf100767136ac7f78e619021776c6be02f
parent 2e6eb521
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -2,6 +2,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.android.mtp"
          android:sharedUserId="android.media">
    <uses-feature android:name="android.hardware.usb.host" />
    <application android:label="@string/app_label">
        <provider
            android:name=".MtpDocumentsProvider"
@@ -14,5 +15,6 @@
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
        <service android:name=".MtpDocumentsService"></service>
    </application>
</manifest>
+28 −30
Original line number Diff line number Diff line
@@ -35,34 +35,42 @@ import java.io.IOException;
 * DocumentsProvider for MTP devices.
 */
public class MtpDocumentsProvider extends DocumentsProvider {
    private static final String TAG = "MtpDocumentsProvider";
    public static final String AUTHORITY = "com.android.mtp.documents";

    static final String AUTHORITY = "com.android.mtp.documents";
    static final String TAG = "MtpDocumentsProvider";
    private static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
            Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
            Root.COLUMN_AVAILABLE_BYTES,
    };

    private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
            Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
            Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
    };

    private MtpManager mDeviceModel;
    private static MtpDocumentsProvider sSingleton;

    private MtpManager mMtpManager;
    private ContentResolver mResolver;

    /**
     * Provides singleton instance to MtpDocumentsService.
     */
    static MtpDocumentsProvider getInstance() {
        return sSingleton;
    }

    @Override
    public boolean onCreate() {
        mDeviceModel = new MtpManager(getContext());
        sSingleton = this;
        mMtpManager = new MtpManager(getContext());
        mResolver = getContext().getContentResolver();
        return true;
    }

    @VisibleForTesting
    void onCreateForTesting(MtpManager deviceModel, ContentResolver resolver) {
        this.mDeviceModel = deviceModel;
    void onCreateForTesting(MtpManager mtpManager, ContentResolver resolver) {
        this.mMtpManager = mtpManager;
        this.mResolver = resolver;
    }

@@ -90,35 +98,21 @@ public class MtpDocumentsProvider extends DocumentsProvider {
        throw new FileNotFoundException();
    }

    // TODO: Remove annotation when the method starts to be used.
    @VisibleForTesting
    void openDevice(int deviceId) {
        try {
            mDeviceModel.openDevice(deviceId);
    void openDevice(int deviceId) throws IOException {
        mMtpManager.openDevice(deviceId);
        notifyRootsUpdate();
        } catch (IOException error) {
            Log.d(TAG, "Failed to open the MTP device: " + deviceId);
        }
    }

    // TODO: Remove annotation when the method starts to be used.
    @VisibleForTesting
    void closeDevice(int deviceId) {
        try {
            mDeviceModel.closeDevice(deviceId);
    void closeDevice(int deviceId) throws IOException {
        mMtpManager.closeDevice(deviceId);
        notifyRootsUpdate();
        } catch (IOException error) {
            Log.d(TAG, "Failed to close the MTP device: " + deviceId);
        }
    }

    // TODO: Remove annotation when the method starts to be used.
    @VisibleForTesting
    void closeAllDevices() {
        boolean closed = false;
        for (int deviceId : mDeviceModel.getOpenedDeviceIds()) {
        for (int deviceId : mMtpManager.getOpenedDeviceIds()) {
            try {
                mDeviceModel.closeDevice(deviceId);
                mMtpManager.closeDevice(deviceId);
                closed = true;
            } catch (IOException d) {
                Log.d(TAG, "Failed to close the MTP device: " + deviceId);
@@ -135,4 +129,8 @@ public class MtpDocumentsProvider extends DocumentsProvider {
                null,
                false);
    }

    boolean hasOpenedDevices() {
        return mMtpManager.getOpenedDeviceIds().length != 0;
    }
}
+111 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2015 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.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.IBinder;
import android.util.Log;

import java.io.IOException;

/**
 * Service to manage lifetime of DocumentsProvider's process.
 * The service prevents the system from killing the process that holds USB connections. The service
 * starts to run when the first MTP device is opened, and stops when the last MTP device is closed.
 */
public class MtpDocumentsService extends Service {
    static final String ACTION_OPEN_DEVICE = "com.android.mtp.action.ACTION_OPEN_DEVICE";
    static final String EXTRA_DEVICE = "device";

    Receiver mReceiver;

    @Override
    public IBinder onBind(Intent intent) {
        // The service is used via intents.
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        final IntentFilter filter = new IntentFilter(UsbManager.ACTION_USB_DEVICE_DETACHED);
        mReceiver = new Receiver();
        registerReceiver(mReceiver, filter);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent == null) {
            // If intent is null, the service was restarted.
            // TODO: Recover opened devices here.
            return START_STICKY;
        }
        if (intent.getAction().equals(ACTION_OPEN_DEVICE)) {
            final UsbDevice device = intent.<UsbDevice>getParcelableExtra(EXTRA_DEVICE);
            try {
                final MtpDocumentsProvider provider = MtpDocumentsProvider.getInstance();
                provider.openDevice(device.getDeviceId());
                return START_STICKY;
            } catch (IOException error) {
                Log.d(MtpDocumentsProvider.TAG, error.getMessage());
            }
        } else {
            Log.d(MtpDocumentsProvider.TAG, "Received unknown intent action.");
        }
        stopSelfIfNeeded();
        return Service.START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        final MtpDocumentsProvider provider = MtpDocumentsProvider.getInstance();
        provider.closeAllDevices();
        unregisterReceiver(mReceiver);
        mReceiver = null;
        super.onDestroy();
    }

    private void stopSelfIfNeeded() {
        final MtpDocumentsProvider provider = MtpDocumentsProvider.getInstance();
        if (!provider.hasOpenedDevices()) {
            stopSelf();
        }
    }

    private class Receiver extends BroadcastReceiver {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(intent.getAction())) {
                final UsbDevice device =
                        (UsbDevice) intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
                final MtpDocumentsProvider provider = MtpDocumentsProvider.getInstance();
                try {
                    provider.closeDevice(device.getDeviceId());
                } catch (IOException error) {
                    Log.d(MtpDocumentsProvider.TAG, error.getMessage());
                }
                stopSelfIfNeeded();
            }
        }
    }
}
+13 −3
Original line number Diff line number Diff line
@@ -37,13 +37,23 @@ public class MtpDocumentsProviderTest extends AndroidTestCase {
        provider.closeDevice(MtpManagerMock.SUCCESS_DEVICE_ID);
        assertEquals(2, resolver.changeCount);

        int exceptionCounter = 0;
        try {
            provider.openDevice(MtpManagerMock.FAILURE_DEVICE_ID);
        } catch (IOException error) {
            exceptionCounter++;
        }
        assertEquals(2, resolver.changeCount);
        try {
            provider.closeDevice(MtpManagerMock.FAILURE_DEVICE_ID);
        } catch (IOException error) {
            exceptionCounter++;
        }
        assertEquals(2, resolver.changeCount);
        assertEquals(2, exceptionCounter);
    }

    public void testCloseAllDevices() {
    public void testCloseAllDevices() throws IOException {
        final ContentResolver resolver = new ContentResolver();
        final MtpDocumentsProvider provider = new MtpDocumentsProvider();
        provider.onCreateForTesting(new MtpManagerMock(getContext()), resolver);