Implemented AttachmentProvider, removed ContentAttachment.getUri()

This commit is contained in:
str4d
2014-12-29 10:50:04 +00:00
parent 60615829da
commit 95b5e6684b
8 changed files with 327 additions and 84 deletions

View File

@@ -1,87 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="i2p.bote.android" >
<manifest
package="i2p.bote.android"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.NFC" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.NFC"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.Bote" >
<service android:name=".service.BoteService" />
android:theme="@style/Theme.Bote">
<service android:name=".service.BoteService"/>
<activity
android:name=".EmailListActivity"
android:label="@string/app_name"
android:launchMode="singleTop" >
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".intro.IntroActivity"
android:parentActivityName=".EmailListActivity" >
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity" />
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".intro.SetupActivity"
android:label="@string/title_activity_setup"
android:parentActivityName=".EmailListActivity" >
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity" />
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".ViewEmailActivity"
android:parentActivityName=".EmailListActivity" >
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity" />
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".NewEmailActivity"
android:label="@string/compose"
android:parentActivityName=".EmailListActivity" >
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity" />
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".addressbook.AddressBookActivity"
android:label="@string/address_book"
android:parentActivityName=".EmailListActivity" >
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity" />
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".addressbook.ViewContactActivity"
android:parentActivityName=".addressbook.AddressBookActivity" >
android:parentActivityName=".addressbook.AddressBookActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.addressbook.AddressBookActivity" />
android:value="i2p.bote.android.addressbook.AddressBookActivity"/>
</activity>
<activity
android:name=".addressbook.EditContactActivity"
android:label="@string/action_new_contact"
android:parentActivityName=".addressbook.ViewContactActivity" >
android:parentActivityName=".addressbook.ViewContactActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.addressbook.ViewContactActivity" />
android:value="i2p.bote.android.addressbook.ViewContactActivity"/>
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="vnd.android.nfc"
<action android:name="android.nfc.action.NDEF_DISCOVERED"/>
<category android:name="android.intent.category.DEFAULT"/>
<data
android:host="ext"
android:pathPrefix="/i2p.bote:contact"/>
android:pathPrefix="/i2p.bote:contact"
android:scheme="vnd.android.nfc"/>
</intent-filter>
<intent-filter>
<action android:name="android.nfc.action.TAG_DISCOVERED"/>
@@ -90,49 +95,57 @@
<activity
android:name=".NetworkInfoActivity"
android:label="@string/network_status"
android:parentActivityName=".EmailListActivity" >
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity" />
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".config.SettingsActivity"
android:label="@string/action_settings"
android:parentActivityName=".EmailListActivity" >
android:parentActivityName=".EmailListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.EmailListActivity" />
android:value="i2p.bote.android.EmailListActivity"/>
</activity>
<activity
android:name=".config.SetPasswordActivity"
android:label="@string/pref_title_change_password"
android:parentActivityName=".config.SettingsActivity" >
android:parentActivityName=".config.SettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.config.SettingsActivity" />
android:value="i2p.bote.android.config.SettingsActivity"/>
</activity>
<activity
android:name=".config.ViewIdentityActivity"
android:parentActivityName=".config.SettingsActivity" >
android:parentActivityName=".config.SettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.config.SettingsActivity" />
android:value="i2p.bote.android.config.SettingsActivity"/>
</activity>
<activity
android:name=".config.EditIdentityActivity"
android:label="@string/title_new_identity"
android:parentActivityName=".config.ViewIdentityActivity" >
android:parentActivityName=".config.ViewIdentityActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.config.ViewIdentityActivity" />
android:value="i2p.bote.android.config.ViewIdentityActivity"/>
</activity>
<activity
android:name=".config.IdentityShipActivity"
android:parentActivityName=".config.SettingsActivity" >
android:parentActivityName=".config.SettingsActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="i2p.bote.android.config.SettingsActivity" />
android:value="i2p.bote.android.config.SettingsActivity"/>
</activity>
<provider
android:name=".provider.AttachmentProvider"
android:authorities="${applicationId}.attachmentprovider"
android:enabled="true"
android:exported="false"
android:grantUriPermissions="true">
</provider>
</application>
</manifest>

View File

@@ -117,8 +117,7 @@ public class EmailListAdapter extends ArrayAdapter<Email> {
List<Part> parts = email.getParts();
for (Part part : parts) {
if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
((ImageView) v.findViewById(
R.id.email_attachment)).setVisibility(View.VISIBLE);
v.findViewById(R.id.email_attachment).setVisibility(View.VISIBLE);
break;
}
}

View File

@@ -339,6 +339,10 @@ public class EmailListFragment extends AuthenticatedListFragment implements
for (int i = (toDelete.size() - 1); i >= 0; i--) {
if (toDelete.valueAt(i)) {
Email email = (Email) listView.getItemAtPosition(toDelete.keyAt(i));
BoteHelper.revokeAttachmentUriPermissions(
getActivity(),
mFolder.getName(),
email);
// The Loader will update mAdapter
I2PBote.getInstance().deleteEmail(mFolder, email.getMessageID());
}

View File

@@ -444,11 +444,11 @@ public class NewEmailFragment extends Fragment {
private void addAttachment(Uri uri) {
// Try to create a ContentAttachment using the provided Uri.
try {
final ContentAttachment attachment = new ContentAttachment(getActivity().getContentResolver(), uri);
final ContentAttachment attachment = new ContentAttachment(getActivity(), uri);
final View v = getActivity().getLayoutInflater().inflate(R.layout.listitem_attachment, mAttachments, false);
v.setTag(attachment);
((TextView) v.findViewById(R.id.filename)).setText(attachment.getFileName());
((TextView) v.findViewById(R.id.size)).setText(attachment.getHumanReadableSize(getActivity()));
((TextView) v.findViewById(R.id.size)).setText(attachment.getHumanReadableSize());
v.findViewById(R.id.remove_attachment).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {

View File

@@ -26,6 +26,7 @@ import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.Part;
import i2p.bote.android.provider.AttachmentProvider;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.android.util.ContentAttachment;
import i2p.bote.email.Email;
@@ -168,15 +169,17 @@ public class ViewEmailFragment extends Fragment {
for (int partIndex=0; partIndex < parts.size(); partIndex++) {
Part part = parts.get(partIndex);
if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
ContentAttachment attachment = new ContentAttachment(part);
ContentAttachment attachment = new ContentAttachment(getActivity(), part);
View a = getActivity().getLayoutInflater().inflate(R.layout.listitem_attachment, attachments, false);
((TextView)a.findViewById(R.id.filename)).setText(attachment.getFileName());
((TextView)a.findViewById(R.id.size)).setText(attachment.getHumanReadableSize(getActivity()));
((TextView)a.findViewById(R.id.size)).setText(attachment.getHumanReadableSize());
a.findViewById(R.id.remove_attachment).setVisibility(View.GONE);
final Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(attachment.getUri());
i.setData(AttachmentProvider.getUriForAttachment(mFolderName, mMessageId, partIndex));
i.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
a.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {

View File

@@ -0,0 +1,208 @@
package i2p.bote.android.provider;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.util.Log;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import javax.mail.MessagingException;
import javax.mail.Part;
import i2p.bote.Util;
import i2p.bote.android.BuildConfig;
import i2p.bote.android.util.BoteHelper;
import i2p.bote.email.Email;
import i2p.bote.fileencryption.PasswordException;
public class AttachmentProvider extends ContentProvider {
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".attachmentprovider";
private static final int RAW_ATTACHMENT = 1;
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(AUTHORITY, "*/*/#/RAW", RAW_ATTACHMENT);
}
private final static String[] OPENABLE_PROJECTION = {
OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
public static Uri getUriForAttachment(String folderName, String messageId, int partNum) {
return new Uri.Builder()
.scheme("content")
.authority(AUTHORITY)
.appendPath(folderName)
.appendPath(messageId)
.appendPath(Integer.toString(partNum))
.appendPath("RAW")
.build();
}
@Override
public boolean onCreate() {
return true;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (sUriMatcher.match(uri) == UriMatcher.NO_MATCH)
throw new IllegalArgumentException("Invalid URI: " + uri);
if (projection == null) {
projection = OPENABLE_PROJECTION;
}
final MatrixCursor cursor = new MatrixCursor(projection, 1);
MatrixCursor.RowBuilder b = cursor.newRow();
try {
Part attachment = getAttachment(uri);
if (attachment != null) {
for (String col : projection) {
switch (col) {
case OpenableColumns.DISPLAY_NAME:
b.add(attachment.getFileName());
break;
case OpenableColumns.SIZE:
b.add(Util.getPartSize(attachment));
break;
default:
b.add(null);
break;
}
}
}
} catch (PasswordException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (MessagingException e) {
e.printStackTrace();
}
return cursor;
}
@Override
public String getType(Uri uri) {
System.out.println("getType(): URI: " + uri);
System.out.println("Match: " + sUriMatcher.match(uri));
if (sUriMatcher.match(uri) != UriMatcher.NO_MATCH) {
try {
Part attachment = getAttachment(uri);
if (attachment != null) {
System.out.println("Content type: " + attachment.getContentType());
return attachment.getContentType();
}
} catch (PasswordException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (MessagingException e) {
e.printStackTrace();
}
}
return null;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
if (sUriMatcher.match(uri) == UriMatcher.NO_MATCH)
throw new FileNotFoundException("Invalid URI: " + uri);
if (!"r".equals(mode))
throw new FileNotFoundException("Attachments can only be read");
ParcelFileDescriptor[] pipe;
try {
pipe = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
Log.e(getClass().getSimpleName(), "Exception opening pipe", e);
throw new FileNotFoundException("Could not open pipe for: "
+ uri.toString());
}
try {
Part attachment = getAttachment(uri);
if (attachment == null)
throw new FileNotFoundException("Unknown email or attachment for URI " + uri);
new TransferThread(attachment.getInputStream(),
new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])).start();
} catch (Exception e) {
Log.e(getClass().getSimpleName(), "Exception accessing attachment", e);
throw new FileNotFoundException("Exception accessing attachment: " + e.getLocalizedMessage());
}
return pipe[0];
}
static class TransferThread extends Thread {
InputStream in;
OutputStream out;
TransferThread(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(getClass().getSimpleName(), "Exception transferring file", e);
}
}
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// Copied from ContentProvider
return uri.buildUpon().appendPath("0").build();
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
private Part getAttachment(Uri uri) throws PasswordException, IOException, MessagingException {
List<String> segments = uri.getPathSegments();
String folderName = segments.get(0);
String messageId = segments.get(1);
int partNum = Integer.valueOf(segments.get(2));
Email email = BoteHelper.getEmail(folderName, messageId);
if (email != null) {
if (partNum >= 0 && partNum < email.getParts().size())
return email.getParts().get(partNum);
}
return null;
}
}

View File

@@ -4,12 +4,14 @@ import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.AsyncTask;
import android.view.LayoutInflater;
import android.view.View;
@@ -29,8 +31,10 @@ import java.util.List;
import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.Part;
import i2p.bote.android.R;
import i2p.bote.android.provider.AttachmentProvider;
import i2p.bote.email.Email;
import i2p.bote.email.EmailDestination;
import i2p.bote.email.EmailIdentity;
@@ -415,4 +419,36 @@ public class BoteHelper extends GeneralHelper {
}
return builder.toString();
}
/**
* Attempt to revoke any URI permissions that were granted on an Email's attachments.
* This is best-effort; exceptions are silently ignored.
*
* @param context the Context in which permissions were granted
* @param folderName where the Email is
* @param email the Email to revoke permissions for
*/
public static void revokeAttachmentUriPermissions(Context context, String folderName, Email email) {
List<Part> parts;
try {
parts = email.getParts();
} catch (Exception e) {
// Nothing we can do, abort
return;
}
for (Part part : parts) {
try {
if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) {
Uri uri = AttachmentProvider.getUriForAttachment(folderName,
email.getMessageID(), parts.indexOf(part));
context.revokeUriPermission(uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
} catch (MessagingException e) {
// Ignore and carry on
}
}
}
}

View File

@@ -4,12 +4,8 @@ import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import android.util.Log;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
@@ -23,22 +19,19 @@ import javax.mail.MessagingException;
import javax.mail.Part;
import i2p.bote.Util;
import i2p.bote.android.Constants;
import i2p.bote.android.R;
import i2p.bote.email.Attachment;
public class ContentAttachment implements Attachment {
private ParcelFileDescriptor mAttachmentPFD;
private Context mCtx;
private String mFileName;
private long mSize;
private DataHandler mDataHandler;
private Uri mUri;
public ContentAttachment(ContentResolver cr, Uri uri) throws FileNotFoundException {
// Get the content resolver instance for this context, and use it
// to get a ParcelFileDescriptor for the file.
mAttachmentPFD = cr.openFileDescriptor(uri, "r");
// If we get to here, the file exists
public ContentAttachment(Context context, final Uri uri) throws FileNotFoundException {
mCtx = context;
// Get the content resolver instance for this context
ContentResolver cr = context.getContentResolver();
Cursor returnCursor = cr.query(
uri,
@@ -46,18 +39,19 @@ public class ContentAttachment implements Attachment {
null, null, null);
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
returnCursor.moveToFirst();
if (!returnCursor.moveToFirst())
throw new FileNotFoundException();
mFileName = returnCursor.getString(nameIndex);
mSize = returnCursor.getLong(sizeIndex);
returnCursor.close();
// Get a regular file descriptor for the file
final FileDescriptor fd = mAttachmentPFD.getFileDescriptor();
final String mimeType = cr.getType(uri);
mDataHandler = new DataHandler(new DataSource() {
@Override
public InputStream getInputStream() throws IOException {
return new FileInputStream(fd);
return mCtx.getContentResolver().openInputStream(uri);
}
@Override
@@ -75,15 +69,14 @@ public class ContentAttachment implements Attachment {
return mFileName;
}
});
// mUri is not set here because uri is only usable by us.
// Viewing attachments is only allowed once the email has been created.
}
public ContentAttachment(final Part part) throws IOException, MessagingException {
public ContentAttachment(Context context, Part part)
throws IOException, MessagingException {
mCtx = context;
mFileName = part.getFileName();
mSize = Util.getPartSize(part);
mDataHandler = part.getDataHandler();
// TODO: Set mUri
}
@Override
@@ -95,7 +88,7 @@ public class ContentAttachment implements Attachment {
return mSize;
}
public String getHumanReadableSize(Context context) {
public String getHumanReadableSize() {
int unit = (63-Long.numberOfLeadingZeros(mSize)) / 10; // 0 if totalBytes<1K, 1 if 1K<=totalBytes<1M, etc.
double value = (double)mSize / (1<<(10*unit));
int formatStr;
@@ -109,7 +102,7 @@ public class ContentAttachment implements Attachment {
formatter.setMaximumFractionDigits(1);
else
formatter.setMaximumFractionDigits(0);
return context.getString(formatStr, formatter.format(value));
return mCtx.getString(formatStr, formatter.format(value));
}
@Override
@@ -117,21 +110,8 @@ public class ContentAttachment implements Attachment {
return mDataHandler;
}
public Uri getUri() {
return mUri;
}
@Override
public boolean clean() {
if (mAttachmentPFD == null)
return true;
try {
mAttachmentPFD.close();
return true;
} catch (IOException e) {
Log.e(Constants.ANDROID_LOG_TAG, "Can't close ParcelFileDescriptor: <" + mFileName + ">", e);
return false;
}
return true;
}
}