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.
Related
I am currently trying to reduce the quality of videos and audio before uploading to and online cloud database. Below is the code I have been using to record videos.
recordVideoIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
Changing the 0 to 1 in EXTRA_VIDEO_QUALITY will increase the quality and vice versa, but the file is still too large to download if it a 30 second or more video.
private void RecordVideoMode() {
Intent recordVideoIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
// Ensure that there's a camera activity to handle the intent
if (recordVideoIntent.resolveActivity(getPackageManager()) != null) {
videoFile = createVideoFile();
// Continue only if the File was successfully created
if (videoFile != null) {
videoURI = FileProvider.getUriForFile(this,
"com.example.android.fileprovider",
videoFile);
recordVideoIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, 0);
recordVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoURI);
startActivityForResult(recordVideoIntent, REQUEST_VIDEO_CAPTURE);
}
}
}
Any help is very much appreciated!
You can go with this two methods :
Encode it to a lower bit rate and/or lower resolution. Have a look here:
Is it possible to compress video on Android?
Try to zip/compress it. Have a look here:
http://www.jondev.net/articles/Zipping_Files_with_Android_%28Programmatically%29
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 am trying to make an app that sends files from my Android Watch to my Android Phone.
The problem I have is that if I record and save multiple files and send all of them at the same time, I do not get all the files back on the phone side. I only receive one file.
The code for sending the file is as follows. This code is implemented on the Watch side.:
public void sendData(View v){
String fname = "_Activity.bin";
int FileCounterCopy = FileCounter;
if(mGoogleApiClient.isConnected()){
for (int i = 0; i < FileCounterCopy ; i++){
String FileName = String.valueOf(i) + fname;
File dataFile = new File(Environment.getExternalStorageDirectory(), FileName);
Log.i("Path", Environment.getExternalStorageDirectory().toString());
Log.i("file", dataFile.toString());
Asset dataAsset = createAssetfromBin(dataFile);
sensorData = PutDataMapRequest.create(SENSOR_DATA_PATH);
sensorData.getDataMap().putAsset("File", dataAsset);
PutDataRequest request = sensorData.asPutDataRequest();
Wearable.DataApi.putDataItem(mGoogleApiClient, request).setResultCallback(new ResultCallback<DataApi.DataItemResult>() {
#Override
public void onResult(DataApi.DataItemResult dataItemResult) {
Log.e("SENDING IMAGE WAS SUCCESSFUL: ", String.valueOf(dataItemResult.getStatus().isSuccess()));
}
});
boolean deleted = dataFile.delete();
Log.i("Deleted", String.valueOf(deleted));
FileCounter--;
}
mTextView.setText(String.valueOf(FileCounter));
Return();
}
else {
Log.d("Not", "Connecteddddddddd");
}
}
The code for receiving the files is as follows and is implemented on the phone side.
#Override
public void onDataChanged(DataEventBuffer dataEvents) {
Counter++;
final List<DataEvent> events = FreezableUtils.freezeIterable(dataEvents);
dataEvents.close();
Log.e("List Size: ", String.valueOf(events.size()));
for (DataEvent event : events) {
if (event.getType() == DataEvent.TYPE_CHANGED) {
Log.v("Data is changed", "========================");
String path = event.getDataItem().getUri().getPath();
if (SENSOR_DATA_PATH.equals(path)) {
DataMapItem dataMapItem = DataMapItem.fromDataItem(event.getDataItem());
fileAsset = dataMapItem.getDataMap().getAsset("File");
myRunnable = createRunnable();
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)
new Thread(myRunnable).start();
}
}
}
status.setText("Received" + " File_"+ String.valueOf(Counter) );
}
Right before the for loop, I check the size of the event and it only shows a size of 1, no matter how many files I save.
I am stuck on how to implement this (tbh I used code from youtube video/online resources so I am not 100% sure on how some of the api works).
Thanks in advance!
You're putting all of the files at the same path, with nothing to differentiate them - so each one you put in overwrites the previous ones. The Data API works much like a filesystem in this regard.
In your sendData method, you need code something like this:
sensorData = PutDataMapRequest.create(SENSOR_DATA_PATH + '/' + dataFile.toString());
And then in onDataChanged, either only check the path prefix...
if (path.startsWith(SENSOR_DATA_PATH)) {
...or, preferably, put the value of SENSOR_DATA_PATH in your manifest declaration as an android:pathPrefix element in the intent-filter of your data receiver. You can then remove the path check from your Java code completely. Docs for that are here: https://developers.google.com/android/reference/com/google/android/gms/wearable/WearableListenerService
One other thing: it's good practice to clear stuff like these files out of the Data API when you're done using them, so that they're not taking up space there.
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.
How to normalise a URL in Java to remove the fragment. I.e. from https://www.website.com#something to https://www.website.com
This is possible with the URL.Normalize code, although in this specific use case I've only got a full absolute URL which needs to remain intact.
I'd like to be able to modify this code slightly to remove the fragment from the URL;
//The website below is just an example. In reality, this URL is unknown and could be anything. Both with and without a fragment depending on the use case
URL absUrl = new URL("https://www.website.com#something");
My thoughts so far is that this is only going to be possible by breaking down the URL into the Protocol + Domain + Path then joining it all back together which does appear to work, but there must be a more elegant way of doing this.
Fragment removal is fairly simple using the conversion methods toURI and toURL. So to convert a URL to a URI:
URL url = /*what have you*/ …
URI u = url.toURI();
To remove any fragment from the URI:
if( u.getFragment() != null ) { // Remake with same parts, less the fragment:
u = new URI( u.getScheme(), u.getSchemeSpecificPart(), /*fragment*/null ); }
In reconstructing a URI from its parts like that, it’s important to use the decoded getters (as shown), not the corresponding raw ones. For authority on this usage, see e.g. the Identity section of the API.
To convert the result back to a URL:
url = u.toURL();
Fragments do not exist as a separate entity in Java URLs. But you can convert a URL into a URI and back to remove a fragment. I did it like this:
URL url;
...
if (url.toString().contains("#")) {
URI uri = null;
try {
uri = new URI(url.getProtocol(), url.getHost(), url.getPath(), null);
String file = "";
if (uri.getPath() != null) {
file += uri.getPath();
}
if (uri.getQuery() != null) {
file += uri.getQuery();
}
url = new URL(uri.getScheme(), uri.getHost(), uri.getPort(), file);
} catch (URISyntaxException e) {
...
} catch (MalformedURLException e) {
...
}
}