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