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

Commit f21e3a96 authored by James Wei's avatar James Wei Committed by Android (Google) Code Review
Browse files

Merge changes I7da699ca,I5a09efef into rvc-dev

* changes:
  MTP: Fix MTP SD card issue
  MTP: Add MtpDatabase unit test
parents 7df1a94f 82062b99
Loading
Loading
Loading
Loading
+9 −4
Original line number Diff line number Diff line
@@ -46,6 +46,7 @@ import android.view.Display;
import android.view.WindowManager;

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

import dalvik.system.CloseGuard;

@@ -408,7 +409,8 @@ public class MtpDatabase implements AutoCloseable {
    }

    @VisibleForNative
    private int beginSendObject(String path, int format, int parent, int storageId) {
    @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) {
@@ -452,7 +454,8 @@ public class MtpDatabase implements AutoCloseable {
    }

    @VisibleForNative
    private int getNumObjects(int storageID, int format, int parent) {
    @VisibleForTesting
    public int getNumObjects(int storageID, int format, int parent) {
        List<MtpStorageManager.MtpObject> objs = mManager.getObjects(parent,
                format, storageID);
        if (objs == null) {
@@ -830,7 +833,8 @@ public class MtpDatabase implements AutoCloseable {
    }

    @VisibleForNative
    private boolean getThumbnailInfo(int handle, long[] outLongs) {
    @VisibleForTesting
    public boolean getThumbnailInfo(int handle, long[] outLongs) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null) {
            return false;
@@ -866,7 +870,8 @@ public class MtpDatabase implements AutoCloseable {
    }

    @VisibleForNative
    private byte[] getThumbnailData(int handle) {
    @VisibleForTesting
    public byte[] getThumbnailData(int handle) {
        MtpStorageManager.MtpObject obj = mManager.getObject(handle);
        if (obj == null) {
            return null;
+1 −1
Original line number Diff line number Diff line
@@ -36,7 +36,7 @@ public class MtpStorage {

    public MtpStorage(StorageVolume volume, int storageId) {
        mStorageId = storageId;
        mPath = volume.getInternalPath();
        mPath = volume.getPath();
        mDescription = volume.getDescription(null);
        mRemovable = volume.isRemovable();
        mMaxFileSize = volume.getMaxFileSize();
+0 −0

Empty file added.

+316 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2020 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.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.FileUtils;
import android.os.UserHandle;
import android.os.storage.StorageManager;
import android.os.storage.StorageVolume;
import android.util.Log;

import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.internal.util.Preconditions;

import org.junit.After;
import org.junit.Assert;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

/**
 * Tests for MtpDatabase functionality.
 */
@RunWith(AndroidJUnit4.class)
public class MtpDatabaseTest {
    private static final String TAG = MtpDatabaseTest.class.getSimpleName();

    private final Context mContext = InstrumentationRegistry.getContext();

    private static final File mBaseDir = InstrumentationRegistry.getContext().getExternalCacheDir();
    private static final String MAIN_STORAGE_DIR = mBaseDir.getPath() + "/" + TAG + "/";
    private static final String TEST_DIRNAME = "/TestIs";

    private static final int MAIN_STORAGE_ID = 0x10001;
    private static final int SCND_STORAGE_ID = 0x20001;
    private static final String MAIN_STORAGE_ID_STR = Integer.toHexString(MAIN_STORAGE_ID);
    private static final String SCND_STORAGE_ID_STR = Integer.toHexString(SCND_STORAGE_ID);

    private static final File mMainStorageDir = new File(MAIN_STORAGE_DIR);

    private static ServerHolder mServerHolder;
    private MtpDatabase mMtpDatabase;

    private static void logMethodName() {
        Log.d(TAG, Thread.currentThread().getStackTrace()[3].getMethodName());
    }

    private static File createNewDir(File parent, String name) {
        File ret = new File(parent, name);
        if (!ret.mkdir())
            throw new AssertionError(
                    "Failed to create file: name=" + name + ", " + parent.getPath());
        return ret;
    }

    private static void writeNewFile(File newFile) {
        try {
            new FileOutputStream(newFile).write(new byte[] {0, 0, 0});
        } catch (IOException e) {
            Assert.fail();
        }
    }

    private static void writeNewFileFromByte(File newFile, byte[] byteData) {
        try {
            new FileOutputStream(newFile).write(byteData);
        } catch (IOException e) {
            Assert.fail();
        }
    }

    private static class ServerHolder {
        @NonNull final MtpServer server;
        @NonNull final MtpDatabase database;

        ServerHolder(@NonNull MtpServer server, @NonNull MtpDatabase database) {
            Preconditions.checkNotNull(server);
            Preconditions.checkNotNull(database);
            this.server = server;
            this.database = database;
        }

        void close() {
            this.database.setServer(null);
        }
    }

    private class OnServerTerminated implements Runnable {
        @Override
        public void run() {
            if (mServerHolder == null) {
                Log.e(TAG, "mServerHolder is unexpectedly null.");
                return;
            }
            mServerHolder.close();
            mServerHolder = null;
        }
    }

    @Before
    public void setUp() {
        FileUtils.deleteContentsAndDir(mMainStorageDir);
        Assert.assertTrue(mMainStorageDir.mkdir());

        StorageVolume mainStorage = new StorageVolume(MAIN_STORAGE_ID_STR,
                mMainStorageDir, mMainStorageDir, "Primary Storage",
				true, false, true, false, -1, UserHandle.CURRENT, "", "");

        final StorageVolume primary = mainStorage;

        mMtpDatabase = new MtpDatabase(mContext, null);

        final MtpServer server =
                new MtpServer(mMtpDatabase, null, false,
                        new OnServerTerminated(), Build.MANUFACTURER,
                        Build.MODEL, "1.0");
        mMtpDatabase.setServer(server);
        mServerHolder = new ServerHolder(server, mMtpDatabase);

        mMtpDatabase.addStorage(mainStorage);
    }

    @After
    public void tearDown() {
        FileUtils.deleteContentsAndDir(mMainStorageDir);
    }

    private File stageFile(int resId, File file) throws IOException {
        try (InputStream source = mContext.getResources().openRawResource(resId);
                OutputStream target = new FileOutputStream(file)) {
            android.os.FileUtils.copy(source, target);
        }
        return file;
    }

    /**
     * Refer to BitmapUtilTests, but keep here,
	 * so as to be aware of the behavior or interface change there
     */
    private void assertBitmapSize(int expectedWidth, int expectedHeight, Bitmap bitmap) {
        Assert.assertTrue(
                "Abnormal bitmap.width: " + bitmap.getWidth(), bitmap.getWidth() >= expectedWidth);
        Assert.assertTrue(
                "Abnormal bitmap.height: " + bitmap.getHeight(),
                bitmap.getHeight() >= expectedHeight);
    }

    private byte[] createJpegRawData(int sourceWidth, int sourceHeight) throws IOException {
        return createRawData(Bitmap.CompressFormat.JPEG, sourceWidth, sourceHeight);
    }

    private byte[] createPngRawData(int sourceWidth, int sourceHeight) throws IOException {
        return createRawData(Bitmap.CompressFormat.PNG, sourceWidth, sourceHeight);
    }

    private byte[] createRawData(Bitmap.CompressFormat format, int sourceWidth, int sourceHeight)
            throws IOException {
        // Create a temp bitmap as our source
        Bitmap b = Bitmap.createBitmap(sourceWidth, sourceHeight, Bitmap.Config.ARGB_8888);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        b.compress(format, 50, outputStream);
        final byte[] data = outputStream.toByteArray();
        outputStream.close();
        return data;
    }

    /**
     * Decodes the bitmap with the given sample size
     */
    public static Bitmap decodeBitmapFromBytes(byte[] bytes, int sampleSize) {
        final BitmapFactory.Options options;
        if (sampleSize <= 1) {
            options = null;
        } else {
            options = new BitmapFactory.Options();
            options.inSampleSize = sampleSize;
        }
        return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options);
    }

    private void testThumbnail(int fileHandle, File imgFile, boolean isGoodThumb)
            throws IOException {
        boolean isValidThumb;
        byte[] byteArray;
        long[] outLongs = new long[3];

        isValidThumb = mMtpDatabase.getThumbnailInfo(fileHandle, outLongs);
        Assert.assertTrue(isValidThumb);

        byteArray = mMtpDatabase.getThumbnailData(fileHandle);

        if (isGoodThumb) {
            Assert.assertNotNull("Fail to generate thumbnail:" + imgFile.getPath(), byteArray);

            Bitmap testBitmap = decodeBitmapFromBytes(byteArray, 4);
            assertBitmapSize(32, 16, testBitmap);
        } else Assert.assertNull("Bad image should return null:" + imgFile.getPath(), byteArray);
    }

    @Test
    @SmallTest
    public void testMtpDatabaseThumbnail() throws IOException {
        int baseHandle;
        int handleJpgBadThumb, handleJpgNoThumb, handleJpgBad;
        int handlePng1, handlePngBad;
        final String baseTestDirStr = mMainStorageDir.getPath() + TEST_DIRNAME;

        logMethodName();

        Log.d(TAG, "testMtpDatabaseThumbnail: Generate and insert tested files.");

        baseHandle = mMtpDatabase.beginSendObject(baseTestDirStr,
                MtpConstants.FORMAT_ASSOCIATION, 0, MAIN_STORAGE_ID);

        File baseDir = new File(baseTestDirStr);
        baseDir.mkdirs();

        final File jpgfileBadThumb = new File(baseDir, "jpgfileBadThumb.jpg");
        final File jpgFileNoThumb = new File(baseDir, "jpgFileNoThumb.jpg");
        final File jpgfileBad = new File(baseDir, "jpgfileBad.jpg");
        final File pngFile1 = new File(baseDir, "pngFile1.png");
        final File pngFileBad = new File(baseDir, "pngFileBad.png");

        handleJpgBadThumb = mMtpDatabase.beginSendObject(jpgfileBadThumb.getPath(),
                MtpConstants.FORMAT_EXIF_JPEG, baseHandle, MAIN_STORAGE_ID);
        stageFile(R.raw.test_bad_thumb, jpgfileBadThumb);

        handleJpgNoThumb = mMtpDatabase.beginSendObject(jpgFileNoThumb.getPath(),
                MtpConstants.FORMAT_EXIF_JPEG, baseHandle, MAIN_STORAGE_ID);
        writeNewFileFromByte(jpgFileNoThumb, createJpegRawData(128, 64));

        handleJpgBad = mMtpDatabase.beginSendObject(jpgfileBad.getPath(),
                MtpConstants.FORMAT_EXIF_JPEG, baseHandle, MAIN_STORAGE_ID);
        writeNewFile(jpgfileBad);

        handlePng1 = mMtpDatabase.beginSendObject(pngFile1.getPath(),
                MtpConstants.FORMAT_PNG, baseHandle, MAIN_STORAGE_ID);
        writeNewFileFromByte(pngFile1, createPngRawData(128, 64));

        handlePngBad = mMtpDatabase.beginSendObject(pngFileBad.getPath(),
                MtpConstants.FORMAT_PNG, baseHandle, MAIN_STORAGE_ID);
        writeNewFile(pngFileBad);

        Log.d(TAG, "testMtpDatabaseThumbnail: Test bad JPG");

        testThumbnail(handleJpgBadThumb, jpgfileBadThumb, false);

        testThumbnail(handleJpgNoThumb, jpgFileNoThumb, false);

        testThumbnail(handleJpgBad, jpgfileBad, false);

        Log.d(TAG, "testMtpDatabaseThumbnail: Test PNG");

        testThumbnail(handlePng1, pngFile1, true);

        Log.d(TAG, "testMtpDatabaseThumbnail: Test bad PNG");

        testThumbnail(handlePngBad, pngFileBad, false);
    }

    @Test
    @SmallTest
    public void testMtpDatabaseExtStorage() throws IOException {
        int numObj;
        StorageVolume[] mVolumes;

        logMethodName();

        mVolumes = StorageManager.getVolumeList(UserHandle.myUserId(), 0);
        // Currently it may need manual setup for 2nd storage on virtual device testing.
        // Thus only run test when 2nd storage exists.
        Assume.assumeTrue(
                "Skip when 2nd storage not available, volume numbers = " + mVolumes.length,
                mVolumes.length >= 2);

        for (int ii = 0; ii < mVolumes.length; ii++) {
            StorageVolume volume = mVolumes[ii];
            // Skip Actual Main storage (Internal Storage),
            // since we use manipulated path as testing Main storage
            if (ii > 0)
                mMtpDatabase.addStorage(volume);
        }

        numObj = mMtpDatabase.getNumObjects(SCND_STORAGE_ID, 0, 0xFFFFFFFF);
        Assert.assertTrue(
                "Fail to get objects in 2nd storage, object numbers = " + numObj, numObj >= 0);
    }
}