diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1dcfab4..ab67e4c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,87 +1,92 @@ - + - - - - + + + + - + android:theme="@style/Theme.Bote"> + + android:launchMode="singleTop"> - + - + + android:parentActivityName=".EmailListActivity"> + android:value="i2p.bote.android.EmailListActivity"/> + android:parentActivityName=".EmailListActivity"> + android:value="i2p.bote.android.EmailListActivity"/> + android:parentActivityName=".EmailListActivity"> + android:value="i2p.bote.android.EmailListActivity"/> + android:parentActivityName=".EmailListActivity"> + android:value="i2p.bote.android.EmailListActivity"/> + android:parentActivityName=".EmailListActivity"> + android:value="i2p.bote.android.EmailListActivity"/> + android:parentActivityName=".addressbook.AddressBookActivity"> + android:value="i2p.bote.android.addressbook.AddressBookActivity"/> + android:parentActivityName=".addressbook.ViewContactActivity"> + android:value="i2p.bote.android.addressbook.ViewContactActivity"/> + - - - + + + + + android:pathPrefix="/i2p.bote:contact" + android:scheme="vnd.android.nfc"/> @@ -90,49 +95,57 @@ + android:parentActivityName=".EmailListActivity"> + android:value="i2p.bote.android.EmailListActivity"/> + android:parentActivityName=".EmailListActivity"> + android:value="i2p.bote.android.EmailListActivity"/> + android:parentActivityName=".config.SettingsActivity"> + android:value="i2p.bote.android.config.SettingsActivity"/> + android:parentActivityName=".config.SettingsActivity"> + android:value="i2p.bote.android.config.SettingsActivity"/> + android:parentActivityName=".config.ViewIdentityActivity"> + android:value="i2p.bote.android.config.ViewIdentityActivity"/> + android:parentActivityName=".config.SettingsActivity"> + android:value="i2p.bote.android.config.SettingsActivity"/> + + + diff --git a/app/src/main/java/i2p/bote/android/EmailListAdapter.java b/app/src/main/java/i2p/bote/android/EmailListAdapter.java index e30a6b8..fca0aeb 100644 --- a/app/src/main/java/i2p/bote/android/EmailListAdapter.java +++ b/app/src/main/java/i2p/bote/android/EmailListAdapter.java @@ -117,8 +117,7 @@ public class EmailListAdapter extends ArrayAdapter { List 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; } } diff --git a/app/src/main/java/i2p/bote/android/EmailListFragment.java b/app/src/main/java/i2p/bote/android/EmailListFragment.java index d30aaa0..9d3a43f 100644 --- a/app/src/main/java/i2p/bote/android/EmailListFragment.java +++ b/app/src/main/java/i2p/bote/android/EmailListFragment.java @@ -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()); } diff --git a/app/src/main/java/i2p/bote/android/NewEmailFragment.java b/app/src/main/java/i2p/bote/android/NewEmailFragment.java index 2d72098..52246e2 100644 --- a/app/src/main/java/i2p/bote/android/NewEmailFragment.java +++ b/app/src/main/java/i2p/bote/android/NewEmailFragment.java @@ -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) { diff --git a/app/src/main/java/i2p/bote/android/ViewEmailFragment.java b/app/src/main/java/i2p/bote/android/ViewEmailFragment.java index 003c363..96773a8 100644 --- a/app/src/main/java/i2p/bote/android/ViewEmailFragment.java +++ b/app/src/main/java/i2p/bote/android/ViewEmailFragment.java @@ -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) { diff --git a/app/src/main/java/i2p/bote/android/provider/AttachmentProvider.java b/app/src/main/java/i2p/bote/android/provider/AttachmentProvider.java new file mode 100644 index 0000000..e47488d --- /dev/null +++ b/app/src/main/java/i2p/bote/android/provider/AttachmentProvider.java @@ -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 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; + } +} diff --git a/app/src/main/java/i2p/bote/android/util/BoteHelper.java b/app/src/main/java/i2p/bote/android/util/BoteHelper.java index 08b2e22..68ae913 100644 --- a/app/src/main/java/i2p/bote/android/util/BoteHelper.java +++ b/app/src/main/java/i2p/bote/android/util/BoteHelper.java @@ -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 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 + } + } + } } diff --git a/app/src/main/java/i2p/bote/android/util/ContentAttachment.java b/app/src/main/java/i2p/bote/android/util/ContentAttachment.java index fda789b..0fceaf9 100644 --- a/app/src/main/java/i2p/bote/android/util/ContentAttachment.java +++ b/app/src/main/java/i2p/bote/android/util/ContentAttachment.java @@ -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; } }