7 package com.google.appinventor.components.runtime.util;
9 import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
11 import android.annotation.SuppressLint;
12 import android.content.Context;
13 import android.content.res.AssetFileDescriptor;
14 import android.graphics.Bitmap;
15 import android.graphics.BitmapFactory;
16 import android.graphics.Rect;
17 import android.graphics.drawable.BitmapDrawable;
18 import android.media.MediaPlayer;
19 import android.media.SoundPool;
20 import android.net.Uri;
21 import android.os.Build;
22 import android.provider.Contacts;
23 import android.util.Log;
24 import android.view.Display;
25 import android.view.WindowManager;
26 import android.widget.VideoView;
32 import java.io.ByteArrayInputStream;
33 import java.io.ByteArrayOutputStream;
35 import java.io.FileDescriptor;
36 import java.io.FileInputStream;
37 import java.io.FilterInputStream;
38 import java.io.IOException;
39 import java.io.InputStream;
40 import java.lang.reflect.Array;
41 import java.net.MalformedURLException;
44 import java.util.HashMap;
46 import java.util.concurrent.ConcurrentHashMap;
55 private enum MediaSource { ASSET, REPL_ASSET, SDCARD, FILE_URL, URL, CONTENT_URI, CONTACT_URI }
57 private static final String LOG_TAG =
"MediaUtil";
64 private static class Synchronizer<T> {
65 private volatile boolean finished =
false;
69 public synchronized void waitfor() {
73 }
catch (InterruptedException e) {
78 public synchronized void wakeup(T result) {
84 public synchronized void error(String error) {
90 public T getResult() {
94 public String getError() {
103 static String fileUrlToFilePath(String mediaPath)
throws IOException {
105 return new File(
new URL(mediaPath).toURI()).getAbsolutePath();
106 }
catch (IllegalArgumentException e) {
107 throw new IOException(
"Unable to determine file path of file url " + mediaPath);
108 }
catch (Exception e) {
109 throw new IOException(
"Unable to determine file path of file url " + mediaPath);
133 @SuppressLint(
"SdCardPath")
134 private static MediaSource determineMediaSource(
Form form, String mediaPath) {
136 || mediaPath.startsWith(
"/sdcard/")) {
137 return MediaSource.SDCARD;
139 }
else if (mediaPath.startsWith(
"content://contacts/")) {
140 return MediaSource.CONTACT_URI;
142 }
else if (mediaPath.startsWith(
"content://")) {
143 return MediaSource.CONTENT_URI;
149 if (mediaPath.startsWith(
"file:")) {
150 return MediaSource.FILE_URL;
153 return MediaSource.URL;
155 }
catch (MalformedURLException e) {
160 if (((
ReplForm)form).isAssetsLoaded())
161 return MediaSource.REPL_ASSET;
163 return MediaSource.ASSET;
166 return MediaSource.ASSET;
180 @SuppressLint(
"SdCardPath")
183 Log.w(LOG_TAG,
"Calling deprecated version of isExternalFileUrl",
new IllegalAccessException());
185 || mediaPath.startsWith(
"file:///sdcard/");
195 @SuppressLint(
"SdCardPath")
197 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
202 || mediaPath.startsWith(
"file:///sdcard");
216 @SuppressLint(
"SdCardPath")
219 Log.w(LOG_TAG,
"Calling deprecated version of isExternalFile",
new IllegalAccessException());
231 @SuppressLint(
"SdCardPath")
233 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
241 private static ConcurrentHashMap<String, String> pathCache =
new ConcurrentHashMap<String, String>(2);
243 private static String findCaseinsensitivePath(
Form form, String mediaPath)
245 if( !pathCache.containsKey(mediaPath) ){
246 String newPath = findCaseinsensitivePathWithoutCache(form, mediaPath);
247 if( newPath ==
null){
250 pathCache.put(mediaPath, newPath);
252 return pathCache.get(mediaPath);
263 private static String findCaseinsensitivePathWithoutCache(Form form, String mediaPath)
265 String[] mediaPathlist = form.getAssets().list(
"");
266 int l = Array.getLength(mediaPathlist);
267 for (
int i=0; i<l; i++){
268 String temp = mediaPathlist[i];
269 if (temp.equalsIgnoreCase(mediaPath)){
283 private static InputStream getAssetsIgnoreCaseInputStream(Form form, String mediaPath)
286 return form.getAssets().open(mediaPath);
288 }
catch (IOException e) {
289 String path = findCaseinsensitivePath(form, mediaPath);
293 return form.getAssets().open(path);
298 private static InputStream openMedia(Form form, String mediaPath, MediaSource mediaSource)
300 switch (mediaSource) {
302 return getAssetsIgnoreCaseInputStream(form,mediaPath);
306 return new FileInputStream(
new java.io.File(URI.create(form.
getAssetPath(mediaPath))));
310 return new FileInputStream(mediaPath);
317 return new URL(mediaPath).openStream();
320 return form.getContentResolver().openInputStream(Uri.parse(mediaPath));
324 InputStream is =
null;
325 if (SdkLevel.getLevel() >= SdkLevel.LEVEL_HONEYCOMB_MR1) {
326 is = HoneycombMR1Util.openContactPhotoInputStreamHelper(form.getContentResolver(),
327 Uri.parse(mediaPath));
329 is = Contacts.People.openContactPhotoInputStream(form.getContentResolver(),
330 Uri.parse(mediaPath));
336 throw new IOException(
"Unable to open contact photo " + mediaPath +
".");
338 throw new IOException(
"Unable to open media " + mediaPath +
".");
341 public static InputStream
openMedia(
Form form, String mediaPath)
throws IOException {
342 return openMedia(form, mediaPath, determineMediaSource(form, mediaPath));
354 MediaSource mediaSource = determineMediaSource(form, mediaPath);
360 InputStream in = openMedia(form, mediaPath, mediaSource);
363 file = File.createTempFile(
"AI_Media_",
null);
368 }
catch (IOException e) {
370 Log.e(LOG_TAG,
"Could not copy media " + mediaPath +
" to temp file " +
371 file.getAbsolutePath());
374 Log.e(LOG_TAG,
"Could not copy media " + mediaPath +
" to temp file.");
385 private static File cacheMediaTempFile(Form form, String mediaPath, MediaSource mediaSource)
387 File tempFile = tempFileMap.get(mediaPath);
390 if (tempFile ==
null || !tempFile.exists()) {
391 Log.i(LOG_TAG,
"Copying media " + mediaPath +
" to temp file...");
393 Log.i(LOG_TAG,
"Finished copying media " + mediaPath +
" to temp file " +
394 tempFile.getAbsolutePath());
395 tempFileMap.put(mediaPath, tempFile);
421 if (mediaPath ==
null || mediaPath.length() == 0) {
424 final Synchronizer syncer =
new Synchronizer<BitmapDrawable>();
427 public void onFailure(String message) {
428 syncer.error(message);
431 public void onSuccess(BitmapDrawable result) {
432 syncer.wakeup(result);
437 BitmapDrawable result = (BitmapDrawable) syncer.getResult();
438 if (result ==
null) {
439 String error = syncer.getError();
440 if (error.startsWith(
"PERMISSION_DENIED:")) {
443 throw new IOException(error);
462 if (mediaPath ==
null || mediaPath.length() == 0) {
467 final MediaSource mediaSource = determineMediaSource(form, mediaPath);
469 Runnable loadImage =
new Runnable() {
477 Log.d(LOG_TAG,
"mediaPath = " + mediaPath);
478 InputStream is =
null;
479 ByteArrayOutputStream bos =
new ByteArrayOutputStream();
480 byte[] buf =
new byte[4096];
484 is = openMedia(form, mediaPath, mediaSource);
485 while ((read = is.read(buf)) > 0) {
486 bos.write(buf, 0, read);
488 buf = bos.toByteArray();
492 }
catch(IOException e) {
493 if (mediaSource == MediaSource.CONTACT_URI) {
495 BitmapDrawable drawable =
new BitmapDrawable(form.getResources(),
496 BitmapFactory.decodeResource(form.getResources(),
497 android.R.drawable.picture_frame,
null));
501 Log.d(LOG_TAG,
"IOException reading file.", e);
508 }
catch(IOException e) {
510 Log.w(LOG_TAG,
"Unexpected error on close", e);
516 }
catch(IOException e) {
521 ByteArrayInputStream bis =
new ByteArrayInputStream(buf);
526 BitmapFactory.Options options = getBitmapOptions(form, bis, mediaPath);
528 BitmapDrawable originalBitmapDrawable =
new BitmapDrawable(form.getResources(), decodeStream(bis,
null, options));
542 originalBitmapDrawable.setTargetDensity(form.getResources().getDisplayMetrics());
543 if ((options.inSampleSize != 1) || (form.deviceDensity() == 1.0f)) {
544 continuation.
onSuccess(originalBitmapDrawable);
547 int scaledWidth = (int) (form.deviceDensity() * originalBitmapDrawable.getIntrinsicWidth());
548 int scaledHeight = (int) (form.deviceDensity() * originalBitmapDrawable.getIntrinsicHeight());
549 Log.d(LOG_TAG,
"form.deviceDensity() = " + form.deviceDensity());
550 Log.d(LOG_TAG,
"originalBitmapDrawable.getIntrinsicWidth() = " + originalBitmapDrawable.getIntrinsicWidth());
551 Log.d(LOG_TAG,
"originalBitmapDrawable.getIntrinsicHeight() = " + originalBitmapDrawable.getIntrinsicHeight());
552 Bitmap scaledBitmap = Bitmap.createScaledBitmap(originalBitmapDrawable.getBitmap(),
553 scaledWidth, scaledHeight,
false);
554 BitmapDrawable scaledBitmapDrawable =
new BitmapDrawable(form.getResources(), scaledBitmap);
555 scaledBitmapDrawable.setTargetDensity(form.getResources().getDisplayMetrics());
556 originalBitmapDrawable =
null;
558 continuation.
onSuccess(scaledBitmapDrawable);
559 }
catch(Exception e) {
560 Log.w(LOG_TAG,
"Exception while loading media.", e);
566 }
catch(IOException e) {
568 Log.w(LOG_TAG,
"Unexpected error on close", e);
577 private static Bitmap decodeStream(InputStream is, Rect outPadding, BitmapFactory.Options opts) {
581 return BitmapFactory.decodeStream(
new FlushedInputStream(is), outPadding, opts);
587 private static class FlushedInputStream
extends FilterInputStream {
588 public FlushedInputStream(InputStream inputStream) {
593 public long skip(
long n)
throws IOException {
594 long totalBytesSkipped = 0;
595 while (totalBytesSkipped < n) {
596 long bytesSkipped = in.skip(n - totalBytesSkipped);
597 if (bytesSkipped == 0L) {
604 totalBytesSkipped += bytesSkipped;
606 return totalBytesSkipped;
610 private static BitmapFactory.Options getBitmapOptions(Form form, InputStream is, String mediaPath) {
612 BitmapFactory.Options options =
new BitmapFactory.Options();
613 options.inJustDecodeBounds =
true;
614 decodeStream(is,
null, options);
615 int imageWidth = options.outWidth;
616 int imageHeight = options.outHeight;
619 Display display = ((WindowManager) form.getSystemService(Context.WINDOW_SERVICE)).
630 if (form.getCompatibilityMode()) {
634 maxWidth = (int) (display.getWidth() / form.deviceDensity());
635 maxHeight = (int) (display.getHeight() / form.deviceDensity());
639 while ((imageWidth / sampleSize > maxWidth) && (imageHeight / sampleSize > maxHeight)) {
642 options =
new BitmapFactory.Options();
643 Log.d(LOG_TAG,
"getBitmapOptions: sampleSize = " + sampleSize +
" mediaPath = " + mediaPath
644 +
" maxWidth = " + maxWidth +
" maxHeight = " + maxHeight +
645 " display width = " + display.getWidth() +
" display height = " + display.getHeight());
646 options.inSampleSize = sampleSize;
659 private static AssetFileDescriptor getAssetsIgnoreCaseAfd(Form form, String mediaPath)
662 return form.getAssets().openFd(mediaPath);
664 }
catch (IOException e) {
665 String path = findCaseinsensitivePath(form, mediaPath);
669 return form.getAssets().openFd(path);
688 MediaSource mediaSource = determineMediaSource(form, mediaPath);
689 switch (mediaSource) {
691 return soundPool.load(getAssetsIgnoreCaseAfd(form,mediaPath), 1);
694 form.assertPermission(READ_EXTERNAL_STORAGE);
698 form.assertPermission(READ_EXTERNAL_STORAGE);
699 return soundPool.load(mediaPath, 1);
703 form.assertPermission(READ_EXTERNAL_STORAGE);
705 return soundPool.load(fileUrlToFilePath(mediaPath), 1);
709 File tempFile = cacheMediaTempFile(form, mediaPath, mediaSource);
710 return soundPool.load(tempFile.getAbsolutePath(), 1);
713 throw new IOException(
"Unable to load audio for contact " + mediaPath +
".");
716 throw new IOException(
"Unable to load audio " + mediaPath +
".");
731 MediaSource mediaSource = determineMediaSource(form, mediaPath);
732 switch (mediaSource) {
734 AssetFileDescriptor afd = getAssetsIgnoreCaseAfd(form,mediaPath);
736 FileDescriptor fd = afd.getFileDescriptor();
737 long offset = afd.getStartOffset();
738 long length = afd.getLength();
739 mediaPlayer.setDataSource(fd, offset, length);
747 form.assertPermission(READ_EXTERNAL_STORAGE);
748 mediaPlayer.setDataSource(form.getAssetPath(mediaPath));
752 form.assertPermission(READ_EXTERNAL_STORAGE);
753 mediaPlayer.setDataSource(mediaPath);
758 form.assertPermission(READ_EXTERNAL_STORAGE);
760 mediaPlayer.setDataSource(fileUrlToFilePath(mediaPath));
767 mediaPlayer.setDataSource(mediaPath);
771 mediaPlayer.setDataSource(form, Uri.parse(mediaPath));
775 throw new IOException(
"Unable to load audio or video for contact " + mediaPath +
".");
777 throw new IOException(
"Unable to load audio or video " + mediaPath +
".");
795 MediaSource mediaSource = determineMediaSource(form, mediaPath);
796 switch (mediaSource) {
799 File tempFile = cacheMediaTempFile(form, mediaPath, mediaSource);
800 videoView.setVideoPath(tempFile.getAbsolutePath());
804 form.assertPermission(READ_EXTERNAL_STORAGE);
805 videoView.setVideoPath(form.getAssetPath(mediaPath));
809 form.assertPermission(READ_EXTERNAL_STORAGE);
810 videoView.setVideoPath(mediaPath);
815 form.assertPermission(READ_EXTERNAL_STORAGE);
817 videoView.setVideoPath(fileUrlToFilePath(mediaPath));
821 videoView.setVideoURI(Uri.parse(mediaPath));
825 throw new IOException(
"Unable to load video for contact " + mediaPath +
".");
827 throw new IOException(
"Unable to load video " + mediaPath +
".");