All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.android.mtp.MtpDatabase Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.mtp;

import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.media.ApplicationMediaCapabilities;
import android.media.ExifInterface;
import android.media.MediaFormat;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.os.storage.StorageVolume;
import android.provider.MediaStore;
import android.provider.MediaStore.Files;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import android.util.SparseArray;
import android.view.Display;
import android.view.WindowManager;

import com.android.internal.annotations.VisibleForNative;
import com.android.internal.annotations.VisibleForTesting;

import dalvik.system.CloseGuard;

import com.google.android.collect.Sets;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.IntStream;

/**
 * MtpDatabase provides an interface for MTP operations that MtpServer can use. To do this, it uses
 * MtpStorageManager for filesystem operations and MediaProvider to get media metadata. File
 * operations are also reflected in MediaProvider if possible.
 * operations
 * {@hide}
 */
public class MtpDatabase implements AutoCloseable {
    private static final String TAG = MtpDatabase.class.getSimpleName();
    private static final int MAX_THUMB_SIZE = (200 * 1024);

    private final Context mContext;
    private final ContentProviderClient mMediaProvider;

    private final AtomicBoolean mClosed = new AtomicBoolean();
    private final CloseGuard mCloseGuard = CloseGuard.get();

    private final HashMap mStorageMap = new HashMap<>();

    // cached property groups for single properties
    private final SparseArray mPropertyGroupsByProperty = new SparseArray<>();

    // cached property groups for all properties for a given format
    private final SparseArray mPropertyGroupsByFormat = new SparseArray<>();

    // SharedPreferences for writable MTP device properties
    private SharedPreferences mDeviceProperties;

    // Cached device properties
    private int mBatteryLevel;
    private int mBatteryScale;
    private int mDeviceType;
    private String mHostType;
    private boolean mSkipThumbForHost = false;
    private volatile boolean mHostIsWindows = false;

    private MtpServer mServer;
    private MtpStorageManager mManager;

    private static final String PATH_WHERE = Files.FileColumns.DATA + "=?";
    private static final String NO_MEDIA = ".nomedia";

    static {
        System.loadLibrary("media_jni");
    }

    private static final int[] PLAYBACK_FORMATS = {
            // allow transferring arbitrary files
            MtpConstants.FORMAT_UNDEFINED,

            MtpConstants.FORMAT_ASSOCIATION,
            MtpConstants.FORMAT_TEXT,
            MtpConstants.FORMAT_HTML,
            MtpConstants.FORMAT_WAV,
            MtpConstants.FORMAT_MP3,
            MtpConstants.FORMAT_MPEG,
            MtpConstants.FORMAT_EXIF_JPEG,
            MtpConstants.FORMAT_TIFF_EP,
            MtpConstants.FORMAT_BMP,
            MtpConstants.FORMAT_GIF,
            MtpConstants.FORMAT_JFIF,
            MtpConstants.FORMAT_PNG,
            MtpConstants.FORMAT_TIFF,
            MtpConstants.FORMAT_WMA,
            MtpConstants.FORMAT_OGG,
            MtpConstants.FORMAT_AAC,
            MtpConstants.FORMAT_MP4_CONTAINER,
            MtpConstants.FORMAT_MP2,
            MtpConstants.FORMAT_3GP_CONTAINER,
            MtpConstants.FORMAT_ABSTRACT_AV_PLAYLIST,
            MtpConstants.FORMAT_WPL_PLAYLIST,
            MtpConstants.FORMAT_M3U_PLAYLIST,
            MtpConstants.FORMAT_PLS_PLAYLIST,
            MtpConstants.FORMAT_XML_DOCUMENT,
            MtpConstants.FORMAT_FLAC,
            MtpConstants.FORMAT_DNG,
            MtpConstants.FORMAT_HEIF,
    };

    private static final int[] FILE_PROPERTIES = {
            MtpConstants.PROPERTY_STORAGE_ID,
            MtpConstants.PROPERTY_OBJECT_FORMAT,
            MtpConstants.PROPERTY_PROTECTION_STATUS,
            MtpConstants.PROPERTY_OBJECT_SIZE,
            MtpConstants.PROPERTY_OBJECT_FILE_NAME,
            MtpConstants.PROPERTY_DATE_MODIFIED,
            MtpConstants.PROPERTY_PERSISTENT_UID,
            MtpConstants.PROPERTY_PARENT_OBJECT,
            MtpConstants.PROPERTY_NAME,
            MtpConstants.PROPERTY_DISPLAY_NAME,
            MtpConstants.PROPERTY_DATE_ADDED,
    };

    private static final int[] AUDIO_PROPERTIES = {
            MtpConstants.PROPERTY_ARTIST,
            MtpConstants.PROPERTY_ALBUM_NAME,
            MtpConstants.PROPERTY_ALBUM_ARTIST,
            MtpConstants.PROPERTY_TRACK,
            MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE,
            MtpConstants.PROPERTY_DURATION,
            MtpConstants.PROPERTY_GENRE,
            MtpConstants.PROPERTY_COMPOSER,
            MtpConstants.PROPERTY_AUDIO_WAVE_CODEC,
            MtpConstants.PROPERTY_BITRATE_TYPE,
            MtpConstants.PROPERTY_AUDIO_BITRATE,
            MtpConstants.PROPERTY_NUMBER_OF_CHANNELS,
            MtpConstants.PROPERTY_SAMPLE_RATE,
    };

    private static final int[] VIDEO_PROPERTIES = {
            MtpConstants.PROPERTY_ARTIST,
            MtpConstants.PROPERTY_ALBUM_NAME,
            MtpConstants.PROPERTY_DURATION,
            MtpConstants.PROPERTY_DESCRIPTION,
    };

    private static final int[] IMAGE_PROPERTIES = {
            MtpConstants.PROPERTY_DESCRIPTION,
    };

    private static final int[] DEVICE_PROPERTIES = {
            MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER,
            MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME,
            MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE,
            MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL,
            MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE,
            MtpConstants.DEVICE_PROPERTY_SESSION_INITIATOR_VERSION_INFO,
    };

    @VisibleForNative
    private int[] getSupportedObjectProperties(int format) {
        switch (format) {
            case MtpConstants.FORMAT_MP3:
            case MtpConstants.FORMAT_WAV:
            case MtpConstants.FORMAT_WMA:
            case MtpConstants.FORMAT_OGG:
            case MtpConstants.FORMAT_AAC:
                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
                        Arrays.stream(AUDIO_PROPERTIES)).toArray();
            case MtpConstants.FORMAT_MPEG:
            case MtpConstants.FORMAT_3GP_CONTAINER:
            case MtpConstants.FORMAT_WMV:
                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
                        Arrays.stream(VIDEO_PROPERTIES)).toArray();
            case MtpConstants.FORMAT_EXIF_JPEG:
            case MtpConstants.FORMAT_GIF:
            case MtpConstants.FORMAT_PNG:
            case MtpConstants.FORMAT_BMP:
            case MtpConstants.FORMAT_DNG:
            case MtpConstants.FORMAT_HEIF:
                return IntStream.concat(Arrays.stream(FILE_PROPERTIES),
                        Arrays.stream(IMAGE_PROPERTIES)).toArray();
            default:
                return FILE_PROPERTIES;
        }
    }

    public static Uri getObjectPropertiesUri(int format, String volumeName) {
        switch (format) {
            case MtpConstants.FORMAT_MP3:
            case MtpConstants.FORMAT_WAV:
            case MtpConstants.FORMAT_WMA:
            case MtpConstants.FORMAT_OGG:
            case MtpConstants.FORMAT_AAC:
                return MediaStore.Audio.Media.getContentUri(volumeName);
            case MtpConstants.FORMAT_MPEG:
            case MtpConstants.FORMAT_3GP_CONTAINER:
            case MtpConstants.FORMAT_WMV:
                return MediaStore.Video.Media.getContentUri(volumeName);
            case MtpConstants.FORMAT_EXIF_JPEG:
            case MtpConstants.FORMAT_GIF:
            case MtpConstants.FORMAT_PNG:
            case MtpConstants.FORMAT_BMP:
            case MtpConstants.FORMAT_DNG:
            case MtpConstants.FORMAT_HEIF:
                return MediaStore.Images.Media.getContentUri(volumeName);
            default:
                return MediaStore.Files.getContentUri(volumeName);
        }
    }

    @VisibleForNative
    private int[] getSupportedDeviceProperties() {
        return DEVICE_PROPERTIES;
    }

    @VisibleForNative
    private int[] getSupportedPlaybackFormats() {
        return PLAYBACK_FORMATS;
    }

    @VisibleForNative
    private int[] getSupportedCaptureFormats() {
        // no capture formats yet
        return null;
    }

    private BroadcastReceiver mBatteryReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
                mBatteryScale = intent.getIntExtra(BatteryManager.EXTRA_SCALE, 0);
                int newLevel = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, 0);
                if (newLevel != mBatteryLevel) {
                    mBatteryLevel = newLevel;
                    if (mServer != null) {
                        // send device property changed event
                        mServer.sendDevicePropertyChanged(
                                MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL);
                    }
                }
            }
        }
    };

    public MtpDatabase(Context context, String[] subDirectories) {
        native_setup();
        mContext = Objects.requireNonNull(context);
        mMediaProvider = context.getContentResolver()
                .acquireContentProviderClient(MediaStore.AUTHORITY);
        mManager = new MtpStorageManager(new MtpStorageManager.MtpNotifier() {
            @Override
            public void sendObjectAdded(int id) {
                if (MtpDatabase.this.mServer != null)
                    MtpDatabase.this.mServer.sendObjectAdded(id);
            }

            @Override
            public void sendObjectRemoved(int id) {
                if (MtpDatabase.this.mServer != null)
                    MtpDatabase.this.mServer.sendObjectRemoved(id);
            }

            @Override
            public void sendObjectInfoChanged(int id) {
                if (MtpDatabase.this.mServer != null)
                    MtpDatabase.this.mServer.sendObjectInfoChanged(id);
            }
        }, subDirectories == null ? null : Sets.newHashSet(subDirectories));

        initDeviceProperties(context);
        mDeviceType = SystemProperties.getInt("sys.usb.mtp.device_type", 0);
        mCloseGuard.open("close");
    }

    public void setServer(MtpServer server) {
        mServer = server;
        // always unregister before registering
        try {
            mContext.unregisterReceiver(mBatteryReceiver);
        } catch (IllegalArgumentException e) {
            // wasn't previously registered, ignore
        }
        // register for battery notifications when we are connected
        if (server != null) {
            mContext.registerReceiver(mBatteryReceiver,
                    new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
        }
    }

    public Context getContext() {
        return mContext;
    }

    @Override
    public void close() {
        mManager.close();
        mCloseGuard.close();
        if (mClosed.compareAndSet(false, true)) {
            if (mMediaProvider != null) {
                mMediaProvider.close();
            }
            native_finalize();
        }
    }

    @Override
    protected void finalize() throws Throwable {
        try {
            if (mCloseGuard != null) {
                mCloseGuard.warnIfOpen();
            }
            close();
        } finally {
            super.finalize();
        }
    }

    public void addStorage(StorageVolume storage) {
        MtpStorage mtpStorage = mManager.addMtpStorage(storage, () -> mHostIsWindows);
        mStorageMap.put(storage.getPath(), mtpStorage);
        if (mServer != null) {
            mServer.addStorage(mtpStorage);
        }
    }

    public void removeStorage(StorageVolume storage) {
        MtpStorage mtpStorage = mStorageMap.get(storage.getPath());
        if (mtpStorage == null) {
            return;
        }
        if (mServer != null) {
            mServer.removeStorage(mtpStorage);
        }
        mManager.removeMtpStorage(mtpStorage);
        mStorageMap.remove(storage.getPath());
    }

    private void initDeviceProperties(Context context) {
        final String devicePropertiesName = "device-properties";
        mDeviceProperties = context.getSharedPreferences(devicePropertiesName,
                Context.MODE_PRIVATE);
        File databaseFile = context.getDatabasePath(devicePropertiesName);

        if (databaseFile.exists()) {
            // for backward compatibility - read device properties from sqlite database
            // and migrate them to shared prefs
            SQLiteDatabase db = null;
            Cursor c = null;
            try {
                db = context.openOrCreateDatabase("device-properties", Context.MODE_PRIVATE, null);
                if (db != null) {
                    c = db.query("properties", new String[]{"_id", "code", "value"},
                            null, null, null, null, null);
                    if (c != null) {
                        SharedPreferences.Editor e = mDeviceProperties.edit();
                        while (c.moveToNext()) {
                            String name = c.getString(1);
                            String value = c.getString(2);
                            e.putString(name, value);
                        }
                        e.commit();
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "failed to migrate device properties", e);
            } finally {
                if (c != null) c.close();
                if (db != null) db.close();
            }
            context.deleteDatabase(devicePropertiesName);
        }
        mHostType = "";
        mSkipThumbForHost = false;
        mHostIsWindows = false;
    }

    @VisibleForNative
    @VisibleForTesting
    public int beginSendObject(String path, int format, int parent, int storageId) {
        MtpStorageManager.MtpObject parentObj =
                parent == 0 ? mManager.getStorageRoot(storageId) : mManager.getObject(parent);
        if (parentObj == null) {
            return -1;
        }

        Path objPath = Paths.get(path);
        return mManager.beginSendObject(parentObj, objPath.getFileName().toString(), format);
    }

    @VisibleForNative
    private void endSendObject(int handle, boolean succeeded) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null || !mManager.endSendObject(obj, succeeded)) {
            Log.e(TAG, "Failed to successfully end send object");
            return;
        }
        // Add the new file to MediaProvider
        if (succeeded) {
            updateMediaStore(mContext, obj.getPath().toFile());
        }
    }

    @VisibleForNative
    private void rescanFile(String path, int handle, int format) {
        MediaStore.scanFile(mContext.getContentResolver(), new File(path));
    }

    @VisibleForNative
    private int[] getObjectList(int storageID, int format, int parent) {
        List objs = mManager.getObjects(parent,
                format, storageID);
        if (objs == null) {
            return null;
        }
        int[] ret = new int[objs.size()];
        for (int i = 0; i < objs.size(); i++) {
            ret[i] = objs.get(i).getId();
        }
        return ret;
    }

    @VisibleForNative
    @VisibleForTesting
    public int getNumObjects(int storageID, int format, int parent) {
        List objs = mManager.getObjects(parent,
                format, storageID);
        if (objs == null) {
            return -1;
        }
        return objs.size();
    }

    @VisibleForNative
    private MtpPropertyList getObjectPropertyList(int handle, int format, int property,
            int groupCode, int depth) {
        // FIXME - implement group support
        if (property == 0) {
            if (groupCode == 0) {
                return new MtpPropertyList(MtpConstants.RESPONSE_PARAMETER_NOT_SUPPORTED);
            }
            return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
        }
        if (depth == 0xFFFFFFFF && (handle == 0 || handle == 0xFFFFFFFF)) {
            // request all objects starting at root
            handle = 0xFFFFFFFF;
            depth = 0;
        }
        if (!(depth == 0 || depth == 1)) {
            // we only support depth 0 and 1
            // depth 0: single object, depth 1: immediate children
            return new MtpPropertyList(MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
        }
        List objs = null;
        MtpStorageManager.MtpObject thisObj = null;
        if (handle == 0xFFFFFFFF) {
            // All objects are requested
            objs = mManager.getObjects(0, format, 0xFFFFFFFF);
            if (objs == null) {
                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
            }
        } else if (handle != 0) {
            // Add the requested object if format matches
            MtpStorageManager.MtpObject obj = mManager.getObject(handle);
            if (obj == null) {
                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
            }
            if (obj.getFormat() == format || format == 0) {
                thisObj = obj;
            }
        }
        if (handle == 0 || depth == 1) {
            if (handle == 0) {
                handle = 0xFFFFFFFF;
            }
            // Get the direct children of root or this object.
            objs = mManager.getObjects(handle, format,
                    0xFFFFFFFF);
            if (objs == null) {
                return new MtpPropertyList(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
            }
        }
        if (objs == null) {
            objs = new ArrayList<>();
        }
        if (thisObj != null) {
            objs.add(thisObj);
        }

        MtpPropertyList ret = new MtpPropertyList(MtpConstants.RESPONSE_OK);
        MtpPropertyGroup propertyGroup;
        for (MtpStorageManager.MtpObject obj : objs) {
            if (property == 0xffffffff) {
                if (format == 0 && handle != 0 && handle != 0xffffffff) {
                    // return properties based on the object's format
                    format = obj.getFormat();
                }
                // Get all properties supported by this object
                // format should be the same between get & put
                propertyGroup = mPropertyGroupsByFormat.get(format);
                if (propertyGroup == null) {
                    final int[] propertyList = getSupportedObjectProperties(format);
                    propertyGroup = new MtpPropertyGroup(propertyList);
                    mPropertyGroupsByFormat.put(format, propertyGroup);
                }
            } else {
                // Get this property value
                propertyGroup = mPropertyGroupsByProperty.get(property);
                if (propertyGroup == null) {
                    final int[] propertyList = new int[]{property};
                    propertyGroup = new MtpPropertyGroup(propertyList);
                    mPropertyGroupsByProperty.put(property, propertyGroup);
                }
            }
            int err = propertyGroup.getPropertyList(mMediaProvider, obj.getVolumeName(), obj, ret);
            if (err != MtpConstants.RESPONSE_OK) {
                return new MtpPropertyList(err);
            }
        }
        return ret;
    }

    private int renameFile(int handle, String newName) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null) {
            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
        }
        Path oldPath = obj.getPath();

        // now rename the file.  make sure this succeeds before updating database
        if (!mManager.beginRenameObject(obj, newName))
            return MtpConstants.RESPONSE_GENERAL_ERROR;
        Path newPath = obj.getPath();
        boolean success = oldPath.toFile().renameTo(newPath.toFile());
        try {
            Os.access(oldPath.toString(), OsConstants.F_OK);
            Os.access(newPath.toString(), OsConstants.F_OK);
        } catch (ErrnoException e) {
            // Ignore. Could fail if the metadata was already updated.
        }

        if (!mManager.endRenameObject(obj, oldPath.getFileName().toString(), success)) {
            Log.e(TAG, "Failed to end rename object");
        }
        if (!success) {
            return MtpConstants.RESPONSE_GENERAL_ERROR;
        }

        updateMediaStore(mContext, oldPath.toFile());
        updateMediaStore(mContext, newPath.toFile());
        return MtpConstants.RESPONSE_OK;
    }

    @VisibleForNative
    private int beginMoveObject(int handle, int newParent, int newStorage) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        MtpStorageManager.MtpObject parent = newParent == 0 ?
                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
        if (obj == null || parent == null)
            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;

        boolean allowed = mManager.beginMoveObject(obj, parent);
        return allowed ? MtpConstants.RESPONSE_OK : MtpConstants.RESPONSE_GENERAL_ERROR;
    }

    @VisibleForNative
    private void endMoveObject(int oldParent, int newParent, int oldStorage, int newStorage,
            int objId, boolean success) {
        MtpStorageManager.MtpObject oldParentObj = oldParent == 0 ?
                mManager.getStorageRoot(oldStorage) : mManager.getObject(oldParent);
        MtpStorageManager.MtpObject newParentObj = newParent == 0 ?
                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
        MtpStorageManager.MtpObject obj = mManager.getObject(objId);
        String name = obj.getName();
        if (newParentObj == null || oldParentObj == null
                ||!mManager.endMoveObject(oldParentObj, newParentObj, name, success)) {
            Log.e(TAG, "Failed to end move object");
            return;
        }
        obj = mManager.getObject(objId);
        if (!success || obj == null)
            return;

        Path path = newParentObj.getPath().resolve(name);
        Path oldPath = oldParentObj.getPath().resolve(name);

        updateMediaStore(mContext, oldPath.toFile());
        updateMediaStore(mContext, path.toFile());
    }

    @VisibleForNative
    private int beginCopyObject(int handle, int newParent, int newStorage) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        MtpStorageManager.MtpObject parent = newParent == 0 ?
                mManager.getStorageRoot(newStorage) : mManager.getObject(newParent);
        if (obj == null || parent == null)
            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
        return mManager.beginCopyObject(obj, parent);
    }

    @VisibleForNative
    private void endCopyObject(int handle, boolean success) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null || !mManager.endCopyObject(obj, success)) {
            Log.e(TAG, "Failed to end copy object");
            return;
        }
        if (!success) {
            return;
        }

        updateMediaStore(mContext, obj.getPath().toFile());
    }

    private static void updateMediaStore(@NonNull Context context, @NonNull File file) {
        final ContentResolver resolver = context.getContentResolver();
        // For file, check whether the file name is .nomedia or not.
        // If yes, scan the parent directory to update all files in the directory.
        if (!file.isDirectory() && file.getName().toLowerCase(Locale.ROOT).endsWith(NO_MEDIA)) {
            MediaStore.scanFile(resolver, file.getParentFile());
        } else {
            MediaStore.scanFile(resolver, file);
        }
    }

    @VisibleForNative
    private int setObjectProperty(int handle, int property,
            long intValue, String stringValue) {
        switch (property) {
            case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
                return renameFile(handle, stringValue);

            default:
                return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
        }
    }

    @VisibleForNative
    private int getDeviceProperty(int property, long[] outIntValue, char[] outStringValue) {
        int length;
        String value;

        switch (property) {
            case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
            case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
                // writable string properties kept in shared preferences
                value = mDeviceProperties.getString(Integer.toString(property), "");
                length = value.length();
                if (length > 255) {
                    length = 255;
                }
                value.getChars(0, length, outStringValue, 0);
                outStringValue[length] = 0;
                return MtpConstants.RESPONSE_OK;
            case MtpConstants.DEVICE_PROPERTY_SESSION_INITIATOR_VERSION_INFO:
                value = mHostType;
                length = value.length();
                if (length > 255) {
                    length = 255;
                }
                value.getChars(0, length, outStringValue, 0);
                outStringValue[length] = 0;
                return MtpConstants.RESPONSE_OK;
            case MtpConstants.DEVICE_PROPERTY_IMAGE_SIZE:
                // use screen size as max image size
                // TODO(b/147721765): Add support for foldables/multi-display devices.
                Display display = ((WindowManager) mContext.getSystemService(
                        Context.WINDOW_SERVICE)).getDefaultDisplay();
                int width = display.getMaximumSizeDimension();
                int height = display.getMaximumSizeDimension();
                String imageSize = Integer.toString(width) + "x" + Integer.toString(height);
                imageSize.getChars(0, imageSize.length(), outStringValue, 0);
                outStringValue[imageSize.length()] = 0;
                return MtpConstants.RESPONSE_OK;
            case MtpConstants.DEVICE_PROPERTY_PERCEIVED_DEVICE_TYPE:
                outIntValue[0] = mDeviceType;
                return MtpConstants.RESPONSE_OK;
            case MtpConstants.DEVICE_PROPERTY_BATTERY_LEVEL:
                outIntValue[0] = mBatteryLevel;
                outIntValue[1] = mBatteryScale;
                return MtpConstants.RESPONSE_OK;
            default:
                return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
        }
    }

    @VisibleForNative
    private int setDeviceProperty(int property, long intValue, String stringValue) {
        switch (property) {
            case MtpConstants.DEVICE_PROPERTY_SYNCHRONIZATION_PARTNER:
            case MtpConstants.DEVICE_PROPERTY_DEVICE_FRIENDLY_NAME:
                // writable string properties kept in shared prefs
                SharedPreferences.Editor e = mDeviceProperties.edit();
                e.putString(Integer.toString(property), stringValue);
                return (e.commit() ? MtpConstants.RESPONSE_OK
                        : MtpConstants.RESPONSE_GENERAL_ERROR);
            case MtpConstants.DEVICE_PROPERTY_SESSION_INITIATOR_VERSION_INFO:
                mHostType = stringValue;
                Log.d(TAG, "setDeviceProperty." + Integer.toHexString(property)
                        + "=" + stringValue);
                if (stringValue.startsWith("Android/")) {
                    mSkipThumbForHost = true;
                } else if (stringValue.startsWith("Windows/")) {
                    mHostIsWindows = true;
                }
                return MtpConstants.RESPONSE_OK;
        }

        return MtpConstants.RESPONSE_DEVICE_PROP_NOT_SUPPORTED;
    }

    @VisibleForNative
    private boolean getObjectInfo(int handle, int[] outStorageFormatParent,
            char[] outName, long[] outCreatedModified) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null) {
            return false;
        }
        outStorageFormatParent[0] = obj.getStorageId();
        outStorageFormatParent[1] = obj.getFormat();
        outStorageFormatParent[2] = obj.getParent().isRoot() ? 0 : obj.getParent().getId();

        int nameLen = Integer.min(obj.getName().length(), 255);
        obj.getName().getChars(0, nameLen, outName, 0);
        outName[nameLen] = 0;

        outCreatedModified[0] = obj.getModifiedTime();
        outCreatedModified[1] = obj.getModifiedTime();
        return true;
    }

    @VisibleForNative
    private int getObjectFilePath(int handle, char[] outFilePath, long[] outFileLengthFormat) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null) {
            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
        }

        String path = obj.getPath().toString();
        int pathLen = Integer.min(path.length(), 4096);
        path.getChars(0, pathLen, outFilePath, 0);
        outFilePath[pathLen] = 0;

        outFileLengthFormat[0] = obj.getSize();
        outFileLengthFormat[1] = obj.getFormat();
        return MtpConstants.RESPONSE_OK;
    }

    @VisibleForNative
    private int openFilePath(String path, boolean transcode) {
        Uri uri = MediaStore.scanFile(mContext.getContentResolver(), new File(path));
        if (uri == null) {
            Log.i(TAG, "Failed to obtain URI for openFile with transcode support: " + path);
            return -1;
        }

        try {
            Log.i(TAG, "openFile with transcode support: " + path);
            Bundle bundle = new Bundle();
            if (transcode) {
                bundle.putParcelable(MediaStore.EXTRA_MEDIA_CAPABILITIES,
                        new ApplicationMediaCapabilities.Builder().addUnsupportedVideoMimeType(
                                MediaFormat.MIMETYPE_VIDEO_HEVC).build());
            } else {
                bundle.putParcelable(MediaStore.EXTRA_MEDIA_CAPABILITIES,
                        new ApplicationMediaCapabilities.Builder().addSupportedVideoMimeType(
                                MediaFormat.MIMETYPE_VIDEO_HEVC).build());
            }
            return mMediaProvider.openTypedAssetFileDescriptor(uri, "*/*", bundle)
                    .getParcelFileDescriptor().detachFd();
        } catch (RemoteException | FileNotFoundException e) {
            Log.w(TAG, "Failed to openFile with transcode support: " + path, e);
            return -1;
        }
    }

    private int getObjectFormat(int handle) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null) {
            return -1;
        }
        return obj.getFormat();
    }

    private byte[] getThumbnailProcess(String path, Bitmap bitmap) {
        try {
            if (bitmap == null) {
                Log.d(TAG, "getThumbnailProcess: Fail to generate thumbnail. Probably unsupported or corrupted image");
                return null;
            }

            ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteStream);

            if (byteStream.size() > MAX_THUMB_SIZE) {
                Log.w(TAG, "getThumbnailProcess: size=" + byteStream.size());
                return null;
            }

            byte[] byteArray = byteStream.toByteArray();

            return byteArray;
        } catch (OutOfMemoryError oomEx) {
            Log.w(TAG, "OutOfMemoryError:" + oomEx);
        }
        return null;
    }

    @VisibleForNative
    @VisibleForTesting
    public boolean getThumbnailInfo(int handle, long[] outLongs) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null) {
            return false;
        }

        String path = obj.getPath().toString();
        switch (obj.getFormat()) {
            case MtpConstants.FORMAT_HEIF:
            case MtpConstants.FORMAT_EXIF_JPEG:
            case MtpConstants.FORMAT_JFIF:
                try {
                    ExifInterface exif = new ExifInterface(path);
                    long[] thumbOffsetAndSize = exif.getThumbnailRange();
                    outLongs[0] = thumbOffsetAndSize != null ? thumbOffsetAndSize[1] : 0;
                    outLongs[1] = exif.getAttributeInt(ExifInterface.TAG_PIXEL_X_DIMENSION, 0);
                    outLongs[2] = exif.getAttributeInt(ExifInterface.TAG_PIXEL_Y_DIMENSION, 0);
                    if (mSkipThumbForHost) {
                        Log.d(TAG, "getThumbnailInfo: Skip runtime thumbnail.");
                        return true;
                    }
                    if (exif.getThumbnailRange() != null) {
                        if ((outLongs[0] == 0) || (outLongs[1] == 0) || (outLongs[2] == 0)) {
                            Log.d(TAG, "getThumbnailInfo: check thumb info:"
                                    + thumbOffsetAndSize[0] + "," + thumbOffsetAndSize[1]
                                    + "," + outLongs[1] + "," + outLongs[2]);
                        }

                        return true;
                    }
                } catch (IOException e) {
                    // ignore and fall through
                }

// Note: above formats will fall through and go on below thumbnail generation if Exif processing fails
            case MtpConstants.FORMAT_PNG:
            case MtpConstants.FORMAT_GIF:
            case MtpConstants.FORMAT_BMP:
                outLongs[0] = MAX_THUMB_SIZE;
            // only non-zero Width & Height needed. Actual size will be retrieved upon getThumbnailData by Host
                outLongs[1] = 320;
                outLongs[2] = 240;
                return true;
        }
        return false;
    }

    @VisibleForNative
    @VisibleForTesting
    public byte[] getThumbnailData(int handle) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null) {
            return null;
        }

        String path = obj.getPath().toString();
        switch (obj.getFormat()) {
            case MtpConstants.FORMAT_HEIF:
            case MtpConstants.FORMAT_EXIF_JPEG:
            case MtpConstants.FORMAT_JFIF:
                try {
                    ExifInterface exif = new ExifInterface(path);

                    if (mSkipThumbForHost) {
                        Log.d(TAG, "getThumbnailData: Skip runtime thumbnail.");
                        return exif.getThumbnail();
                    }
                    if (exif.getThumbnailRange() != null)
                        return exif.getThumbnail();
                } catch (IOException e) {
                    // ignore and fall through
                }

// Note: above formats will fall through and go on below thumbnail generation if Exif processing fails
            case MtpConstants.FORMAT_PNG:
            case MtpConstants.FORMAT_GIF:
            case MtpConstants.FORMAT_BMP:
                {
                    Bitmap bitmap = ThumbnailUtils.createImageThumbnail(path, MediaStore.Images.Thumbnails.MINI_KIND);
                    byte[] byteArray = getThumbnailProcess(path, bitmap);

                    return byteArray;
                }
        }
        return null;
    }

    @VisibleForNative
    private int beginDeleteObject(int handle) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null) {
            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
        }
        if (!mManager.beginRemoveObject(obj)) {
            return MtpConstants.RESPONSE_GENERAL_ERROR;
        }
        return MtpConstants.RESPONSE_OK;
    }

    @VisibleForNative
    private void endDeleteObject(int handle, boolean success) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null) {
            return;
        }
        if (!mManager.endRemoveObject(obj, success))
            Log.e(TAG, "Failed to end remove object");
        if (success)
            deleteFromMedia(obj, obj.getPath(), obj.isDir());
    }

    private void deleteFromMedia(MtpStorageManager.MtpObject obj, Path path, boolean isDir) {
        final Uri objectsUri = MediaStore.Files.getContentUri(obj.getVolumeName());
        try {
            // Delete the object(s) from MediaProvider, but ignore errors.
            if (isDir) {
                // recursive case - delete all children first
                mMediaProvider.delete(objectsUri,
                        // the 'like' makes it use the index, the 'lower()' makes it correct
                        // when the path contains sqlite wildcard characters
                        "_data LIKE ?1 AND lower(substr(_data,1,?2))=lower(?3)",
                        new String[]{path + "/%", Integer.toString(path.toString().length() + 1),
                                path.toString() + "/"});
            }

            String[] whereArgs = new String[]{path.toString()};
            if (mMediaProvider.delete(objectsUri, PATH_WHERE, whereArgs) == 0) {
                Log.i(TAG, "MediaProvider didn't delete " + path);
            }
            updateMediaStore(mContext, path.toFile());
        } catch (Exception e) {
            Log.d(TAG, "Failed to delete " + path + " from MediaProvider");
        }
    }

    @VisibleForNative
    private int[] getObjectReferences(int handle) {
        return null;
    }

    @VisibleForNative
    private int setObjectReferences(int handle, int[] references) {
        return MtpConstants.RESPONSE_OPERATION_NOT_SUPPORTED;
    }

    @VisibleForNative
    private long mNativeContext;

    private native final void native_setup();
    private native final void native_finalize();
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy