1 /* 2 * Copyright (C) 2020 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 package android.mtp; 17 18 import android.annotation.NonNull; 19 import android.content.Context; 20 import android.graphics.Bitmap; 21 import android.graphics.BitmapFactory; 22 import android.os.Build; 23 import android.os.FileUtils; 24 import android.os.UserHandle; 25 import android.os.storage.StorageManager; 26 import android.os.storage.StorageVolume; 27 import android.util.Log; 28 29 import androidx.test.InstrumentationRegistry; 30 import androidx.test.filters.SmallTest; 31 import androidx.test.runner.AndroidJUnit4; 32 33 import com.android.internal.util.Preconditions; 34 35 import org.junit.After; 36 import org.junit.Assert; 37 import org.junit.Assume; 38 import org.junit.Before; 39 import org.junit.Test; 40 import org.junit.runner.RunWith; 41 42 import java.io.ByteArrayOutputStream; 43 import java.io.File; 44 import java.io.FileOutputStream; 45 import java.io.IOException; 46 import java.io.InputStream; 47 import java.io.OutputStream; 48 49 /** 50 * Tests for MtpDatabase functionality. 51 */ 52 @RunWith(AndroidJUnit4.class) 53 public class MtpDatabaseTest { 54 private static final String TAG = MtpDatabaseTest.class.getSimpleName(); 55 56 private final Context mContext = InstrumentationRegistry.getContext(); 57 58 private static final File mBaseDir = InstrumentationRegistry.getContext().getExternalCacheDir(); 59 private static final String MAIN_STORAGE_DIR = mBaseDir.getPath() + "/" + TAG + "/"; 60 private static final String TEST_DIRNAME = "/TestIs"; 61 62 private static final int MAIN_STORAGE_ID = 0x10001; 63 private static final int SCND_STORAGE_ID = 0x20001; 64 private static final String MAIN_STORAGE_ID_STR = Integer.toHexString(MAIN_STORAGE_ID); 65 private static final String SCND_STORAGE_ID_STR = Integer.toHexString(SCND_STORAGE_ID); 66 67 private static final File mMainStorageDir = new File(MAIN_STORAGE_DIR); 68 69 private static ServerHolder mServerHolder; 70 private MtpDatabase mMtpDatabase; 71 logMethodName()72 private static void logMethodName() { 73 Log.d(TAG, Thread.currentThread().getStackTrace()[3].getMethodName()); 74 } 75 createNewDir(File parent, String name)76 private static File createNewDir(File parent, String name) { 77 File ret = new File(parent, name); 78 if (!ret.mkdir()) 79 throw new AssertionError( 80 "Failed to create file: name=" + name + ", " + parent.getPath()); 81 return ret; 82 } 83 writeNewFile(File newFile)84 private static void writeNewFile(File newFile) { 85 try { 86 new FileOutputStream(newFile).write(new byte[] {0, 0, 0}); 87 } catch (IOException e) { 88 Assert.fail(); 89 } 90 } 91 writeNewFileFromByte(File newFile, byte[] byteData)92 private static void writeNewFileFromByte(File newFile, byte[] byteData) { 93 try { 94 new FileOutputStream(newFile).write(byteData); 95 } catch (IOException e) { 96 Assert.fail(); 97 } 98 } 99 100 private static class ServerHolder { 101 @NonNull final MtpServer server; 102 @NonNull final MtpDatabase database; 103 ServerHolder(@onNull MtpServer server, @NonNull MtpDatabase database)104 ServerHolder(@NonNull MtpServer server, @NonNull MtpDatabase database) { 105 Preconditions.checkNotNull(server); 106 Preconditions.checkNotNull(database); 107 this.server = server; 108 this.database = database; 109 } 110 close()111 void close() { 112 this.database.setServer(null); 113 } 114 } 115 116 private class OnServerTerminated implements Runnable { 117 @Override run()118 public void run() { 119 if (mServerHolder == null) { 120 Log.e(TAG, "mServerHolder is unexpectedly null."); 121 return; 122 } 123 mServerHolder.close(); 124 mServerHolder = null; 125 } 126 } 127 128 @Before setUp()129 public void setUp() { 130 FileUtils.deleteContentsAndDir(mMainStorageDir); 131 Assert.assertTrue(mMainStorageDir.mkdir()); 132 133 StorageVolume mainStorage = new StorageVolume(MAIN_STORAGE_ID_STR, 134 mMainStorageDir, mMainStorageDir, "Primary Storage", 135 true, false, true, false, -1, UserHandle.CURRENT, null /* uuid */, "", ""); 136 137 final StorageVolume primary = mainStorage; 138 139 mMtpDatabase = new MtpDatabase(mContext, null); 140 141 final MtpServer server = 142 new MtpServer(mMtpDatabase, null, false, 143 new OnServerTerminated(), Build.MANUFACTURER, 144 Build.MODEL, "1.0"); 145 mMtpDatabase.setServer(server); 146 mServerHolder = new ServerHolder(server, mMtpDatabase); 147 148 mMtpDatabase.addStorage(mainStorage); 149 } 150 151 @After tearDown()152 public void tearDown() { 153 FileUtils.deleteContentsAndDir(mMainStorageDir); 154 } 155 stageFile(int resId, File file)156 private File stageFile(int resId, File file) throws IOException { 157 try (InputStream source = mContext.getResources().openRawResource(resId); 158 OutputStream target = new FileOutputStream(file)) { 159 android.os.FileUtils.copy(source, target); 160 } 161 return file; 162 } 163 164 /** 165 * Refer to BitmapUtilTests, but keep here, 166 * so as to be aware of the behavior or interface change there 167 */ assertBitmapSize(int expectedWidth, int expectedHeight, Bitmap bitmap)168 private void assertBitmapSize(int expectedWidth, int expectedHeight, Bitmap bitmap) { 169 Assert.assertTrue( 170 "Abnormal bitmap.width: " + bitmap.getWidth(), bitmap.getWidth() >= expectedWidth); 171 Assert.assertTrue( 172 "Abnormal bitmap.height: " + bitmap.getHeight(), 173 bitmap.getHeight() >= expectedHeight); 174 } 175 createJpegRawData(int sourceWidth, int sourceHeight)176 private byte[] createJpegRawData(int sourceWidth, int sourceHeight) throws IOException { 177 return createRawData(Bitmap.CompressFormat.JPEG, sourceWidth, sourceHeight); 178 } 179 createPngRawData(int sourceWidth, int sourceHeight)180 private byte[] createPngRawData(int sourceWidth, int sourceHeight) throws IOException { 181 return createRawData(Bitmap.CompressFormat.PNG, sourceWidth, sourceHeight); 182 } 183 createRawData(Bitmap.CompressFormat format, int sourceWidth, int sourceHeight)184 private byte[] createRawData(Bitmap.CompressFormat format, int sourceWidth, int sourceHeight) 185 throws IOException { 186 // Create a temp bitmap as our source 187 Bitmap b = Bitmap.createBitmap(sourceWidth, sourceHeight, Bitmap.Config.ARGB_8888); 188 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); 189 b.compress(format, 50, outputStream); 190 final byte[] data = outputStream.toByteArray(); 191 outputStream.close(); 192 return data; 193 } 194 195 /** 196 * Decodes the bitmap with the given sample size 197 */ decodeBitmapFromBytes(byte[] bytes, int sampleSize)198 public static Bitmap decodeBitmapFromBytes(byte[] bytes, int sampleSize) { 199 final BitmapFactory.Options options; 200 if (sampleSize <= 1) { 201 options = null; 202 } else { 203 options = new BitmapFactory.Options(); 204 options.inSampleSize = sampleSize; 205 } 206 return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); 207 } 208 testThumbnail(int fileHandle, File imgFile, boolean isGoodThumb)209 private void testThumbnail(int fileHandle, File imgFile, boolean isGoodThumb) 210 throws IOException { 211 boolean isValidThumb; 212 byte[] byteArray; 213 long[] outLongs = new long[3]; 214 215 isValidThumb = mMtpDatabase.getThumbnailInfo(fileHandle, outLongs); 216 Assert.assertTrue(isValidThumb); 217 218 byteArray = mMtpDatabase.getThumbnailData(fileHandle); 219 220 if (isGoodThumb) { 221 Assert.assertNotNull("Fail to generate thumbnail:" + imgFile.getPath(), byteArray); 222 223 Bitmap testBitmap = decodeBitmapFromBytes(byteArray, 4); 224 assertBitmapSize(32, 16, testBitmap); 225 } else Assert.assertNull("Bad image should return null:" + imgFile.getPath(), byteArray); 226 } 227 228 @Test 229 @SmallTest testMtpDatabaseThumbnail()230 public void testMtpDatabaseThumbnail() throws IOException { 231 int baseHandle; 232 int handleJpgBadThumb, handleJpgNoThumb, handleJpgBad; 233 int handlePng1, handlePngBad; 234 final String baseTestDirStr = mMainStorageDir.getPath() + TEST_DIRNAME; 235 236 logMethodName(); 237 238 Log.d(TAG, "testMtpDatabaseThumbnail: Generate and insert tested files."); 239 240 baseHandle = mMtpDatabase.beginSendObject(baseTestDirStr, 241 MtpConstants.FORMAT_ASSOCIATION, 0, MAIN_STORAGE_ID); 242 243 File baseDir = new File(baseTestDirStr); 244 baseDir.mkdirs(); 245 246 final File jpgfileBadThumb = new File(baseDir, "jpgfileBadThumb.jpg"); 247 final File jpgFileNoThumb = new File(baseDir, "jpgFileNoThumb.jpg"); 248 final File jpgfileBad = new File(baseDir, "jpgfileBad.jpg"); 249 final File pngFile1 = new File(baseDir, "pngFile1.png"); 250 final File pngFileBad = new File(baseDir, "pngFileBad.png"); 251 252 handleJpgBadThumb = mMtpDatabase.beginSendObject(jpgfileBadThumb.getPath(), 253 MtpConstants.FORMAT_EXIF_JPEG, baseHandle, MAIN_STORAGE_ID); 254 stageFile(R.raw.test_bad_thumb, jpgfileBadThumb); 255 256 handleJpgNoThumb = mMtpDatabase.beginSendObject(jpgFileNoThumb.getPath(), 257 MtpConstants.FORMAT_EXIF_JPEG, baseHandle, MAIN_STORAGE_ID); 258 writeNewFileFromByte(jpgFileNoThumb, createJpegRawData(128, 64)); 259 260 handleJpgBad = mMtpDatabase.beginSendObject(jpgfileBad.getPath(), 261 MtpConstants.FORMAT_EXIF_JPEG, baseHandle, MAIN_STORAGE_ID); 262 writeNewFile(jpgfileBad); 263 264 handlePng1 = mMtpDatabase.beginSendObject(pngFile1.getPath(), 265 MtpConstants.FORMAT_PNG, baseHandle, MAIN_STORAGE_ID); 266 writeNewFileFromByte(pngFile1, createPngRawData(128, 64)); 267 268 handlePngBad = mMtpDatabase.beginSendObject(pngFileBad.getPath(), 269 MtpConstants.FORMAT_PNG, baseHandle, MAIN_STORAGE_ID); 270 writeNewFile(pngFileBad); 271 272 Log.d(TAG, "testMtpDatabaseThumbnail: Test bad JPG"); 273 274 // Now we support to generate thumbnail if embedded thumbnail is corrupted or not existed 275 testThumbnail(handleJpgBadThumb, jpgfileBadThumb, true); 276 277 testThumbnail(handleJpgNoThumb, jpgFileNoThumb, true); 278 279 testThumbnail(handleJpgBad, jpgfileBad, false); 280 281 Log.d(TAG, "testMtpDatabaseThumbnail: Test PNG"); 282 283 testThumbnail(handlePng1, pngFile1, true); 284 285 Log.d(TAG, "testMtpDatabaseThumbnail: Test bad PNG"); 286 287 testThumbnail(handlePngBad, pngFileBad, false); 288 } 289 290 @Test 291 @SmallTest testMtpDatabaseExtStorage()292 public void testMtpDatabaseExtStorage() throws IOException { 293 int numObj; 294 StorageVolume[] mVolumes; 295 296 logMethodName(); 297 298 mVolumes = StorageManager.getVolumeList(UserHandle.myUserId(), 0); 299 // Currently it may need manual setup for 2nd storage on virtual device testing. 300 // Thus only run test when 2nd storage exists. 301 Assume.assumeTrue( 302 "Skip when 2nd storage not available, volume numbers = " + mVolumes.length, 303 mVolumes.length >= 2); 304 305 for (int ii = 0; ii < mVolumes.length; ii++) { 306 StorageVolume volume = mVolumes[ii]; 307 // Skip Actual Main storage (Internal Storage), 308 // since we use manipulated path as testing Main storage 309 if (ii > 0) 310 mMtpDatabase.addStorage(volume); 311 } 312 313 numObj = mMtpDatabase.getNumObjects(SCND_STORAGE_ID, 0, 0xFFFFFFFF); 314 Assert.assertTrue( 315 "Fail to get objects in 2nd storage, object numbers = " + numObj, numObj >= 0); 316 } 317 } 318