Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

android 11 support (SDK 30) - get error when saving an image #199

Open
YisaHsu opened this issue Feb 25, 2021 · 7 comments
Open

android 11 support (SDK 30) - get error when saving an image #199

YisaHsu opened this issue Feb 25, 2021 · 7 comments

Comments

@YisaHsu
Copy link

YisaHsu commented Feb 25, 2021

When this sentence is executed(if whereClause != ""), an error occurs:

 final Cursor cursor = context.getContentResolver().query(
   collection,
   columnValues.toArray(new String[columns.length()]),
   whereClause, null, sortOrder);

(com/terikon/cordova/photolibrary/PhotoLibraryService.java line : 283)

@josuegr0487
Copy link

Same error, there is a try catch, it returns this message "java.lang.IllegalArgumentException: Invalid token /storage/emulated/0/Pictures/imageX.jpg", value of WhereClause

@YisaHsu
Copy link
Author

YisaHsu commented Feb 25, 2021

Cursor cursor; if (whereClause == "") { cursor= context.getContentResolver().query( collection, columnValues.toArray(new String[columns.length()]), whereClause, null, sortOrder); } else { Uri uri = Uri.parse(whereClause); cursor= context.getContentResolver().query( uri, columnValues.toArray(new String[columns.length()]), null, null, null); }

`  public void saveImage(final Context context, final CordovaInterface cordova, final String url, String album, final JSONObjectRunnable completion)
throws IOException, URISyntaxException {

saveMedia(context, cordova, url, album, imageMimeToExtension, new FilePathRunnable() {
  @Override
  public void run(String filePath, Uri uri) {
    try {
      // Find the saved image in the library and return it as libraryItem
      String whereClause = "(" + MediaStore.MediaColumns.DATA + " = \"" + filePath + "\")";
      whereClause = uri.toString();
      queryLibrary(context, whereClause, new ChunkResultRunnable() {
        @Override
        public void run(ArrayList<JSONObject> chunk, int chunkNum, boolean isLastChunk) {
          completion.run(chunk.size() == 1 ? chunk.get(0) : null);
        }
      });
    } catch (Exception e) {
      completion.run(null);
    }
  }
});

}
`

I modified these codes and it seems to work.

@josuegr0487
Copy link

where to make the changes?

@YisaHsu
Copy link
Author

YisaHsu commented Feb 26, 2021

https://bak.infolight.com/android/YUPIN/PhotoLibraryService.java.txt

I only modified one file, and the link to the content of the modified file is above.

@eddsaura
Copy link

https://bak.infolight.com/android/YUPIN/PhotoLibraryService.java.txt

I only modified one file, and the link to the content of the modified file is above.

You are the man, Yisa.

@josegutierro
Copy link

#203

We did a PR with the @YisaHsu solution, we can confirm it's working on Android 11 and lower.

@frankhasen
Copy link

based on @YisaHsu s patch I added a few null checks for having no albums yet.
In the manifest you will need android:requestLegacyExternalStorage="true" on the application.
I am on ionic v6 and it is tested on Android 10, 11 and 9.

package com.terikon.cordova.photolibrary;

import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.media.ExifInterface;
import android.media.MediaScannerConnection;
import android.media.ThumbnailUtils;
import android.net.Uri;
import android.os.Environment;
import android.os.SystemClock;
import android.provider.MediaStore;
import android.util.Base64;

import org.apache.cordova.CordovaInterface;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.net.URL;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.TimeZone;

public class PhotoLibraryService {

  // TODO: implement cache
  //int cacheSize = 4 * 1024 * 1024; // 4MB
  //private LruCache<String, byte[]> imageCache = new LruCache<String, byte[]>(cacheSize);

  protected PhotoLibraryService() {
    dateFormatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
    dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
  }

  public static final String PERMISSION_ERROR = "Permission Denial: This application is not allowed to access Photo data.";

  public static PhotoLibraryService getInstance() {
    if (instance == null) {
      synchronized (PhotoLibraryService.class) {
        if (instance == null) {
          instance = new PhotoLibraryService();
        }
      }
    }
    return instance;
  }

  public void getLibrary(Context context, PhotoLibraryGetLibraryOptions options, ChunkResultRunnable completion) throws JSONException {

    String whereClause = "";
    queryLibrary(context, options.itemsInChunk, options.chunkTimeSec, options.includeAlbumData, whereClause, completion);

  }

  public ArrayList<JSONObject> getAlbums(Context context) throws JSONException {

    // All columns here: https://developer.android.com/reference/android/provider/MediaStore.Images.ImageColumns.html,
    // https://developer.android.com/reference/android/provider/MediaStore.MediaColumns.html
    JSONObject columns = new JSONObject() {{
      put("id", MediaStore.Images.ImageColumns.BUCKET_ID);
      put("title", MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME);
    }};

    final ArrayList<JSONObject> queryResult = queryContentProvider(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, "1) GROUP BY 1,(2");

    return queryResult;

  }

  public PictureData getThumbnail(Context context, String photoId, int thumbnailWidth, int thumbnailHeight, double quality) throws IOException {

    Bitmap bitmap = null;

    String imageURL = getImageURL(photoId);
    File imageFile = new File(imageURL);

    // TODO: maybe it never worth using MediaStore.Images.Thumbnails.getThumbnail, as it returns sizes less than 512x384?
    if (thumbnailWidth == 512 && thumbnailHeight == 384) { // In such case, thumbnail will be cached by MediaStore
      int imageId = getImageId(photoId);
      // For some reason and against documentation, MINI_KIND image can be returned in size different from 512x384, so the image will be scaled later if needed
      bitmap = MediaStore.Images.Thumbnails.getThumbnail(
        context.getContentResolver(),
        imageId ,
        MediaStore.Images.Thumbnails.MINI_KIND,
        (BitmapFactory.Options) null);
    }

    if (bitmap == null) { // No free caching here
      Uri imageUri = Uri.fromFile(imageFile);
      BitmapFactory.Options options = new BitmapFactory.Options();

      options.inJustDecodeBounds = true;
      InputStream is = context.getContentResolver().openInputStream(imageUri);
      BitmapFactory.decodeStream(is, null, options);

      // get bitmap with size of closest power of 2
      options.inSampleSize = calculateInSampleSize(options, thumbnailWidth, thumbnailHeight);
      options.inJustDecodeBounds = false;
      is = context.getContentResolver().openInputStream(imageUri);
      bitmap = BitmapFactory.decodeStream(is, null, options);
      is.close();
    }

    if (bitmap != null) {

      // correct image orientation
      int orientation = getImageOrientation(imageFile);
      Bitmap rotatedBitmap = rotateImage(bitmap, orientation);
      if (bitmap != rotatedBitmap) {
        bitmap.recycle();
      }

      Bitmap thumbnailBitmap = ThumbnailUtils.extractThumbnail(rotatedBitmap, thumbnailWidth, thumbnailHeight);
      if (rotatedBitmap != thumbnailBitmap) {
        rotatedBitmap.recycle();
      }

      // TODO: cache bytes for performance

      byte[] bytes = getJpegBytesFromBitmap(thumbnailBitmap, quality);
      String mimeType = "image/jpeg";

      thumbnailBitmap.recycle();

      return new PictureData(bytes, mimeType);

    }

    return null;

  }

  public PictureAsStream getPhotoAsStream(Context context, String photoId) throws IOException {

    int imageId = getImageId(photoId);
    String imageURL = getImageURL(photoId);
    File imageFile = new File(imageURL);
    Uri imageUri = Uri.fromFile(imageFile);

    String mimeType = queryMimeType(context, imageId);

    InputStream is = context.getContentResolver().openInputStream(imageUri);

    if (mimeType.equals("image/jpeg")) {
      int orientation = getImageOrientation(imageFile);
      if (orientation > 1) { // Image should be rotated

        Bitmap bitmap = BitmapFactory.decodeStream(is, null, null);
        is.close();

        Bitmap rotatedBitmap = rotateImage(bitmap, orientation);

        bitmap.recycle();

        // Here we perform conversion with data loss, but it seems better than handling orientation in JavaScript.
        // Converting to PNG can be an option to prevent data loss, but in price of very large files.
        byte[] bytes = getJpegBytesFromBitmap(rotatedBitmap, 1.0); // minimize data loss with 1.0 quality

        is = new ByteArrayInputStream(bytes);
      }
    }

    return new PictureAsStream(is, mimeType);

  }

  public PictureData getPhoto(Context context, String photoId) throws IOException {

    PictureAsStream pictureAsStream = getPhotoAsStream(context, photoId);

    byte[] bytes =  readBytes(pictureAsStream.getStream());
    pictureAsStream.getStream().close();

    return new PictureData(bytes, pictureAsStream.getMimeType());

  }

  public void saveImage(final Context context, final CordovaInterface cordova, final String url, String album, final JSONObjectRunnable completion)
    throws IOException, URISyntaxException {

    saveMedia(context, cordova, url, album, imageMimeToExtension, new FilePathRunnable() {
      @Override
      public void run(String filePath, Uri uri) {
        try {
          // Find the saved image in the library and return it as libraryItem
          String whereClause = "(" + MediaStore.MediaColumns.DATA + " = \"" + filePath + "\")";
          whereClause = uri.toString();
          queryLibrary(context, whereClause, new ChunkResultRunnable() {
            @Override
            public void run(ArrayList<JSONObject> chunk, int chunkNum, boolean isLastChunk) {
              completion.run(chunk.size() == 1 ? chunk.get(0) : null);
            }
          });
        } catch (Exception e) {
          completion.run(null);
        }
      }
    });

  }

  public void saveVideo(final Context context, final CordovaInterface cordova, String url, String album)
    throws IOException, URISyntaxException {

    saveMedia(context, cordova, url, album, videMimeToExtension, new FilePathRunnable() {
      @Override
      public void run(String filePath, Uri uri) {
        // TODO: call queryLibrary and return libraryItem of what was saved
      }
    });
  }

  public class PictureData {

    public final byte[] bytes;
    public final String mimeType;

    public PictureData(byte[] bytes, String mimeType) {
      this.bytes = bytes;
      this.mimeType = mimeType;
    }

  }

  public class PictureAsStream {

    public PictureAsStream(InputStream stream, String mimeType) {
      this.stream = stream;
      this.mimeType = mimeType;
    }

    public InputStream getStream() { return this.stream; }

    public String getMimeType() { return this.mimeType; }

    private InputStream stream;
    private String mimeType;

  }

  private static PhotoLibraryService instance = null;

  private SimpleDateFormat dateFormatter;

  private Pattern dataURLPattern = Pattern.compile("^data:(.+?)/(.+?);base64,");

  private ArrayList<JSONObject> queryContentProvider(Context context, Uri collection, JSONObject columns, String whereClause) throws JSONException {

    final ArrayList<String> columnNames = new ArrayList<String>();
    final ArrayList<String> columnValues = new ArrayList<String>();

    Iterator<String> iteratorFields = columns.keys();

    while (iteratorFields.hasNext()) {
      String column = iteratorFields.next();

      columnNames.add(column);
      columnValues.add("" + columns.getString(column));
    }

    final String sortOrder = MediaStore.Images.Media.DATE_TAKEN + " DESC";

    Cursor cursor;
    if (whereClause == "")
    {
        cursor= context.getContentResolver().query(
              collection,
              columnValues.toArray(new String[columns.length()]),
              whereClause, null, sortOrder);
    }
    else
    {
        Uri uri = Uri.parse(whereClause);
        cursor= context.getContentResolver().query(
              uri,
              columnValues.toArray(new String[columns.length()]),
              null, null, null);
    }

    final ArrayList<JSONObject> buffer = new ArrayList<JSONObject>();

    if (cursor != null && cursor.moveToFirst()) {
      do {
        JSONObject item = new JSONObject();

        for (String column : columnNames) {
          int columnIndex = cursor.getColumnIndex(columns.get(column).toString());

          if (column.startsWith("int.")) {
            item.put(column.substring(4), cursor.getInt(columnIndex));
            if (column.substring(4).equals("width") && item.getInt("width") == 0) {
              System.err.println("cursor: " + cursor.getInt(columnIndex));
            }
          } else if (column.startsWith("float.")) {
            item.put(column.substring(6), cursor.getFloat(columnIndex));
          } else if (column.startsWith("date.")) {
            long intDate = cursor.getLong(columnIndex);
            Date date = new Date(intDate);
            item.put(column.substring(5), dateFormatter.format(date));
          } else {
            item.put(column, cursor.getString(columnIndex));
          }
        }
        buffer.add(item);

        // TODO: return partial result

      }
      while (cursor.moveToNext());
    }
    if(cursor != null){
      cursor.close();
    }

    return buffer;

  }

  private void queryLibrary(Context context, String whereClause, ChunkResultRunnable completion) throws JSONException {
    queryLibrary(context, 0, 0, false, whereClause, completion);
  }

  private void queryLibrary(Context context, int itemsInChunk, double chunkTimeSec, boolean includeAlbumData, String whereClause, ChunkResultRunnable completion)
    throws JSONException {

    // All columns here: https://developer.android.com/reference/android/provider/MediaStore.Images.ImageColumns.html,
    // https://developer.android.com/reference/android/provider/MediaStore.MediaColumns.html
    JSONObject columns = new JSONObject() {{
      put("int.id", MediaStore.Images.Media._ID);
      put("fileName", MediaStore.Images.ImageColumns.DISPLAY_NAME);
      put("int.width", MediaStore.Images.ImageColumns.WIDTH);
      put("int.height", MediaStore.Images.ImageColumns.HEIGHT);
      put("albumId", MediaStore.Images.ImageColumns.BUCKET_ID);
      put("date.creationDate", MediaStore.Images.ImageColumns.DATE_TAKEN);
      put("float.latitude", MediaStore.Images.ImageColumns.LATITUDE);
      put("float.longitude", MediaStore.Images.ImageColumns.LONGITUDE);
      put("nativeURL", MediaStore.MediaColumns.DATA); // will not be returned to javascript
    }};

    final ArrayList<JSONObject> queryResults = queryContentProvider(context, MediaStore.Images.Media.EXTERNAL_CONTENT_URI, columns, whereClause);

    ArrayList<JSONObject> chunk = new ArrayList<JSONObject>();

    long chunkStartTime = SystemClock.elapsedRealtime();
    int chunkNum = 0;

    for (int i=0; i<queryResults.size(); i++) {
      JSONObject queryResult = queryResults.get(i);

      // swap width and height if needed
      try {
        int orientation = getImageOrientation(new File(queryResult.getString("nativeURL")));
        if (isOrientationSwapsDimensions(orientation)) { // swap width and height
          int tempWidth = queryResult.getInt("width");
          queryResult.put("width", queryResult.getInt("height"));
          queryResult.put("height", tempWidth);
        }
      } catch (IOException e) {
        // Do nothing
      }

      // photoId is in format "imageid;imageurl"
      queryResult.put("id",
          queryResult.get("id") + ";" +
          queryResult.get("nativeURL"));

      queryResult.remove("nativeURL"); // Not needed

      String albumId = queryResult.getString("albumId");
      queryResult.remove("albumId");
      if (includeAlbumData) {
        JSONArray albumsArray = new JSONArray();
        albumsArray.put(albumId);
        queryResult.put("albumIds", albumsArray);
      }

      chunk.add(queryResult);

      if (i == queryResults.size() - 1) { // Last item
        completion.run(chunk, chunkNum, true);
      } else if ((itemsInChunk > 0 && chunk.size() == itemsInChunk) || (chunkTimeSec > 0 && (SystemClock.elapsedRealtime() - chunkStartTime) >= chunkTimeSec*1000)) {
        completion.run(chunk, chunkNum, false);
        chunkNum += 1;
        chunk = new ArrayList<JSONObject>();
        chunkStartTime = SystemClock.elapsedRealtime();
      }

    }

  }

  private String queryMimeType(Context context, int imageId) {

    Cursor cursor = context.getContentResolver().query(
      MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
      new String[] { MediaStore.Images.ImageColumns.MIME_TYPE },
      MediaStore.MediaColumns._ID + "=?",
      new String[] {Integer.toString(imageId)}, null);

    if (cursor != null && cursor.moveToFirst()) {
      String mimeType = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE));
      cursor.close();

      return mimeType;

    }
    if (cursor != null){
      cursor.close();
    }

    return null;
  }

  // From https://developer.android.com/training/displaying-bitmaps/load-bitmap.html
  private static int calculateInSampleSize(

    BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

      final int halfHeight = height / 2;
      final int halfWidth = width / 2;

      // Calculate the largest inSampleSize value that is a power of 2 and keeps both
      // height and width larger than the requested height and width.
      while ((halfHeight / inSampleSize) >= reqHeight
        && (halfWidth / inSampleSize) >= reqWidth) {
        inSampleSize *= 2;
      }
    }

    return inSampleSize;

  }

  private static byte[] getJpegBytesFromBitmap(Bitmap bitmap, double quality) {

    ByteArrayOutputStream stream = new ByteArrayOutputStream();
    bitmap.compress(Bitmap.CompressFormat.JPEG, (int)(quality * 100), stream);

    return stream.toByteArray();

  }

  private static void copyStream(InputStream source, OutputStream target) throws IOException {

    int bufferSize = 1024;
    byte[] buffer = new byte[bufferSize];

    int len;
    while ((len = source.read(buffer)) != -1) {
      target.write(buffer, 0, len);
    }

  }

  private static byte[] readBytes(InputStream inputStream) throws IOException {

    ByteArrayOutputStream byteBuffer = new ByteArrayOutputStream();

    int bufferSize = 1024;
    byte[] buffer = new byte[bufferSize];

    int len;
    while ((len = inputStream.read(buffer)) != -1) {
      byteBuffer.write(buffer, 0, len);
    }

    return byteBuffer.toByteArray();

  }

  // photoId is in format "imageid;imageurl;[swap]"
  private static int getImageId(String photoId) {
    return Integer.parseInt(photoId.split(";")[0]);
  }

  // photoId is in format "imageid;imageurl;[swap]"
  private static String getImageURL(String photoId) {
    return photoId.split(";")[1];
  }

  private static int getImageOrientation(File imageFile) throws IOException {

    ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
    int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);

    return orientation;

  }

  // see http://www.daveperrett.com/articles/2012/07/28/exif-orientation-handling-is-a-ghetto/
  private static Bitmap rotateImage(Bitmap source, int orientation) {

    Matrix matrix = new Matrix();

    switch (orientation) {
      case ExifInterface.ORIENTATION_NORMAL: // 1
          return source;
      case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: // 2
        matrix.setScale(-1, 1);
        break;
      case ExifInterface.ORIENTATION_ROTATE_180: // 3
        matrix.setRotate(180);
        break;
      case ExifInterface.ORIENTATION_FLIP_VERTICAL: // 4
        matrix.setRotate(180);
        matrix.postScale(-1, 1);
        break;
      case ExifInterface.ORIENTATION_TRANSPOSE: // 5
        matrix.setRotate(90);
        matrix.postScale(-1, 1);
        break;
      case ExifInterface.ORIENTATION_ROTATE_90: // 6
        matrix.setRotate(90);
        break;
      case ExifInterface.ORIENTATION_TRANSVERSE: // 7
        matrix.setRotate(-90);
        matrix.postScale(-1, 1);
        break;
      case ExifInterface.ORIENTATION_ROTATE_270: // 8
        matrix.setRotate(-90);
        break;
      default:
        return source;
    }

    return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix, false);

  }

  // Returns true if orientation rotates image by 90 or 270 degrees.
  private static boolean isOrientationSwapsDimensions(int orientation) {
    return orientation == ExifInterface.ORIENTATION_TRANSPOSE // 5
      || orientation == ExifInterface.ORIENTATION_ROTATE_90 // 6
      || orientation == ExifInterface.ORIENTATION_TRANSVERSE // 7
      || orientation == ExifInterface.ORIENTATION_ROTATE_270; // 8
  }

  private static File makeAlbumInPhotoLibrary(String album) {
    File albumDirectory = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), album);
    if (!albumDirectory.exists()) {
      albumDirectory.mkdirs();
    }
    return albumDirectory;
  }

  private File getImageFileName(File albumDirectory, String extension) {
    Calendar calendar = Calendar.getInstance();
    String dateStr = calendar.get(Calendar.YEAR) +
      "-" + calendar.get(Calendar.MONTH) +
      "-" + calendar.get(Calendar.DAY_OF_MONTH);
    int i = 1;
    File result;
    do {
      String fileName = dateStr + "-" + i + extension;
      i += 1;
      result = new File(albumDirectory, fileName);
    } while (result.exists());
    return result;
  }

  private void addFileToMediaLibrary(Context context, File file, final FilePathRunnable completion) {

    String filePath = file.getAbsolutePath();

    MediaScannerConnection.scanFile(context, new String[]{filePath}, null, new MediaScannerConnection.OnScanCompletedListener() {
      @Override
      public void onScanCompleted(String path, Uri uri) {
        completion.run(path, uri);
      }
    });

  }

  private Map<String, String> imageMimeToExtension = new HashMap<String, String>(){{
    put("jpeg", ".jpg");
  }};

  private Map<String, String> videMimeToExtension = new HashMap<String, String>(){{
    put("quicktime", ".mov");
    put("ogg", ".ogv");
  }};

  private void saveMedia(Context context, CordovaInterface cordova, String url, String album, Map<String, String> mimeToExtension, FilePathRunnable completion)
    throws IOException, URISyntaxException {

    File albumDirectory = makeAlbumInPhotoLibrary(album);
    File targetFile;

    if (url.startsWith("data:")) {

      Matcher matcher = dataURLPattern.matcher(url);
      if (!matcher.find()) {
        throw new IllegalArgumentException("The dataURL is in incorrect format");
      }
      String mime = matcher.group(2);
      int dataPos = matcher.end();

      String base64 = url.substring(dataPos); // Use substring and not replace to keep memory footprint small
      byte[] decoded = Base64.decode(base64, Base64.DEFAULT);

      if (decoded == null) {
        throw new IllegalArgumentException("The dataURL could not be decoded");
      }

      String extension = mimeToExtension.get(mime);
      if (extension == null) {
        extension = "." + mime;
      }

      targetFile = getImageFileName(albumDirectory, extension);

      FileOutputStream os = new FileOutputStream(targetFile);

      os.write(decoded);

      os.flush();
      os.close();

    } else {

      String extension = url.contains(".") ? url.substring(url.lastIndexOf(".")) : "";
      targetFile = getImageFileName(albumDirectory, extension);

      InputStream is;
      FileOutputStream os = new FileOutputStream(targetFile);

      if(url.startsWith("file:///android_asset/")) {
        String assetUrl = url.replace("file:///android_asset/", "");
        is = cordova.getActivity().getApplicationContext().getAssets().open(assetUrl);
      } else {
        is = new URL(url).openStream();
      }

      copyStream(is, os);

      os.flush();
      os.close();
      is.close();

    }

    addFileToMediaLibrary(context, targetFile, completion);

  }

  public interface ChunkResultRunnable {

    void run(ArrayList<JSONObject> chunk, int chunkNum, boolean isLastChunk);

  }

  public interface FilePathRunnable {

    void run(String filePath, Uri uri);

  }

  public interface JSONObjectRunnable {

    void run(JSONObject result);

  }

}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

5 participants