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