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

Commit ad370615 authored by Daichi Hirono's avatar Daichi Hirono Committed by Android (Google) Code Review
Browse files

Merge "Add MtpDocumentsService."

parents 114ef308 2efe4cac
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);