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

Commit 5d5b0a32 authored by Mike Lockwood's avatar Mike Lockwood Committed by Android (Google) Code Review
Browse files

Merge "MTP: Partial implementation of the GetObjectPropList command"

parents c0aaccc4 e2ad6ec1
Loading
Loading
Loading
Loading
+24 −0
Original line number Diff line number Diff line
@@ -21,6 +21,30 @@ package android.media;
 */
public final class MtpConstants {

// MTP Data Types
    public static final int TYPE_UNDEFINED = 0x0000;
    public static final int TYPE_INT8 = 0x0001;
    public static final int TYPE_UINT8 = 0x0002;
    public static final int TYPE_INT16 = 0x0003;
    public static final int TYPE_UINT16 = 0x0004;
    public static final int TYPE_INT32 = 0x0005;
    public static final int TYPE_UINT32 = 0x0006;
    public static final int TYPE_INT64 = 0x0007;
    public static final int TYPE_UINT64 = 0x0008;
    public static final int TYPE_INT128 = 0x0009;
    public static final int TYPE_UINT128 = 0x000A;
    public static final int TYPE_AINT8 = 0x4001;
    public static final int TYPE_AUINT8 = 0x4002;
    public static final int TYPE_AINT16 = 0x4003;
    public static final int TYPE_AUINT16 = 0x4004;
    public static final int TYPE_AINT32 = 0x4005;
    public static final int TYPE_AUINT32 = 0x4006;
    public static final int TYPE_AINT64 = 0x4007;
    public static final int TYPE_AUINT64 = 0x4008;
    public static final int TYPE_AINT128 = 0x4009;
    public static final int TYPE_AUINT128 = 0x400A;
    public static final int TYPE_STR = 0xFFFF;

// MTP Response Codes
    public static final int RESPONSE_UNDEFINED = 0x2000;
    public static final int RESPONSE_OK = 0x2001;
+255 −139
Original line number Diff line number Diff line
@@ -30,6 +30,7 @@ import android.provider.MediaStore.Files;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.MediaColumns;
import android.provider.Mtp;
import android.text.format.Time;
import android.util.Log;

import java.io.File;
@@ -428,6 +429,26 @@ public class MtpDatabase {
        }
    }

    private String queryAudio(int id, String column) {
        Cursor c = null;
        try {
            c = mMediaProvider.query(Audio.Media.getContentUri(mVolumeName),
                            new String [] { Files.FileColumns._ID, column },
                            ID_WHERE, new String[] { Integer.toString(id) }, null);
            if (c != null && c.moveToNext()) {
                return c.getString(1);
            } else {
                return "";
            }
        } catch (Exception e) {
            return null;
        } finally {
            if (c != null) {
                c.close();
            }
        }
    }

    private String queryGenre(int id) {
        Cursor c = null;
        try {
@@ -450,7 +471,7 @@ public class MtpDatabase {
        }
    }

    private boolean queryInt(int id, String column, long[] outValue) {
    private Long queryLong(int id, String column) {
        Cursor c = null;
        try {
            // for now we are only reading properties from the "objects" table
@@ -458,17 +479,15 @@ public class MtpDatabase {
                            new String [] { Files.FileColumns._ID, column },
                            ID_WHERE, new String[] { Integer.toString(id) }, null);
            if (c != null && c.moveToNext()) {
                outValue[0] = c.getLong(1);
                return true;
                return new Long(c.getLong(1));
            }
            return false;
        } catch (Exception e) {
            return false;
        } finally {
            if (c != null) {
                c.close();
            }
        }
        return null;
    }

    private String nameFromPath(String path) {
@@ -485,98 +504,165 @@ public class MtpDatabase {
        return path.substring(start, end);
    }

    private int renameFile(int handle, String newName) {
        Cursor c = null;

        // first compute current path
        String path = null;
        String[] whereArgs = new String[] {  Integer.toString(handle) };
        try {
            c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE, whereArgs, null);
            if (c != null && c.moveToNext()) {
                path = externalToMediaPath(c.getString(1));
            }
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException in getObjectFilePath", e);
            return MtpConstants.RESPONSE_GENERAL_ERROR;
        } finally {
            if (c != null) {
                c.close();
    private String formatDateTime(long seconds) {
        Time time = new Time(Time.TIMEZONE_UTC);
        time.set(seconds * 1000);
        String result = time.format("%Y-%m-%dT%H:%M:%SZ");
        Log.d(TAG, "formatDateTime returning " + result);
        return result;
    }

    private MtpPropertyList getObjectPropertyList(int handle, int format, int property,
                        int groupCode, int depth) {
        // FIXME - implement group support
        // For now we only support a single property at a time
        if (groupCode != 0) {
            return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_GROUP_UNSUPPORTED);
        }
        if (path == null) {
            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
        if (depth > 1) {
            return new MtpPropertyList(0, MtpConstants.RESPONSE_SPECIFICATION_BY_DEPTH_UNSUPPORTED);
        }

        // now rename the file.  make sure this succeeds before updating database
        File oldFile = new File(path);
        int lastSlash = path.lastIndexOf('/');
        if (lastSlash <= 1) {
            return MtpConstants.RESPONSE_GENERAL_ERROR;
        }
        String newPath = path.substring(0, lastSlash + 1) + newName;
        File newFile = new File(newPath);
        boolean success = oldFile.renameTo(newFile);
        Log.d(TAG, "renaming "+ path + " to " + newPath + (success ? " succeeded" : " failed"));
        if (!success) {
            return MtpConstants.RESPONSE_GENERAL_ERROR;
        String column = null;
        int type = MtpConstants.TYPE_UNDEFINED;

         switch (property) {
            case MtpConstants.PROPERTY_STORAGE_ID:
                // no query needed until we support multiple storage units
                // for now it is always mStorageID
                type = MtpConstants.TYPE_UINT32;
                break;
             case MtpConstants.PROPERTY_OBJECT_FORMAT:
                column = Files.FileColumns.FORMAT;
                type = MtpConstants.TYPE_UINT16;
                break;
            case MtpConstants.PROPERTY_PROTECTION_STATUS:
                // protection status is always 0
                type = MtpConstants.TYPE_UINT16;
                break;
            case MtpConstants.PROPERTY_OBJECT_SIZE:
                column = Files.FileColumns.SIZE;
                type = MtpConstants.TYPE_UINT64;
                break;
            case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
                column = Files.FileColumns.DATA;
                type = MtpConstants.TYPE_STR;
                break;
            case MtpConstants.PROPERTY_NAME:
                column = MediaColumns.TITLE;
                type = MtpConstants.TYPE_STR;
                break;
            case MtpConstants.PROPERTY_DATE_MODIFIED:
                column = Files.FileColumns.DATE_MODIFIED;
                type = MtpConstants.TYPE_STR;
                break;
            case MtpConstants.PROPERTY_DATE_ADDED:
                column = Files.FileColumns.DATE_ADDED;
                type = MtpConstants.TYPE_STR;
                break;
            case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
                column = Audio.AudioColumns.YEAR;
                type = MtpConstants.TYPE_STR;
                break;
            case MtpConstants.PROPERTY_PARENT_OBJECT:
                column = Files.FileColumns.PARENT;
                type = MtpConstants.TYPE_UINT32;
                break;
            case MtpConstants.PROPERTY_PERSISTENT_UID:
                // PUID is concatenation of storageID and object handle
                type = MtpConstants.TYPE_UINT128;
                break;
            case MtpConstants.PROPERTY_DURATION:
                column = Audio.AudioColumns.DURATION;
                type = MtpConstants.TYPE_UINT32;
                break;
            case MtpConstants.PROPERTY_TRACK:
                column = Audio.AudioColumns.TRACK;
                type = MtpConstants.TYPE_UINT16;
                break;
            case MtpConstants.PROPERTY_DISPLAY_NAME:
                column = MediaColumns.DISPLAY_NAME;
                type = MtpConstants.TYPE_STR;
                break;
            case MtpConstants.PROPERTY_ARTIST:
                type = MtpConstants.TYPE_STR;
                break;
            case MtpConstants.PROPERTY_ALBUM_NAME:
                type = MtpConstants.TYPE_STR;
                break;
            case MtpConstants.PROPERTY_ALBUM_ARTIST:
                column = Audio.AudioColumns.ALBUM_ARTIST;
                type = MtpConstants.TYPE_STR;
                break;
            case MtpConstants.PROPERTY_GENRE:
                // genre requires a special query
                type = MtpConstants.TYPE_STR;
                break;
            case MtpConstants.PROPERTY_COMPOSER:
                column = Audio.AudioColumns.COMPOSER;
                type = MtpConstants.TYPE_STR;
                break;
            case MtpConstants.PROPERTY_DESCRIPTION:
                column = Images.ImageColumns.DESCRIPTION;
                type = MtpConstants.TYPE_STR;
                break;
            default:
                return new MtpPropertyList(0, MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED);
        }

        // finally update database
        ContentValues values = new ContentValues();
        values.put(Files.FileColumns.DATA, newPath);
        int updated = 0;
        Cursor c = null;
        try {
            // note - we are relying on a special case in MediaProvider.update() to update
            // the paths for all children in the case where this is a directory.
            updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException in mMediaProvider.update", e);
            if (column != null) {
                c = mMediaProvider.query(mObjectsUri,
                        new String [] { Files.FileColumns._ID, column },
                        // depth 0: single record, depth 1: immediate children
                        (depth == 0 ? ID_WHERE : PARENT_WHERE),
                        new String[] { Integer.toString(handle) }, null);
                if (c == null) {
                    return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
                }
        if (updated == 0) {
            Log.e(TAG, "Unable to update path for " + path + " to " + newPath);
            // this shouldn't happen, but if it does we need to rename the file to its original name
            newFile.renameTo(oldFile);
            return MtpConstants.RESPONSE_GENERAL_ERROR;
            } else if (depth == 1) {
                c = mMediaProvider.query(mObjectsUri,
                        new String [] { Files.FileColumns._ID },
                        PARENT_WHERE, new String[] { Integer.toString(handle) }, null);
                if (c == null) {
                    return new MtpPropertyList(0, MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
                }

        return MtpConstants.RESPONSE_OK;
            }

    private int getObjectProperty(int handle, int property,
                            long[] outIntValue, char[] outStringValue) {
        Log.d(TAG, "getObjectProperty: " + property);
        String column = null;
        boolean isString = false;
            int count = (c == null ? 1 : c.getCount());
            MtpPropertyList result = new MtpPropertyList(count, MtpConstants.RESPONSE_OK);

            for (int index = 0; index < count; index++) {
                if (c != null) {
                    c.moveToNext();
                }
                if (depth == 1) {
                    handle = (int)c.getLong(0);
                }

                switch (property) {
                    // handle special cases here
                    case MtpConstants.PROPERTY_STORAGE_ID:
                outIntValue[0] = mStorageID;
                return MtpConstants.RESPONSE_OK;
            case MtpConstants.PROPERTY_OBJECT_FORMAT:
                column = Files.FileColumns.FORMAT;
                        result.setProperty(index, handle, property, MtpConstants.TYPE_UINT32,
                                mStorageID);
                        break;
                    case MtpConstants.PROPERTY_PROTECTION_STATUS:
                        // protection status is always 0
                outIntValue[0] = 0;
                return MtpConstants.RESPONSE_OK;
            case MtpConstants.PROPERTY_OBJECT_SIZE:
                column = Files.FileColumns.SIZE;
                        result.setProperty(index, handle, property, MtpConstants.TYPE_UINT16, 0);
                        break;
                    case MtpConstants.PROPERTY_OBJECT_FILE_NAME:
                        // special case - need to extract file name from full path
                String value = queryString(handle, Files.FileColumns.DATA);
                        String value = c.getString(1);
                        if (value != null) {
                    value = nameFromPath(value);
                    value.getChars(0, value.length(), outStringValue, 0);
                    outStringValue[value.length()] = 0;
                    return MtpConstants.RESPONSE_OK;
                            result.setProperty(index, handle, property, nameFromPath(value));
                        } else {
                    return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
                            result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
                        }
                        break;
                    case MtpConstants.PROPERTY_NAME:
                        // first try title
                String name = queryString(handle, MediaColumns.TITLE);
                        String name = c.getString(1);
                        // then try name
                        if (name == null) {
                            name = queryString(handle, Audio.PlaylistsColumns.NAME);
@@ -589,95 +675,125 @@ public class MtpDatabase {
                            }
                        }
                        if (name != null) {
                    name.getChars(0, name.length(), outStringValue, 0);
                    outStringValue[name.length()] = 0;
                    return MtpConstants.RESPONSE_OK;
                            result.setProperty(index, handle, property, name);
                        } else {
                    return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
                            result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
                        }
            case MtpConstants.PROPERTY_DATE_MODIFIED:
                column = Files.FileColumns.DATE_MODIFIED;
                        break;
                    case MtpConstants.PROPERTY_DATE_MODIFIED:
                    case MtpConstants.PROPERTY_DATE_ADDED:
                column = Files.FileColumns.DATE_ADDED;
                        // convert from seconds to DateTime
                        result.setProperty(index, handle, property, formatDateTime(c.getInt(1)));
                        break;
                    case MtpConstants.PROPERTY_ORIGINAL_RELEASE_DATE:
                column = Audio.AudioColumns.YEAR;
                break;
            case MtpConstants.PROPERTY_PARENT_OBJECT:
                column = Files.FileColumns.PARENT;
                        // release date is stored internally as just the year
                        int year = c.getInt(1);
                        String dateTime = Integer.toString(year) + "0101T000000";
                        result.setProperty(index, handle, property, dateTime);
                        break;
                    case MtpConstants.PROPERTY_PERSISTENT_UID:
                        // PUID is concatenation of storageID and object handle
                        long puid = mStorageID;
                        puid <<= 32;
                        puid += handle;
                outIntValue[0] = puid;
                return MtpConstants.RESPONSE_OK;
            case MtpConstants.PROPERTY_DURATION:
                column = Audio.AudioColumns.DURATION;
                        result.setProperty(index, handle, property, MtpConstants.TYPE_UINT128, puid);
                        break;
                    case MtpConstants.PROPERTY_TRACK:
                if (queryInt(handle, Audio.AudioColumns.TRACK, outIntValue)) {
                    // track is stored in lower 3 decimal digits
                    outIntValue[0] %= 1000;
                    return MtpConstants.RESPONSE_OK;
                } else {
                    return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
                }
            case MtpConstants.PROPERTY_DISPLAY_NAME:
                column = MediaColumns.DISPLAY_NAME;
                isString = true;
                        result.setProperty(index, handle, property, MtpConstants.TYPE_UINT16,
                                    c.getInt(1) % 1000);
                        break;
                    case MtpConstants.PROPERTY_ARTIST:
                column = Audio.AudioColumns.ARTIST;
                isString = true;
                        result.setProperty(index, handle, property, queryAudio(handle, Audio.AudioColumns.ARTIST));
                        break;
                    case MtpConstants.PROPERTY_ALBUM_NAME:
                column = Audio.AudioColumns.ALBUM;
                isString = true;
                break;
            case MtpConstants.PROPERTY_ALBUM_ARTIST:
                column = Audio.AudioColumns.ALBUM_ARTIST;
                isString = true;
                        result.setProperty(index, handle, property, queryAudio(handle, Audio.AudioColumns.ALBUM));
                        break;
                    case MtpConstants.PROPERTY_GENRE:
                        String genre = queryGenre(handle);
                        if (genre != null) {
                    genre.getChars(0, genre.length(), outStringValue, 0);
                    outStringValue[genre.length()] = 0;
                    return MtpConstants.RESPONSE_OK;
                            result.setProperty(index, handle, property, genre);
                        } else {
                    return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
                            result.setResult(MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE);
                        }
            case MtpConstants.PROPERTY_COMPOSER:
                column = Audio.AudioColumns.COMPOSER;
                isString = true;
                break;
            case MtpConstants.PROPERTY_DESCRIPTION:
                column = Images.ImageColumns.DESCRIPTION;
                isString = true;
                        break;
                    default:
                return MtpConstants.RESPONSE_OBJECT_PROP_NOT_SUPPORTED;
                        if (type == MtpConstants.TYPE_STR) {
                            result.setProperty(index, handle, property, c.getString(1));
                        } else {
                            result.setProperty(index, handle, property, type, c.getLong(1));
                        }
                }
            }

        if (isString) {
            String value = queryString(handle, column);
            if (value != null) {
                value.getChars(0, value.length(), outStringValue, 0);
                outStringValue[value.length()] = 0;
                return MtpConstants.RESPONSE_OK;
            return result;
        } catch (RemoteException e) {
            return new MtpPropertyList(0, MtpConstants.RESPONSE_GENERAL_ERROR);
        } finally {
            if (c != null) {
                c.close();
            }
        } else {
            if (queryInt(handle, column, outIntValue)) {
                return MtpConstants.RESPONSE_OK;
        }
        // impossible to get here, so no return statement
    }
        // query failed if we get here

    private int renameFile(int handle, String newName) {
        Cursor c = null;

        // first compute current path
        String path = null;
        String[] whereArgs = new String[] {  Integer.toString(handle) };
        try {
            c = mMediaProvider.query(mObjectsUri, PATH_PROJECTION, ID_WHERE, whereArgs, null);
            if (c != null && c.moveToNext()) {
                path = externalToMediaPath(c.getString(1));
            }
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException in getObjectFilePath", e);
            return MtpConstants.RESPONSE_GENERAL_ERROR;
        } finally {
            if (c != null) {
                c.close();
            }
        }
        if (path == null) {
            return MtpConstants.RESPONSE_INVALID_OBJECT_HANDLE;
        }

        // now rename the file.  make sure this succeeds before updating database
        File oldFile = new File(path);
        int lastSlash = path.lastIndexOf('/');
        if (lastSlash <= 1) {
            return MtpConstants.RESPONSE_GENERAL_ERROR;
        }
        String newPath = path.substring(0, lastSlash + 1) + newName;
        File newFile = new File(newPath);
        boolean success = oldFile.renameTo(newFile);
        Log.d(TAG, "renaming "+ path + " to " + newPath + (success ? " succeeded" : " failed"));
        if (!success) {
            return MtpConstants.RESPONSE_GENERAL_ERROR;
        }

        // finally update database
        ContentValues values = new ContentValues();
        values.put(Files.FileColumns.DATA, newPath);
        int updated = 0;
        try {
            // note - we are relying on a special case in MediaProvider.update() to update
            // the paths for all children in the case where this is a directory.
            updated = mMediaProvider.update(mObjectsUri, values, ID_WHERE, whereArgs);
        } catch (RemoteException e) {
            Log.e(TAG, "RemoteException in mMediaProvider.update", e);
        }
        if (updated == 0) {
            Log.e(TAG, "Unable to update path for " + path + " to " + newPath);
            // this shouldn't happen, but if it does we need to rename the file to its original name
            newFile.renameTo(oldFile);
            return MtpConstants.RESPONSE_GENERAL_ERROR;
        }

        return MtpConstants.RESPONSE_OK;
    }

    private int setObjectProperty(int handle, int property,
                            long intValue, String stringValue) {
        Log.d(TAG, "setObjectProperty: " + property);
+76 −0
Original line number Diff line number Diff line
/*
 * 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.media;

/**
 * Encapsulates the ObjectPropList dataset used by the GetObjectPropList command.
 * The fields of this class are read by JNI code in android_media_MtpDatabase.cpp
 *
 * {@hide}
 */

public class MtpPropertyList {

    // number of results returned
    public final int        mCount;
    // result code for GetObjectPropList
    public int              mResult;
    // list of object handles (first field in quadruplet)
    public final int[]      mObjectHandles;
    // list of object propery codes (second field in quadruplet)
    public final int[]      mPropertyCodes;
    // list of data type codes (third field in quadruplet)
    public final int[]     mDataTypes;
    // list of long int property values (fourth field in quadruplet, when value is integer type)
    public long[]     mLongValues;
    // list of long int property values (fourth field in quadruplet, when value is string type)
    public String[]   mStringValues;

    // constructor only called from MtpDatabase
    public MtpPropertyList(int count, int result) {
        mCount = count;
        mResult = result;
        mObjectHandles = new int[count];
        mPropertyCodes = new int[count];
        mDataTypes = new int[count];
        // mLongValues and mStringValues are created lazily since both might not be necessary
    }

    public void setProperty(int index, int handle, int property, int type, long value) {
        if (mLongValues == null) {
            mLongValues = new long[mCount];
        }
        mObjectHandles[index] = handle;
        mPropertyCodes[index] = property;
        mDataTypes[index] = type;
        mLongValues[index] = value;
    }

    public void setProperty(int index, int handle, int property, String value) {
        if (mStringValues == null) {
            mStringValues = new String[mCount];
        }
        mObjectHandles[index] = handle;
        mPropertyCodes[index] = property;
        mDataTypes[index] = MtpConstants.TYPE_STR;
        mStringValues[index] = value;
    }

    public void setResult(int result) {
        mResult = result;
    }
}
Loading