MtpDocumentsProvider.java revision b36b15586a5d3d0de590773ce4392fa3a82af66a
1c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono/*
2c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono * Copyright (C) 2015 The Android Open Source Project
3c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono *
4c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono * Licensed under the Apache License, Version 2.0 (the "License");
5c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono * you may not use this file except in compliance with the License.
6c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono * You may obtain a copy of the License at
7c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono *
8c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono *      http://www.apache.org/licenses/LICENSE-2.0
9c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono *
10c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono * Unless required by applicable law or agreed to in writing, software
11c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono * distributed under the License is distributed on an "AS IS" BASIS,
12c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono * See the License for the specific language governing permissions and
14c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono * limitations under the License.
15c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono */
16c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono
17c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hironopackage com.android.mtp;
18c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono
19f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hironoimport static com.android.internal.util.Preconditions.checkArgument;
20f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono
21d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hironoimport android.content.ContentResolver;
223faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hironoimport android.content.res.AssetFileDescriptor;
2317c8d8bcdae4b31d295431fc089db81bf3721c54Daichi Hironoimport android.content.res.Resources;
24c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hironoimport android.database.Cursor;
253faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hironoimport android.graphics.Point;
269e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hironoimport android.media.MediaFile;
279e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hironoimport android.mtp.MtpConstants;
28bb430fa930fa0d0700e46e7b4881de2a252223ddTomasz Mikolajewskiimport android.mtp.MtpObjectInfo;
29c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hironoimport android.os.CancellationSignal;
30c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hironoimport android.os.ParcelFileDescriptor;
31f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hironoimport android.os.storage.StorageManager;
32c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hironoimport android.provider.DocumentsContract.Document;
33c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hironoimport android.provider.DocumentsContract.Root;
34bb430fa930fa0d0700e46e7b4881de2a252223ddTomasz Mikolajewskiimport android.provider.DocumentsContract;
35c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hironoimport android.provider.DocumentsProvider;
36d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hironoimport android.util.Log;
37d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono
38e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hironoimport com.android.internal.annotations.GuardedBy;
39d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hironoimport com.android.internal.annotations.VisibleForTesting;
40d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono
41c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hironoimport java.io.FileNotFoundException;
42d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hironoimport java.io.IOException;
43b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hironoimport java.util.Arrays;
444c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewskiimport java.util.HashMap;
454c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewskiimport java.util.Map;
46c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono
47d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono/**
48d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono * DocumentsProvider for MTP devices.
49d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono */
50c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hironopublic class MtpDocumentsProvider extends DocumentsProvider {
512efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono    static final String AUTHORITY = "com.android.mtp.documents";
522efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono    static final String TAG = "MtpDocumentsProvider";
536baa16e9109046661fef8dcc25b8754ac68bcdaeDaichi Hirono    static final String[] DEFAULT_ROOT_PROJECTION = new String[] {
54c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono            Root.COLUMN_ROOT_ID, Root.COLUMN_FLAGS, Root.COLUMN_ICON,
55c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono            Root.COLUMN_TITLE, Root.COLUMN_DOCUMENT_ID,
56c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono            Root.COLUMN_AVAILABLE_BYTES,
57c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    };
586baa16e9109046661fef8dcc25b8754ac68bcdaeDaichi Hirono    static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[] {
59c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono            Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE,
60c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono            Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED,
61c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono            Document.COLUMN_FLAGS, Document.COLUMN_SIZE,
62c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    };
63c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono
64e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono    private final Object mDeviceListLock = new Object();
65e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono
662efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono    private static MtpDocumentsProvider sSingleton;
672efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono
682efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono    private MtpManager mMtpManager;
69d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono    private ContentResolver mResolver;
70e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono    @GuardedBy("mDeviceListLock")
714c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski    private Map<Integer, DeviceToolkit> mDeviceToolkits;
728b9024f0c20b1b79df1f2d0bc2f1a82f726b1176Daichi Hirono    private RootScanner mRootScanner;
7317c8d8bcdae4b31d295431fc089db81bf3721c54Daichi Hirono    private Resources mResources;
74dc47344660035b27a564ab6d9c9a9b58ec340347Daichi Hirono    private MtpDatabase mDatabase;
75f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono    private AppFuse mAppFuse;
76d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono
772efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono    /**
782efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono     * Provides singleton instance to MtpDocumentsService.
792efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono     */
802efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono    static MtpDocumentsProvider getInstance() {
812efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono        return sSingleton;
822efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono    }
832efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono
84c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    @Override
85c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    public boolean onCreate() {
862efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono        sSingleton = this;
8717c8d8bcdae4b31d295431fc089db81bf3721c54Daichi Hirono        mResources = getContext().getResources();
882efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono        mMtpManager = new MtpManager(getContext());
89d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono        mResolver = getContext().getContentResolver();
904c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski        mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
9147eb192b2704e27272ca94a95680cac40b6bff3fDaichi Hirono        mDatabase = new MtpDatabase(getContext(), MtpDatabaseConstants.FLAG_DATABASE_IN_FILE);
92dc47344660035b27a564ab6d9c9a9b58ec340347Daichi Hirono        mRootScanner = new RootScanner(mResolver, mResources, mMtpManager, mDatabase);
93f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        mAppFuse = new AppFuse(TAG, new AppFuseCallback());
94f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        // TODO: Mount AppFuse on demands.
95e6054c0ff07005c73bd10a362f039933dc00f8bdDaichi Hirono        try {
96e6054c0ff07005c73bd10a362f039933dc00f8bdDaichi Hirono            mAppFuse.mount(getContext().getSystemService(StorageManager.class));
97e6054c0ff07005c73bd10a362f039933dc00f8bdDaichi Hirono        } catch (IOException e) {
98e6054c0ff07005c73bd10a362f039933dc00f8bdDaichi Hirono            Log.e(TAG, "Failed to start app fuse.", e);
99e6054c0ff07005c73bd10a362f039933dc00f8bdDaichi Hirono            return false;
100e6054c0ff07005c73bd10a362f039933dc00f8bdDaichi Hirono        }
101e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        resume();
102c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono        return true;
103c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    }
104c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono
105d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono    @VisibleForTesting
106b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono    boolean onCreateForTesting(
107dc47344660035b27a564ab6d9c9a9b58ec340347Daichi Hirono            Resources resources,
108dc47344660035b27a564ab6d9c9a9b58ec340347Daichi Hirono            MtpManager mtpManager,
109dc47344660035b27a564ab6d9c9a9b58ec340347Daichi Hirono            ContentResolver resolver,
110b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono            MtpDatabase database,
111b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono            StorageManager storageManager) {
11217c8d8bcdae4b31d295431fc089db81bf3721c54Daichi Hirono        mResources = resources;
1136baa16e9109046661fef8dcc25b8754ac68bcdaeDaichi Hirono        mMtpManager = mtpManager;
1146baa16e9109046661fef8dcc25b8754ac68bcdaeDaichi Hirono        mResolver = resolver;
1154c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski        mDeviceToolkits = new HashMap<Integer, DeviceToolkit>();
116dc47344660035b27a564ab6d9c9a9b58ec340347Daichi Hirono        mDatabase = database;
117dc47344660035b27a564ab6d9c9a9b58ec340347Daichi Hirono        mRootScanner = new RootScanner(mResolver, mResources, mMtpManager, mDatabase);
118b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono        mAppFuse = new AppFuse(TAG, new AppFuseCallback());
119b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono        // TODO: Mount AppFuse on demands.
120b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono        try {
121b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono            mAppFuse.mount(storageManager);
122b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono        } catch (IOException e) {
123b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono            Log.e(TAG, "Failed to start app fuse.", e);
124b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono            return false;
125b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono        }
126e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        resume();
127b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono        return true;
128d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono    }
129d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono
130c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    @Override
131c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    public Cursor queryRoots(String[] projection) throws FileNotFoundException {
13250d17aa871d9ca645a8e7af64df8866b85aee245Daichi Hirono        if (projection == null) {
13350d17aa871d9ca645a8e7af64df8866b85aee245Daichi Hirono            projection = MtpDocumentsProvider.DEFAULT_ROOT_PROJECTION;
13450d17aa871d9ca645a8e7af64df8866b85aee245Daichi Hirono        }
135dc47344660035b27a564ab6d9c9a9b58ec340347Daichi Hirono        final Cursor cursor = mDatabase.queryRoots(projection);
13650d17aa871d9ca645a8e7af64df8866b85aee245Daichi Hirono        cursor.setNotificationUri(
13750d17aa871d9ca645a8e7af64df8866b85aee245Daichi Hirono                mResolver, DocumentsContract.buildRootsUri(MtpDocumentsProvider.AUTHORITY));
13850d17aa871d9ca645a8e7af64df8866b85aee245Daichi Hirono        return cursor;
139c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    }
140c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono
141c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    @Override
142c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    public Cursor queryDocument(String documentId, String[] projection)
143c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono            throws FileNotFoundException {
144e5323b7493f2bc1537d7e6b2d4595d69fd01d72eDaichi Hirono        if (projection == null) {
145e5323b7493f2bc1537d7e6b2d4595d69fd01d72eDaichi Hirono            projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
146e5323b7493f2bc1537d7e6b2d4595d69fd01d72eDaichi Hirono        }
1479e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono        return mDatabase.queryDocument(documentId, projection);
148c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    }
149c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono
150c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    @Override
151124d060bc980c7555616ff9d07a4dc3b8f3cd341Daichi Hirono    public Cursor queryChildDocuments(String parentDocumentId,
152124d060bc980c7555616ff9d07a4dc3b8f3cd341Daichi Hirono            String[] projection, String sortOrder) throws FileNotFoundException {
153124d060bc980c7555616ff9d07a4dc3b8f3cd341Daichi Hirono        if (projection == null) {
154124d060bc980c7555616ff9d07a4dc3b8f3cd341Daichi Hirono            projection = MtpDocumentsProvider.DEFAULT_DOCUMENT_PROJECTION;
155124d060bc980c7555616ff9d07a4dc3b8f3cd341Daichi Hirono        }
15647eb192b2704e27272ca94a95680cac40b6bff3fDaichi Hirono        final Identifier parentIdentifier = mDatabase.createIdentifier(parentDocumentId);
157124d060bc980c7555616ff9d07a4dc3b8f3cd341Daichi Hirono        try {
1584c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski            return getDocumentLoader(parentIdentifier).queryChildDocuments(
1594c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski                    projection, parentIdentifier);
160124d060bc980c7555616ff9d07a4dc3b8f3cd341Daichi Hirono        } catch (IOException exception) {
161124d060bc980c7555616ff9d07a4dc3b8f3cd341Daichi Hirono            throw new FileNotFoundException(exception.getMessage());
162124d060bc980c7555616ff9d07a4dc3b8f3cd341Daichi Hirono        }
163c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    }
164c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono
165c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono    @Override
1668ba419119d50a031160cab54bef6899bd0051ea9Daichi Hirono    public ParcelFileDescriptor openDocument(
1678ba419119d50a031160cab54bef6899bd0051ea9Daichi Hirono            String documentId, String mode, CancellationSignal signal)
1688ba419119d50a031160cab54bef6899bd0051ea9Daichi Hirono                    throws FileNotFoundException {
1699e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono        final Identifier identifier = mDatabase.createIdentifier(documentId);
1708ba419119d50a031160cab54bef6899bd0051ea9Daichi Hirono        try {
171b80a3cfd05fc7492dd59b7f8d4337eb5e29088c2Tomasz Mikolajewski            switch (mode) {
172b80a3cfd05fc7492dd59b7f8d4337eb5e29088c2Tomasz Mikolajewski                case "r":
173f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                    final long fileSize = getFileSize(documentId);
174f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                    // MTP getPartialObject operation does not support files that are larger than 4GB.
175f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                    // Fallback to non-seekable file descriptor.
176f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                    // TODO: Use getPartialObject64 for MTP devices that support Android vendor
177f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                    // extension.
178b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono                    if (fileSize <= 0xffffffffl) {
179f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                        return mAppFuse.openFile(Integer.parseInt(documentId));
180f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                    } else {
181f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                        return getPipeManager(identifier).readDocument(mMtpManager, identifier);
182f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                    }
183b80a3cfd05fc7492dd59b7f8d4337eb5e29088c2Tomasz Mikolajewski                case "w":
18481d74743107b372424fb8f7439357bdd608f8cafTomasz Mikolajewski                    // TODO: Clear the parent document loader task (if exists) and call notify
18581d74743107b372424fb8f7439357bdd608f8cafTomasz Mikolajewski                    // when writing is completed.
1864c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski                    return getPipeManager(identifier).writeDocument(
1874c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski                            getContext(), mMtpManager, identifier);
188f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                case "rw":
189f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                    // TODO: Add support for "rw" mode.
190b80a3cfd05fc7492dd59b7f8d4337eb5e29088c2Tomasz Mikolajewski                    throw new UnsupportedOperationException(
191f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                            "The provider does not support 'rw' mode.");
192f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                default:
193f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                    throw new IllegalArgumentException("Unknown mode for openDocument: " + mode);
194b80a3cfd05fc7492dd59b7f8d4337eb5e29088c2Tomasz Mikolajewski            }
1958ba419119d50a031160cab54bef6899bd0051ea9Daichi Hirono        } catch (IOException error) {
1968ba419119d50a031160cab54bef6899bd0051ea9Daichi Hirono            throw new FileNotFoundException(error.getMessage());
1978ba419119d50a031160cab54bef6899bd0051ea9Daichi Hirono        }
198d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono    }
199d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono
2003faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono    @Override
2013faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono    public AssetFileDescriptor openDocumentThumbnail(
2023faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono            String documentId,
2033faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono            Point sizeHint,
2043faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono            CancellationSignal signal) throws FileNotFoundException {
2059e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono        final Identifier identifier = mDatabase.createIdentifier(documentId);
2063faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono        try {
2073faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono            return new AssetFileDescriptor(
2084c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski                    getPipeManager(identifier).readThumbnail(mMtpManager, identifier),
209573c1fbc5f98f033681e378ec965136bce49c899Daichi Hirono                    0,  // Start offset.
2103faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono                    AssetFileDescriptor.UNKNOWN_LENGTH);
2113faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono        } catch (IOException error) {
2123faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono            throw new FileNotFoundException(error.getMessage());
2133faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono        }
2143faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono    }
2153faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono
2163faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono    @Override
2173faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono    public void deleteDocument(String documentId) throws FileNotFoundException {
2183faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono        try {
2199e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono            final Identifier identifier = mDatabase.createIdentifier(documentId);
2209e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono            final Identifier parentIdentifier =
2219e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono                    mDatabase.createIdentifier(mDatabase.getParentId(documentId));
2223faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono            mMtpManager.deleteDocument(identifier.mDeviceId, identifier.mObjectHandle);
2239e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono            mDatabase.deleteDocument(documentId);
2244c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski            getDocumentLoader(parentIdentifier).clearTask(parentIdentifier);
2259e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono            notifyChildDocumentsChange(parentIdentifier.mDocumentId);
2263faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono        } catch (IOException error) {
2273faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono            throw new FileNotFoundException(error.getMessage());
2283faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono        }
2293faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono    }
2303faa43a4a6f270e2e1e2ec55b77508084af16757Daichi Hirono
2316baa16e9109046661fef8dcc25b8754ac68bcdaeDaichi Hirono    @Override
2326baa16e9109046661fef8dcc25b8754ac68bcdaeDaichi Hirono    public void onTrimMemory(int level) {
233e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        synchronized (mDeviceListLock) {
234e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono            for (final DeviceToolkit toolkit : mDeviceToolkits.values()) {
235e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono                toolkit.mDocumentLoader.clearCompletedTasks();
236e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono            }
237e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono        }
2386baa16e9109046661fef8dcc25b8754ac68bcdaeDaichi Hirono    }
2396baa16e9109046661fef8dcc25b8754ac68bcdaeDaichi Hirono
24087763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski    @Override
24187763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski    public String createDocument(String parentDocumentId, String mimeType, String displayName)
24287763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski            throws FileNotFoundException {
24387763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski        try {
2449e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono            final Identifier parentId = mDatabase.createIdentifier(parentDocumentId);
245df544176b10f536969de1ed143b0ba57123fcb93Tomasz Mikolajewski            final ParcelFileDescriptor pipe[] = ParcelFileDescriptor.createReliablePipe();
246df544176b10f536969de1ed143b0ba57123fcb93Tomasz Mikolajewski            pipe[0].close();  // 0 bytes for a new document.
2479e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono            final int formatCode = Document.MIME_TYPE_DIR.equals(mimeType) ?
2489e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono                    MtpConstants.FORMAT_ASSOCIATION :
2499e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono                    MediaFile.getFormatCode(displayName, mimeType);
2509e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono            final MtpObjectInfo info = new MtpObjectInfo.Builder()
2519e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono                    .setStorageId(parentId.mStorageId)
2529e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono                    .setParent(parentId.mObjectHandle)
2539e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono                    .setFormat(formatCode)
2549e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono                    .setName(displayName)
2559e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono                    .build();
2569e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono            final int objectHandle = mMtpManager.createDocument(parentId.mDeviceId, info, pipe[1]);
2579e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono            final MtpObjectInfo infoWithHandle =
2589e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono                    new MtpObjectInfo.Builder(info).setObjectHandle(objectHandle).build();
2599e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono            final String documentId = mDatabase.putNewDocument(
2609e8a4fa78f5b9e3964dca84ad4047210d35c4013Daichi Hirono                    parentId.mDeviceId, parentDocumentId, infoWithHandle);
2614c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski            getDocumentLoader(parentId).clearTask(parentId);
26287763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski            notifyChildDocumentsChange(parentDocumentId);
26387763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski            return documentId;
26487763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski        } catch (IOException error) {
26587763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski            Log.e(TAG, error.getMessage());
26687763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski            throw new FileNotFoundException(error.getMessage());
26787763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski        }
26887763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski    }
26987763e6a91a54e7995cfda9b7e80162f02ac4cbcTomasz Mikolajewski
2702efe4cac49efd87c5f14f48051a5556f437cedc9Daichi Hirono    void openDevice(int deviceId) throws IOException {
271e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        synchronized (mDeviceListLock) {
272e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono            mMtpManager.openDevice(deviceId);
273e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono            mDeviceToolkits.put(
274e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono                    deviceId, new DeviceToolkit(mMtpManager, mResolver, mDatabase));
275e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono        }
276e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono        mRootScanner.resume();
277d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono    }
278d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono
279e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono    void closeDevice(int deviceId) throws IOException, InterruptedException {
280e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        synchronized (mDeviceListLock) {
281e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono            closeDeviceInternal(deviceId);
282e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono        }
28320754c5a112e418c11cc88176283db2c4bf2efd6Daichi Hirono        mRootScanner.resume();
284d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono    }
285d5152429e11d3a831a7993dc67ddf69f69bd3912Daichi Hirono
286a57d9ed09003acd8b2beb0494a2bd32f7030cc11Daichi Hirono    int[] getOpenedDeviceIds() {
287e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        synchronized (mDeviceListLock) {
288a57d9ed09003acd8b2beb0494a2bd32f7030cc11Daichi Hirono            return mMtpManager.getOpenedDeviceIds();
289a57d9ed09003acd8b2beb0494a2bd32f7030cc11Daichi Hirono        }
290a57d9ed09003acd8b2beb0494a2bd32f7030cc11Daichi Hirono    }
291a57d9ed09003acd8b2beb0494a2bd32f7030cc11Daichi Hirono
292a57d9ed09003acd8b2beb0494a2bd32f7030cc11Daichi Hirono    String getDeviceName(int deviceId) throws IOException {
293a57d9ed09003acd8b2beb0494a2bd32f7030cc11Daichi Hirono        synchronized (mDeviceListLock) {
29420754c5a112e418c11cc88176283db2c4bf2efd6Daichi Hirono            for (final MtpDeviceRecord device : mMtpManager.getDevices()) {
29520754c5a112e418c11cc88176283db2c4bf2efd6Daichi Hirono                if (device.deviceId == deviceId) {
29620754c5a112e418c11cc88176283db2c4bf2efd6Daichi Hirono                    return device.name;
29720754c5a112e418c11cc88176283db2c4bf2efd6Daichi Hirono                }
29820754c5a112e418c11cc88176283db2c4bf2efd6Daichi Hirono            }
29920754c5a112e418c11cc88176283db2c4bf2efd6Daichi Hirono            throw new IOException("Not found the device: " + Integer.toString(deviceId));
300e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        }
30150d17aa871d9ca645a8e7af64df8866b85aee245Daichi Hirono    }
30250d17aa871d9ca645a8e7af64df8866b85aee245Daichi Hirono
303e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono    /**
304e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono     * Finalize the content provider for unit tests.
305e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono     */
306e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono    @Override
307e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono    public void shutdown() {
308e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        synchronized (mDeviceListLock) {
309e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono            try {
310e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono                for (final int id : mMtpManager.getOpenedDeviceIds()) {
311e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono                    closeDeviceInternal(id);
312e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono                }
313e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono            } catch (InterruptedException|IOException e) {
314e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono                // It should fail unit tests by throwing runtime exception.
315e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono                throw new RuntimeException(e);
316e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono            } finally {
317e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono                mDatabase.close();
318b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono                mAppFuse.close();
319e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono                super.shutdown();
320e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono            }
321e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono        }
322e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono    }
323e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono
3245fecc6cf032bbbc2616dd2342a50656bf2857832Daichi Hirono    private void notifyChildDocumentsChange(String parentDocumentId) {
3255fecc6cf032bbbc2616dd2342a50656bf2857832Daichi Hirono        mResolver.notifyChange(
3265fecc6cf032bbbc2616dd2342a50656bf2857832Daichi Hirono                DocumentsContract.buildChildDocumentsUri(AUTHORITY, parentDocumentId),
3275fecc6cf032bbbc2616dd2342a50656bf2857832Daichi Hirono                null,
3285fecc6cf032bbbc2616dd2342a50656bf2857832Daichi Hirono                false);
3295fecc6cf032bbbc2616dd2342a50656bf2857832Daichi Hirono    }
3304c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski
331e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono    /**
332be38848969c91ba9bc3ec8eee31017a34905acfcDaichi Hirono     * Clears MTP identifier in the database.
333e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono     */
334e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono    private void resume() {
335e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        synchronized (mDeviceListLock) {
336e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono            mDatabase.getMapper().clearMapping();
337e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        }
338e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono    }
339e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono
340e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono    private void closeDeviceInternal(int deviceId) throws IOException, InterruptedException {
341e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        // TODO: Flush the device before closing (if not closed externally).
342e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        getDeviceToolkit(deviceId).mDocumentLoader.clearTasks();
343e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        mDeviceToolkits.remove(deviceId);
344e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        mMtpManager.closeDevice(deviceId);
345a57d9ed09003acd8b2beb0494a2bd32f7030cc11Daichi Hirono        if (getOpenedDeviceIds().length == 0) {
346e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono            mRootScanner.pause();
347e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        }
348e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono    }
349e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono
3504c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski    private DeviceToolkit getDeviceToolkit(int deviceId) throws FileNotFoundException {
351e0282dd7d409dce7738173162e766ce9317ef804Daichi Hirono        synchronized (mDeviceListLock) {
352e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono            final DeviceToolkit toolkit = mDeviceToolkits.get(deviceId);
353e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono            if (toolkit == null) {
354e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono                throw new FileNotFoundException();
355e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono            }
356e1d57710fb38dae2747aadca0e5b6f98a84a0514Daichi Hirono            return toolkit;
3574c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski        }
3584c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski    }
3594c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski
3604c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski    private PipeManager getPipeManager(Identifier identifier) throws FileNotFoundException {
3614c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski        return getDeviceToolkit(identifier.mDeviceId).mPipeManager;
3624c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski    }
3634c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski
3644c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski    private DocumentLoader getDocumentLoader(Identifier identifier) throws FileNotFoundException {
3654c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski        return getDeviceToolkit(identifier.mDeviceId).mDocumentLoader;
3664c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski    }
3674c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski
368f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono    private long getFileSize(String documentId) throws FileNotFoundException {
369f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        final Cursor cursor = mDatabase.queryDocument(
370f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                documentId,
371f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                MtpDatabase.strings(Document.COLUMN_SIZE, Document.COLUMN_DISPLAY_NAME));
372f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        try {
373f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono            if (cursor.moveToNext()) {
374f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                return cursor.getLong(0);
375f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono            } else {
376f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono                throw new FileNotFoundException();
377f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono            }
378f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        } finally {
379f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono            cursor.close();
380f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        }
381f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono    }
382f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono
3834c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski    private static class DeviceToolkit {
3844c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski        public final PipeManager mPipeManager;
3854c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski        public final DocumentLoader mDocumentLoader;
3864c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski
38747eb192b2704e27272ca94a95680cac40b6bff3fDaichi Hirono        public DeviceToolkit(MtpManager manager, ContentResolver resolver, MtpDatabase database) {
3884c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski            mPipeManager = new PipeManager();
38947eb192b2704e27272ca94a95680cac40b6bff3fDaichi Hirono            mDocumentLoader = new DocumentLoader(manager, resolver, database);
3904c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski        }
3914c1d3dde05308cb10187269dd9824c9bfdbb27deTomasz Mikolajewski    }
392f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono
393f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono    private class AppFuseCallback implements AppFuse.Callback {
394f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        final byte[] mBytes = new byte[AppFuse.MAX_READ];
395f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono
396f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        @Override
397f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        public byte[] getObjectBytes(int inode, long offset, int size) throws IOException {
398f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono            final Identifier identifier = mDatabase.createIdentifier(Integer.toString(inode));
399b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono            final long readSize = mMtpManager.getPartialObject(
400b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono                    identifier.mDeviceId, identifier.mObjectHandle, offset, size, mBytes);
401b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono            // TODO: Change signature so that getObjectBytes can return read size without copying
402b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono            // bytes.
403b36b15586a5d3d0de590773ce4392fa3a82af66aDaichi Hirono            return Arrays.copyOf(mBytes, (int) readSize);
404f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        }
405f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono
406f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        @Override
407f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        public long getFileSize(int inode) throws FileNotFoundException {
408f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono            return MtpDocumentsProvider.this.getFileSize(String.valueOf(inode));
409f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono        }
410f52ef008c76566f7118a80bf28f599ba48d7c578Daichi Hirono    }
411c00d5d4d82620beba271e63875b93ad9cc39523fDaichi Hirono}
412