I want to create something like "PDF Viewer app". Application will search for all *.pdf files in location chosen by user. User can choose this folder by this function:
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE);
Then I get DocumentFile (folder):
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == getActivity().RESULT_OK && requestCode == REQUEST_CODE) {
Uri uriTree = data.getData();
DocumentFile documentFile = DocumentFile.fromTreeUri(getActivity(), uriTree);
//rest of code here
}
}
Why I chose this method of selecting folder? Because I want to make possible to choose Secondary Storage (you know, in Android >= 5.0, you can't access Secondary Storage with Java.io.file).
Ok, so I get folder with all *.pdf as DocumentFile. Then I call:
for(DocumentFile file: documentFile.listFiles()){
String fileNameToDisplay = file.getName();
}
And this is VERY SLOW. It takes almost 30 seconds when there are ~600 files in chosen folder. To prove it, I chose directory from External Storage (not secondary storage), and then I tried two solutions: DocumentFile and File.
File version looks like it:
File f = new File(Environment.getExternalStorageDirectory()+"/pdffiles");
for(File file: f.listFiles()){
String fileNameToDisplay = file.getName();
}
}
Second version works about 500x faster. There is almost no time in displaying all files on List View.
Why is DocumentFile so slow?
If you read the source code of TreeDocumentFile, you will find that each call to listFiles() and getName() invokes ContentResolver#query() under the hood. Like CommonsWare said, this would perform hundreds of queries, which is very inefficient.
Here is the source code of listFiles():
#Override
public DocumentFile[] listFiles() {
final ContentResolver resolver = mContext.getContentResolver();
final Uri childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(mUri,
DocumentsContract.getDocumentId(mUri));
final ArrayList<Uri> results = new ArrayList<>();
Cursor c = null;
try {
c = resolver.query(childrenUri, new String[] {
DocumentsContract.Document.COLUMN_DOCUMENT_ID }, null, null, null);
while (c.moveToNext()) {
final String documentId = c.getString(0);
final Uri documentUri = DocumentsContract.buildDocumentUriUsingTree(mUri,
documentId);
results.add(documentUri);
}
} catch (Exception e) {
Log.w(TAG, "Failed query: " + e);
} finally {
closeQuietly(c);
}
final Uri[] result = results.toArray(new Uri[results.size()]);
final DocumentFile[] resultFiles = new DocumentFile[result.length];
for (int i = 0; i < result.length; i++) {
resultFiles[i] = new TreeDocumentFile(this, mContext, result[i]);
}
return resultFiles;
}
In this function call, listFiles() made a query that only selects the document ID column. However, in your case you also want the file name for each file. Therefore, you can add the column COLUMN_DISPLAY_NAME to the query. This would retrieve the filename and document ID (which later you will convert it into Uri) in a single query and is much more efficient. There are also many other columns available such as file type, file size, and last modified time, which you may want to retrieve them as well.
c = resolver.query(mUri, new String[] {
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
DocumentsContract.Document.COLUMN_DISPLAY_NAME
}, null, null, null);
Within the while loop, retrieve the filename by
final String filename = c.getString(1);
The above modified code is able to instantly retrieve the Uri and filename of a directory with 1000+ files.
In summary, my recommendation is to avoid using DocumentFile if you are working with more than just a few files. Instead use ContentResolver#query() to retrieve the Uri and other information by selecting multiple columns in the query. For file operations, use the static methods in the DocumentsContract class by passing the appropriate Uri's.
By the way, it seems that the sortOrder parameter of ContentResolver#query() gets completely ignored in the above code snippet when tested on Android 11 and Android 9. I would manually sort the results instead of relying on the query order.
Why is DocumentFile so slow?
For ~600 files you are performing ~600 requests of a ContentProvider to get the display name, which means ~600 IPC transactions.
Instead, use MediaStore to query for all indexed media with the application/pdf MIME type.
In case someone comes up here still looking for a solution,
I built a wrapper over this with some pretty good performance.
You can check the wrapper & performance info. here:
https://github.com/ItzNotABug/DocumentFileCompat
To use DocumentsContract to obtain children documents, see https://developer.android.com/reference/android/provider/DocumentsContract.html#buildChildDocumentsUriUsingTree(android.net.Uri, java.lang.String).
The Uri returned from ACTION_OPEN_DOCUMENT_TREE is a tree document URI. Use the above method to build the Uri to query all children documents.
The root document ID can be obtained using https://developer.android.com/reference/android/provider/DocumentsContract.html#getTreeDocumentId(android.net.Uri) with the Uri returned from ACTION_OPEN_TREE_DOCUMENT.
To obtain a speed little less than the one from File use DocumentsContract instead of DocumentFile to list the content of trees obtained with Intent.ACTION_OPEN_DOCUMENT_TREE.
Related
I'm trying to make a simple Android app for my own phone that looks at my Gallery, and for each image/video file:
creates a Bitmap thumbnail of the file
records the file's absolute path on the phone
Essentially, I'll have the following data model:
public class MediaFileModel {
private Bitmap thumbnail;
private String absPath;
// ctor, getters and setters omitted for brevity
}
And I need something that looks at all the files in my Gallery and yields a List<MediaFileModel>. My best attempt is here:
public List<MediaFileModel> getAllGalleryMedia() {
String[] projection = { MediaStore.MediaColumns.DATA };
List<MediaFileModel> galleryMedia = new ArrayList<>();
Cursor cursor = getActivity().getContentResolver().query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
null);
// I believe here I'm iterating over all the gallery files (???)
while(cursor.moveToNext()) {
MediaFileModel next = new MediaFileModel();
// is this how I get the abs path of the current file?!?
String absPath = cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.DATA));
Bitmap thumbnail = null;
if (true /* ??? cursor is pointing to a media file that is an image/photo ??? */) {
thumbnail = ThumbnailUtils.extractThumbnail(BitmapFactory.decodeFile(absPath), 64, 64);
} else {
// else we have a video (I assume???)
thumbnail = ThumbnailUtils.createVideoThumbnail(absPath, MediaStore.Images.Thumbnails.MINI_KIND);
}
next.setThumbnail(thumbnail);
next.setAbsPath(absPath);
galleryMedia.add(next);
}
return galleryMedia;
}
However I'm not sure if my media query is setup correctly and I'm definitely not sure how to determine whether the file is an image/photo of a video, which I (believe I) need so that I can use the correct method for obtaining the thumbnail Bitmap.
Can anyone help nudge me over the finish line here?
Here is a code I was using for something similar, I hope this will help:
//The code has been removed
Note: Sorry I've removed the code because I am not sure if it has some portion copied from other open-sourced codes, I am using it in my apps but I can't remember if it is quoted from other source, so I've removed it, also it could be auto-completed using github copilot
I have an Android app that needs to let the user select some pictures from the gallery and send these pictures to the backend (together with some other data).
To allow the user to select the pictures I have the following in my Fragment:
private void pickImages() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
startActivityForResult(intent, PICK_PHOTO_FOR_AVATAR);
}
I get the result of the selected photos by the user in here:
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PICK_PHOTO_FOR_AVATAR && resultCode == Activity.RESULT_OK) {
if (data == null) {
//Display an error
Toast.makeText(getActivity(), "There was an error getting the pictures", Toast.LENGTH_LONG).show();
return;
}
ClipData clipData = data.getClipData();
String fileName = null, extension = null;
//if ClipData is null, then we have a regular file
if (clipData == null) {
//get the selected file uri
fileName = FileUtils.getPath(getActivity(), data.getData());
//obtain the extension of the file
int index = fileName.lastIndexOf('.');
if (index > 0) {
extension = fileName.substring(index + 1);
if (extension.equals("jpg") || extension.equals("png") || extension.equals("bmp") || extension.equals("jpeg"))
isAttachedFile = true;
}
}
ArrayList<Uri> photosUris = new ArrayList<>();
//for each image in the list of images, add it to the filesUris
if (clipData != null) for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
Uri uri = item.getUri();
switch (i) {
case 0:
picture1Uri = uri;
break;
case 1:
picture2Uri = uri;
break;
}
photosUris.add(uri);
}
else if (isAttachedFile) {
Uri uri = Uri.parse(fileName);
picture1Uri = uri;
photosUris.add(uri);
}
uris = photosUris;
if (picture1Uri != null) {
image1.setVisibility(View.VISIBLE);
image1.setImageURI(picture1Uri);
}
if (picture2Uri != null) {
image2.setVisibility(View.VISIBLE);
image2.setImageURI(picture2Uri);
}
}
I then send the list of URIs to the Presenter, where I execute my MultiPart Retrofit call to the backend:
//obtain the file(s) information of the message, if any
if (uris != null && uris.size() > 0) {
for (int i = 0; i < uris.size(); i++) {
File file = null;
//this is the corect way to encode the pictures
String encodedPath = uris.get(i).getEncodedPath();
file = new File(encodedPath);
builder.addFormDataPart("photos[]", file.getName(), RequestBody.create(MediaType.parse("multipart/form-data"), file));
}
}
MultipartBody requestBody = builder.build();
//send the newly generated ticket
Call<GenerateNewTicketResponse> generateNewTicketCall = OperatorApplication.getApiClient().generateNewTicket(Constants.BEARER + accessToken, requestBody);
The problem is that this sometimes works, sometimes doesn't. Sometimes I get the error "java.io.FileNotFoundException", which throws me in the onFailure() callback of the Retrofit call.
I found the following stackoverflow post Reading File from Uri gives java.io.FileNotFoundException: open failed: ENOENT but I'm not exactly sure how to implement the general suggestion in that response to my particular situation.
What would be the right way to get the right path towards the pictures selected by the user such that I can create files out of them and attach them in my MultiPart request?
Commonsware suggested to
Use a ContentResolver and openInputStream() to get an InputStream on the content pointed to by the Uri. Then, pass that to your decoding logic, such as BitmapFactory and its decodeStream() method.
, but I'm not sure exactly how to do that programmatically.
Any help would be appreciated.
To allow the user to select the pictures I have the following in my Fragment:
This code is using ACTION_GET_CONTENT. Particularly on Android 7.0+, generally that (and ACTION_OPEN_DOCUMENT) will return Uri values with a content scheme. Your code assumes that you are getting Uri values with a file scheme, where the path actually has meaning. Moreover, your code assumes that the user is picking files on the filesystem that you can access, and there is nothing that forces the user to do that. ACTION_GET_CONTENT can be supported by apps where their content is:
A local file on external storage
A local file on internal storage for the other app
A local file on removable storage
A local file that is encrypted and needs to be decrypted on the fly
A stream of bytes held in a BLOB column in a database
A piece of content on the Internet that needs to be downloaded by the other app first
Content that is generated on the fly
...and so on
Instead of using RequestBody.create(), use the InputStreamRequestBody from this OkHttp issue comment. You provide the same media type as before, but instead of a File (that you are incorrectly creating), you provide a ContentResolver (from getContentResolver() on a Context) and the Uri.
This blog post demonstrates how to use InputStreamRequestBody (specifically a Kotlin port of the original) to upload content in this fashion. This blog post provides another look at the same problem and a similar solution.
I have an Android app that needs to let the user select some pictures from the gallery and send these pictures to the backend (together with some other data).
To allow the user to select the pictures I have the following in my Fragment:
private void pickImages() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");
intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
startActivityForResult(intent, PICK_PHOTO_FOR_AVATAR);
}
I get the result of the selected photos by the user in here:
#Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == PICK_PHOTO_FOR_AVATAR && resultCode == Activity.RESULT_OK) {
if (data == null) {
//Display an error
Toast.makeText(getActivity(), "There was an error getting the pictures", Toast.LENGTH_LONG).show();
return;
}
ClipData clipData = data.getClipData();
String fileName = null, extension = null;
//if ClipData is null, then we have a regular file
if (clipData == null) {
//get the selected file uri
fileName = FileUtils.getPath(getActivity(), data.getData());
//obtain the extension of the file
int index = fileName.lastIndexOf('.');
if (index > 0) {
extension = fileName.substring(index + 1);
if (extension.equals("jpg") || extension.equals("png") || extension.equals("bmp") || extension.equals("jpeg"))
isAttachedFile = true;
}
}
ArrayList<Uri> photosUris = new ArrayList<>();
//for each image in the list of images, add it to the filesUris
if (clipData != null) for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
Uri uri = item.getUri();
switch (i) {
case 0:
picture1Uri = uri;
break;
case 1:
picture2Uri = uri;
break;
}
photosUris.add(uri);
}
else if (isAttachedFile) {
Uri uri = Uri.parse(fileName);
picture1Uri = uri;
photosUris.add(uri);
}
uris = photosUris;
if (picture1Uri != null) {
image1.setVisibility(View.VISIBLE);
image1.setImageURI(picture1Uri);
}
if (picture2Uri != null) {
image2.setVisibility(View.VISIBLE);
image2.setImageURI(picture2Uri);
}
}
I then send the list of URIs to the Presenter, where I execute my MultiPart Retrofit call to the backend:
//obtain the file(s) information of the message, if any
if (uris != null && uris.size() > 0) {
for (int i = 0; i < uris.size(); i++) {
File file = null;
//this is the corect way to encode the pictures
String encodedPath = uris.get(i).getEncodedPath();
file = new File(encodedPath);
builder.addFormDataPart("photos[]", file.getName(), RequestBody.create(MediaType.parse("multipart/form-data"), file));
}
}
MultipartBody requestBody = builder.build();
//send the newly generated ticket
Call<GenerateNewTicketResponse> generateNewTicketCall = OperatorApplication.getApiClient().generateNewTicket(Constants.BEARER + accessToken, requestBody);
The problem is that this sometimes works, sometimes doesn't. Sometimes I get the error "java.io.FileNotFoundException", which throws me in the onFailure() callback of the Retrofit call.
I found the following stackoverflow post Reading File from Uri gives java.io.FileNotFoundException: open failed: ENOENT but I'm not exactly sure how to implement the general suggestion in that response to my particular situation.
What would be the right way to get the right path towards the pictures selected by the user such that I can create files out of them and attach them in my MultiPart request?
Commonsware suggested to
Use a ContentResolver and openInputStream() to get an InputStream on the content pointed to by the Uri. Then, pass that to your decoding logic, such as BitmapFactory and its decodeStream() method.
, but I'm not sure exactly how to do that programmatically.
Any help would be appreciated.
To allow the user to select the pictures I have the following in my Fragment:
This code is using ACTION_GET_CONTENT. Particularly on Android 7.0+, generally that (and ACTION_OPEN_DOCUMENT) will return Uri values with a content scheme. Your code assumes that you are getting Uri values with a file scheme, where the path actually has meaning. Moreover, your code assumes that the user is picking files on the filesystem that you can access, and there is nothing that forces the user to do that. ACTION_GET_CONTENT can be supported by apps where their content is:
A local file on external storage
A local file on internal storage for the other app
A local file on removable storage
A local file that is encrypted and needs to be decrypted on the fly
A stream of bytes held in a BLOB column in a database
A piece of content on the Internet that needs to be downloaded by the other app first
Content that is generated on the fly
...and so on
Instead of using RequestBody.create(), use the InputStreamRequestBody from this OkHttp issue comment. You provide the same media type as before, but instead of a File (that you are incorrectly creating), you provide a ContentResolver (from getContentResolver() on a Context) and the Uri.
This blog post demonstrates how to use InputStreamRequestBody (specifically a Kotlin port of the original) to upload content in this fashion. This blog post provides another look at the same problem and a similar solution.
I have an image processing app. In my code, I pass my service an ArrayList and the service does the rest. Now I want to extend the functionality of my app and let users be able to go to the gallery, pick one picture and, using the share button, send it to my app to be processed.
I want to reuse the most code as possible, so I decided that a good way to go would be converting those URIs returned by the send action to actual file paths.
My solution works as expected with QuickPic, but not with Google Photos. My code is as follows:
//MainFragment.onCreateView()
Intent intent = getActivity().getIntent();
if(Intent.ACTION_SEND.equals(intent.getAction()) {
handleSingleImage(intent);
}
private void handleSingleImage(Intent intent) {
Uri uri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
Log.d("MYURISTRING", uri.toString());
ArrayList<String> selectedPaths = new ArrayList<String>();
String path = Utils.getRealPathFromURI(getActivity(), uri);
selectedPaths.add(path);
Utils.startProcessPhotosService(getActivity(), MainFragment.this, selectedPaths);
}
//Utils
public static String getRealPathFromURI(Context context, Uri contentUri) {
/*String[] proj = {MediaStore.Images.Media.DATA};
CursorLoader cursorLoader = new CursorLoader(context, contentUri, proj, null, null, null);
Cursor cursor = cursorLoader.loadInBackground();
int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
return cursor.getString(columnIndex);*/
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(contentUri, null, null, null, null);
cursor.moveToFirst();
String path = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA));
cursor.close();
return path;
}
If I test this with a photo from QuickPic app, everything works as expected, and the URI on the log is as follows:
content://media/external/images/media/135695
But if I test this with Google Photos, my app crashes, and the URI is as follows:
content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Fimages%2Fmedia%2F135669/ACTUAL
How can I do to support both styles of URI (and, possibly, more than these)?
Thank you
so I decided that a good way to go would be converting those URIs returned by the send action to actual file paths
That is so not a good way to go.
As I have already pointed out a couple of times today, and dozens upon dozens of times in the past months, a Uri is not a file. You cannot reliably get a local file path for a Uri. There may not even be a local path, let alone one that you can access.
If you wish to use the content represented by the Uri, use getContentResolver().openInputStream(), or things that in turn use it (e.g., Picasso for image loading).
I am storing small images inside a table. The field called "questionImage" is storing these images.
The table looks as follows:
I have two questions:
How can I retrieve the file name of the image files? For example, the first object in the table above has an image named "5D_C.png". How can I get that name using Java in Aandroid? I am using Eclipse, if that makes any difference.
Is there a way to import these images in a batch? For example, if I didn't have any image fields, I could have made an Excel sheet of my data and exported it as CSV into Parse.com. Is the same workflow achievable with image fields inside a class?
First you have to make a Parse query to obtain for the objects you are interested in. See here
Then, in the callback, you can iterate over these elements to collect the file names. Something like this:
public void done(List<ParseObject> objsList, ParseException e) {
if (e == null) {
Log.d("score", "Retrieved " + objsList.size() + " things");
ArrayList<String> fileNames = new ArrayList<>();
for (ParseObject ob : objsList) {
//This is the important part
String fileName = ob.getParseFile("questionImage").getName();
fileNames.add(fileName);
}
} else {
Log.d("score", "Error: " + e.getMessage());
}
}
Similarily, if you want to obtain the files, you can (in the same interation), get the file and save it
byte[] bitmapdata = ob.getParseFile("questionImage").getData();
Bitmap bm = BitmapFactory.decodeByteArray(bitmapdata, 0, bitmapdata.length);
//Save bitmap in a file
Hope this helps.
UPDATE: not sure if I understood correctly your question. With "import" you mean get the images from parse or upload them to parse. Parse is not designed to make massive operations (batch operations are quite limited).