diff --git a/app/src/main/java/i2p/bote/android/NewEmailFragment.java b/app/src/main/java/i2p/bote/android/NewEmailFragment.java index 0baf19f..2d72098 100644 --- a/app/src/main/java/i2p/bote/android/NewEmailFragment.java +++ b/app/src/main/java/i2p/bote/android/NewEmailFragment.java @@ -1,14 +1,20 @@ package i2p.bote.android; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; +import android.content.ClipData; import android.content.Context; import android.content.DialogInterface; +import android.content.Intent; import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.support.v4.app.Fragment; import android.text.Editable; import android.text.TextWatcher; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -18,6 +24,7 @@ import android.view.ViewGroup; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.ImageView; +import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; @@ -25,6 +32,7 @@ import com.tokenautocomplete.FilteredArrayAdapter; import net.i2p.data.DataFormatException; +import java.io.FileNotFoundException; import java.io.IOException; import java.security.GeneralSecurityException; import java.util.ArrayList; @@ -40,6 +48,7 @@ import javax.mail.internet.InternetAddress; import i2p.bote.I2PBote; import i2p.bote.android.util.BoteHelper; import i2p.bote.android.util.ContactsCompletionView; +import i2p.bote.android.util.ContentAttachment; import i2p.bote.android.util.Person; import i2p.bote.email.Attachment; import i2p.bote.email.Email; @@ -89,6 +98,8 @@ public class NewEmailFragment extends Fragment { public static final String QUOTE_MSG_TYPE = "type"; + private static final int REQUEST_FILE = 1; + private String mSenderKey; Spinner mSpinner; @@ -100,6 +111,7 @@ public class NewEmailFragment extends Fragment { ContactsCompletionView mBcc; EditText mSubject; EditText mContent; + LinearLayout mAttachments; boolean mMoreVisible; boolean mDirty; @@ -137,6 +149,7 @@ public class NewEmailFragment extends Fragment { mBcc = (ContactsCompletionView) view.findViewById(R.id.bcc); mSubject = (EditText) view.findViewById(R.id.subject); mContent = (EditText) view.findViewById(R.id.message); + mAttachments = (LinearLayout) view.findViewById(R.id.attachments); String quoteMsgFolder = getArguments().getString(QUOTE_MSG_FOLDER); String quoteMsgId = getArguments().getString(QUOTE_MSG_ID); @@ -357,6 +370,10 @@ public class NewEmailFragment extends Fragment { @Override public boolean onOptionsItemSelected(final MenuItem item) { switch (item.getItemId()) { + case R.id.action_attach_file: + requestFile(); + return true; + case R.id.action_send_email: if (sendEmail()) mCallbacks.onTaskFinished(); @@ -388,6 +405,64 @@ public class NewEmailFragment extends Fragment { } } + private void requestFile() { + Intent i = new Intent(Intent.ACTION_GET_CONTENT); + i.setType("*/*"); + i.addCategory(Intent.CATEGORY_OPENABLE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) + i.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + startActivityForResult( + Intent.createChooser(i, + getResources().getString(R.string.select_attachment)), + REQUEST_FILE); + } + + @SuppressLint("NewApi") + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (resultCode != Activity.RESULT_OK) { + if (resultCode == Activity.RESULT_CANCELED) { + System.out.println("Cancelled"); + } + return; + } + + switch (requestCode) { + case REQUEST_FILE: + addAttachment(data.getData()); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && + data.getClipData() != null) { + ClipData clipData = data.getClipData(); + for (int i = 0; i < clipData.getItemCount(); i++) { + addAttachment(clipData.getItemAt(i).getUri()); + } + } + break; + } + } + + private void addAttachment(Uri uri) { + // Try to create a ContentAttachment using the provided Uri. + try { + final ContentAttachment attachment = new ContentAttachment(getActivity().getContentResolver(), 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())); + v.findViewById(R.id.remove_attachment).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + attachment.clean(); + mAttachments.removeView(v); + } + }); + mAttachments.addView(v); + } catch (FileNotFoundException e) { + e.printStackTrace(); + Log.e(Constants.ANDROID_LOG_TAG, "File not found: " + uri); + } + } + private boolean sendEmail() { Email email = new Email(I2PBote.getInstance().getConfiguration().getIncludeSentTime()); try { @@ -433,11 +508,25 @@ public class NewEmailFragment extends Fragment { email.setSubject(mSubject.getText().toString(), "UTF-8"); + // Extract the attachments + List attachments = new ArrayList(); + for (int i = 0; i < mAttachments.getChildCount(); i++) { + View v = mAttachments.getChildAt(i); + attachments.add((Attachment) v.getTag()); + } + // Set the text and add attachments - email.setContent(mContent.getText().toString(), (List) null); + email.setContent(mContent.getText().toString(), attachments); // Send the email I2PBote.getInstance().sendEmail(email); + + // Clean up attachments + for (Attachment attachment : attachments) { + if (!attachment.clean()) + Log.e(Constants.ANDROID_LOG_TAG, "Can't clean up attachment: <" + attachment + ">"); + } + return true; } catch (PasswordException e) { // TODO Auto-generated catch block diff --git a/app/src/main/java/i2p/bote/android/ViewEmailFragment.java b/app/src/main/java/i2p/bote/android/ViewEmailFragment.java index 8c1b2ad..0a5c5c0 100644 --- a/app/src/main/java/i2p/bote/android/ViewEmailFragment.java +++ b/app/src/main/java/i2p/bote/android/ViewEmailFragment.java @@ -20,11 +20,15 @@ import android.widget.Toast; import java.io.IOException; import java.security.GeneralSecurityException; import java.text.DateFormat; +import java.util.List; import javax.mail.Address; import javax.mail.MessagingException; +import javax.mail.Part; +import i2p.bote.Util; import i2p.bote.android.util.BoteHelper; +import i2p.bote.android.util.ContentAttachment; import i2p.bote.email.Email; import i2p.bote.fileencryption.PasswordException; @@ -84,6 +88,7 @@ public class ViewEmailFragment extends Fragment { TextView sent = (TextView) v.findViewById(R.id.email_sent); TextView received = (TextView) v.findViewById(R.id.email_received); TextView content = (TextView) v.findViewById(R.id.email_content); + LinearLayout attachments = (LinearLayout) v.findViewById(R.id.attachments); try { String fromAddress = email.getOneFromAddress(); @@ -160,6 +165,19 @@ public class ViewEmailFragment extends Fragment { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) content.setTextIsSelectable(true); + List parts = email.getParts(); + for (int partIndex=0; partIndex < parts.size(); partIndex++) { + Part part = parts.get(partIndex); + if (Part.ATTACHMENT.equalsIgnoreCase(part.getDisposition())) { + ContentAttachment attachment = new ContentAttachment(part); + final 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())); + a.findViewById(R.id.remove_attachment).setVisibility(View.GONE); + attachments.addView(a); + } + } + // Prepare fields for replying mIsAnonymous = email.isAnonymous(); } catch (MessagingException e) { diff --git a/app/src/main/java/i2p/bote/android/util/ContentAttachment.java b/app/src/main/java/i2p/bote/android/util/ContentAttachment.java new file mode 100644 index 0000000..9861d6a --- /dev/null +++ b/app/src/main/java/i2p/bote/android/util/ContentAttachment.java @@ -0,0 +1,129 @@ +package i2p.bote.android.util; + +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; +import java.io.OutputStream; +import java.text.NumberFormat; +import java.util.Locale; + +import javax.activation.DataHandler; +import javax.activation.DataSource; +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 String mFileName; + private long mSize; + private DataHandler mDataHandler; + + 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 + + Cursor returnCursor = cr.query( + uri, + new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, + null, null, null); + int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE); + returnCursor.moveToFirst(); + 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); + } + + @Override + public OutputStream getOutputStream() throws IOException { + throw new IOException("Cannot write to attachments"); + } + + @Override + public String getContentType() { + return mimeType; + } + + @Override + public String getName() { + return mFileName; + } + }); + } + + public ContentAttachment(final Part part) throws IOException, MessagingException { + mFileName = part.getFileName(); + mSize = Util.getPartSize(part); + mDataHandler = part.getDataHandler(); + } + + @Override + public String getFileName() { + return mFileName; + } + + public long getSize() { + return mSize; + } + + public String getHumanReadableSize(Context context) { + 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; + switch (unit) { + case 0: formatStr = R.string.n_bytes; break; + case 1: formatStr = R.string.n_kilobytes; break; + default: formatStr = R.string.n_megabytes; + } + NumberFormat formatter = NumberFormat.getInstance(Locale.getDefault()); + if (value < 100) + formatter.setMaximumFractionDigits(1); + else + formatter.setMaximumFractionDigits(0); + return context.getString(formatStr, formatter.format(value)); + } + + @Override + public DataHandler getDataHandler() { + return mDataHandler; + } + + @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; + } + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_attach_file_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_attach_file_white_24dp.png new file mode 100644 index 0000000..66299b8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_attach_file_white_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_attach_file_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_attach_file_white_24dp.png new file mode 100644 index 0000000..fc62a5b Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_attach_file_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_attach_file_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_attach_file_white_24dp.png new file mode 100644 index 0000000..db23fd6 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_attach_file_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_attach_file_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_attach_file_white_24dp.png new file mode 100644 index 0000000..7256ca3 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_attach_file_white_24dp.png differ diff --git a/app/src/main/res/layout/fragment_new_email.xml b/app/src/main/res/layout/fragment_new_email.xml index 506ad11..6357506 100644 --- a/app/src/main/res/layout/fragment_new_email.xml +++ b/app/src/main/res/layout/fragment_new_email.xml @@ -68,6 +68,12 @@ android:ems="10" android:hint="@string/compose_email" android:inputType="textMultiLine"/> + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_view_email.xml b/app/src/main/res/layout/fragment_view_email.xml index 6205564..423f8ca 100644 --- a/app/src/main/res/layout/fragment_view_email.xml +++ b/app/src/main/res/layout/fragment_view_email.xml @@ -221,6 +221,12 @@ android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:textAppearance="@style/TextAppearance.AppCompat.Primary"/> + + diff --git a/app/src/main/res/menu/new_email.xml b/app/src/main/res/menu/new_email.xml index 5d06f72..af7612f 100644 --- a/app/src/main/res/menu/new_email.xml +++ b/app/src/main/res/menu/new_email.xml @@ -2,6 +2,12 @@ + + Log out Touch the lock icon to log in. New email + Attach file + Select a file to attach + %s B + %s KB + %s MB Send email Connect to network Disconnect from network