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 android.mtp;
18
19import android.content.BroadcastReceiver;
20import android.content.ContentProviderClient;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.Intent;
24import android.content.IntentFilter;
25import android.content.SharedPreferences;
26import android.database.Cursor;
27import android.database.sqlite.SQLiteDatabase;
28import android.media.MediaScanner;
29import android.net.Uri;
30import android.os.BatteryManager;
31import android.os.RemoteException;
32import android.os.SystemProperties;
33import android.os.storage.StorageVolume;
34import android.provider.MediaStore;
35import android.provider.MediaStore.Audio;
36import android.provider.MediaStore.Files;
37import android.provider.MediaStore.MediaColumns;
38import android.system.ErrnoException;
39import android.system.Os;
40import android.system.OsConstants;
41import android.util.Log;
42import android.view.Display;
43import android.view.WindowManager;
44
45import dalvik.system.CloseGuard;
46
47import com.google.android.collect.Sets;
48
49import java.io.File;
50import java.nio.file.Path;
51import java.nio.file.Paths;
52import java.util.ArrayList;
53import java.util.Arrays;
54import java.util.HashMap;
55import java.util.Iterator;
56import java.util.Locale;
57import java.util.concurrent.atomic.AtomicBoolean;
58import java.util.stream.IntStream;
59import java.util.stream.Stream;
60
61/**
62 * MtpDatabase provides an interface for MTP operations that MtpServer can use. To do this, it uses
63 * MtpStorageManager for filesystem operations and MediaProvider to get media metadata. File
64 * operations are also reflected in MediaProvider if possible.
65 * operations
66 * {@hide}
67 */
68public class MtpDatabase implements AutoCloseable {
69    private static final String TAG = MtpDatabase.class.getSimpleName();
70
71    private final Context mContext;
72    private final ContentProviderClient mMediaProvider;
73    private final String mVolumeName;
74    private final Uri mObjectsUri;
75    private final MediaScanner mMediaScanner;
76
77    private final AtomicBoolean mClosed = new AtomicBoolean();
78    private final CloseGuard mCloseGuard = CloseGuard.get();
79
80    private final HashMap<String, MtpStorage> mStorageMap = new HashMap<>();
81
82    // cached property groups for single properties
83    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByProperty = new HashMap<>();
84
85    // cached property groups for all properties for a given format
86    private final HashMap<Integer, MtpPropertyGroup> mPropertyGroupsByFormat = new HashMap<>();
87
88    // SharedPreferences for writable MTP device properties
89    private SharedPreferences mDeviceProperties;
90
91    // Cached device properties
92    private int mBatteryLevel;
93    private int mBatteryScale;
94    private int mDeviceType;
95
96    private MtpServer mServer;
97    private MtpStorageManager mManager;
98
99    private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
100    private static final String[] ID_PROJECTION = new String[] {Files.FileColumns._ID};
101    private static final String[] PATH_PROJECTION = new String[] {Files.FileColumns.DATA};
102    private static final String NO_MEDIA = ".nomedia";
103
104    static {
105        System.loadLibrary("media_jni");
106    }
107
108    private static final int[] PLAYBACK_FORMATS = {
109            // allow transferring arbitrary files
110            MtpConstants.FORMAT_UNDEFINED,
111
112            MtpConstants.FORMAT_ASSOCIATION,
113            MtpConstants.FORMAT_TEXT,
114            MtpConstants.FORMAT_HTML,
115            MtpConstants.FORMAT_WAV,
116            MtpConstants.FORMAT_MP3,
117            MtpConstants.FORMAT_MPEG,
118            MtpConstants.FORMAT_EXIF_JPEG,
119            MtpConstants.FORMAT_TIFF_EP,
120            MtpConstants.FORMAT_BMP,
121            MtpConstants.FORMAT_GIF,
122            MtpConstants.FORMAT_JFIF,
123            MtpConstants.FORMAT_PNG,
124            MtpConstants.FORMAT_TIFF,
125            MtpConstants.FORMAT_WMA,
126            MtpConstants.FORMAT_OGG,
127            MtpConstants.FORMAT_AAC,
128            MtpConstants.FORMAT_MP4_CONTAINER,
129            MtpConstants.FORMAT_MP2,
130            MtpConstants.FORMAT_3GP_CONTAINER,
131            MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
132            MtpConstants.FORMAT_WPL_PLAYLIST,
133            MtpConstants.FORMAT_M3U_PLAYLIST,
134            MtpConstants.FORMAT_PLS_PLAYLIST,
135            MtpConstants.FORMAT_XML_DOCUMENT,
136            MtpConstants.FORMAT_FLAC,
137            MtpConstants.FORMAT_DNG,
138            MtpConstants.FORMAT_HEIF,
139    };
140
141    private static final int[] FILE_PROPERTIES = {
142            MtpConstants.PROPERTY_STORAGE_ID,
143            MtpConstants.PROPERTY_OBJECT_FORMAT,
144            MtpConstants.PROPERTY_PROTECTION_STATUS,
145            MtpConstants.PROPERTY_OBJECT_SIZE,
146            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
147            MtpConstants.PROPERTY_DATE_MODIFIED,
148            MtpConstants.PROPERTY_PERSISTENT_UID,
149            MtpConstants.PROPERTY_PARENT_OBJECT,
150            MtpConstants.PROPERTY_NAME,
151            MtpConstants.PROPERTY_DISPLAY_NAME,
152            MtpConstants.PROPERTY_DATE_ADDED,
153    };
154
155    private static final int[] AUDIO_PROPERTIES = {
156            MtpConstants.PROPERTY_ARTIST,
157            MtpConstants.PROPERTY_ALBUM_NAME,
158            MtpConstants.PROPERTY_ALBUM_ARTIST,
159            MtpConstants.PROPERTY_TRACK,
160            MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
161            MtpConstants.PROPERTY_DURATION,
162            MtpConstants.PROPERTY_GENRE,
163            MtpConstants.PROPERTY_COMPOSER,
164            MtpConstants.PROPERTY_AUDIO_WAVE_CODEC,
165            MtpConstants.PROPERTY_BITRATE_TYPE,
166            MtpConstants.PROPERTY_AUDIO_BITRATE,
167            MtpConstants.PROPERTY_NUMBER_OF_CHANNELS,
168            MtpConstants.PROPERTY_SAMPLE_RATE,
169    };
170
171    private static final int[] VIDEO_PROPERTIES = {
172            MtpConstants.PROPERTY_ARTIST,
173            MtpConstants.PROPERTY_ALBUM_NAME,
174            MtpConstants.PROPERTY_DURATION,
175            MtpConstants.PROPERTY_DESCRIPTION,
176    };
177
178    private static final int[] IMAGE_PROPERTIES = {
179            MtpConstants.PROPERTY_DESCRIPTION,
180    };
181
182    private static final int[] DEVICE_PROPERTIES = {
183            MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
184            MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
185            MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
186            MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
187            MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,
188    };
189
190    private int[] getSupportedObjectProperties(int format) {
191        switch (format) {
192            case MtpConstants.FORMAT_MP3:
193            case MtpConstants.FORMAT_WAV:
194            case MtpConstants.FORMAT_WMA:
195            case MtpConstants.FORMAT_OGG:
196            case MtpConstants.FORMAT_AAC:
197                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
198                        Arrays.stream(AUDIO_PROPERTIES)).toArray();
199            case MtpConstants.FORMAT_MPEG:
200            case MtpConstants.FORMAT_3GP_CONTAINER:
201            case MtpConstants.FORMAT_WMV:
202                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
203                        Arrays.stream(VIDEO_PROPERTIES)).toArray();
204            case MtpConstants.FORMAT_EXIF_JPEG:
205            case MtpConstants.FORMAT_GIF:
206            case MtpConstants.FORMAT_PNG:
207            case MtpConstants.FORMAT_BMP:
208            case MtpConstants.FORMAT_DNG:
209            case MtpConstants.FORMAT_HEIF:
210                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
211                        Arrays.stream(IMAGE_PROPERTIES)).toArray();
212            default:
213                return FILE_PROPERTIES;
214        }
215    }
216
217    private int[] getSupportedDeviceProperties() {
218        return DEVICE_PROPERTIES;
219    }
220
221    private int[] getSupportedPlaybackFormats() {
222        return PLAYBACK_FORMATS;
223    }
224
225    private int[] getSupportedCaptureFormats() {
226        // no capture formats yet
227        return null;
228    }
229
230    private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
231        @Override
232        public void onReceive(Context context, Intent intent) {
233            String action = intent.getAction();
234            if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
235                mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
236                int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
237                if (newLevel != mBatteryLevel) {
238                    mBatteryLevel = newLevel;
239                    if (mServer != null) {
240                        // send device property changed event
241                        mServer.sendDevicePropertyChanged(
242                                MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL);
243                    }
244                }
245            }
246        }
247    };
248
249    public MtpDatabase(Context context, String volumeName,
250            String[] subDirectories) {
251        native_setup();
252        mContext = context;
253        mMediaProvider = context.getContentResolver()
254                .acquireContentProviderClient(MediaStore.AUTHORITY);
255        mVolumeName = volumeName;
256        mObjectsUri = Files.getMtpObjectsUri(volumeName);
257        mMediaScanner = new MediaScanner(context, mVolumeName);
258        mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() {
259            @Override
260            public void sendObjectAdded(int id) {
261                if (MtpDatabase.this.mServer != null)
262                    MtpDatabase.this.mServer.sendObjectAdded(id);
263            }
264
265            @Override
266            public void sendObjectRemoved(int id) {
267                if (MtpDatabase.this.mServer != null)
268                    MtpDatabase.this.mServer.sendObjectRemoved(id);
269            }
270        }, subDirectories == null ? null : Sets.newHashSet(subDirectories));
271
272        initDeviceProperties(context);
273        mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0);
274        mCloseGuard.open("close");
275    }
276
277    public void setServer(MtpServer server) {
278        mServer = server;
279        // always unregister before registering
280        try {
281            mContext.unregisterReceiver(mBatteryReceiver);
282        } catch (IllegalArgumentException e) {
283            // wasn't previously registered, ignore
284        }
285        // register for battery notifications when we are connected
286        if (server != null) {
287            mContext.registerReceiver(mBatteryReceiver,
288                    new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
289        }
290    }
291
292    @Override
293    public void close() {
294        mManager.close();
295        mCloseGuard.close();
296        if (mClosed.compareAndSet(false, true)) {
297            mMediaScanner.close();
298            if (mMediaProvider != null) {
299                mMediaProvider.close();
300            }
301            native_finalize();
302        }
303    }
304
305    @Override
306    protected void finalize() throws Throwable {
307        try {
308            if (mCloseGuard != null) {
309                mCloseGuard.warnIfOpen();
310            }
311            close();
312        } finally {
313            super.finalize();
314        }
315    }
316
317    public void addStorage(StorageVolume storage) {
318        MtpStorage mtpStorage = mManager.addMtpStorage(storage);
319        mStorageMap.put(storage.getPath(), mtpStorage);
320        if (mServer != null) {
321            mServer.addStorage(mtpStorage);
322        }
323    }
324
325    public void removeStorage(StorageVolume storage) {
326        MtpStorage mtpStorage = mStorageMap.get(storage.getPath());
327        if (mtpStorage == null) {
328            return;
329        }
330        if (mServer != null) {
331            mServer.removeStorage(mtpStorage);
332        }
333        mManager.removeMtpStorage(mtpStorage);
334        mStorageMap.remove(storage.getPath());
335    }
336
337    private void initDeviceProperties(Context context) {
338        final String devicePropertiesName = "device-properties";
339        mDeviceProperties = context.getSharedPreferences(devicePropertiesName,
340                Context.MODE_PRIVATE);
341        File databaseFile = context.getDatabasePath(devicePropertiesName);
342
343        if (databaseFile.exists()) {
344            // for backward compatibility - read device properties from sqlite database
345            // and migrate them to shared prefs
346            SQLiteDatabase db = null;
347            Cursor c = null;
348            try {
349                db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
350                if (db != null) {
351                    c = db.query("properties", new String[]{"_id", "code", "value"},
352                            null, null, null, null, null);
353                    if (c != null) {
354                        SharedPreferences.Editor e = mDeviceProperties.edit();
355                        while (c.moveToNext()) {
356                            String name = c.getString(1);
357                            String value = c.getString(2);
358                            e.putString(name, value);
359                        }
360                        e.commit();
361                    }
362                }
363            } catch (Exception e) {
364                Log.e(TAG, "failed to migrate device properties", e);
365            } finally {
366                if (c != null) c.close();
367                if (db != null) db.close();
368            }
369            context.deleteDatabase(devicePropertiesName);
370        }
371    }
372
373    private int beginSendObject(String path, int format, int parent, int storageId) {
374        MtpStorageManager.MtpObject parentObj =
375                parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent);
376        if (parentObj == null) {
377            return -1;
378        }
379
380        Path objPath = Paths.get(path);
381        return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format);
382    }
383
384    private void endSendObject(int handle, boolean succeeded) {
385        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
386        if (obj == null || !mManager.endSendObject(obj, succeeded)) {
387            Log.e(TAG, "Failed to successfully end send object");
388            return;
389        }
390        // Add the new file to MediaProvider
391        if (succeeded) {
392            String path = obj.getPath().toString();
393            int format = obj.getFormat();
394            // Get parent info from MediaProvider, since the id is different from MTP's
395            ContentValues values = new ContentValues();
396            values.put(Files.FileColumns.DATA, path);
397            values.put(Files.FileColumns.FORMAT, format);
398            values.put(Files.FileColumns.SIZE, obj.getSize());
399            values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
400            try {
401                if (obj.getParent().isRoot()) {
402                    values.put(Files.FileColumns.PARENT, 0);
403                } else {
404                    int parentId = findInMedia(obj.getParent().getPath());
405                    if (parentId != -1) {
406                        values.put(Files.FileColumns.PARENT, parentId);
407                    } else {
408                        // The parent isn't in MediaProvider. Don't add the new file.
409                        return;
410                    }
411                }
412
413                Uri uri = mMediaProvider.insert(mObjectsUri, values);
414                if (uri != null) {
415                    rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
416                }
417            } catch (RemoteException e) {
418                Log.e(TAG, "RemoteException in beginSendObject", e);
419            }
420        }
421    }
422
423    private void rescanFile(String path, int handle, int format) {
424        // handle abstract playlists separately
425        // they do not exist in the file system so don't use the media scanner here
426        if (format == MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST) {
427            // extract name from path
428            String name = path;
429            int lastSlash = name.lastIndexOf('/');
430            if (lastSlash >= 0) {
431                name = name.substring(lastSlash + 1);
432            }
433            // strip trailing ".pla" from the name
434            if (name.endsWith(".pla")) {
435                name = name.substring(0, name.length() - 4);
436            }
437
438            ContentValues values = new ContentValues(1);
439            values.put(Audio.Playlists.DATA, path);
440            values.put(Audio.Playlists.NAME, name);
441            values.put(Files.FileColumns.FORMAT, format);
442            values.put(Files.FileColumns.DATE_MODIFIED, System.currentTimeMillis() / 1000);
443            values.put(MediaColumns.MEDIA_SCANNER_NEW_OBJECT_ID, handle);
444            try {
445                mMediaProvider.insert(
446                        Audio.Playlists.EXTERNAL_CONTENT_URI, values);
447            } catch (RemoteException e) {
448                Log.e(TAG, "RemoteException in endSendObject", e);
449            }
450        } else {
451            mMediaScanner.scanMtpFile(path, handle, format);
452        }
453    }
454
455    private int[] getObjectList(int storageID, int format, int parent) {
456        Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent,
457                format, storageID);
458        if (objectStream == null) {
459            return null;
460        }
461        return objectStream.mapToInt(MtpStorageManager.MtpObject::getId).toArray();
462    }
463
464    private int getNumObjects(int storageID, int format, int parent) {
465        Stream<MtpStorageManager.MtpObject> objectStream = mManager.getObjects(parent,
466                format, storageID);
467        if (objectStream == null) {
468            return -1;
469        }
470        return (int) objectStream.count();
471    }
472
473    private MtpPropertyList getObjectPropertyList(int handle, int format, int property,
474            int groupCode, int depth) {
475        // FIXME - implement group support
476        if (property == 0) {
477            if (groupCode == 0) {
478                return new MtpPropertyList(MtpConstants.RESPONSE_PARAMETER_NOT_SUPPORTED);
479            }
480            return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
481        }
482        if (depth == 0xFFFFFFFF && (handle == 0 || handle == 0xFFFFFFFF)) {
483            // request all objects starting at root
484            handle = 0xFFFFFFFF;
485            depth = 0;
486        }
487        if (!(depth == 0 || depth == 1)) {
488            // we only support depth 0 and 1
489            // depth 0: single object, depth 1: immediate children
490            return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
491        }
492        Stream<MtpStorageManager.MtpObject> objectStream = Stream.of();
493        if (handle == 0xFFFFFFFF) {
494            // All objects are requested
495            objectStream = mManager.getObjects(0, format, 0xFFFFFFFF);
496            if (objectStream == null) {
497                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
498            }
499        } else if (handle != 0) {
500            // Add the requested object if format matches
501            MtpStorageManager.MtpObject obj = mManager.getObject(handle);
502            if (obj == null) {
503                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
504            }
505            if (obj.getFormat() == format || format == 0) {
506                objectStream = Stream.of(obj);
507            }
508        }
509        if (handle == 0 || depth == 1) {
510            if (handle == 0) {
511                handle = 0xFFFFFFFF;
512            }
513            // Get the direct children of root or this object.
514            Stream<MtpStorageManager.MtpObject> childStream = mManager.getObjects(handle, format,
515                    0xFFFFFFFF);
516            if (childStream == null) {
517                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
518            }
519            objectStream = Stream.concat(objectStream, childStream);
520        }
521
522        MtpPropertyList ret = new MtpPropertyList(MtpConstants.RESPONSE_OK);
523        MtpPropertyGroup propertyGroup;
524        Iterator<MtpStorageManager.MtpObject> iter = objectStream.iterator();
525        while (iter.hasNext()) {
526            MtpStorageManager.MtpObject obj = iter.next();
527            if (property == 0xffffffff) {
528                // Get all properties supported by this object
529                propertyGroup = mPropertyGroupsByFormat.get(obj.getFormat());
530                if (propertyGroup == null) {
531                    int[] propertyList = getSupportedObjectProperties(format);
532                    propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
533                            propertyList);
534                    mPropertyGroupsByFormat.put(format, propertyGroup);
535                }
536            } else {
537                // Get this property value
538                final int[] propertyList = new int[]{property};
539                propertyGroup = mPropertyGroupsByProperty.get(property);
540                if (propertyGroup == null) {
541                    propertyGroup = new MtpPropertyGroup(mMediaProvider, mVolumeName,
542                            propertyList);
543                    mPropertyGroupsByProperty.put(property, propertyGroup);
544                }
545            }
546            int err = propertyGroup.getPropertyList(obj, ret);
547            if (err != MtpConstants.RESPONSE_OK) {
548                return new MtpPropertyList(err);
549            }
550        }
551        return ret;
552    }
553
554    private int renameFile(int handle, String newName) {
555        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
556        if (obj == null) {
557            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
558        }
559        Path oldPath = obj.getPath();
560
561        // now rename the file.  make sure this succeeds before updating database
562        if (!mManager.beginRenameObject(obj, newName))
563            return MtpConstants.RESPONSE_GENERAL_ERROR;
564        Path newPath = obj.getPath();
565        boolean success = oldPath.toFile().renameTo(newPath.toFile());
566        try {
567            Os.access(oldPath.toString(), OsConstants.F_OK);
568            Os.access(newPath.toString(), OsConstants.F_OK);
569        } catch (ErrnoException e) {
570            // Ignore. Could fail if the metadata was already updated.
571        }
572
573        if (!mManager.endRenameObject(obj, oldPath.getFileName().toString(), success)) {
574            Log.e(TAG, "Failed to end rename object");
575        }
576        if (!success) {
577            return MtpConstants.RESPONSE_GENERAL_ERROR;
578        }
579
580        // finally update MediaProvider
581        ContentValues values = new ContentValues();
582        values.put(Files.FileColumns.DATA, newPath.toString());
583        String[] whereArgs = new String[]{oldPath.toString()};
584        try {
585            // note - we are relying on a special case in MediaProvider.update() to update
586            // the paths for all children in the case where this is a directory.
587            mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs);
588        } catch (RemoteException e) {
589            Log.e(TAG, "RemoteException in mMediaProvider.update", e);
590        }
591
592        // check if nomedia status changed
593        if (obj.isDir()) {
594            // for directories, check if renamed from something hidden to something non-hidden
595            if (oldPath.getFileName().startsWith(".") && !newPath.startsWith(".")) {
596                // directory was unhidden
597                try {
598                    mMediaProvider.call(MediaStore.UNHIDE_CALL, newPath.toString(), null);
599                } catch (RemoteException e) {
600                    Log.e(TAG, "failed to unhide/rescan for " + newPath);
601                }
602            }
603        } else {
604            // for files, check if renamed from .nomedia to something else
605            if (oldPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)
606                    && !newPath.getFileName().toString().toLowerCase(Locale.US).equals(NO_MEDIA)) {
607                try {
608                    mMediaProvider.call(MediaStore.UNHIDE_CALL,
609                            oldPath.getParent().toString(), null);
610                } catch (RemoteException e) {
611                    Log.e(TAG, "failed to unhide/rescan for " + newPath);
612                }
613            }
614        }
615        return MtpConstants.RESPONSE_OK;
616    }
617
618    private int beginMoveObject(int handle, int newParent, int newStorage) {
619        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
620        MtpStorageManager.MtpObject parent = newParent == 0 ?
621                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
622        if (obj == null || parent == null)
623            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
624
625        boolean allowed = mManager.beginMoveObject(obj, parent);
626        return allowed ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR;
627    }
628
629    private void endMoveObject(int oldParent, int newParent, int oldStorage, int newStorage,
630            int objId, boolean success) {
631        MtpStorageManager.MtpObject oldParentObj = oldParent == 0 ?
632                mManager.getStorageRoot(oldStorage) : mManager.getObject(oldParent);
633        MtpStorageManager.MtpObject newParentObj = newParent == 0 ?
634                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
635        MtpStorageManager.MtpObject obj = mManager.getObject(objId);
636        String name = obj.getName();
637        if (newParentObj == null || oldParentObj == null
638                ||!mManager.endMoveObject(oldParentObj, newParentObj, name, success)) {
639            Log.e(TAG, "Failed to end move object");
640            return;
641        }
642
643        obj = mManager.getObject(objId);
644        if (!success || obj == null)
645            return;
646        // Get parent info from MediaProvider, since the id is different from MTP's
647        ContentValues values = new ContentValues();
648        Path path = newParentObj.getPath().resolve(name);
649        Path oldPath = oldParentObj.getPath().resolve(name);
650        values.put(Files.FileColumns.DATA, path.toString());
651        if (obj.getParent().isRoot()) {
652            values.put(Files.FileColumns.PARENT, 0);
653        } else {
654            int parentId = findInMedia(path.getParent());
655            if (parentId != -1) {
656                values.put(Files.FileColumns.PARENT, parentId);
657            } else {
658                // The new parent isn't in MediaProvider, so delete the object instead
659                deleteFromMedia(oldPath, obj.isDir());
660                return;
661            }
662        }
663        // update MediaProvider
664        Cursor c = null;
665        String[] whereArgs = new String[]{oldPath.toString()};
666        try {
667            int parentId = -1;
668            if (!oldParentObj.isRoot()) {
669                parentId = findInMedia(oldPath.getParent());
670            }
671            if (oldParentObj.isRoot() || parentId != -1) {
672                // Old parent exists in MediaProvider - perform a move
673                // note - we are relying on a special case in MediaProvider.update() to update
674                // the paths for all children in the case where this is a directory.
675                mMediaProvider.update(mObjectsUri, values, PATH_WHERE, whereArgs);
676            } else {
677                // Old parent doesn't exist - add the object
678                values.put(Files.FileColumns.FORMAT, obj.getFormat());
679                values.put(Files.FileColumns.SIZE, obj.getSize());
680                values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
681                Uri uri = mMediaProvider.insert(mObjectsUri, values);
682                if (uri != null) {
683                    rescanFile(path.toString(),
684                            Integer.parseInt(uri.getPathSegments().get(2)), obj.getFormat());
685                }
686            }
687        } catch (RemoteException e) {
688            Log.e(TAG, "RemoteException in mMediaProvider.update", e);
689        }
690    }
691
692    private int beginCopyObject(int handle, int newParent, int newStorage) {
693        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
694        MtpStorageManager.MtpObject parent = newParent == 0 ?
695                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
696        if (obj == null || parent == null)
697            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
698        return mManager.beginCopyObject(obj, parent);
699    }
700
701    private void endCopyObject(int handle, boolean success) {
702        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
703        if (obj == null || !mManager.endCopyObject(obj, success)) {
704            Log.e(TAG, "Failed to end copy object");
705            return;
706        }
707        if (!success) {
708            return;
709        }
710        String path = obj.getPath().toString();
711        int format = obj.getFormat();
712        // Get parent info from MediaProvider, since the id is different from MTP's
713        ContentValues values = new ContentValues();
714        values.put(Files.FileColumns.DATA, path);
715        values.put(Files.FileColumns.FORMAT, format);
716        values.put(Files.FileColumns.SIZE, obj.getSize());
717        values.put(Files.FileColumns.DATE_MODIFIED, obj.getModifiedTime());
718        try {
719            if (obj.getParent().isRoot()) {
720                values.put(Files.FileColumns.PARENT, 0);
721            } else {
722                int parentId = findInMedia(obj.getParent().getPath());
723                if (parentId != -1) {
724                    values.put(Files.FileColumns.PARENT, parentId);
725                } else {
726                    // The parent isn't in MediaProvider. Don't add the new file.
727                    return;
728                }
729            }
730            if (obj.isDir()) {
731                mMediaScanner.scanDirectories(new String[]{path});
732            } else {
733                Uri uri = mMediaProvider.insert(mObjectsUri, values);
734                if (uri != null) {
735                    rescanFile(path, Integer.parseInt(uri.getPathSegments().get(2)), format);
736                }
737            }
738        } catch (RemoteException e) {
739            Log.e(TAG, "RemoteException in beginSendObject", e);
740        }
741    }
742
743    private int setObjectProperty(int handle, int property,
744            long intValue, String stringValue) {
745        switch (property) {
746            case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
747                return renameFile(handle, stringValue);
748
749            default:
750                return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
751        }
752    }
753
754    private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) {
755        switch (property) {
756            case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
757            case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
758                // writable string properties kept in shared preferences
759                String value = mDeviceProperties.getString(Integer.toString(property), "");
760                int length = value.length();
761                if (length > 255) {
762                    length = 255;
763                }
764                value.getChars(0, length, outStringValue, 0);
765                outStringValue[length] = 0;
766                return MtpConstants.RESPONSE_OK;
767            case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
768                // use screen size as max image size
769                Display display = ((WindowManager) mContext.getSystemService(
770                        Context.WINDOW_SERVICE)).getDefaultDisplay();
771                int width = display.getMaximumSizeDimension();
772                int height = display.getMaximumSizeDimension();
773                String imageSize = Integer.toString(width) + "x" + Integer.toString(height);
774                imageSize.getChars(0, imageSize.length(), outStringValue, 0);
775                outStringValue[imageSize.length()] = 0;
776                return MtpConstants.RESPONSE_OK;
777            case MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE:
778                outIntValue[0] = mDeviceType;
779                return MtpConstants.RESPONSE_OK;
780            case MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL:
781                outIntValue[0] = mBatteryLevel;
782                outIntValue[1] = mBatteryScale;
783                return MtpConstants.RESPONSE_OK;
784            default:
785                return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
786        }
787    }
788
789    private int setDeviceProperty(int property, long intValue, String stringValue) {
790        switch (property) {
791            case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
792            case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
793                // writable string properties kept in shared prefs
794                SharedPreferences.Editor e = mDeviceProperties.edit();
795                e.putString(Integer.toString(property), stringValue);
796                return (e.commit() ? MtpConstants.RESPONSE_OK
797                        : MtpConstants.RESPONSE_GENERAL_ERROR);
798        }
799
800        return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
801    }
802
803    private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
804            char[] outName, long[] outCreatedModified) {
805        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
806        if (obj == null) {
807            return false;
808        }
809        outStorageFormatParent[0] = obj.getStorageId();
810        outStorageFormatParent[1] = obj.getFormat();
811        outStorageFormatParent[2] = obj.getParent().isRoot() ? 0 : obj.getParent().getId();
812
813        int nameLen = Integer.min(obj.getName().length(), 255);
814        obj.getName().getChars(0, nameLen, outName, 0);
815        outName[nameLen] = 0;
816
817        outCreatedModified[0] = obj.getModifiedTime();
818        outCreatedModified[1] = obj.getModifiedTime();
819        return true;
820    }
821
822    private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
823        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
824        if (obj == null) {
825            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
826        }
827
828        String path = obj.getPath().toString();
829        int pathLen = Integer.min(path.length(), 4096);
830        path.getChars(0, pathLen, outFilePath, 0);
831        outFilePath[pathLen] = 0;
832
833        outFileLengthFormat[0] = obj.getSize();
834        outFileLengthFormat[1] = obj.getFormat();
835        return MtpConstants.RESPONSE_OK;
836    }
837
838    private int getObjectFormat(int handle) {
839        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
840        if (obj == null) {
841            return -1;
842        }
843        return obj.getFormat();
844    }
845
846    private int beginDeleteObject(int handle) {
847        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
848        if (obj == null) {
849            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
850        }
851        if (!mManager.beginRemoveObject(obj)) {
852            return MtpConstants.RESPONSE_GENERAL_ERROR;
853        }
854        return MtpConstants.RESPONSE_OK;
855    }
856
857    private void endDeleteObject(int handle, boolean success) {
858        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
859        if (obj == null) {
860            return;
861        }
862        if (!mManager.endRemoveObject(obj, success))
863            Log.e(TAG, "Failed to end remove object");
864        if (success)
865            deleteFromMedia(obj.getPath(), obj.isDir());
866    }
867
868    private int findInMedia(Path path) {
869        int ret = -1;
870        Cursor c = null;
871        try {
872            c = mMediaProvider.query(mObjectsUri, ID_PROJECTION, PATH_WHERE,
873                    new String[]{path.toString()}, null, null);
874            if (c != null && c.moveToNext()) {
875                ret = c.getInt(0);
876            }
877        } catch (RemoteException e) {
878            Log.e(TAG, "Error finding " + path + " in MediaProvider");
879        } finally {
880            if (c != null)
881                c.close();
882        }
883        return ret;
884    }
885
886    private void deleteFromMedia(Path path, boolean isDir) {
887        try {
888            // Delete the object(s) from MediaProvider, but ignore errors.
889            if (isDir) {
890                // recursive case - delete all children first
891                mMediaProvider.delete(mObjectsUri,
892                        // the 'like' makes it use the index, the 'lower()' makes it correct
893                        // when the path contains sqlite wildcard characters
894                        "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
895                        new String[]{path + "/%", Integer.toString(path.toString().length() + 1),
896                                path.toString() + "/"});
897            }
898
899            String[] whereArgs = new String[]{path.toString()};
900            if (mMediaProvider.delete(mObjectsUri, PATH_WHERE, whereArgs) > 0) {
901                if (!isDir && path.toString().toLowerCase(Locale.US).endsWith(NO_MEDIA)) {
902                    try {
903                        String parentPath = path.getParent().toString();
904                        mMediaProvider.call(MediaStore.UNHIDE_CALL, parentPath, null);
905                    } catch (RemoteException e) {
906                        Log.e(TAG, "failed to unhide/rescan for " + path);
907                    }
908                }
909            } else {
910                Log.i(TAG, "Mediaprovider didn't delete " + path);
911            }
912        } catch (Exception e) {
913            Log.d(TAG, "Failed to delete " + path + " from MediaProvider");
914        }
915    }
916
917    private int[] getObjectReferences(int handle) {
918        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
919        if (obj == null)
920            return null;
921        // Translate this handle to the MediaProvider Handle
922        handle = findInMedia(obj.getPath());
923        if (handle == -1)
924            return null;
925        Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
926        Cursor c = null;
927        try {
928            c = mMediaProvider.query(uri, PATH_PROJECTION, null, null, null, null);
929            if (c == null) {
930                return null;
931            }
932                ArrayList<Integer> result = new ArrayList<>();
933                while (c.moveToNext()) {
934                    // Translate result handles back into handles for this session.
935                    String refPath = c.getString(0);
936                    MtpStorageManager.MtpObject refObj = mManager.getByPath(refPath);
937                    if (refObj != null) {
938                        result.add(refObj.getId());
939                    }
940                }
941                return result.stream().mapToInt(Integer::intValue).toArray();
942        } catch (RemoteException e) {
943            Log.e(TAG, "RemoteException in getObjectList", e);
944        } finally {
945            if (c != null) {
946                c.close();
947            }
948        }
949        return null;
950    }
951
952    private int setObjectReferences(int handle, int[] references) {
953        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
954        if (obj == null)
955            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
956        // Translate this handle to the MediaProvider Handle
957        handle = findInMedia(obj.getPath());
958        if (handle == -1)
959            return MtpConstants.RESPONSE_GENERAL_ERROR;
960        Uri uri = Files.getMtpReferencesUri(mVolumeName, handle);
961        ArrayList<ContentValues> valuesList = new ArrayList<>();
962        for (int id : references) {
963            // Translate each reference id to the MediaProvider Id
964            MtpStorageManager.MtpObject refObj = mManager.getObject(id);
965            if (refObj == null)
966                continue;
967            int refHandle = findInMedia(refObj.getPath());
968            if (refHandle == -1)
969                continue;
970            ContentValues values = new ContentValues();
971            values.put(Files.FileColumns._ID, refHandle);
972            valuesList.add(values);
973        }
974        try {
975            if (mMediaProvider.bulkInsert(uri, valuesList.toArray(new ContentValues[0])) > 0) {
976                return MtpConstants.RESPONSE_OK;
977            }
978        } catch (RemoteException e) {
979            Log.e(TAG, "RemoteException in setObjectReferences", e);
980        }
981        return MtpConstants.RESPONSE_GENERAL_ERROR;
982    }
983
984    // used by the JNI code
985    private long mNativeContext;
986
987    private native final void native_setup();
988    private native final void native_finalize();
989}
990