In my app I can download a document (docx for example) and open it in QuickOffice. After edditing the document I use the save button and after succesfully saved it, I hit the share button and select my app so I can reupload it.
My problem is, is that the uri i got is not the usual uri you would expect as content://storage/map/file.docx or something like that. I get this from quickoffice:
content://com.quickoffice.android.quickcommon.FileContentProvider/zEV5qmvBJOg2GGWldHMJnNK687Ur6qLGbbMbxj0IxV9cDv2mN8XTGqRrEqU4KIfeZuQNMKMJ_eDx%0AN4YiNZwDShhb4E8%3D%0A
My question is, how can I turn this uri to the real path uri from the file (content://storage/map/file.docx for example)
There is no "real path".
A ContentProvider is welcome to store its content wherever it wants, which may not be a file (e.g., BLOB column in a database) and, even if it is, it may not be a file which you can access (e.g., internal storage for the app hosting the ContentProvider.
Please use the various methods on ContentResolver, such as openInputStream(), to access the contents of this provider.
Please use the following code. it worked fine for me.
public static String getContentName(ContentResolver resolver, Uri uri){
String[] ATTACHMENT_META_COLUMNS = {
OpenableColumns.DISPLAY_NAME,
OpenableColumns.SIZE
};
String name = "";
int size= 0;
Cursor metadataCursor = resolver.query(uri, ATTACHMENT_META_COLUMNS, null, null, null);
if (metadataCursor != null) {
try {
if (metadataCursor.moveToFirst()) {
name = metadataCursor.getString(0);
size = metadataCursor.getInt(1);
}
} finally {
metadataCursor.close();
}
}
if (name == null) {
name = uri.getLastPathSegment();
}
return name;
}
Related
I'm trying to implement a file sending functionality in my Android app (any files are allowed, and the files don't belong to my app). From the ACTION_OPEN_DOCUMENT I receive an InputStream, then I make a temp File object with the name I'm getting from ContentResolver's OpenableColumns.DISPLAY_NAME, and then send the file. The reason I do all of this is that I work with a 3rd party API which allows for File objects only.
But the OpenableColumns.DISPLAY_NAME doesn't guarantee that I get the file name with a file extension as stated in the docs. As far as I understand, there is no way to get the actual filename or physical path of a file with the Scoped Storage enforced in the newer versions of Android. Therefore, I have to check if a filename contains an extension, and if not - get the file's MIME type with ContentResolver and the most common extension for it using the MimeTypeMap. This approach feels to be not very reliable since I have to rely on both ContentResolver correctly determining the MIME type and MimeTypeMap retrieving the correct extension. Getting the extension is crucial at least because users should be able to download and open files on their PC from a desktop app.
So, is it possible to get a filename or at least file extension with a 100% guarantee with scoped storage enabled? Or maybe is there a more efficient way to handle my situation? I'd appreciate some help with this.
Try this method, it helped me:
public static String getFileName(Uri uri, Context context) {
String result = null;
if (uri.getScheme().equals("content")) {
Cursor cursor = context.getContentResolver().query(uri, null, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} finally {
cursor.close();
}
}
if (result == null) {
result = uri.getPath();
int cut = result.lastIndexOf('/');
if (cut != -1) {
result = result.substring(cut + 1);
}
}
return result;
}
In Android Q, save pictures in app-specific directory,
path like = /data/user/0/xxx.xxx.xxx/files/phone/abc.jpg
not save in the external storage, use Device FileExplorer to view,
need to check if file exist, avoid to download again
,but in Android Q file.exist() not work
File newFile = new File(path);
newFile.exists();
always return false
this question. I need to use MediaStore or SAF to resolver it.
or other function to check it.
If I use MediaStore to check. use ContentResolver. May be like this:
public void getPhotoCursor(Uri uri) {
Cursor cursor = getContentResolver().query(uri, null, null, null, null, null);
try {
if (cursor != null && cursor.moveToFirst()) {
String displayName = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
}
} finally {
cursor.close();
}
}
But I can't get the Uri form app-specific directory. If I get the Uri, how to use file descriptor to check.
or use SAF to check.
File testFile = new File(getExternalFilesDir()+"phone", "abc.jpg");
FileProvider.getUriForFile(,,testFile);
Intent testIntent = new Intent(Intent.ACTION_VIEW);
testIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
testIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
testIntent.setDataAndType();
startActivity(testIntent);
In the ActivityResult to check it
Any help will be apperciated
is my fault, every time open APP I will delete all the .jpg from APP-specific.
so into APP I want to check avoid download again. file exist always return false.
I have a method below:
private String getRealPathFromUriForVideos(Uri selectedVideoUri) {
String wholeID = DocumentsContract.getDocumentId(selectedVideoUri);
String id = wholeID.split(":")[1];
String[] column = { MediaStore.Video.Media.DATA };
String sel = MediaStore.Video.Media._ID + "=?";
Cursor cursor = getContentResolver().query(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, column, sel, new String[]{ id }, null);
String filePath = "";
int columnIndex = cursor.getColumnIndex(column[0]);
if (cursor.moveToFirst()) {
filePath = cursor.getString(columnIndex);
}
cursor.close();
return filePath;
}
This works just fine getting the file for videos that hte user selects. However, I want to allow users to also create new videos (from my app) and then get the URI and the file from there. The URI for newly created videos is: content://media/external/video/media/41. For selected videos is like content://com.android.providers.media.documents/document/video%3A42.
It works with the second one but not the first one. First one I get IllegalArgumentException because its not a document URI. How can I get the file from the first URI?
This works just fine getting the file for videos that hte user selects
It may work in a few situations. It will not work in general. A Uri that you get from something like ACTION_OPEN_DOCUMENT does not have to represent a file, let alone one that you can access via the filesystem, let alone one that this script-kiddie algorithm will let you access.
The URI for newly created videos is: content://media/external/video/media/41
Not necessarily. I suppose that there is a way that you get a Uri like that for a recorded video, though off the top of my head I cannot think of a recommended way that would give you such a Uri. If you are using MediaRecorder or ACTION_VIDEO_CAPTURE, you create your own file (and, for ACTION_VIDEO_CAPTURE, your own Uri for that file). And, if you are creating your own file, you know where that file is.
Need to be able to upload to my server
For the video, record to a file that you control, then use that file.
Use some library that lets you upload from a Uri or InputStream. Otherwise:
Use ContentResolver and openFileInput() to get an InputStream on the content represented by the Uri
Create a FileOutputStream on some file that you control (e.g., in getCacheDir())
Copy the content from the InputStream to the OutputStream
Use your copy for the upload
Delete your copy when the work is done
You treat a foreign Uri as if it were a URL to a Web server: stream the content.
Seems to get it from the second URI I need this:
private String getRealPathFromUriForImagesAndVideo(Uri contentUri) {
Cursor cursor = null;
try {
String[] proj = {MediaStore.Images.Media.DATA};
cursor = getContentResolver().query(contentUri, proj, null, null, null);
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
return cursor.getString(column_index);
} catch (Exception e) {
return contentUri.getPath();
} finally {
if (cursor != null) {
cursor.close();
}
}
}
Background
I have an App-Manager app, which allows to send APK files to other apps.
Up until Android 4.4 (including), all I had to do for this task is to send the paths to the original APK files (all were under "/data/app/..." which is accessible even without root).
This is the code for sending the files (docs available here) :
intent=new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.setType("*/*");
final ArrayList<Uri> uris=new ArrayList<>();
for(...)
uris.add(Uri.fromFile(new File(...)));
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM,uris);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_NO_HISTORY|Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET|Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
The problem
What I did worked since all apps' APK files had a unique name (which was their package name).
Ever since Lollipop (5.0), all apps' APK files are simply named "base.APK" , which make other apps unable to comprehend attaching them.
This means I have some options to send the APK files. This is what I was thinking about:
copy them all to a folder, rename them all to unique names and then send them.
compress them all to a single file and then send it. The compression level could be minimal, as APK files are already compressed anyway.
The problem is that I would have to send the files as quickly as possible, and if I really have to have those temporary files (unless there is another solution), to also dispose them as quickly as possible.
Thing is, I don't get notified when third party apps have finished handling the temporary file, and I also think that choosing multiple files would take quite some time to prepare no matter what I choose.
Another issue is that some apps (like Gmail) actually forbid sending APK files.
The question
Is there an alternative to the solutions I've thought of? Is there maybe a way to solve this problem with all the advantages I had before (quick and without junk files left behind) ?
Maybe some sort of way to monitor the file? or create a stream instead of a real file?
Will putting the temporary file inside a cache folder help in any way?
Any app registered for that Intent should be able to process files with the same file name but different paths. To be able to cope with the fact that access to files provided by other apps can only be accessed while the receiving Activity is running (see Security Exception when trying to access a Picasa image on device running 4.2 or SecurityException when downloading Images with the Universal-Image-Downloader) receiving apps need to copy the files to a directory they have permanently access to. My guess is that some apps haven't implemented that copy process to deal with identical file names (when copied the file path would likely be the same for all files).
I'd suggest to serve the files through a ContentProvider instead of directly from the file system. That way you can create a unique file name for each file you want to send.
Receiving apps "should" receive files more or less like this:
ContentResolver contentResolver = context.getContentResolver();
Cursor cursor = contentResolver.query(uri, new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }, null, null, null);
// retrieve name and size columns from the cursor...
InputStream in = contentResolver.openInputStream(uri);
// copy file from the InputStream
Since apps should open the file using contentResolver.openInputStream() a ContentProvider should/will work instead of just passing a file uri in the Intent. Of course there might be apps that misbehave and this needs to be tested thoroughly but in case some apps won't handle ContentProvider served files you could add two different share options (one legacy and the regular one).
For the ContentProvider part there's this:
https://developer.android.com/reference/android/support/v4/content/FileProvider.html
Unfortunately there's also this:
A FileProvider can only generate a content URI for files in
directories that you specify beforehand
If you can define all directories you want to share files from when the app is built, the FileProvider would be your best option.
I'm assuming your app would want to share files from any directory, so you'll need your own ContentProvider implementation.
The problems to solve are:
How do you include the file path in the Uri in order to extract the very same path at a later stage (in the ContentProvider)?
How do you create a unique file name that you can return in the ContentProvider to the receiving app? This unique file name needs to be the same for multiple calls to the ContentProvider meaning you can't create a unique id whenever the ContentProvider is called or you'd get a different one with each call.
Problem 1
A ContentProvider Uri consists of a scheme (content://), an authority and the path segment(s), e.g.:
content://lb.com.myapplication2.fileprovider/123/base.apk
There are many solutions to the first problem. What I suggest is to base64 encode the file path and use it as the last segment in the Uri:
Uri uri = Uri.parse("content://lb.com.myapplication2.fileprovider/" + new String(Base64.encode(filename.getBytes(), Base64.DEFAULT));
If the file path is e.g.:
/data/data/com.google.android.gm/base.apk
then the resulting Uri would be:
content://lb.com.myapplication2.fileprovider/L2RhdGEvZGF0YS9jb20uZ29vZ2xlLmFuZHJvaWQuZ20vYmFzZS5hcGs=
To retrieve the file path in the ContentProvider simply do:
String lastSegment = uri.getLastPathSegment();
String filePath = new String(Base64.decode(lastSegment, Base64.DEFAULT) );
Problem 2
The solution is pretty simple. We include a unique identifier in the Uri generated when we create the Intent. This identifier is part of the Uri and can be extracted by the ContentProvider:
String encodedFileName = new String(Base64.encode(filename.getBytes(), Base64.DEFAULT));
String uniqueId = UUID.randomUUID().toString();
Uri uri = Uri.parse("content://lb.com.myapplication2.fileprovider/" + uniqueId + "/" + encodedFileName );
If the file path is e.g.:
/data/data/com.google.android.gm/base.apk
then the resulting Uri would be:
content://lb.com.myapplication2.fileprovider/d2788038-53da-4e84-b10a-8d4ef95e8f5f/L2RhdGEvZGF0YS9jb20uZ29vZ2xlLmFuZHJvaWQuZ20vYmFzZS5hcGs=
To retrieve the unique identifier in the ContentProvider simply do:
List<String> segments = uri.getPathSegments();
String uniqueId = segments.size() > 0 ? segments.get(0) : "";
The unique file name the ContentProvider returns would be the original file name (base.apk) plus the unique identifier inserted after the base file name. E.g. base.apk becomes base<unique id>.apk.
While this might all sound very abstract, it should become clear with the full code:
Intent
intent=new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.setType("*/*");
final ArrayList<Uri> uris=new ArrayList<>();
for(...)
String encodedFileName = new String(Base64.encode(filename.getBytes(), Base64.DEFAULT));
String uniqueId = UUID.randomUUID().toString();
Uri uri = Uri.parse("content://lb.com.myapplication2.fileprovider/" + uniqueId + "/" + encodedFileName );
uris.add(uri);
}
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM,uris);
ContentProvider
public class FileProvider extends ContentProvider {
private static final String[] DEFAULT_PROJECTION = new String[] {
MediaColumns.DATA,
MediaColumns.DISPLAY_NAME,
MediaColumns.SIZE,
};
#Override
public boolean onCreate() {
return true;
}
#Override
public String getType(Uri uri) {
String fileName = getFileName(uri);
if (fileName == null) return null;
return MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileName);
}
#Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
String fileName = getFileName(uri);
if (fileName == null) return null;
File file = new File(fileName);
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
String fileName = getFileName(uri);
if (fileName == null) return null;
String[] columnNames = (projection == null) ? DEFAULT_PROJECTION : projection;
MatrixCursor ret = new MatrixCursor(columnNames);
Object[] values = new Object[columnNames.length];
for (int i = 0, count = columnNames.length; i < count; i++) {
String column = columnNames[i];
if (MediaColumns.DATA.equals(column)) {
values[i] = uri.toString();
}
else if (MediaColumns.DISPLAY_NAME.equals(column)) {
values[i] = getUniqueName(uri);
}
else if (MediaColumns.SIZE.equals(column)) {
File file = new File(fileName);
values[i] = file.length();
}
}
ret.addRow(values);
return ret;
}
private String getFileName(Uri uri) {
String path = uri.getLastPathSegment();
return path != null ? new String(Base64.decode(path, Base64.DEFAULT)) : null;
}
private String getUniqueName(Uri uri) {
String path = getFileName(uri);
List<String> segments = uri.getPathSegments();
if (segments.size() > 0 && path != null) {
String baseName = FilenameUtils.getBaseName(path);
String extension = FilenameUtils.getExtension(path);
String uniqueId = segments.get(0);
return baseName + uniqueId + "." + extension;
}
return null;
}
#Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0; // not supported
}
#Override
public int delete(Uri uri, String arg1, String[] arg2) {
return 0; // not supported
}
#Override
public Uri insert(Uri uri, ContentValues values) {
return null; // not supported
}
}
Note:
My sample code uses the org.apache.commons library for the file name manipulations (FilenameUtils.getXYZ)
using base64 encoding for the file path is a valid approach because all character used in base64 ([a-zA-Z0-9_-=] according to this https://stackoverflow.com/a/6102233/534471) are valid in an Uri path (0-9, a-z, A-Z, _-!.~'()*,;:$&+=/# --> see https://developer.android.com/reference/java/net/URI.html)
Your manifest would have to define the ContentProvider like so:
<provider
android:name="lb.com.myapplication2.fileprovider.FileProvider"
android:authorities="lb.com.myapplication2.fileprovider"
android:exported="true"
android:grantUriPermissions="true"
android:multiprocess="true"/>
It won't work without android:grantUriPermissions="true" and android:exported="true" because the other app wouldn't have permission to access the ContentProvider (see also http://developer.android.com/guide/topics/manifest/provider-element.html#exported) . android:multiprocess="true" on the other hand is optional but should make it more efficient.
Here's a working solution for using SymLinks. Disadvantages:
works from API 14, and not on API 10 , not sure about in between.
uses reflection, so might not work in the future, and on some devices.
must create the symlinks in the path of "getFilesDir", so you have to manage them by yourself, and create unique files names as needed.
The sample shares the APK of the current app.
Code:
public class SymLinkActivity extends Activity{
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(lb.com.myapplication2.R.layout.activity_main);
final Intent intent=new Intent(Intent.ACTION_SEND_MULTIPLE);
intent.setType(MimeTypeMap.getSingleton().getMimeTypeFromExtension("apk"));
final String filePath;
try
{
final android.content.pm.ApplicationInfo applicationInfo=getPackageManager().getApplicationInfo(getPackageName(),0);
filePath=applicationInfo.sourceDir;
}
catch(NameNotFoundException e)
{
e.printStackTrace();
finish();
return;
}
final File file=new File(filePath);
final String symcLinksFolderPath=getFilesDir().getAbsolutePath();
findViewById(R.id.button).setOnClickListener(new android.view.View.OnClickListener(){
#Override
public void onClick(final android.view.View v)
{
final File symlink=new File(symcLinksFolderPath,"CustomizedNameOfApkFile-"+System.currentTimeMillis()+".apk");
symlink.getParentFile().mkdirs();
File[] oldSymLinks=new File(symcLinksFolderPath).listFiles();
if(oldSymLinks!=null)
{
for(java.io.File child : oldSymLinks)
if(child.getName().endsWith(".apk"))
child.delete();
}
symlink.delete();
// do some dirty reflection to create the symbolic link
try
{
final Class<?> libcore=Class.forName("libcore.io.Libcore");
final java.lang.reflect.Field fOs=libcore.getDeclaredField("os");
fOs.setAccessible(true);
final Object os=fOs.get(null);
final java.lang.reflect.Method method=os.getClass().getMethod("symlink",String.class,String.class);
method.invoke(os,file.getAbsolutePath(),symlink.getAbsolutePath());
final ArrayList<Uri> uris=new ArrayList<>();
uris.add(Uri.fromFile(symlink));
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM,uris);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_NO_HISTORY|Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET|Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
startActivity(intent);
android.widget.Toast.makeText(SymLinkActivity.this,"succeeded ?",android.widget.Toast.LENGTH_SHORT).show();
}
catch(Exception e)
{
android.widget.Toast.makeText(SymLinkActivity.this,"failed :(",android.widget.Toast.LENGTH_SHORT).show();
e.printStackTrace();
// TODO handle the exception
}
}
});
}
}
EDIT: for the symlink part, for Android API 21 and above, you can use this instead of reflection :
Os.symlink(originalFilePath,symLinkFilePath);
What I am trying to achieve is sounds very familiar, it has been posted many times here and there in Stack Overflow as well, but I'm unable to get it done.
The scenario is, I receive a mail with attachment having custom extension in it. The extension is recognized by my app and it needs the FilePath to process it.
Currently, when I get the attachment in my app using getIntent().getData() all I get is path of the form content://
I have seen methods to convert media content of the type content:// to FilePath like /sdcard/file.ext but I was unable to convert the attachment using that. May be its obvious.
Is there any way that I can process the content:// type without actually downloading it.
Currently from the k9 mail app, when I get the custom extension, it shows my app in the list and opens it through it, but I need FilePath like /sdcard/file.ext and I'm only able to get content:// type.
I hope I made the question clear.
Please Help.
Regards.
A content:// Uri does not necessarily point to a file on the sdcard.
It is more likely that it points to any kind of data stored in a database
or to a content provider that gives you access to the private file storage of another app.
I think the later one is the case with mail attachments (if the content provider is not requesting it directly from a web server). So converting the content:// Uri to a path will not work.
I did the following (not sure if it works also for k9 mail app)
Uri uri = intent.getData();
if (uri.getScheme().equals("content")) {
String fileName = ContentProviderUtils.getAttachmentName(this, uri);
if (fileName.toLowerCase().endsWith(".ext")) {
InputStream is = this.getContentResolver().openInputStream(uri);
// do something
} else {
// not correct extension
return;
}
} else if (uri.getScheme().equals("file")) {
String path = uri.getPath();
if (path.toLowerCase().endsWith(".ext")) {
InputStream is = new FileInputStream(path);
// do something
} else {
// not correct extension
return;
}
}
The attachment name can be found by
public static String getAttachmentName(Context ctxt, Uri contentUri) {
Cursor cursor = ctxt.getContentResolver().query(contentUri, new String[]{MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null);
String res = "";
if (cursor != null){
cursor.moveToFirst();
int nameIdx = cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME);
res = cursor.getString(nameIdx);
cursor.close();
}
return res;
}