Image Loader / Explorer Dialog Box (Android 10 update at bottom)

Edit: You must give Read permission!

Following short code will let you choose your favorite Image Explorer, where you can select your file which will be loaded in the pixels[] array.
Please comment if any error.

import android.net.Uri;
import android.database.Cursor;
import android.content.Intent;
import android.provider.MediaStore;
import android.app.Activity;
import android.content.Context;

Activity act;
Context mC;
PImage img;
boolean img_loaded;

void settings() {
  fullScreen();
}

void setup() {
  background(#F9C454);
  fill(#4A3A16);
  textSize(38);
  textAlign(CENTER);
  text("Click on screen", width/2, height/3);
  act = this.getActivity();
  mC= act.getApplicationContext();
}

void draw() {
  if (img_loaded)  image(img, 0, 0);
}

void mousePressed() {  
  searchImage();
}

public void searchImage() {
  Intent intent = new Intent(Intent.ACTION_PICK, 
    android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
  intent.setType("image/*");
  act.startActivityForResult(intent, 1);
}

@Override 
  void onActivityResult(int requestCode, int resultCode, Intent data) {
  super.onActivityResult(requestCode, resultCode, data);
  if (requestCode == 1) {
    if (resultCode == Activity.RESULT_OK) {
      if (data != null) {
        Uri selectedImage = data.getData();
        String[] filePathColumn = { MediaStore.Images.Media.DATA };
        Cursor cursor = mC.getContentResolver().query(selectedImage, 
          filePathColumn, null, null, null);
        cursor.moveToFirst();
        int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
        String imgDecodableString = cursor.getString(columnIndex);
        cursor.close();
        println( imgDecodableString);
        img = loadImage( imgDecodableString);
        img_loaded = true;
      } else {
        println("Cancelled");
      }
    }
  }
}

!

The code works to select the image but then nothing shows on the screen after the image is selected.

What API level (Android version) are you using?
Did you give the READ_EXTERNAL_STORAGE permission?

Hi @noel I am using APDE on my android phone and also Processing 1.5.1 on my Windows desktop

However you are correct. How to enable the READ_EXTERNAL_STORAGE
READ_INTERNAL_STORAGE?

I did not do that as yet. Thanks for the heads up.

To give permissions on APDE you use the three dots on the right top,
Then Sketch Properties / Sketch Permissions.
With Api I mean this.

Hi @noel
requestPermission(“android.permission.READ_EXTERNAL_STORAGE”, “initRead”);
requestPermission(“android.permission.WRITE_EXTERNAL_STORAGE”, “initWrite”);

I am about to insert this in setup()

In the APDE this is not necessary.
You can do it as described above

Yes thanks did that and testing it as an apk app and preview.

It allows me to get all the way to select the image. Then the app and the sketch closes.

With Api I mean this.

I have just tested it on Kitkat 4.4 and Lollipop 5.1 working perfect.

I am using APDE so that may be the issue.

@noel

I will keep trying

@adaptinnovative === when the app closes what is the error message, if any; if there is nothing it’s probably a problem about permissions not granted; tested with Android 6 MM && Android 8: everything works fine (not with APDE)

testing on android 10 this currently does not work. I have copied code to apde set permissions, and I can browse files but then it crashes back to main screen.

when building from windows this is the console output.

FATAL EXCEPTION: main
Process: processing.test.sketch_201017g, PID: 19256
java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=1, result=-1, data=Intent { dat=content://com.mi.android.globalFileexplorer.myprovider/external_files/Pictures/Screenshots/Screenshot_20200628-011849.jpg flg=0x1 }} to activity {processing.test.sketch_201017g/processing.test.sketch_201017g.MainActivity}: java.lang.IllegalArgumentException: File /storage/emulated/0/Pictures/Screenshots/Screenshot_20200628-011849.jpg contains a path separator
	at android.app.ActivityThread.deliverResults(ActivityThread.java:5471)
	at android.app.ActivityThread.handleSendResult(ActivityThread.java:5512)
	at android.app.servertransaction.ActivityResultItem.execute(ActivityResultItem.java:51)
	at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:149)
	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:103)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2386)
	at android.os.Handler.dispatchMessage(Handler.java:107)
	at android.os.Looper.loop(Looper.java:213)
	at android.app.ActivityThread.main(ActivityThread.java:8178)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1101)
Caused by: java.lang.IllegalArgumentException: File /storage/emulated/0/Pictures/Screenshots/Screenshot_20200628-011849.jpg contains a path separator
	at android.app.ContextImpl.makeFilename(ContextImpl.java:2841)
	at android.app.ContextImpl.openFileInput(ContextImpl.java:626)
	at android.content.ContextWrapper.openFileInput(ContextWrapper.java:206)
	at processing.core.PSurfaceNone.openFileInput(PSurfaceNone.java:331)
	at processing.core.PApplet.createInputRaw(PApplet.java:5001)
	at processing.core.PApplet.createInput(PApplet.java:4809)
	at processing.core.PApplet.loadImage(PApplet.java:3949)
	at processing.test.sketch_201017g.sketch_201017g.onActivityResult(sketch_201017g.java:82)
	at processing.test.sketch_201017g.MainActivity.onActivityResult(MainActivity.java:47)
	at android.app.Activity.dispatchActivityResult(Activity.java:8413)
	at android.app.ActivityThread.deliverResults(ActivityThread.java:5464)
	... 11 more
AndroidDevice: cannot find process id, console output will be disabled.
AndroidDevice: cannot find process id, console output will be disabled.
AndroidDevice: cannot find process id, console output will be disabled.
/storage/emulated/0/DCIM/Camera/IMG_20201013_090053.jpg
FATAL EXCEPTION: main
Process: processing.test.sketch_201017g, PID: 19902
java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=1, result=-1, data=Intent { flg=0x1 hwFlg=0x10 (has extras) }} to activity {processing.test.sketch_201017g/processing.test.sketch_201017g.MainActivity}: java.lang.IllegalArgumentException: File /storage/emulated/0/DCIM/Camera/IMG_20201013_090053.jpg contains a path separator
	at android.app.ActivityThread.deliverResults(ActivityThread.java:5471)
	at android.app.ActivityThread.handleSendResult(ActivityThread.java:5512)
	at android.app.servertransaction.ActivityResultItem.execute(ActivityResultItem.java:51)
	at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:149)
	at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:103)
	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2386)
	at android.os.Handler.dispatchMessage(Handler.java:107)
	at android.os.Looper.loop(Looper.java:213)
	at android.app.ActivityThread.main(ActivityThread.java:8178)
	at java.lang.reflect.Method.invoke(Native Method)
	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1101)
Caused by: java.lang.IllegalArgumentException: File /storage/emulated/0/DCIM/Camera/IMG_20201013_090053.jpg contains a path separator
	at android.app.ContextImpl.makeFilename(ContextImpl.java:2841)
	at android.app.ContextImpl.openFileInput(ContextImpl.java:626)
	at android.content.ContextWrapper.openFileInput(ContextWrapper.java:206)
	at processing.core.PSurfaceNone.openFileInput(PSurfaceNone.java:331)
	at processing.core.PApplet.createInputRaw(PApplet.java:5001)
	at processing.core.PApplet.createInput(PApplet.java:4809)
	at processing.core.PApplet.loadImage(PApplet.java:3949)
	at processing.test.sketch_201017g.sketch_201017g.onActivityResult(sketch_201017g.java:82)
	at processing.test.sketch_201017g.MainActivity.onActivityResult(MainActivity.java:47)
	at android.app.Activity.dispatchActivityResult(Activity.java:8413)
	at android.app.ActivityThread.deliverResults(ActivityThread.java:5464)
	... 11 more

I am having a hard time to solve this.
Android 10 introduced “Scoped Storage” in the name of privacy and security.
You can no longer access files outside your app directly but must create a new file and then “stream” the bytes array of the Uri into the new file to be displayed.
For me, the most difficult part is that almost all example codes are written in Kotlin.
I’ve tried to contact @akenaton, but unfortunately, I didn’t get response until now since August 27. I hope he is well.
But I’ll keep trying.

1 Like

I have just retried and I have successfully opened a file by forcing the android sdk version to 20. However this only works through APDE as there is no way to change it in the windows version.

I also came across some code

one to stream file to a bitmap. Which by my understanding isnt ideal because it requires a possible file duplication. I’m not sure whether the PImage then makes a symbolic link to the original bitmap or whether it has to convert it somehow.

The second part is supposed to check for permissions, as the code above fails, with the following error;

open failed: EACCES (Permission denied) android
import android.net.Uri;
import android.database.Cursor;
import android.content.Intent;
import android.provider.MediaStore;
import android.app.Activity;
import android.content.Context;
import android.os.Environment;
import android.Manifest;
//import android.appcompat.app.AppCompatActivity;
import java.io.*;
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.io.FileInputStream;
import java.awt.FlowLayout;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import android.graphics.*;
private static final int REQUEST_EXTERNAL_STORAGE = 1;
Activity act;
Context mC;
PImage img;
boolean img_loaded;
String s;

void settings() {
  fullScreen();
}

void setup() {
  //verifyStoragePermissions();
  s = Environment.getExternalStorageDirectory().getAbsolutePath();
  println(s);
  background(#F9C454);
  fill(#4A3A16);
  textSize(38);
  textAlign(CENTER);
  text("Click on screen", width/2, height/3);
  act = this.getActivity();
  mC= act.getApplicationContext();
}

void draw() {
  if (img_loaded&&img!=null)  image(img, 0, 0);
}

void mousePressed() {  
  searchImage();
}

public void searchImage() {
  Intent intent = new Intent(Intent.ACTION_PICK, 
    android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
  intent.setType("image/*");
  //Activity a = 
  act.startActivityForResult(intent, 1);
}

@Override 
  void onActivityResult(int requestCode, int resultCode, Intent data) {

  super.onActivityResult(requestCode, resultCode, data);
  if (requestCode == 1) {
    if (resultCode == Activity.RESULT_OK) {
      if (data != null) {
        Uri selectedImage = data.getData();
        String[] filePathColumn = { MediaStore.Images.Media.DATA };
        Cursor cursor = mC.getContentResolver().query(selectedImage, 
          filePathColumn, null, null, null);
        cursor.moveToFirst();
        int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
        String imgDecodableString = cursor.getString(columnIndex);
        cursor.close();
        println( imgDecodableString);
        img = loadImage( imgDecodableString);
        img_loaded = true;
      } else {
        println("Cancelled");
      }
    }
  }
}

//new PImage function;
@Override
public PImage loadImage(String filename) { //, Object params) {
  //    return loadImage(filename, null);
 
  println("Processing CUSTOM loading image....",filename);
  File imgFile = new File(filename);
  InputStream stream=null;
  try {
    //stream =  surface.getAssets().open(filename); //new FileInputStream(filename);//createInput(filename);
    stream = new FileInputStream(imgFile);
  }
  //catch(FileNotFoundException e) {}
  catch(IOException e) {
    e.printStackTrace();
  }
 

  if (stream == null) {
    System.err.println("Could not find the image " + filename + ".");
    return null;
  }
  //    long t = System.currentTimeMillis();
  Bitmap bitmap = null;
  try {
    bitmap = BitmapFactory.decodeStream(stream);
  } 
  finally {
    try {
      stream.close();
      stream = null;
    } 
    catch (IOException e) {
    }
  }
  //    int much = (int) (System.currentTimeMillis() - t);
  //    println("loadImage(" + filename + ") was " + nfc(much));
  if (bitmap == null) {
    System.err.println("Could not load the image because the bitmap was empty.");
    return null;
  } else {
    PImage image = new PImage(bitmap);
    image.parent = this;
    return image;
  }
}

//--------------------------------------------------------------------------------------------------------------------
permission checking code
//--------------------------------------------------------------------------------------------------------------------
private static String[] PERMISSIONS_STORAGE = {
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE
};

/**
 * Checks if the app has permission to write to device storage
 *
 * If the app does not has permission then the user will be prompted to grant permissions
 *
 * @param activity
 */
public static void verifyStoragePermissions(Activity activity) {
    // Check if we have write permission
    int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);

    if (permission != PackageManager.PERMISSION_GRANTED) {
        // We don't have permission so prompt the user
        ActivityCompat.requestPermissions(
                activity,
                PERMISSIONS_STORAGE,
                REQUEST_EXTERNAL_STORAGE
        );
    }
};

Please note the sketch wont run if the permission checking code isn’t commented out. Looking online I found that code as a solution on stackoverflow, but have absolutely no idea what to do with it…

I shall see if downgrading android studio to an older sdk would be a solution. This clearly wont be acceptable for everyone but, for people who just want to get there sketches running on their android devices quickly it could be a solution.

I also attempted to add the following line in the xml in the application tag.

android:requestLegacyExternalStorage="true"

however this results in a build failure too.

glad I’m not the only one who struggled finding a solution. And coming from processing as a 2nd language I can say that android documentation is extremely frustrating.

after further testing the maximum target sdk that can currently be used is 22. 22 and below will prompt user for permissions to stored media, anything after does not ask for user permissions.

Ok so have tried all afternoon to try and link processing to a lower version of the api however this is a no go. I can remove apis for android studio, but processing doesn’t recognize anything below android 9 api.

Unbelievable, I have set the SDK target to 20 (Kitkat 4.4!) and even the original code still works!
Although it gives a warning that the code was made for older versions. I don’t know if this will work on the Play Store, but it’s still great. How on earth did you think of lowering that much?
It seems that Android 11 really will only accept Scoped Storage, so I will continue my research to do it as they want it, but at least we have it working again. Thanks a lot.

1 Like

I’ll try and get the second bit of my code working later as that should solve the issue, however I am completely new to android studio and have no idea how intents and activity work or are accessed later in code.

Might be a good starting point if you don’t know where to start.

dear @Noel: i have not answered because since 1 5 days i am in hospital… i begin to day only to walk (slowly) and think… slowly,the same…I hope to be able to give some loolk to that next week. sorry

Oh dear. Sorry to hear that.
I wish you a quick recovery.
Take your time.
Best wishes.
Noel

solution found.

I’ve dissected the ketai library and manage to extract the necessary code to request any permissions from android.

make sure to have the right import package and make sure to also add the request permission line in your manifest or it will not work.

each permission then needs its own class instance in the code see below.

Please tell me if this breaks any programming practices as I honestly dont know much about coding for standards and such so might have missed a few things.



Permission eStorage,cam;

void setup(){
  // pass papplet instance along with the name of the permission you wish to address in string form
  // make sure the permission request is also added to the manifest
  eStorage = new Permission(this,"WRITE_EXTERNAL_STORAGE");
  cam = new Permission(this,"CAMERA");
};

void draw(){
  
};

public class Permission{
  
  PApplet parent;
  
  public boolean requestedPortraitImage = false;

  public Permission(PApplet pParent,String permissionName) {
    parent = pParent;
    parent.requestPermission("android.permission."+permissionName, "onPermissionResult", this);
    //parent.requestPermission("android.permission.WRITE_EXTERNAL_STORAGE", "onPermissionResult",this);

  };

  public void onPermissionResult(boolean granted) {
    if (!granted) {
// change text here to fit your requirements
      PApplet.println("User did not grant camera permission.  Camera is disabled.");
    }
  };

};
1 Like

Not only that, but the method

Environment.getExternalStorageDirectory()

is deprecated above Pie (version 28) It can still be used on Android 10 using

android:requestLegacyExternalStorage=“true”

in the Manifest, but above that, you need another solution.
But it came to my mind that we can access the InputStream from the Uri, and decode it into an android bitmap, to then get the pixels from it, and store it in the pixels array directly.
So this is my code tested using APDE on a Lollipop 5.1 device and on Android 10.
Setting the target SDK from 17 to 29.

import android.net.Uri; 
import android.database.Cursor; 
import android.content.Intent; 
import android.provider.MediaStore; 
import android.app.Activity; 
import android.content.Context; 
import android.graphics.Bitmap; 
import java.io.ByteArrayInputStream; 
import java.io.ByteArrayOutputStream; 
import java.io.File; 
import java.io.FileOutputStream; 
import android.os.Environment; 
import android.graphics.BitmapFactory; 
import android.Manifest; 
import android.content.pm.PackageManager; 
import android.os.Build; 
import android.os.Build.VERSION_CODES; 
import processing.core.PConstants;

Activity activity; 
Context context; 
PImage img; 
boolean image_loaded; 

void setup() { 
  fullScreen(); 
  background(#F9C454); 
  fill(#4A3A16); 
  textSize(38); 
  textAlign(CENTER); 
  text("Click to open Image Explorer", width/2, height/3); 
  activity = this.getActivity(); 
  context = activity.getApplicationContext(); 
  if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) { 
    requestParticularPermission();
  }
} 

void draw() { 
  if (image_loaded) {
    background(255);
    image(img, 0, 0);
  }
} 

void mousePressed() { 
  openImageExplorer();
} 

@Override 
  void onActivityResult(int requestCode, int resultCode, Intent data) { 
  super.onActivityResult(requestCode, resultCode, data); 
  if (requestCode == 1) { 
    if (resultCode == activity.RESULT_OK) { 
      if (data != null) {
        Uri image_uri = data.getData(); 
        String[] filePathColumn = { MediaStore.Images.Media.DATA };
        Cursor cursor = context.getContentResolver().query(image_uri, filePathColumn, null, null, null); 
        cursor.moveToFirst(); 
        int columnIndex = cursor.getColumnIndex(filePathColumn[0]); 
        String imgDecodableString = cursor.getString(columnIndex); 
        cursor.close();
        println(imgDecodableString); 
        if (Build.VERSION.SDK_INT >= 28) {
          try { 
            InputStream ips = context.getContentResolver().openInputStream(image_uri); 
            Bitmap bitmap = BitmapFactory.decodeStream(ips);
            img = new PImage(bitmap.getWidth(), bitmap.getHeight(), PConstants.ARGB);
            bitmap.getPixels(img.pixels, 0, img.width, 0, 0, img.width, img.height);
            img.updatePixels();
            image_loaded = true;
          }
          catch (Exception e) { 
            e.printStackTrace();
          }
        } else { 
          img = loadImage(imgDecodableString); 
          image_loaded = true;
        }
      } else {
        println("No data");
      }
    }
  }
} 

private static String[] PERMISSIONS_STORAGE = { Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE }; 
private void requestParticularPermission() { 
  activity.requestPermissions(PERMISSIONS_STORAGE, 2020);
}

public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { 
  switch (requestCode) { 
  case 2020: 
    if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { 
      println("permissions granted");
    } else { 
      println("permissions not granted");
    } 
    break; 
  default: 
    activity.onRequestPermissionsResult(requestCode, permissions, grantResults);
  }
}

void openImageExplorer() {
  Intent intent = new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI); 
  intent.setType("image/*"); 
  activity.startActivityForResult(intent, 1);
}