ImagePicker, maxSavedFiles=1000. Choose destination directory. Source code

A few years ago (2017) I made an ImagePicker extension with which you could save up to 1000 files and you could choose the destination directory.

https://groups.google.com/g/mitappinventortest/c/oqeVId2RuOk/m/hQBVVtuoAAAJ

I have updated the code.

  • Up to 1000 image files can be saved.

  • You can choose the destination directory (if it does not exist, it is created).

  • The default directory is:
    /storage/emulated/0/Pictures/_app_inventor_image_picker

  • It has the GetAsdPath block.

I have only tested it on an Android 9, (MIT Companion and installed).
In Android > 9 it is better to set the destination directory to ASD.

p190_ImagePickerY.aia (15.1 KB)

Extension com.KIO4_ImagePickerY.aix in:
http://kio4.com/appinventor/290_extension_imagepicker_10.htm

5 Likes

- Source code.

Source code is an adaptation of:
https://github.com/mit-cml/appinventor-sources/blob/master/appinventor/components/src/com/google/appinventor/components/runtime/ImagePicker.java

///////////////////////////////////////////////////////////////////
KIO4_ImagePickerY.java
///////////////////////////////////////////////////////////////////

// -*- Mode: java; c-basic-offset: 2; -*-
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2012 MIT, All rights reserved
// Released under the Apache License, Version 2.0
// http://www.apache.org/licenses/LICENSE-2.0

// Modificado por Juan A. Villalpando.
// kio4.com
// Noviembre 2017. Agosto 2022.

package com.KIO4_ImagePickerY;

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.util.Arrays;
import java.util.Comparator;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.Intent;
import android.net.Uri;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import android.webkit.MimeTypeMap;

import com.google.appinventor.components.annotations.DesignerComponent;
import com.google.appinventor.components.annotations.DesignerProperty;
import com.google.appinventor.components.annotations.PropertyCategory;
import com.google.appinventor.components.annotations.SimpleEvent;
import com.google.appinventor.components.annotations.SimpleFunction;
import com.google.appinventor.components.annotations.SimpleObject;
import com.google.appinventor.components.annotations.SimpleProperty;
import com.google.appinventor.components.annotations.UsesPermissions;
import com.google.appinventor.components.common.ComponentCategory;
import com.google.appinventor.components.common.PropertyTypeConstants;
import com.google.appinventor.components.runtime.util.MediaUtil;
import com.google.appinventor.components.runtime.*;

// Otras importaciones.
import com.google.appinventor.components.runtime.util.ErrorMessages;
import com.google.appinventor.components.runtime.util.MediaUtil;
import com.google.appinventor.components.runtime.util.AnimationUtil;
import com.google.appinventor.components.runtime.AndroidNonvisibleComponent;
import com.google.appinventor.components.runtime.ActivityResultListener;
import com.google.appinventor.components.runtime.ComponentContainer;
import com.google.appinventor.components.runtime.EventDispatcher;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import android.content.Context;


/**
 * Component enabling a user to select an image from the phone's gallery.
 *
 * @author halabelson@google.com (Hal Abelson)
 */
@DesignerComponent(version = 1,
    description = "When the user open ImagePickerY, the " +
          "device's image gallery appears, and the user can choose an image. After an image is " +
          "picked, it is saved, and the <code>Selected</code> " +
          "property will be the name of the file where the image is stored. You can store up to 1000 images." +
		  "Default folder to store is /storage/emulated/0/Pictures/_app_inventor_image_picker, but you can change that folder.",
    category = ComponentCategory.EXTENSION,
	nonVisible = true,
	iconName = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAIAAADZrBkAAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAEnQAABJ0Ad5mH3gAAAJNSURBVDhPlVPPaxNBFJ7Z3fxqkkLIJZBTkEQ9WfwbehFBIx48VESh6EEoemov4tFfUDwVoTcv6k1R8CIeNKcICrG2TZoUo1FMCZro/prd2Rm/2Q2lQnLwOzxe3rxvv/e9mVDXdRO3jpH/gbP8gcqbh3cWnicSCUopSkIIKSUiEDVFSVRBD2Pe3NNzGkrgWJbVarUajUa32+Wcp1KpmRDJZBI5gCRMZwzDAEXRHMdB92Xr/lLw4NLwXrvdBjPSDIIASRAIInUqdY9bp3dXxjTf923bRtJ5v4M4Go1AQAJgKkoMKbnFu/Ofrp7YXI7qigZdzIPk0PEyYiaTQYQIWIL4pt+Z/3jt1NYdFPehBo3H44VCYbW7CM1sNlssFlE0TVMKyYVT3b0bdv4Dtcn2+Rcwi5vAtFCOxWJwe+RxddwyCUoNTjASNOOxONyY5p+jT870Fl/hKNwN0SjVwwVCn1DML5Q35UMSgRq8S8fjo+hbWG+9Xq/V3m43txlzNY1qOkXEqfqGHXwPPJ0FIyYGpt90bYFXMxyOQLuReUSShDDy8MdKqVQaS0ipaGfbq4gHcTIcjzFGMupWsGHkEQHARsIhJwGPYnZ2Fsn+rYwpUplTahORTqfL5fL64LrneeDncrnIv8v7A+fdVDVN0/L5PPxUKhXcZCqVhpDt9/ac2oXOy6k07FUEQteNmIE/h8b4r6G78c16drHzGqdTaXgfXDJfWCz4aflf+tab6tbalc+b0SnFg0jenot+HMTGwhokXbHn8N5vr7n0tT8+AAj5C+YNX6G0QHC+AAAAAElFTkSuQmCC")
	// LAS TRES LINEAS DE ARRIBA FUERON MODIFICADAS.

@UsesPermissions(permissionNames = "android.permission.WRITE_EXTERNAL_STORAGE")
@SimpleObject(external = true) // ESTO HA SIDO MODIFICADO.
 public class KIO4_ImagePickerY extends AndroidNonvisibleComponent implements Component, ActivityResultListener {
	
  private ComponentContainer container;
  private Context context;
  protected int requestCode;
  private static final String LOG_TAG = "KIO4_ImagePickerY";
  private static final String DEFAULT_imagePickerDirectoryName = "/storage/emulated/0/Pictures/_app_inventor_image_picker";

  // directory on external storage for storing the files for the saved images
  private String imagePickerDirectoryName = "/storage/emulated/0/Pictures/_app_inventor_image_picker";

  // prefix for image file names
  private static final String FILE_PREFIX = "img_";

 // max number of files to save in image directory
  private static int maxSavedFiles = 1000; // ESTA LINEA HA SIDO MODIFICADA.

  // The media path (URI) for the selected image file created by MediaUtil
  private String selectionURI;

  // The path to the saved image
  private String selectionSavedImage = "";

  /**
   * Create a new ImagePicker component.
   *
   * @param container the parent container.
   */
  public KIO4_ImagePickerY(ComponentContainer container) {
        super(container.$form());
        this.container = container;
	    context = (Context) container.$context();
		ImagePickerDirectoryName(DEFAULT_imagePickerDirectoryName);
  }

  /**
   * Path to the file containing the image that was selected.
   */
  @SimpleProperty(description = "Path to the file containing the image that was selected.",
      category = PropertyCategory.BEHAVIOR)
  public String Selection() {
    return selectionSavedImage;
  }
  
   @SimpleProperty(category = PropertyCategory.BEHAVIOR, description = "Directory destiny. Set absolute address: /storage/emulated/..." )
    public String ImagePickerDirectoryName() {
        return imagePickerDirectoryName;
    }

    @DesignerProperty(editorType = PropertyTypeConstants.PROPERTY_TYPE_STRING, defaultValue = KIO4_ImagePickerY.DEFAULT_imagePickerDirectoryName + "")
    @SimpleProperty(description = "imagePickerDirectoryName. ")
    public void ImagePickerDirectoryName(String nuevoImagePickerDirectoryName) {
        this.imagePickerDirectoryName = nuevoImagePickerDirectoryName;
    }
	

  protected Intent getIntent() {
    return new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI);
  }

  /**
   * Callback method to get the result returned by the image picker activity
   *
   * @param requestCode a code identifying the request.
   * @param resultCode a code specifying success or failure of the activity
   * @param data the returned data, in this case an Intent whose data field
   *        contains the image's content URI.
   */
  public void resultReturned(int requestCode, int resultCode, Intent data) {
    if (requestCode == this.requestCode && resultCode == Activity.RESULT_OK) {
      Uri selectedImage = data.getData();
      selectionURI = selectedImage.toString();
      Log.i(LOG_TAG, "selectionURI = " + selectionURI);

      // get the file type extension from the intent data Uri
      ContentResolver cR = container.$context().getContentResolver();
      MimeTypeMap mime = MimeTypeMap.getSingleton();
      String extension = "." + mime.getExtensionFromMimeType(cR.getType(selectedImage));
      Log.i(LOG_TAG, "extension = " + extension);

      // save the image to a temp file in external storage, using a name
      // that includes the extension
      saveSelectedImageToExternalStorage(extension);
      AfterPicking();
    }
  }

  private void saveSelectedImageToExternalStorage(String extension) {
    // clear imageFile for new save attempt
    // This will be the stored picture
    selectionSavedImage = "";
    // create a temp file for holding the image that was picked
    // This is not the external stored file: This is in the internal directory used by MediaUtil
    File tempFile = null;

    // copy the picture at the image URI to the temp file
    try {
      tempFile = MediaUtil.copyMediaToTempFile(container.$form(), selectionURI);
    } catch (IOException e) {
      Log.i(LOG_TAG, "copyMediaToTempFile failed: " + e.getMessage());
      container.$form().dispatchErrorOccurredEvent(this, "ImagePicker",
          ErrorMessages.ERROR_CANNOT_COPY_MEDIA, e.getMessage());
      return;
    }

    // copy the temp file to external storage
    Log.i(LOG_TAG, "temp file path is: " + tempFile.getPath());
    // Copy file will signal a screen error if the copy fails.
    copyToExternalStorageAndDeleteSource(tempFile, extension);
  }

  private void copyToExternalStorageAndDeleteSource(File source, String extension) {

    File dest = null;
    InputStream inStream = null;
    OutputStream outStream = null;

    // String fullDirname = Environment.getExternalStorageDirectory() + imagePickerDirectoryName;
	String fullDirname = imagePickerDirectoryName;
    File destDirectory = new File(fullDirname);

    try {
      destDirectory.mkdirs();
      dest = File.createTempFile (FILE_PREFIX, extension,  destDirectory);

      selectionSavedImage = dest.getPath();
      // Uncomment this to delete imageFile when the application stops
      // dest.deleteOnExit();
      Log.i(LOG_TAG, "saved file path is: " + selectionSavedImage);

      inStream = new FileInputStream(source);
      outStream = new FileOutputStream(dest);

      byte[] buffer = new byte[1024];

      int length;
      // copy the file content in bytes
      while ((length = inStream.read(buffer)) > 0){
        outStream.write(buffer, 0, length);
      }

      inStream.close();
      outStream.close();
      Log.i(LOG_TAG, "Image was copied to " + selectionSavedImage);
      // this can be uncommented to show the alert, but the alert
      // is pretty annoying
      // new (container.$form()).ShowAlert("Image was copied to " + selectedImage);

    } catch(IOException e) {
      String err =  "destination is " + selectionSavedImage + ": " + "error is "  + e.getMessage();
      Log.i(LOG_TAG, "copyFile failed. " + err);
      container.$form().dispatchErrorOccurredEvent(this, "SaveImage",
          ErrorMessages.ERROR_CANNOT_SAVE_IMAGE, err);
      selectionSavedImage = "";
      dest.delete();
    }

    // clean up the temp file.  This isn't critical because MudiaUtil.copyMediaToTempFile
    // marks this with deleteOnExit, but it's nice to clean up here.
    source.delete();
    trimDirectory(maxSavedFiles, destDirectory);
  }

  // keep only the last N files, where N = maxSavedFiles
  private void trimDirectory(int maxSavedFiles, File directory) {

    File[] files = directory.listFiles();

    Arrays.sort(files, new Comparator<File>(){
      public int compare(File f1, File f2)
      {
        return Long.valueOf(f1.lastModified()).compareTo(f2.lastModified());
      } });

    int excess = files.length - maxSavedFiles;
    for (int i = 0; i < excess; i++) {
      files[i].delete();
    }

  }
  
  ////////////////////////// Obtenido de PICKER.java //////////////////////////////////////////////////
   public void click() {
    BeforePicking();
    if (requestCode == 0) { // only need to register once
      requestCode = container.$form().registerForActivityResult(this);
    }
    container.$context().startActivityForResult(getIntent(), requestCode);
   // String openAnim = container.$form().getOpenAnimType();
   // AnimationUtil.ApplyOpenScreenAnimation(container.$context(), openAnim);
  }

  // Functions

  /**
   * Opens the picker, as though the user clicked on it.
   */
  @SimpleFunction(description = "Opens the picker, as though the user clicked on it.")
  public void Open() {
    click();
  }

  // Events

  /**
   * Event to raise when the button of the component is clicked or the list is shown
   * using the Open block.  This event occurs before the list of items is displayed, and 
   * can be used to prepare the list before it is shown.
   */
  @SimpleEvent
  public void BeforePicking() {
    EventDispatcher.dispatchEvent(this, "BeforePicking");
  }

  /**
   * Event to be raised after the picker activity returns its
   * result and the properties have been filled in.
   */
  @SimpleEvent
  public void AfterPicking() {
    EventDispatcher.dispatchEvent(this, "AfterPicking");
  }
  
 /////////////////// Obtener el directorio ASD  /storage/emulated/0/Android/data/namepackage/files /////////////
@SimpleFunction(description = "Get App Specific Directory path (ASD). Example: /storage/emulated/0/Android/data/<namepackage>/files")
public String GetAsdPath() {
return context.getExternalFilesDir(null).toString();
}

}
2 Likes

:+1:

Works fine also on Android 11+ without storage permissions (APK) for the Shared folder /Download.

But on Android 11+ the Image component needs a full path and you must request WRITE permission on Android < 11.

EDIT: This one should now work on all Android versions:
p190_ImagePickerY_Anke.aia (20.7 KB)

2 Likes

@Anke
Right! The destination directory selection process is very good. Great!

- Experimenting.

Let's keep experimenting with this source code.
Let's make small modifications to see what we get.
Note that the obtained extensions are only experimental, I have not modified the name of the variables.

1.- VideoPicker.

  • We observe that there is MediaStore.Video.Media.

  • So we change these lines and get a VideoPicker.
  protected Intent getIntent() {
	  // return new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI);
	   return new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Video.Media.INTERNAL_CONTENT_URI);
  }

p190_ImagePickerY_Anke_video.aia (33.9 KB)

2.- AudioPicker.

Now with MediaStore.Audio.Media.

  protected Intent getIntent() {
	  // return new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI);
	  // return new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Video.Media.INTERNAL_CONTENT_URI);
	   return new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Audio.Media.EXTERNAL_CONTENT_URI);
  }

p190_ImagePickerY_Anke_audio.aia (47.6 KB)