AttachmentsView.java revision 1ddcf0f2bf44d3c9db89112ef52510d9b2433ac4
1/** 2 * Copyright (c) 2011, Google Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16package com.android.mail.compose; 17 18import com.android.mail.R; 19import com.android.mail.providers.Account; 20import com.android.mail.providers.Attachment; 21import com.android.mail.providers.Message; 22import com.android.mail.providers.UIProvider; 23import com.android.mail.utils.LogUtils; 24import com.google.common.annotations.VisibleForTesting; 25import com.google.common.collect.Lists; 26 27import android.content.ContentResolver; 28import android.content.Context; 29import android.database.Cursor; 30import android.database.sqlite.SQLiteException; 31import android.net.Uri; 32import android.os.ParcelFileDescriptor; 33import android.provider.OpenableColumns; 34import android.text.TextUtils; 35import android.util.AttributeSet; 36import android.view.Gravity; 37import android.view.View; 38import android.widget.LinearLayout; 39import android.widget.Toast; 40 41import java.io.FileNotFoundException; 42import java.io.IOException; 43import java.util.ArrayList; 44 45/* 46 * View for displaying attachments in the compose screen. 47 */ 48class AttachmentsView extends LinearLayout { 49 private static final String LOG_TAG = new LogUtils().getLogTag(); 50 private ArrayList<Attachment> mAttachments; 51 private AttachmentDeletedListener mChangeListener; 52 53 public AttachmentsView(Context context) { 54 this(context, null); 55 } 56 57 public AttachmentsView(Context context, AttributeSet attrs) { 58 super(context, attrs); 59 mAttachments = Lists.newArrayList(); 60 } 61 62 /** 63 * Set a listener for changes to the attachments. 64 * @param listener 65 */ 66 public void setAttachmentChangesListener(AttachmentDeletedListener listener) { 67 mChangeListener = listener; 68 } 69 70 /** 71 * Add an attachment and update the ui accordingly. 72 * @param attachment 73 */ 74 public void addAttachment(final Attachment attachment) { 75 if (!isShown()) { 76 setVisibility(View.VISIBLE); 77 } 78 mAttachments.add(attachment); 79 80 final AttachmentComposeView attachmentView = 81 new AttachmentComposeView(getContext(), attachment); 82 83 attachmentView.addDeleteListener(new OnClickListener() { 84 @Override 85 public void onClick(View v) { 86 deleteAttachment(attachmentView, attachment); 87 } 88 }); 89 90 91 addView(attachmentView, new LinearLayout.LayoutParams( 92 LinearLayout.LayoutParams.MATCH_PARENT, 93 LinearLayout.LayoutParams.MATCH_PARENT)); 94 } 95 96 @VisibleForTesting 97 protected void deleteAttachment(final AttachmentComposeView attachmentView, 98 final Attachment attachment) { 99 mAttachments.remove(attachment); 100 removeView(attachmentView); 101 if (mChangeListener != null) { 102 mChangeListener.onAttachmentDeleted(); 103 } 104 if (mAttachments.size() == 0) { 105 setVisibility(View.GONE); 106 } 107 } 108 109 /** 110 * Get all attachments being managed by this view. 111 * @return attachments. 112 */ 113 public ArrayList<Attachment> getAttachments() { 114 return mAttachments; 115 } 116 117 /** 118 * Delete all attachments being managed by this view. 119 */ 120 public void deleteAllAttachments() { 121 mAttachments.clear(); 122 removeAllViews(); 123 } 124 125 /** 126 * See if all the attachments in this view are synced. 127 */ 128 public boolean areAttachmentsSynced() { 129 for (Attachment a : mAttachments) { 130 if (a.isSynced) { 131 return true; 132 } 133 } 134 return false; 135 } 136 137 /** 138 * Get the total size of all attachments currently in this view. 139 */ 140 public long getTotalAttachmentsSize() { 141 long totalSize = 0; 142 for (Attachment attachment : mAttachments) { 143 totalSize += attachment.size; 144 } 145 return totalSize; 146 } 147 148 /** 149 * Interface to implement to be notified about changes to the attachments. 150 * @author mindyp@google.com 151 * 152 */ 153 public interface AttachmentDeletedListener { 154 public void onAttachmentDeleted(); 155 } 156 157 /** 158 * When an attachment is too large to be added to a message, show a toast. 159 * This method also updates the position of the toast so that it is shown 160 * clearly above they keyboard if it happens to be open. 161 */ 162 private void showAttachmentTooBigToast() { 163 Toast t = Toast.makeText(getContext(), R.string.generic_attachment_problem, 164 Toast.LENGTH_LONG); 165 t.setText(R.string.too_large_to_attach); 166 t.setGravity(Gravity.CENTER_HORIZONTAL, 0, 167 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset)); 168 t.show(); 169 } 170 171 /** 172 * Adds an attachment 173 * @param uri the uri to attach 174 * @param contentType the type of the resource pointed to by the URI or null if the type is 175 * unknown 176 * @param doSave whether the message should be saved 177 * 178 * @return int size of the attachment added. 179 * @throws AttachmentFailureException if an error occurs adding the attachment. 180 */ 181 public long addAttachment(Account account, Uri uri, boolean doSave, boolean isLocal) 182 throws AttachmentFailureException { 183 final ContentResolver contentResolver = getContext().getContentResolver(); 184 String contentType = contentResolver.getType(uri); 185 if (uri == null || TextUtils.isEmpty(uri.getPath())) { 186 showAttachmentTooBigToast(); 187 return -1; 188 } 189 190 if (contentType == null) contentType = ""; 191 192 Attachment attachment = new Attachment(); 193 // partId will be assigned by the engine. 194 attachment.name = null; 195 attachment.mimeType = contentType; 196 attachment.size = 0; 197 attachment.contentUri = uri.toString(); 198 attachment.origin = isLocal ? Attachment.LOCAL_FILE : Attachment.SERVER_ATTACHMENT; 199 attachment.originExtras = uri.toString(); 200 201 Cursor metadataCursor = null; 202 try { 203 metadataCursor = contentResolver.query( 204 uri, new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, 205 null, null, null); 206 if (metadataCursor != null) { 207 try { 208 if (metadataCursor.moveToNext()) { 209 attachment.name = metadataCursor.getString(0); 210 attachment.size = metadataCursor.getInt(1); 211 } 212 } finally { 213 metadataCursor.close(); 214 } 215 } 216 } catch (SQLiteException ex) { 217 // One of the two columns is probably missing, let's make one more attempt to get at 218 // least one. 219 // Note that the documentations in Intent#ACTION_OPENABLE and 220 // OpenableColumns seem to contradict each other about whether these columns are 221 // required, but it doesn't hurt to fail properly. 222 223 // Let's try to get DISPLAY_NAME 224 try { 225 metadataCursor = 226 getOptionalColumn(contentResolver, uri, OpenableColumns.DISPLAY_NAME); 227 if (metadataCursor != null && metadataCursor.moveToNext()) { 228 attachment.name = metadataCursor.getString(0); 229 } 230 } finally { 231 if (metadataCursor != null) metadataCursor.close(); 232 } 233 234 // Let's try to get SIZE 235 try { 236 metadataCursor = 237 getOptionalColumn(contentResolver, uri, OpenableColumns.SIZE); 238 if (metadataCursor != null && metadataCursor.moveToNext()) { 239 attachment.size = metadataCursor.getInt(0); 240 } else { 241 // Unable to get the size from the metadata cursor. Open the file and seek. 242 attachment.size = getSizeFromFile(uri, contentResolver); 243 } 244 } finally { 245 if (metadataCursor != null) metadataCursor.close(); 246 } 247 } catch (SecurityException e) { 248 // We received a security exception when attempting to add an 249 // attachment. Warn the user. 250 // TODO(pwestbro): determine if we need more specific text in the toast. 251 Toast.makeText(getContext(), 252 R.string.generic_attachment_problem, Toast.LENGTH_LONG).show(); 253 throw new AttachmentFailureException("Security Exception from attachment uri", e); 254 } 255 256 if (attachment.name == null) { 257 attachment.name = uri.getLastPathSegment(); 258 } 259 260 int maxSize = UIProvider.getMailMaxAttachmentSize(account.name); 261 262 // Error getting the size or the size was too big. 263 if (attachment.size == -1 || attachment.size > maxSize) { 264 showAttachmentTooBigToast(); 265 throw new AttachmentFailureException("Attachment too large to attach"); 266 } else if ((getTotalAttachmentsSize() 267 + attachment.size) > maxSize) { 268 showAttachmentTooBigToast(); 269 throw new AttachmentFailureException("Attachment too large to attach"); 270 } else { 271 addAttachment(attachment); 272 } 273 274 return attachment.size; 275 } 276 277 278 public void addAttachments(Account account, Message refMessage) { 279 boolean hasAttachments = refMessage.hasAttachments; 280 if (hasAttachments) { 281 String attachmentQuery = refMessage.attachmentListUri; 282 Cursor attachmentCursor = null; 283 try { 284 attachmentCursor = getContext().getContentResolver().query( 285 Uri.parse(attachmentQuery), UIProvider.ATTACHMENT_PROJECTION, null, null, 286 null); 287 String attachmentUri; 288 while (attachmentCursor.moveToNext()) { 289 attachmentUri = attachmentCursor.getString(UIProvider.ATTACHMENT_URI_COLUMN); 290 if (!TextUtils.isEmpty(attachmentUri)) { 291 addAttachment(account, Uri.parse(attachmentUri), false, false); 292 } 293 } 294 } catch (AttachmentFailureException e) { 295 // A toast has already been shown to the user, no need to do 296 // anything. 297 LogUtils.e(LOG_TAG, e, "Error adding attachment"); 298 } finally { 299 if (attachmentCursor != null) { 300 attachmentCursor.close(); 301 } 302 } 303 } 304 } 305 306 @VisibleForTesting 307 protected int getSizeFromFile(Uri uri, ContentResolver contentResolver) { 308 int size = -1; 309 ParcelFileDescriptor file = null; 310 try { 311 file = contentResolver.openFileDescriptor(uri, "r"); 312 size = (int) file.getStatSize(); 313 } catch (FileNotFoundException e) { 314 LogUtils.w(LOG_TAG, "Error opening file to obtain size."); 315 } finally { 316 try { 317 if (file != null) { 318 file.close(); 319 } 320 } catch (IOException e) { 321 LogUtils.w(LOG_TAG, "Error closing file opened to obtain size."); 322 } 323 } 324 return size; 325 } 326 327 /** 328 * @return a cursor to the requested column or null if an exception occurs while trying 329 * to query it. 330 */ 331 private Cursor getOptionalColumn(ContentResolver contentResolver, Uri uri, String columnName) { 332 Cursor result = null; 333 try { 334 result = contentResolver.query(uri, new String[]{columnName}, null, null, null); 335 } catch (SQLiteException ex) { 336 // ignore, leave result null 337 } 338 return result; 339 } 340 341 /** 342 * Class containing information about failures when adding attachments. 343 */ 344 static class AttachmentFailureException extends Exception { 345 private static final long serialVersionUID = 1L; 346 347 public AttachmentFailureException(String error) { 348 super(error); 349 } 350 public AttachmentFailureException(String detailMessage, Throwable throwable) { 351 super(detailMessage, throwable); 352 } 353 } 354} 355