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