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