I'm working on an app where I'm downloading a PDF file, saving it to internal storage and then opening that file in other app using FileProvider.
Note: It may be a duplicate question, I've gone through most of the questions on StackOverflow, but still didn't find the solution.
The file is getting downloaded fine but when I'm opening it, it is empty.
The downlaoded file is 30 kb and it has 5 pages but all are empty.
Initially, I thought it is empty because the other app doesn't have permission to open the file, but I did another thing to check whether it is a permission issue. I've saved the file to external storage, still, it was empty. So, it means it is not a permission issue.
Please Note:
Along with pdf file, there is some .xls file as well and when I'm opening those in excel android app, it says cannot open the file. This indicates, that there is some issue while writing the byte stream.
Retrofit Interface.java
#GET(ApiConstants.END_POINT_DOWNLOAD_DOCUMENT)
#Streaming
Call<ResponseBody> downloadDocument(#Query("bucket") String bucket, #Query("filename") String fileName);
Code to Download the file: Here I'm checking if a file is already there, then return the file, otherwise download the file.
public LiveData<Resource<File>> openOrDownloadFile(String bucket, String fileName) {
MutableLiveData<Resource<File>> documentLiveData = new MutableLiveData<>();
documentLiveData.postValue(Resource.loading(null));
Context context = MyApp.getInstance();
final File file = new File(context.getFilesDir(), fileName);
if (file.exists()) {
documentLiveData.postValue(Resource.success(file));
} else {
Call<ResponseBody> call = apiService.downloadDocument(bucket, fileName);
call.enqueue(new Callback<ResponseBody>() {
#Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
if (response.isSuccessful()) {
appExecutors.diskIO().execute(new Runnable() {
#Override
public void run() {
try {
InputStream inputStream = null;
OutputStream outputStream = null;
try {
byte[] fileReader = new byte[4096];
inputStream = response.body().byteStream();
outputStream = new FileOutputStream(file);
while (true) {
int read = inputStream.read(fileReader);
if (read == -1) {
break;
}
outputStream.write(fileReader, 0, read);
}
documentLiveData.postValue(Resource.success(file));
outputStream.flush();
} catch (IOException e) {
documentLiveData.postValue(Resource.error("Error: Unable to save file/n"+e.getLocalizedMessage(), null));
} finally {
if (inputStream != null) {
inputStream.close();
}
if (outputStream != null) {
outputStream.close();
}
}
} catch (IOException e) {
Log.e(AppConstants.TAG, e.getMessage(), e);
documentLiveData.postValue(Resource.error("Error: Unable to save file/n"+e.getLocalizedMessage(), null));
}
}
});
} else {
documentLiveData.postValue(Resource.error("Unable to download file", null));
}
}
#Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
documentLiveData.postValue(Resource.error(t.getLocalizedMessage(), null));
}
});
}
return documentLiveData;
}
Fragment Code
private void onItemClickListener(Document document) {
mDocumentsViewModel.openORDownloadFile(document.getType(), document.getName()).observe(this, new Observer<Resource<File>>() {
#Override
public void onChanged(#Nullable Resource<File> fileResource) {
binding.setResource(fileResource);
if (fileResource.status == Status.SUCCESS) {
openFile(fileResource.data);
}
}
});
}
void openFile(File file) {
Intent intent = new Intent(Intent.ACTION_VIEW);
Uri uri = FileProvider.getUriForFile(getContext(), BuildConfig.APPLICATION_ID, file);
intent.setDataAndType(uri, mDocumentsViewModel.getMimeType(file.getAbsolutePath()));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
PackageManager pm = getActivity().getPackageManager();
if (intent.resolveActivity(pm) != null) {
startActivity(intent);
} else {
Toast.makeText(getActivity(), "This file cannot be opened on this device. Please download some compatible app from play store", Toast.LENGTH_SHORT).show();
}
}
Following are the versions :
ext.retrofit_version = "2.4.0"
ext.okhttp_version = "3.8.0"
I'm struggling with this issue, it'll be a great help if you can point out the issue. Thank you.
Update: The problem was with the backend APIs. My code was correct. Once they've fixed the problem at there side, it started working at my side without any changes.
Related
Typical Scenario
In order to upload a file using WebView, it's typically needed to override WebChromeClient, and start a file chooser Activity for result:
...
webView.setWebChromeClient(new WebChromeClient() {
#Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams) {
mFilePathCallback = filePathCallback;
Intent intent = new Intent(MainActivity.this, FilePickerActivity.class);
startActivityForResult(intent, 1);
return true;
}
});
...
Then, once file is selected (for simplicity, only a single file can be selected at a time), onActivityResult() is called with a file uri stored in a data object. So, the uri is retrieved and handed over to filePathCallback and the file gets uploaded:
...
#Override
protected void onActivityResult(int requestCode, int resultCode, #Nullable Intent data) {
if (requestCode == 1) {
mFilePathCallback.onReceiveValue(new Uri[] {data.getData()});
} else {
super.onActivityResult(requestCode, resultCode, data);
}
}
...
Basically, filePathCallback requires a uri to a file so that to upload it.
The problem
What if I have only an InputStream that contains the data I need to upload rather than a file with a URI? And I cannot save that data to a file so that to generate a URI (for security reasons). Is there a way to upload the data as a file in such case? In particular, can I convert an InputStream to a content URI and then upload it?
Approach 1
This approach works fine, if uri is generated using Uri.fromFile() as below:
...
Uri uri = Uri.fromFile(file);
...
Approach 2
If I implement my own ContentProvider and override openFile there in such a way, that it uses ParcelFileDescriptor.open() to create a ParcelFileDescriptor, then uploading a file based on a uri provided by getContentUri(...) is working without problems:
FileUploadProvider.java
public class FileUploadProvider extends ContentProvider {
public static Uri getContentUri(String name) {
return new Uri.Builder()
.scheme("content")
.authority(PROVIDER_AUTHORITY)
.appendEncodedPath(name)
.appendQueryParameter(OpenableColumns.DISPLAY_NAME, name)
.build();
}
#Nullable
#Override
public ParcelFileDescriptor openFile(#NonNull Uri uri, #NonNull String mode) throws FileNotFoundException {
List<String> segments = uri.getPathSegments();
File file = new File(getContext().getApplicationContext().getCacheDir(),
TextUtils.join(File.separator, segments));
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
...
}
Approach 3
However, if I create a ParcelFileDescriptor with help of ParcelFileDescriptor.createPipe(), then file upload never finishes, so it basically doesn't work:
...
public ParcelFileDescriptor openFile(#NonNull Uri uri, #NonNull String mode) throws FileNotFoundException {
List<String> segments = uri.getPathSegments();
File file = new File(getContext().getApplicationContext().getCacheDir(),
TextUtils.join(File.separator, segments));
InputStream inputStream = new FileInputStream(file);
ParcelFileDescriptor[] pipe;
try {
pipe = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
throw new RuntimeException(e);
}
ParcelFileDescriptor readPart = pipe[0];
ParcelFileDescriptor writePart = pipe[1];
try {
IOUtils.copy(inputStream, new ParcelFileDescriptor.AutoCloseOutputStream(writePart));
} catch (IOException e) {
throw new RuntimeException(e);
}
return readPart;
}
...
Approach 4
To make matters worse, if I create a ParcelFileDescriptor with help of MemoryFile, then file upload never finishes as well:
...
public ParcelFileDescriptor openFile(#NonNull Uri uri, #NonNull String mode) throws FileNotFoundException {
List<String> segments = uri.getPathSegments();
File file = new File(getContext().getApplicationContext().getCacheDir(),
TextUtils.join(File.separator, segments));
try {
MemoryFile memoryFile = new MemoryFile(file.getName(), (int) file.length());
byte[] fileBytes = Files.readAllBytes(file.toPath());
memoryFile.writeBytes(fileBytes, 0, 0, (int) file.length());
Method method = memoryFile.getClass().getDeclaredMethod("getFileDescriptor");
FileDescriptor fileDescriptor = (FileDescriptor) method.invoke(memoryFile);
Constructor<ParcelFileDescriptor> constructor = ParcelFileDescriptor.class.getConstructor(FileDescriptor.class);
ParcelFileDescriptor parcelFileDescriptor = constructor.newInstance(fileDescriptor);
return parcelFileDescriptor;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
...
Why is file upload not working for Approach 3 and Approach 4?
Example
I created a sample app to showcase my issue here.
Below are steps to follow to reproduce it. First log in to your gmail account and click "create new message".
Solution
My colleagues have found a solution to this with help of StorageManager - please read more about the component here.
First, create a callback that handles file system requests from ProxyFileDescriptor in FileUploadProvider:
private static class CustomProxyFileDescriptorCallback extends ProxyFileDescriptorCallback {
private ByteArrayInputStream inputStream;
private long length;
public CustomProxyFileDescriptorCallback(File file) {
try {
byte[] fileBytes = Files.readAllBytes(file.toPath());
length = fileBytes.length;
inputStream = new ByteArrayInputStream(fileBytes);
} catch (IOException e) {
// do nothing here
}
}
#Override
public long onGetSize() {
return length;
}
#Override
public int onRead(long offset, int size, byte[] out) {
inputStream.skip(offset);
return inputStream.read(out,0, size);
}
#Override
public void onRelease() {
try {
inputStream.close();
} catch (IOException e) {
//ignore this for now
}
inputStream = null;
}
}
Then, create a file descriptor using StorageManager that will read the InputStream with help of the callback above.
...
StorageManager storageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
ParcelFileDescriptor descriptor;
try {
descriptor = storageManager.openProxyFileDescriptor(
ParcelFileDescriptor.MODE_READ_ONLY,
new CustomProxyFileDescriptorCallback(file),
new Handler(Looper.getMainLooper()));
} catch (IOException e) {
throw new RuntimeException(e);
}
return descriptor;
...
Please find the full code on github here.
Limitation
This approach is working only for Android API higher than 25.
While using MediaRecorder, we don't have pause/resume for API level below 24.
So there can be a way to do this is:
On pause event stop the recorder and create the recorded file.
And on resume start recording again and create another file and keep doing so until user presses stop.
And at last merge all files.
Many people asked this question on SO, but couldn't find anyway to solve this. People talk about creating multiple media files by stopping recording on pause action and restarting on resume. So my question is How can we merge/join all media file programmatically?
Note: in my case MPEG4 container - m4a for audio and mp4 for video.
I tried using SequenceInputStream to merge multiple InputStream of respective generated recorded files. But it always results the first file only.
Code Snippet:
Enumeration<InputStream> enu = Collections.enumeration(inputStreams);
SequenceInputStream sqStream = new SequenceInputStream(enu);
while ((oneByte = sqStream.read(buffer)) != -1) {
fileOutputStream.write(buffer, 0, oneByte);
}
sqStream.close();
while (enu.hasMoreElements()) {
InputStream element = enu.nextElement();
element.close();
}
fileOutputStream.flush();
fileOutputStream.close();
I could solve this problem using mp4parser library. Thanks much to author of this library :)
Add below dependency in your gradle file:
compile 'com.googlecode.mp4parser:isoparser:1.0.2'
The solution is to stop recorder when user pause and start again on resume as already mentioned in many other answers in stackoverflow. Store all the audio/video files generated in an array and use below method to merge all media files. The example is also taken from mp4parser library and modified little bit as per my need.
public static boolean mergeMediaFiles(boolean isAudio, String sourceFiles[], String targetFile) {
try {
String mediaKey = isAudio ? "soun" : "vide";
List<Movie> listMovies = new ArrayList<>();
for (String filename : sourceFiles) {
listMovies.add(MovieCreator.build(filename));
}
List<Track> listTracks = new LinkedList<>();
for (Movie movie : listMovies) {
for (Track track : movie.getTracks()) {
if (track.getHandler().equals(mediaKey)) {
listTracks.add(track);
}
}
}
Movie outputMovie = new Movie();
if (!listTracks.isEmpty()) {
outputMovie.addTrack(new AppendTrack(listTracks.toArray(new Track[listTracks.size()])));
}
Container container = new DefaultMp4Builder().build(outputMovie);
FileChannel fileChannel = new RandomAccessFile(String.format(targetFile), "rw").getChannel();
container.writeContainer(fileChannel);
fileChannel.close();
return true;
}
catch (IOException e) {
Log.e(LOG_TAG, "Error merging media files. exception: "+e.getMessage());
return false;
}
}
Use flag isAudio as true for Audio files and false for Video files.
Another solution is merging with FFmpeg
Add this line to your app build.gradle
implementation 'com.writingminds:FFmpegAndroid:0.3.2'
And use below code to merge videos.
String textFile = "";
try {
textFile = getTextFile().getAbsolutePath();
} catch (IOException e) {
e.printStackTrace();
}
String[] cmd = new String[]{
"-y",
"-f",
"concat",
"-safe",
"0",
"-i",
textFile,
"-c",
"copy",
"-preset",
"ultrafast",
getVideoFilePath()};
mergeVideos(cmd);
getTextFile()
private File getTextFile() throws IOException {
videoFiles = new String[]{firstPath, secondPath, thirdPatch};
File file = new File(getActivity().getExternalFilesDir(null), System.currentTimeMillis() + "inputFiles.txt");
FileOutputStream out = new FileOutputStream(file, false);
PrintWriter writer = new PrintWriter(out);
StringBuilder builder = new StringBuilder();
for (String path : videoFiles) {
if (path != null) {
builder.append("file ");
builder.append("\'");
builder.append(path);
builder.append("\'\n");
}
}
builder.deleteCharAt(builder.length() - 1);
String text = builder.toString();
writer.print(text);
writer.close();
out.close();
return file;
}
getVideoFilePath()
private String getVideoFilePath() {
final File dir = getActivity().getExternalFilesDir(null);
return (dir == null ? "" : (dir.getAbsolutePath() + "/"))
+ System.currentTimeMillis() + ".mp4";
}
mergeVideos()
private void mergeVideos(String[] cmd) {
FFmpeg ffmpeg = FFmpeg.getInstance(getActivity());
try {
ffmpeg.execute(cmd, new ExecuteBinaryResponseHandler() {
#Override
public void onStart() {
startTime = System.currentTimeMillis();
}
#Override
public void onProgress(String message) {
}
#Override
public void onFailure(String message) {
Toast.makeText(getActivity(), "Failed " + message, Toast.LENGTH_SHORT).show();
}
#Override
public void onSuccess(String message) {
}
#Override
public void onFinish() {
Toast.makeText(getActivity(), "Videos are merged", Toast.LENGTH_SHORT).show();
}
});
} catch (FFmpegCommandAlreadyRunningException e) {
// Handle if FFmpeg is already running
}
}
Run this code before merging
private void checkFfmpegSupport() {
FFmpeg ffmpeg = FFmpeg.getInstance(this);
try {
ffmpeg.loadBinary(new LoadBinaryResponseHandler() {
#Override
public void onStart() {
}
#Override
public void onFailure() {
Toast.makeText(VouchActivity.this, "FFmpeg not supported on this device :(", Toast.LENGTH_SHORT).show();
}
#Override
public void onSuccess() {
}
#Override
public void onFinish() {
}
});
} catch (FFmpegNotSupportedException e) {
// Handle if FFmpeg is not supported by device
}
}
I am trying to append data to my existing file on Google Drive using the procedure given at https://developers.google.com/drive/android/files#making_modifications
Although control is going correctly but no change is getting reflected in the file.
public void addHistoryEntry(final String location) {
final DriveFile file = Drive.DriveApi.getFile(getClient(), historyFileId);
file.open(getClient(), DriveFile.MODE_READ_WRITE, null)
.setResultCallback(new ResultCallback<DriveContentsResult>() {
#Override
public void onResult(DriveContentsResult driveContentsResult) {
if (!driveContentsResult.getStatus().isSuccess()) {
L.c("Problem in Writing to file");
return;
}
DriveContents contents = driveContentsResult.getDriveContents();
try {
ParcelFileDescriptor parcelFileDescriptor = contents.getParcelFileDescriptor();
FileInputStream fileInputStream = new FileInputStream(parcelFileDescriptor.getFileDescriptor());
// Read to the end of the file.
fileInputStream.read(new byte[fileInputStream.available()]);
// Append to the file.
FileOutputStream fileOutputStream = new FileOutputStream(parcelFileDescriptor
.getFileDescriptor());
Writer writer = new OutputStreamWriter(fileOutputStream);
writer.write(location+"\n");
} catch (IOException e) {
e.printStackTrace();
}
contents.commit(getClient(), null).setResultCallback(new ResultCallback<Status>() {
#Override
public void onResult(Status status) {
if(status.isSuccess()) L.c("Write to file successful");
else L.c("Write to file failed");
}
});
}
});
}
Please help me to debug the code.
Can you try flushing your OutputStream, i.e. writer.flush(), before committing the contents?
Also, instead of reading over all the existing bytes in the InputStream, consider using FileOutputStream#getChannel since FileChannel has facilities to quickly seek to the end of file, i.e. fileChannel.position(fileChannel.size())
In my app i have started download service,it is working fine in background.During download my testing team doing force stop and clear data or Uninstall.But After uninstall or clear data still my Download service is running in background.During download i have installed the same app again but it is misbehaving some thing.While uninstall or clear data or force stop i have to cancel the download How?
public class FileDownloaderService extends IntentService {
private CarcarePreferences preferences;
public FileDownloaderService() {
super("FileDownloaderService");
}
#Override
public void onCreate() {
super.onCreate();
preferences = CarcarePreferences.getCarcarePreferencesObject(getApplicationContext());
DBHelper.getInstance(getApplicationContext()).open();
downloadManager = (DownloadManager) getSystemService(DOWNLOAD_SERVICE);
}
#Override
protected void onHandleIntent(Intent intent) {
Bundle extras = intent.getExtras();
if (extras == null) {
return;
}
if (extras.containsKey("ResultReceiver")) {
resultReceiver = extras.getParcelable("ResultReceiver");
}
if (extras.containsKey("ContentToDownload")) {
contentToDownload = extras.getInt("ContentToDownload");
} else {
return;
}
if (contentToDownload != Carcare.ContentToDownload.IMAGES) {
isDefaultVehicle = extras.getBoolean("IsDefaultVehicle");
fetchVehicle();
}
switch (contentToDownload) {
case Carcare.ContentToDownload.HEADUNIT_IMAGES:
if (extras.containsKey("HeadUnits")) {
headUnits = (ArrayList<Unit>) extras.getSerializable("Units");
downloadHeadUnits();
resultReceiver.send(0, null);
}
break;
}
}
private void fetchVehicle() {
Object[] objects;
if (isDefaultVehicle) {
objects = DBAdapter.getAllVehicles(preferences.getDefaultModel(),
preferences.getDefaultYear(), isDefaultVehicle);
} else {
objects = DBAdapter.getAllVehicles(preferences.getCurrentModel(),
preferences.getCurrentYear(), isDefaultVehicle);
}
vehicle = (Vehicle) objects[0];
}
private void downloadHeadUnits() {
mHeadUnitDir = SdUtils.getDir(this);
//clearHeadUnits();
for (CUnit unit : Units) {
String fileName = mDir + "/" + unit.getGuid() + ".png";
InputStream stream = null;
final HttpGet httpRequest = new HttpGet(unit.getHuImageUrl());
httpRequest.setHeader(HTTP.CONN_DIRECTIVE, HTTP.CONN_KEEP_ALIVE);
try {
File file = new File(fileName);
if (!file.exists()) {
FileOutputStream out = new FileOutputStream(file); //openFileOutput(fileName);
stream = new DefaultHttpClient().execute(httpRequest).getEntity().getContent();
Bitmap bitmap = BitmapFactory.decodeStream(stream);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
}
} catch (IOException ex) {
ex.printStackTrace();
} catch (IllegalStateException ex) {
ex.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void download() {
cancelDownload(Carcare.FileType.QRG, vehicle.getPath());
deleteDoc(vehicle.getQRGPath());
DownloadManager.Request request = new DownloadManager.Request(Uri.parse(vehicle.getUrl()));
request.setDestinationUri(Uri.parse(vehicle.getPath()));
request.setTitle("Unit");
request.setDescription("Quick Reference Guide");
preferences.setDownloadID(Carcare.FileType.QRG, downloadManager.enqueue(request));
}
}
You must use a Service.
In the Service's onDestroy(), you can write the code to finish the DownloadManager.
The Service will be killed before the app is about to uninstall.
This way the Download will stop.
Take a look at the remove() method of the DownloadManager.
It says:
public int remove (long... ids) Added in API level 9
Cancel downloads and remove them from the download manager. Each
download will be stopped if it was running, and it will no longer be
accessible through the download manager. If there is a downloaded
file, partial or complete, it is deleted. Parameters ids the IDs of
the downloads to remove Returns
the number of downloads actually removed
Edit
To intercept your application uninstall take a look at this answer.
The only way to get and visualize the data table of my database inside external devices is by atribution of privilege of superuser privilege in external device? Don't exist another way that allow visualize the data tables as in emulator?
I make this question because this way of superuser privilege not inspire me security.
Thanks for your attention (PS: Sorry by mistakes, but english is not my mother language :) )
You can add functionality to export the database file from the internal read-only app storage to the SD-Card by simply letting your app copy the file.
Then use whatever ways you have to get it from there. Works on any device and no root required.
private void exportDb() {
File database = getDatabasePath("myDb.db");
File sdCard = new File(Environment.getExternalStorageDirectory(), "myDb.db");
if (copy(database, sdCard)) {
Toast.makeText(this, "Get db from " + sdCard.getPath(), Toast.LENGTH_LONG).show();
} else {
Toast.makeText(this, "Copying the db failed", Toast.LENGTH_LONG).show();
}
}
private static boolean copy(File src, File target) {
// try creating necessary directories
target.mkdirs();
boolean success = false;
FileOutputStream out = null;
FileInputStream in = null;
try {
out = new FileOutputStream(target);
in = new FileInputStream(src);
byte[] buffer = new byte[8 * 1024];
int read;
while ((read = in.read(buffer)) != -1) {
out.write(buffer, 0, read);
}
success = true;
} catch (FileNotFoundException e) {
// maybe log
} catch (IOException e) {
// maybe log
} finally {
close(in);
close(out);
}
if (!success) {
// try to delete failed attempts
target.delete();
}
return success;
}
private static void close(final Closeable closeMe) {
if (closeMe != null)
try {
closeMe.close();
} catch (IOException ignored) {
// ignored
}
}