I want to allow users of my Android app to export the SQLite database file for content they create. My current solution copies the file to private storage (/data/data/com.package.name/files/Content.db), then creates a URI for this file and opens the Share dialog. This is working, allowing me to export the database file using Dropbox, for example. Here is the code I'm using, partially adapted from https://stackoverflow.com/a/2661882 -
private void exportContent() {
copyContentToPrivateStorage();
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("application/octet-stream");
Uri uri = new FileProvider().getDatabaseURI(this);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivity(Intent.createChooser(intent, "Backup via:"));
}
private void copyContentToPrivateStorage() {
// From https://stackoverflow.com/a/2661882
try {
File data = Environment.getDataDirectory();
File sd = getFilesDir();
if (sd.canWrite()) {
String currentDBPath = "//data//com.package.name//databases//Content.db";
String backupDBPath = "Content.db";
File currentDB = new File(data, currentDBPath);
File backupDB = new File(sd, backupDBPath);
if (currentDB.exists()) {
FileChannel src = new FileInputStream(currentDB).getChannel();
FileChannel dst = new FileOutputStream(backupDB).getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();
}
}
} catch (Exception e) {
Toast.makeText(this, e.toString(), Toast.LENGTH_LONG).show();
}
}
public class FileProvider extends android.support.v4.content.FileProvider {
public Uri getDatabaseURI(Context c) {
File exportFile = new File(c.getFilesDir(), "Content.db");
Uri uri = getUriForFile(c, "com.package.name.fileprovider", exportFile);
c.grantUriPermission("*", uri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
return uri;
}
}
It seems like I should be able to directly create a URI from the existing database path, instead of doing an intermediate copy. Is there a way to do this?
I could keep doing the intermediate copy, but I believe it would be bad practice to leave the second copy of the database in the data directory longer than necessary. Is there a way to clean it up and delete it after the chosen app has finished using the URI to share the file?
I solved this on my own. I'm documenting it here, per Neil's request.
This is where I launch the export/backup from my activity:
public class MyActivity {
private void exportUserContent() {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("application/octet-stream");
Uri uri = new FileProvider().getDatabaseURI(this);
intent.putExtra(Intent.EXTRA_STREAM, uri);
startActivity(Intent.createChooser(intent, "Backup via:"));
}
}
The FileProvider:
public class FileProvider extends android.support.v4.content.FileProvider {
public Uri getDatabaseURI(Context c) {
// https://developer.android.com/reference/android/support/v4/content/FileProvider.html
// old approach that worked until 2020-ish
// File data = Environment.getDataDirectory();
// String dbName = "UserContent.db";
// String currentDBPath = "//data//com.url.myapp//databases//" + dbName;
// File exportFile = new File(data, currentDBPath);
File exportFile = c.getDatabasePath(dbName); // new approach
return getFileUri(c, exportFile);
}
public Uri getFileUri(Context c, File f){
return getUriForFile(c, "com.url.myapp.fileprovider", f);
}
}
Inside AndroidManifest.xml:
<manifest ...>
<application ...>
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.url.myapp.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/filepaths" />
</provider>
Inside \app\src\main\res\xml\filepaths.xml
(I think the first entry is the relevant one, but I'll include the whole file for completeness):
<paths>
<files-path
path="../databases/"
name="mydatabases"/>
<files-path
path=""
name="migrations"/>
<external-path
path=""
name="external"/>
</paths>
Share the SQLite database using content providers. This tutorial can guide you more on SQLite database and content provider: Android SQLite DB and Content Provider
Here's how I solved this with a custom ContentProvider:
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import java.io.File;
import java.io.FileNotFoundException;
/**
* ContentProvider to share SQLite database
*
* Credit: https://android.googlesource.com/platform/frameworks/support/+/android-support-lib-19.1.0/v4/java/android/support/v4/content/FileProvider.java
*/
public class MyProvider extends ContentProvider {
private final File file = new File("/data/data/com.example.provider/databases", "mydatabase.db");
#Override
public boolean onCreate() {
return true;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
if (projection == null) {
projection = new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
}
String[] cols = new String[projection.length];
Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = file.getName();
} else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = file.length();
}
}
cols = copyOf(cols, i);
values = copyOf(values, i);
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
#Override
public String getType(Uri uri) {
return "application/octet-stream";
}
#Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("No external inserts");
}
#Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("No external updates");
}
#Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
#Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
private static String[] copyOf(String[] original, int newLength) {
final String[] result = new String[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
private static Object[] copyOf(Object[] original, int newLength) {
final Object[] result = new Object[newLength];
System.arraycopy(original, 0, result, 0, newLength);
return result;
}
}
Then in the manifest:
<provider
android:name="com.example.appname.MyProvider"
android:authorities="com.example.provider">
</provider>
Related
I'm building a secure android application that runs with IOCipher and SQLCipher. My app is storing PDF, DOC, DOCX, XLS, XLSX files that are intended to be openned by a third party application. Currently I can open all these type of files but DOCX. When I open a docx file that is stored in IOCipher using this method:
File file = new File(path);
Uri contentUri = Uri.parse(VFSContentProvider.FILES_URI + file.getName());
Intent intent = new Intent();
intent.setAction(Intent.ACTION_VIEW);
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setData(contentUri);
Microsoft Word prompt me with that error message :
Can't open file "myFile.docx.docx".
This happen For all my docx file, it seems to append the mime type twice at the end of the file when it's returned by the content provider...
Here is my IOCipher Content Provider:
public class VFSContentProvider extends ContentProvider {
public static final String TAG = "VFSContentProvider";
public static final Uri FILES_URI = Uri
.parse("content://com.plante.android.cobalt.VFSContentProvider/");
private MimeTypeMap mimeTypeMap;
#Override
public boolean onCreate() {
mimeTypeMap = MimeTypeMap.getSingleton();
return true;
}
#Override
public String getType(Uri uri) {
String fileExtension = MimeTypeMap.getFileExtensionFromUrl(uri.toString());
return mimeTypeMap.getMimeTypeFromExtension(fileExtension);
}
#Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
ParcelFileDescriptor[] pipe = null;
InputStream in = null;
try {
pipe = ParcelFileDescriptor.createPipe();
String path = uri.getPath();
Log.i(TAG, "streaming " + path);
// BufferedInputStream could help, AutoCloseOutputStream conflicts
in = new FileInputStream(new File(path));
new PipeFeederThread(in, new AutoCloseOutputStream(pipe[1])).start();
} catch (IOException e) {
Log.e(TAG, "Error opening pipe", e);
throw new FileNotFoundException("Could not open pipe for: "
+ uri.toString());
}
return (pipe[0]);
}
#Override
public Cursor query(Uri url, String[] projection, String selection,
String[] selectionArgs, String sort) {
// throw new RuntimeException("Operation not supported");
return null;
}
#Override
public Uri insert(Uri uri, ContentValues initialValues) {
throw new RuntimeException("Operation not supported");
}
#Override
public int update(Uri uri, ContentValues values, String where,
String[] whereArgs) {
throw new RuntimeException("Operation not supported");
}
#Override
public int delete(Uri uri, String where, String[] whereArgs) {
throw new RuntimeException("Operation not supported");
}
static class PipeFeederThread extends Thread {
InputStream in;
OutputStream out;
PipeFeederThread(InputStream in, OutputStream out) {
this.in = in;
this.out = out;
}
#Override
public void run() {
byte[] buf = new byte[8192];
int len;
try {
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
in.close();
out.flush();
out.close();
} catch (IOException e) {
Log.e(TAG, "File transfer failed:", e);
}
}
}
Here is the log file:
03-16 13:38:20.714 800-1325/? I/ActivityManager: START u0 {act=android.intent.action.VIEW dat=content://com.plante.android.cobalt.VFSContentProvider/0c8dbc0e1671e127ed9fcb2786b218d379f12888Finance_Corporate_BCP_Checklist_2014_MH20150115.docx flg=0x13000003 cmp=com.microsoft.office.word/com.microsoft.office.apphost.LaunchActivity} from uid 10182 on display 0
I've been trying and trying to get this to work. I've gone through many examples with no luck. The problem is that the image is not attached when I try to share it, for example, using an email client. I managed to get this to work when using external storage but internal storage suits my needs better.
I click a button and then the image is saved to internal storage and right after that it is shared but there's no image.
This is from the Activity class:
int width = size.x;
int height = size.y;
Bitmap shareBmp = Bitmap.createBitmap(screenBmp, 0, 0, width, height);
ContextWrapper wrapper = new ContextWrapper(getApplicationContext());
File directory = wrapper.getDir("images", Context.MODE_PRIVATE);
File filePath = new File(directory, "share.png");
FileOutputStream fos;
try
{
fos = new FileOutputStream(filePath);
shareBmp.compress(Bitmap.CompressFormat.PNG, 90, fos);
fos.close();
}
catch (Exception aE){}
Uri uri = Uri.parse("content://com.example.Test/share.png");
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
intent.putExtra(Intent.EXTRA_STREAM, uri);
intent.setType("image/png");
startActivity(Intent.createChooser(intent, "Share Image"));
Here's the ImageProvider class:
public class ImageProvider extends ContentProvider
{
#Override
public ParcelFileDescriptor openFile(Uri aUri, String aMode) throwsFileNotFoundException
{
File file = new File(getContext().getFilesDir(), aUri.getPath());
if (file.exists())
{
return (ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY));
}
throw new FileNotFoundException(aUri.getPath());
}
#Override
public boolean onCreate()
{
return false;
}
#Override
public int delete(Uri aUri, String aSelection, String[] aSelectionArgs)
{
return 0;
}
#Override
public String getType(Uri aUri)
{
return null;
}
#Override
public Uri insert(Uri aUri, ContentValues aValues)
{
return null;
}
#Override
public Cursor query(Uri aUri, String[] aProjection, String aSelection, String[] aSelectionArgs, String aSortOrder)
{
return null;
}
#Override
public int update(Uri aUri, ContentValues aValues, String aSelection, String[] aSelectionArgs)
{
return 0;
}
}
This is from the manifest:
<provider
android:name=".ImageProvider"
android:authorities="com.example.Test"
android:exported="true"/>
Only your app can see/read/write files in it's app specific -private- storage. So you cannot ask other app to share from it as they cannot even 'see' those files there.
I'm trying to share an image from my assets folder. My code is:
Intent share = new Intent(Intent.ACTION_SEND);
share.setType("image/jpg");
share.putExtra(Intent.EXTRA_STREAM, Uri.parse("file:///assets/myImage.jpg"));
startActivity(Intent.createChooser(share, "Share This Image"));
but it doesn't work. Do you have any ideas?
It is possible to share files (images including) from the assets folder through a custom ContentProvider
You need to extend ContentProvider, register it in your manifest and implement the openAssetFile method. You can then assess the assets via Uris
#Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
AssetManager am = getContext().getAssets();
String file_name = uri.getLastPathSegment();
if(file_name == null)
throw new FileNotFoundException();
AssetFileDescriptor afd = null;
try {
afd = am.openFd(file_name);
} catch (IOException e) {
e.printStackTrace();
}
return afd;
}
Complementing what #intrepidis answered:
You will need override methods like example class above:
package com.android.example;
import android.content.ContentProvider;
import android.net.Uri;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import java.io.FileNotFoundException;
import android.content.ContentValues;
import android.database.Cursor;
import java.io.IOException;
import android.os.CancellationSignal;
public class AssetsProvider extends ContentProvider
{
#Override
public AssetFileDescriptor openAssetFile( Uri uri, String mode ) throws FileNotFoundException
{
Log.v( TAG, "AssetsGetter: Open asset file" );
AssetManager am = getContext( ).getAssets( );
String file_name = uri.getLastPathSegment( );
if( file_name == null )
throw new FileNotFoundException( );
AssetFileDescriptor afd = null;
try
{
afd = am.openFd( file_name );
}
catch(IOException e)
{
e.printStackTrace( );
}
return afd;//super.openAssetFile(uri, mode);
}
#Override
public String getType( Uri p1 )
{
// TODO: Implement this method
return null;
}
#Override
public int delete( Uri p1, String p2, String[] p3 )
{
// TODO: Implement this method
return 0;
}
#Override
public Cursor query( Uri p1, String[] p2, String p3, String[] p4, String p5 )
{
// TODO: Implement this method
return null;
}
#Override
public Cursor query( Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal )
{
// TODO: Implement this method
return super.query( uri, projection, selection, selectionArgs, sortOrder, cancellationSignal );
}
#Override
public Uri insert( Uri p1, ContentValues p2 )
{
// TODO: Implement this method
return null;
}
#Override
public boolean onCreate( )
{
// TODO: Implement this method
return false;
}
#Override
public int update( Uri p1, ContentValues p2, String p3, String[] p4 )
{
// TODO: Implement this method
return 0;
}
}
I needed to override two times the query method.
And add these lines above tag in your androidmanifest.xml:
<provider
android:name="com.android.example.AssetsProvider"
android:authorities="com.android.example"
android:grantUriPermissions="true"
android:exported="true" />
And with this, all work like a charm :D
This blog explains it all:
http://nowherenearithaca.blogspot.co.uk/2012/03/too-easy-using-contentprovider-to-send.html
Basically, this goes in the manifest:
<provider android:name="yourclass.that.extendsContentProvider" android:authorities="com.yourdomain.whatever"/>
The content provider class has this:
#Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
AssetManager am = getContext().getAssets();
String file_name = uri.getLastPathSegment();
if(file_name == null)
throw new FileNotFoundException();
AssetFileDescriptor afd = null;
try {
afd = am.openFd(file_name);
} catch (IOException e) {
e.printStackTrace();
}
return afd;//super.openAssetFile(uri, mode);
}
And the calling code does this:
Uri theUri = Uri.parse("content://com.yourdomain.whatever/someFileInAssetsFolder");
Intent theIntent = new Intent(Intent.ACTION_SEND);
theIntent.setType("image/*");
theIntent.putExtra(Intent.EXTRA_STREAM,theUri);
theIntent.putExtra(android.content.Intent.EXTRA_SUBJECT,"Subject for message");
theIntent.putExtra(android.content.Intent.EXTRA_TEXT, "Body for message");
startActivity(theIntent);
Many apps require you to provide name and size of the image. So here is an improved code (using Google's FileProvider code as an example):
public class AssetsProvider extends ContentProvider {
private final static String LOG_TAG = AssetsProvider.class.getName();
private static final String[] COLUMNS = {
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE };
#Override
public boolean onCreate() {
return true;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
/**
* Source: {#link FileProvider#query(Uri, String[], String, String[], String)} .
*/
if (projection == null) {
projection = COLUMNS;
}
final AssetManager am = getContext().getAssets();
final String path = getRelativePath(uri);
long fileSize = 0;
try {
final AssetFileDescriptor afd = am.openFd(path);
fileSize = afd.getLength();
afd.close();
} catch(IOException e) {
Log.e(LOG_TAG, "Can't open asset file", e);
}
final String[] cols = new String[projection.length];
final Object[] values = new Object[projection.length];
int i = 0;
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols[i] = OpenableColumns.DISPLAY_NAME;
values[i++] = uri.getLastPathSegment();
} else if (OpenableColumns.SIZE.equals(col)) {
cols[i] = OpenableColumns.SIZE;
values[i++] = fileSize;
}
}
final MatrixCursor cursor = new MatrixCursor(cols, 1);
cursor.addRow(values);
return cursor;
}
#Override
public String getType(Uri uri) {
/**
* Source: {#link FileProvider#getType(Uri)} .
*/
final String file_name = uri.getLastPathSegment();
final int lastDot = file_name.lastIndexOf('.');
if (lastDot >= 0) {
final String extension = file_name.substring(lastDot + 1);
final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
if (mime != null) {
return mime;
}
}
return "application/octet-stream";
}
#Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
#Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
#Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
#Override
public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
final AssetManager am = getContext().getAssets();
final String path = getRelativePath(uri);
if(path == null) {
throw new FileNotFoundException();
}
AssetFileDescriptor afd = null;
try {
afd = am.openFd(path);
} catch(IOException e) {
Log.e(LOG_TAG, "Can't open asset file", e);
}
return afd;
}
private String getRelativePath(Uri uri) {
String path = uri.getPath();
if (path.charAt(0) == '/') {
path = path.substring(1);
}
return path;
}
}
Since none of the other answers here worked for me (in 2019) I made a workaround by copying the asset to the app's internal file directory and then sharing this file.
In my case, I needed to share a pdf file from the assets folder.
In the AndroidManifest.xml add a file provider (no need to use a custom one):
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/filepaths" />
</provider>
Create a filepaths.xml file in res/xml/
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path
name="root"
path="/" />
</paths>
Of course you should use a subdirectory here if you manage other files in your app directory.
Now in the class where you want to trigger the share intent.
1. Create an empty file in the files directory
private fun createFileInFilesDir(filename: String): File {
val file = File(filesDir.path + "/" + filename)
if (file.exists()) {
if (!file.delete()) {
throw IOException()
}
}
if (!file.createNewFile()) {
throw IOException()
}
return file
}
2. Copy the content of the asset to the file
private fun copyAssetToFile(assetName: String, file: File) {
val buffer = ByteArray(1024)
val inputStream = assets.open(assetName)
val outputStream: OutputStream = FileOutputStream(file)
while (inputStream.read(buffer) > 0) {
outputStream.write(buffer)
}
}
3. Create a share intent for the file
private fun createIntentForFile(file: File, intentAction: String): Intent {
val uri = FileProvider.getUriForFile(this, applicationContext.packageName + ".provider", file)
val intent = Intent(intentAction)
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
intent.setDataAndType(uri, "application/pdf")
return intent
}
4. Execute 1-3 and fire the intent
private fun sharePdfAsset(assetName: String, intentAction: String) {
try {
val file = createFileInFilesDir(assetName)
copyAssetToFile(assetName, file)
val intent = createIntentForFile(file, intentAction)
startActivity(Intent.createChooser(intent, null))
} catch (e: IOException) {
e.printStackTrace()
AlertDialog.Builder(this)
.setTitle(R.string.error)
.setMessage(R.string.share_error)
.show()
}
}
5. Call the function
sharePdfAsset("your_pdf_asset.pdf", Intent.ACTION_SEND)
If you want to delete the file after sharing it, you probably could use startActivityForResult() and delete it afterwards. By changing the intentAction you can also use this process for an "open with..." action by using Intent.ACTION_VIEW.
For assets, filesDir, ... you need to be in an Activity or have a Context of course.
AFAIK, there's no way to share an image from the assets folder. But it's possible to share resources from the res folder.
To share from assets folder I can only recommend the cwac-provider library (StreamProvider).
Among avoiding to develop your own content provider, it adds some support for capricious legacy apps (check USE_LEGACY_CURSOR_WRAPPER).
I´m trying to export an image from my application to other applications like twitter, facebook or other apps accepting it images.
I'm doing the following:
private void exportImage()
{
storeBitmapOnDisk(this.bitmap);
Intent i = new Intent(Intent.ACTION_SEND);
i.addCategory(Intent.CATEGORY_LAUNCHER);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
File imgFile = context.getFileStreamPath("export.png");
Uri uri = Uri.fromFile(imgFile);
i.putExtra(Intent.EXTRA_STREAM, uri);
i.setType("image/png");
i.setComponent(componentName);
context.startActivity(i);
}
private void storeBitmapOnDisk(Bitmap bitmap)
{
try
{
FileOutputStream outStream = context.openFileOutput("export.png",
Context.MODE_WORLD_READABLE);
bitmap.compress(Bitmap.CompressFormat.PNG, 50, outStream);
outStream.close();
}
catch (IOException e)
{
Log.d(TAG, e.getMessage());
}
}
This is not working because I'm writing internal storage with is not accessible by other applications. As I don't want to use external storage (SD card) I think what I need is a ContentProvider but all examples I saw is about custom ContentPovider are using a SQlite database to store the data. I can't figure out how I could store a bitmap in internal storage and make it available through a ContentProvider to other applications without storing my bitmap in a database. MatrixCursor seems not to be adapted too...
SOLUTION I create a custom Content Provider and set the path of the file in the the content provider as the Uri EXTRA_STREAM extra parameter of the intent:
First my provider class where I only needed to override openFile which is not clear in the doc...
public class ExportContentProvider extends ContentProvider
{
public static Uri CONTENT_URI = Uri
.parse("content://com.path.to.my.provider");
#Override
public int delete(Uri uri, String selection, String[] selectionArgs)
{
return 0;
}
#Override
public String getType(Uri uri)
{
return null;
}
#Override
public Uri insert(Uri uri, ContentValues values)
{
return null;
}
#Override
public boolean onCreate()
{
return false;
}
#Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
{
int imode = 0;
if (mode.contains("w"))
imode |= ParcelFileDescriptor.MODE_WRITE_ONLY;
if (mode.contains("r"))
imode |= ParcelFileDescriptor.MODE_READ_ONLY;
if (mode.contains("+"))
imode |= ParcelFileDescriptor.MODE_APPEND;
try
{
return ParcelFileDescriptor.open(new File(uri.getEncodedPath()), imode);
}
catch (FileNotFoundException e)
{
e.printStackTrace();
}
return null;
}
#Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder)
{
return null;
}
#Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs)
{
return 0;
}
}
And then my export function:
private void exportImage()
{
storeBitmapOnDisk();
Intent i = new Intent(Intent.ACTION_SEND);
i.addCategory(Intent.CATEGORY_LAUNCHER);
i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
i.putExtra(Intent.EXTRA_STREAM, Uri.withAppendedPath(
ExportContentProvider.CONTENT_URI, context.getFileStreamPath(
"export.png").getAbsolutePath()));
i.setType("image/png");
i.setComponent(componentName);
if (exportFileObserver == null)
{
this.bitmapPath = context.getFileStreamPath("export.png")
.getAbsolutePath();
exportFileObserver = new ExportFileObserver(this.bitmapPath);
exportFileObserver.startWatching();
}
listener.launchExportActivity(Dms.ACT_IMAGE_EXPORT, i);
}
Nevertheless there are 2 problems remaining:
- When trying to delete my temporary export.png file on my internal memory using result of the activity I received the onResult when the activity is launch...
- It s not working for mail app in my 2.3 emulator. I'm getting "File to large" message...
Somenone tested that ?
By default, files saved to the internal storage are private to your application and other applications cannot access them (nor can the user).
I am able to see the file "/data/data/package_name/files/ in file explore in DDMS, but when i attached the above file URI using imageUri in email , then i saw that attached file is of 0kb.
i have used the default email APIs of Android.
Can anyone suggest me ,how to attach a file in email that is private to the application?
although i am successful able to save the file in SD card and attaching the file from SD card , this is working fine.
But if SD card is not available and saving the file to the internal storage , then how can i attach them in email.
String FILENAME = "hello_file.txt";
String string = "hello world!";FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
fos.write(string.getBytes());
fos.close();
File imageFile = getFileStreamPath(FILENAME );
Uri imageUri = Uri.fromFile(imageFile);
final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
emailIntent.setType("*/*");
emailIntent.putExtra(android.content.Intent.EXTRA_STREAM,imageUri);
this.startActivityForResult(Intent.createChooser(emailIntent, "Send mail..."),SUB_ACTIVITY);
When you try to attach file from internal storage, GMail writes an error to the log:
ERROR/Gmail(...): file:// attachment paths must point to file:///mnt/sdcard.
E-mail application would show you the attached file even if it didn't physically exist.
As for an external storage, documentation says that:
Every Android-compatible device supports a shared "external storage" that you can use to save files. This can be a removable storage media (such as an SD card) or an internal (non-removable) storage.
That means you don't have to worry about device not having an external storage at all. Still, external storage can be unavailable at times. Refer to http://developer.android.com/guide/topics/data/data-storage.html#filesExternal
Android: Attaching files from internal cache to Gmail
package com.stephendnicholas.gmailattach;
import java.io.File;
import java.io.FileNotFoundException;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
public class CachedFileProvider extends ContentProvider {
private static final String CLASS_NAME = "CachedFileProvider";
// The authority is the symbolic name for the provider class
public static final String AUTHORITY = "com.stephendnicholas.gmailattach.provider";
// UriMatcher used to match against incoming requests
private UriMatcher uriMatcher;
#Override
public boolean onCreate() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// Add a URI to the matcher which will match against the form
// 'content://com.stephendnicholas.gmailattach.provider/*'
// and return 1 in the case that the incoming Uri matches this pattern
uriMatcher.addURI(AUTHORITY, "*", 1);
return true;
}
#Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
String LOG_TAG = CLASS_NAME + " - openFile";
Log.v(LOG_TAG,
"Called with uri: '" + uri + "'." + uri.getLastPathSegment());
// Check incoming Uri against the matcher
switch (uriMatcher.match(uri)) {
// If it returns 1 - then it matches the Uri defined in onCreate
case 1:
// The desired file name is specified by the last segment of the
// path
// E.g.
// 'content://com.stephendnicholas.gmailattach.provider/Test.txt'
// Take this and build the path to the file
String fileLocation = getContext().getCacheDir() + File.separator
+ uri.getLastPathSegment();
// Create & return a ParcelFileDescriptor pointing to the file
// Note: I don't care what mode they ask for - they're only getting
// read only
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(
fileLocation), ParcelFileDescriptor.MODE_READ_ONLY);
return pfd;
// Otherwise unrecognised Uri
default:
Log.v(LOG_TAG, "Unsupported uri: '" + uri + "'.");
throw new FileNotFoundException("Unsupported uri: "
+ uri.toString());
}
}
// //////////////////////////////////////////////////////////////
// Not supported / used / required for this example
// //////////////////////////////////////////////////////////////
#Override
public int update(Uri uri, ContentValues contentvalues, String s,
String[] as) {
return 0;
}
#Override
public int delete(Uri uri, String s, String[] as) {
return 0;
}
#Override
public Uri insert(Uri uri, ContentValues contentvalues) {
return null;
}
#Override
public String getType(Uri uri) {
return null;
}
#Override
public Cursor query(Uri uri, String[] projection, String s, String[] as1,
String s1) {
return null;
}
}
<provider android:name="CachedFileProvider" android:authorities="com.stephendnicholas
public static void createCachedFile(Context context, String fileName,
String content) throws IOException {
File cacheFile = new File(context.getCacheDir() + File.separator
+ fileName);
cacheFile.createNewFile();
FileOutputStream fos = new FileOutputStream(cacheFile);
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF8");
PrintWriter pw = new PrintWriter(osw);
pw.println(content);
pw.flush();
pw.close();
}
public static Intent getSendEmailIntent(Context context, String email,
String subject, String body, String fileName) {
final Intent emailIntent = new Intent(
android.content.Intent.ACTION_SEND);
//Explicitly only use Gmail to send
emailIntent.setClassName("com.google.android.gm","com.google.android.gm.ComposeActivityGmail");
emailIntent.setType("plain/text");
//Add the recipients
emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL,
new String[] { email });
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, body);
//Add the attachment by specifying a reference to our custom ContentProvider
//and the specific file of interest
emailIntent.putExtra(
Intent.EXTRA_STREAM,
Uri.parse("content://" + CachedFileProvider.AUTHORITY + "/"
+ fileName));
return emailIntent;
}
enter code here
In order to share a private file you need to use a ContentProvider to provide access to your file by other applications. Here's a great example: Android: Attaching files from internal cache to Gmail.
Also, although the tutorial mentions that you need to declare your provider in the Android manifest file, it does not specify that it should be contained in <application>, so make sure that when you declare it is within <application> </application>.
This Code may help you out to get idea about attachment:
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
buttonSend = (Button) findViewById(R.id.buttonSend);
textTo = (EditText) findViewById(R.id.editTextTo);
textSubject = (EditText) findViewById(R.id.editTextSubject);
textMessage = (EditText) findViewById(R.id.editTextMessage);
buttonSend.setOnClickListener( new OnClickListener() {
#Override
public void onClick(View v) {
String to = textTo.getText().toString();
String subject = textSubject.getText().toString();
String message = textMessage.getText().toString();
Intent i = new Intent(Intent.ACTION_SEND);
i.setType("plain/text");
File data = null;
try {
Date dateVal = new Date();
String filename = dateVal.toString();
data = File.createTempFile("Report", ".csv");
FileWriter out = (FileWriter) GenerateCsv.generateCsvFile(
data, "Name,Data1");
i.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(data));
i.putExtra(Intent.EXTRA_EMAIL, new String[] { to });
i.putExtra(Intent.EXTRA_SUBJECT, subject);
i.putExtra(Intent.EXTRA_TEXT, message);
startActivity(Intent.createChooser(i, "E-mail"));
} catch (IOException e) {
e.printStackTrace();
}
}
});
}
public class GenerateCsv
{
public static FileWriter generateCsvFile(File sFileName,String fileContent)
{
FileWriter writer = null;
try {
writer = new FileWriter(sFileName);
writer.append(fileContent);
writer.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return writer;
}
}
The above code requires you add the following permission to your manifest file:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
Try using Context.MODE_WORLD_READABLE instead of Context.MODE_PRIVATE when saving the file. Then other apps will have access to the file.
I have also experienced this problem using internal files and although I have used openFileInput with MODE_WORLD_READABLE on /data/data//files/testFileName.txt and used the URI.parse with the extra "/" (see below), the received test emailed is still lacking the desired attachment. So sorry but there is no answer, except try to use External files on the SD Card - which is my next experiment!
Code :
File tmpFile = new File(context.getFilesDir(), mfileName);
Log.d(TAG, tmpFile.toString());
// This shows: /data/data/org.eddiem.adeveloper.flatfiletest/files/testFile.csv
//File tmpFile2 = new File(context.getFileStreamPath(mfileName), mfileName);
//Log.v(TAG, tmpFile2.toString());
// This also shows: /data/data/org.eddiem.adeveloper.flatfiletest/files/testFile.csv
//Uri uri = Uri.fromFile(new File(context.getFileStreamPath(mfileName), mfileName));
Uri uri = Uri.parse("file://" + tmpFile.toString());
//Uri uri = Uri.fromFile(new File(Environment.getExternalStorageDirectory(),
// mfileName));
Log.d(TAG, "Uri-path is: " + uri.getPath()); // or .toString()
Intent i = new Intent(android.content.Intent.ACTION_SEND);
i.setType("text/plain");
i.putExtra(Intent.EXTRA_EMAIL, new String[]{"eddie.moxey#sky.com"});
i.putExtra(Intent.EXTRA_SUBJECT, "Test Email - with Attachment");
i.putExtra(Intent.EXTRA_TEXT, "This is a test Email with an Attachment.");
i.putExtra(Intent.EXTRA_STREAM, uri);
//startActivity(Intent.createChooser(i, "Select application"));
startActivity(Intent.createChooser(i, "Send mail"));
I was facing the same issue and following worked for me.
First send Broadcast to notify device that file is created / mounted.
For example:
sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED,Uri.parse("file://"+storagePath)));
Then use code to send mail with attachment.
Intent email = new Intent(Intent.ACTION_SEND);
email.putExtra(Intent.EXTRA_EMAIL, "Receiver Email Address" );
email.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
email.putExtra(Intent.EXTRA_SUBJECT, "Subject");
email.putExtra(Intent.EXTRA_TEXT,"Email Text");
//Mime type of the attachment (or) u can use sendIntent.setType("*/*")
//email.setType("text/plain");
email.setType("application/YourMimeType");
//Full Path to the attachment
email.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://"+storagePath));
try
{
startActivity(Intent.createChooser(email, "Send Message..."));
}
catch (android.content.ActivityNotFoundException ex)
{
}