AttachmentsView.java revision 237129ddf57a1e25469d8990322fb7913e18ae20
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 android.content.ContentResolver; 19import android.content.Context; 20import android.database.Cursor; 21import android.database.sqlite.SQLiteException; 22import android.net.Uri; 23import android.os.ParcelFileDescriptor; 24import android.provider.OpenableColumns; 25import android.text.TextUtils; 26import android.util.AttributeSet; 27import android.view.Gravity; 28import android.view.View; 29import android.view.ViewGroup; 30import android.widget.LinearLayout; 31import android.widget.Toast; 32 33import com.android.mail.R; 34import com.android.mail.providers.Account; 35import com.android.mail.providers.Attachment; 36import com.android.mail.providers.Message; 37import com.android.mail.providers.UIProvider; 38import com.android.mail.ui.AttachmentTile; 39import com.android.mail.ui.AttachmentTileGrid; 40import com.android.mail.utils.LogUtils; 41import com.google.common.annotations.VisibleForTesting; 42import com.google.common.collect.Lists; 43 44import java.io.FileNotFoundException; 45import java.io.IOException; 46import java.util.ArrayList; 47 48/* 49 * View for displaying attachments in the compose screen. 50 */ 51class AttachmentsView extends LinearLayout { 52 private static final String LOG_TAG = new LogUtils().getLogTag(); 53 private ArrayList<Attachment> mAttachments; 54 private AttachmentDeletedListener mChangeListener; 55 private AttachmentTileGrid mTileGrid; 56 private LinearLayout mAttachmentLayout; 57 58 public AttachmentsView(Context context) { 59 this(context, null); 60 } 61 62 public AttachmentsView(Context context, AttributeSet attrs) { 63 super(context, attrs); 64 mAttachments = Lists.newArrayList(); 65 } 66 67 @Override 68 protected void onFinishInflate() { 69 super.onFinishInflate(); 70 71 mTileGrid = (AttachmentTileGrid) findViewById(R.id.attachment_tile_grid); 72 mAttachmentLayout = (LinearLayout) findViewById(R.id.attachment_bar_list); 73 } 74 75 /** 76 * Set a listener for changes to the attachments. 77 * @param listener 78 */ 79 public void setAttachmentChangesListener(AttachmentDeletedListener listener) { 80 mChangeListener = listener; 81 } 82 83 /** 84 * Add an attachment and update the ui accordingly. 85 * @param attachment 86 */ 87 public void addAttachment(final Attachment attachment) { 88 if (!isShown()) { 89 setVisibility(View.VISIBLE); 90 } 91 mAttachments.add(attachment); 92 93 // If we have an attachment that should be shown in a tiled look, 94 // set up the tile and add it to the tile grid. 95 if (AttachmentTile.isTiledAttachment(attachment)) { 96 final ComposeAttachmentTile attachmentTile = 97 mTileGrid.addComposeTileFromAttachment(attachment); 98 attachmentTile.addDeleteListener(new OnClickListener() { 99 @Override 100 public void onClick(View v) { 101 deleteAttachment(attachmentTile, attachment); 102 } 103 }); 104 // Otherwise, use the old bar look and add it to the new 105 // inner LinearLayout. 106 } else { 107 final AttachmentComposeView attachmentView = 108 new AttachmentComposeView(getContext(), attachment); 109 110 attachmentView.addDeleteListener(new OnClickListener() { 111 @Override 112 public void onClick(View v) { 113 deleteAttachment(attachmentView, attachment); 114 } 115 }); 116 117 118 mAttachmentLayout.addView(attachmentView, new LinearLayout.LayoutParams( 119 LinearLayout.LayoutParams.MATCH_PARENT, 120 LinearLayout.LayoutParams.MATCH_PARENT)); 121 } 122 } 123 124 @VisibleForTesting 125 protected void deleteAttachment(final View attachmentView, 126 final Attachment attachment) { 127 mAttachments.remove(attachment); 128 ((ViewGroup) attachmentView.getParent()).removeView(attachmentView); 129 if (mChangeListener != null) { 130 mChangeListener.onAttachmentDeleted(); 131 } 132 if (mAttachments.size() == 0) { 133 setVisibility(View.GONE); 134 } 135 } 136 137 /** 138 * Get all attachments being managed by this view. 139 * @return attachments. 140 */ 141 public ArrayList<Attachment> getAttachments() { 142 return mAttachments; 143 } 144 145 /** 146 * Delete all attachments being managed by this view. 147 */ 148 public void deleteAllAttachments() { 149 mAttachments.clear(); 150 mTileGrid.removeAllViews(); 151 mAttachmentLayout.removeAllViews(); 152 } 153 154 /** 155 * Get the total size of all attachments currently in this view. 156 */ 157 public long getTotalAttachmentsSize() { 158 long totalSize = 0; 159 for (Attachment attachment : mAttachments) { 160 totalSize += attachment.size; 161 } 162 return totalSize; 163 } 164 165 /** 166 * Interface to implement to be notified about changes to the attachments. 167 * 168 */ 169 public interface AttachmentDeletedListener { 170 public void onAttachmentDeleted(); 171 } 172 173 /** 174 * When an attachment is too large to be added to a message, show a toast. 175 * This method also updates the position of the toast so that it is shown 176 * clearly above they keyboard if it happens to be open. 177 */ 178 private void showAttachmentTooBigToast() { 179 showErrorToast(R.string.too_large_to_attach); 180 } 181 182 private void showGenericAttachmentError() { 183 showErrorToast(R.string.generic_attachment_problem); 184 } 185 186 private void showErrorToast(int resId) { 187 Toast t = Toast.makeText(getContext(), resId, 188 Toast.LENGTH_LONG); 189 t.setText(resId); 190 t.setGravity(Gravity.CENTER_HORIZONTAL, 0, 191 getResources().getDimensionPixelSize(R.dimen.attachment_toast_yoffset)); 192 t.show(); 193 } 194 195 /** 196 * Generate an {@link Attachment} object for a given local content URI. Attempts to populate 197 * the {@link Attachment#name}, {@link Attachment#size}, and {@link Attachment#contentType} 198 * fields using a {@link ContentResolver}. 199 * 200 * @param contentUri 201 * @return an Attachment object 202 * @throws AttachmentFailureException 203 */ 204 public Attachment generateLocalAttachment(Uri contentUri) throws AttachmentFailureException { 205 // FIXME: do not query resolver for type on the UI thread 206 final ContentResolver contentResolver = getContext().getContentResolver(); 207 String contentType = contentResolver.getType(contentUri); 208 if (contentUri == null || TextUtils.isEmpty(contentUri.getPath())) { 209 showGenericAttachmentError(); 210 throw new AttachmentFailureException("Attachment too large to attach"); 211 } 212 213 if (contentType == null) contentType = ""; 214 215 final Attachment attachment = new Attachment(); 216 attachment.uri = null; // URI will be assigned by the provider upon send/save 217 attachment.name = null; 218 attachment.contentType = contentType; 219 attachment.size = 0; 220 attachment.contentUri = contentUri; 221 222 Cursor metadataCursor = null; 223 try { 224 metadataCursor = contentResolver.query( 225 contentUri, new String[]{OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}, 226 null, null, null); 227 if (metadataCursor != null) { 228 try { 229 if (metadataCursor.moveToNext()) { 230 attachment.name = metadataCursor.getString(0); 231 attachment.size = metadataCursor.getInt(1); 232 } 233 } finally { 234 metadataCursor.close(); 235 } 236 } 237 } catch (SQLiteException ex) { 238 // One of the two columns is probably missing, let's make one more attempt to get at 239 // least one. 240 // Note that the documentations in Intent#ACTION_OPENABLE and 241 // OpenableColumns seem to contradict each other about whether these columns are 242 // required, but it doesn't hurt to fail properly. 243 244 // Let's try to get DISPLAY_NAME 245 try { 246 metadataCursor = getOptionalColumn(contentResolver, contentUri, 247 OpenableColumns.DISPLAY_NAME); 248 if (metadataCursor != null && metadataCursor.moveToNext()) { 249 attachment.name = metadataCursor.getString(0); 250 } 251 } finally { 252 if (metadataCursor != null) metadataCursor.close(); 253 } 254 255 // Let's try to get SIZE 256 try { 257 metadataCursor = 258 getOptionalColumn(contentResolver, contentUri, OpenableColumns.SIZE); 259 if (metadataCursor != null && metadataCursor.moveToNext()) { 260 attachment.size = metadataCursor.getInt(0); 261 } else { 262 // Unable to get the size from the metadata cursor. Open the file and seek. 263 attachment.size = getSizeFromFile(contentUri, contentResolver); 264 } 265 } finally { 266 if (metadataCursor != null) metadataCursor.close(); 267 } 268 } catch (SecurityException e) { 269 // We received a security exception when attempting to add an 270 // attachment. Warn the user. 271 // TODO(pwestbro): determine if we need more specific text in the toast. 272 Toast.makeText(getContext(), 273 R.string.generic_attachment_problem, Toast.LENGTH_LONG).show(); 274 throw new AttachmentFailureException("Security Exception from attachment uri", e); 275 } 276 277 if (attachment.name == null) { 278 attachment.name = contentUri.getLastPathSegment(); 279 } 280 281 return attachment; 282 } 283 284 /** 285 * Adds a local attachment by file path. 286 * @param account 287 * @param contentUri the uri of the local file path 288 * 289 * @return size of the attachment added. 290 * @throws AttachmentFailureException if an error occurs adding the attachment. 291 */ 292 public long addAttachment(Account account, Uri contentUri) 293 throws AttachmentFailureException { 294 return addAttachment(account, generateLocalAttachment(contentUri)); 295 } 296 297 /** 298 * Adds an attachment of either local or remote origin, checking to see if the attachment 299 * exceeds file size limits. 300 * @param account 301 * @param attachment the attachment to be added. 302 * 303 * @return size of the attachment added. 304 * @throws AttachmentFailureException if an error occurs adding the attachment. 305 */ 306 public long addAttachment(Account account, Attachment attachment) 307 throws AttachmentFailureException { 308 int maxSize = UIProvider.getMailMaxAttachmentSize(account.name); 309 310 // Error getting the size or the size was too big. 311 // FIXME: exceptions should not be used to direct control flow 312 if (attachment.size == -1 || attachment.size > maxSize) { 313 showAttachmentTooBigToast(); 314 throw new AttachmentFailureException("Attachment too large to attach"); 315 } else if ((getTotalAttachmentsSize() 316 + attachment.size) > maxSize) { 317 showAttachmentTooBigToast(); 318 throw new AttachmentFailureException("Attachment too large to attach"); 319 } else { 320 addAttachment(attachment); 321 } 322 323 return attachment.size; 324 } 325 326 327 public void addAttachments(Account account, Message refMessage) { 328 if (refMessage.hasAttachments) { 329 try { 330 for (Attachment a : refMessage.getAttachments()) { 331 addAttachment(account, a); 332 } 333 } catch (AttachmentFailureException e) { 334 // A toast has already been shown to the user, no need to do 335 // anything. 336 LogUtils.e(LOG_TAG, e, "Error adding attachment"); 337 } 338 } 339 } 340 341 @VisibleForTesting 342 protected int getSizeFromFile(Uri uri, ContentResolver contentResolver) { 343 int size = -1; 344 ParcelFileDescriptor file = null; 345 try { 346 file = contentResolver.openFileDescriptor(uri, "r"); 347 size = (int) file.getStatSize(); 348 } catch (FileNotFoundException e) { 349 LogUtils.w(LOG_TAG, "Error opening file to obtain size."); 350 } finally { 351 try { 352 if (file != null) { 353 file.close(); 354 } 355 } catch (IOException e) { 356 LogUtils.w(LOG_TAG, "Error closing file opened to obtain size."); 357 } 358 } 359 return size; 360 } 361 362 /** 363 * @return a cursor to the requested column or null if an exception occurs while trying 364 * to query it. 365 */ 366 private Cursor getOptionalColumn(ContentResolver contentResolver, Uri uri, String columnName) { 367 Cursor result = null; 368 try { 369 result = contentResolver.query(uri, new String[]{columnName}, null, null, null); 370 } catch (SQLiteException ex) { 371 // ignore, leave result null 372 } 373 return result; 374 } 375 376 /** 377 * Class containing information about failures when adding attachments. 378 */ 379 static class AttachmentFailureException extends Exception { 380 private static final long serialVersionUID = 1L; 381 382 public AttachmentFailureException(String error) { 383 super(error); 384 } 385 public AttachmentFailureException(String detailMessage, Throwable throwable) { 386 super(detailMessage, throwable); 387 } 388 } 389} 390