blob: 20cc84589cfc17f98b8fd2a9d3030ce5dcf16f29 [file] [log] [blame]
Andy Huang88fc42e2012-03-08 15:02:43 -08001/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.browse;
19
20import android.app.AlertDialog;
21import android.app.LoaderManager;
22import android.app.ProgressDialog;
23import android.content.ActivityNotFoundException;
24import android.content.AsyncQueryHandler;
25import android.content.ContentValues;
26import android.content.Context;
27import android.content.DialogInterface;
28import android.content.Intent;
29import android.content.Loader;
30import android.database.Cursor;
31import android.net.Uri;
32import android.os.Bundle;
33import android.util.AttributeSet;
34import android.view.LayoutInflater;
35import android.view.MenuItem;
36import android.view.View;
37import android.view.View.OnClickListener;
38import android.view.ViewGroup;
39import android.widget.Button;
40import android.widget.ImageView;
41import android.widget.LinearLayout;
42import android.widget.PopupMenu.OnMenuItemClickListener;
43import android.widget.ProgressBar;
44import android.widget.TextView;
45
46import com.android.mail.R;
47import com.android.mail.browse.AttachmentLoader.AttachmentCursor;
48import com.android.mail.providers.Attachment;
49import com.android.mail.providers.UIProvider.AttachmentColumns;
50import com.android.mail.providers.UIProvider.AttachmentDestination;
51import com.android.mail.providers.UIProvider.AttachmentState;
52import com.android.mail.utils.AttachmentUtils;
53import com.android.mail.utils.LogUtils;
54import com.android.mail.utils.MimeType;
55import com.android.mail.utils.Utils;
56
57/**
58 * View for a single attachment in conversation view. Shows download status and allows launching
59 * intents to act on an attachment.
60 *
61 */
62public class MessageHeaderAttachment extends LinearLayout implements OnClickListener,
63 OnMenuItemClickListener, DialogInterface.OnCancelListener,
64 DialogInterface.OnDismissListener, LoaderManager.LoaderCallbacks<Cursor> {
65
66 private LoaderManager mLoaderManager;
67 private Attachment mAttachment;
68 private ImageView mIcon;
69 private TextView mTitle;
70 private TextView mSubTitle;
71 private String mAttachmentSizeText;
72 private String mDisplayType;
73 private ProgressDialog mViewProgressDialog;
74 private AttachmentCommandHandler mCommandHandler;
75 private ProgressBar mProgress;
76 private Button mPreviewButton;
77 private Button mViewButton;
78 private Button mSaveButton;
79 private Button mInfoButton;
80 private Button mPlayButton;
81 private Button mInstallButton;
82 private Button mCancelButton;
83
84 private static final String LOG_TAG = new LogUtils().getLogTag();
85
86 private class AttachmentCommandHandler extends AsyncQueryHandler {
87
88 public AttachmentCommandHandler() {
89 super(getContext().getContentResolver());
90 }
91
92 /**
93 * Asynchronously begin an update() on a ContentProvider and initialize a loader to watch
94 * for resulting changes on this attachment.
95 *
96 */
97 public void sendCommand(ContentValues params) {
98 startUpdate(0, null, mAttachment.uri, params, null, null);
99 mLoaderManager.initLoader(mAttachment.uri.hashCode(), Bundle.EMPTY,
100 MessageHeaderAttachment.this);
101 }
102
103 }
104
105 public MessageHeaderAttachment(Context context) {
106 super(context);
107 }
108
109 public MessageHeaderAttachment(Context context, AttributeSet attrs) {
110 super(context, attrs);
111
112 mCommandHandler = new AttachmentCommandHandler();
113 }
114
115 public static MessageHeaderAttachment inflate(LayoutInflater inflater, ViewGroup parent,
116 LoaderManager loaderManager) {
117 MessageHeaderAttachment view = (MessageHeaderAttachment) inflater.inflate(
118 R.layout.conversation_message_attachment, parent, false);
119
120 view.mLoaderManager = loaderManager;
121
122 return view;
123 }
124
125 /**
126 * Render most of the UI using given immutable attachment properties. This happens immediately
127 * upon instantiation.
128 *
129 */
130 public void render(Attachment attachment) {
131 mAttachment = attachment;
132
133 mTitle.setText(attachment.name);
134
135 mAttachmentSizeText = AttachmentUtils.convertToHumanReadableSize(getContext(),
136 attachment.size);
137 mDisplayType = AttachmentUtils.getDisplayType(getContext(), attachment);
138 updateSubtitleText(null);
139
140 if (mAttachment.isImage() && mAttachment.thumbnailUri != null) {
141 // FIXME: this decodes on the UI thread. Also, it doesn't handle large images, so
142 // using the full image is out of the question.
143 mIcon.setImageURI(mAttachment.thumbnailUri);
144 }
145 if (mIcon.getDrawable() == null) {
146 // not an image, or image load failed. fall back to default.
147 mIcon.setImageResource(R.drawable.ic_menu_attachment_holo_light);
148 mIcon.setScaleType(ImageView.ScaleType.CENTER);
149 }
150
151 mProgress.setMax(attachment.size);
152
153 updateActions();
154
155 if (mAttachment.isDownloading()) {
156 mLoaderManager.initLoader(mAttachment.uri.hashCode(), Bundle.EMPTY, this);
157 // TODO: clean up loader when the view is detached
158 }
159 }
160
161 private void updateStatus(Attachment newAttachment) {
162
163 LogUtils.d(LOG_TAG, "got attachment update: uri=%s dled=%d state=%d contentUri=%s MIME=%s",
164 newAttachment.uri, newAttachment.downloadedSize, newAttachment.state,
165 newAttachment.contentUri, newAttachment.mimeType);
166
167 mAttachment = newAttachment;
168
169 final boolean showProgress = newAttachment.size > 0 && newAttachment.downloadedSize > 0
170 && newAttachment.downloadedSize < newAttachment.size;
171
172 if (mViewProgressDialog != null && mViewProgressDialog.isShowing()) {
173 mViewProgressDialog.setProgress(newAttachment.downloadedSize);
174 mViewProgressDialog.setIndeterminate(showProgress);
175
176 if (!newAttachment.isDownloading()) {
177 mViewProgressDialog.dismiss();
178 }
179
180 if (newAttachment.state == AttachmentState.SAVED) {
181 sendViewIntent();
182 }
183 } else {
184
185 if (newAttachment.isDownloading()) {
186 mProgress.setProgress(newAttachment.downloadedSize);
187 setProgressVisible(true);
188 } else {
189 setProgressVisible(false);
190 }
191
192 }
193
194 if (newAttachment.state == AttachmentState.FAILED) {
195 mSubTitle.setText(getResources().getString(R.string.download_failed));
196 } else {
197 updateSubtitleText(newAttachment.isSavedToExternal() ?
198 getResources().getString(R.string.saved) : null);
199 }
200
201 updateActions();
202 }
203
204 private void setProgressVisible(boolean visible) {
205 if (visible) {
206 mProgress.setVisibility(VISIBLE);
207 mSubTitle.setVisibility(INVISIBLE);
208 } else {
209 mProgress.setVisibility(GONE);
210 mSubTitle.setVisibility(VISIBLE);
211 }
212 }
213
214 private void updateSubtitleText(String prefix) {
215 // TODO: make this a formatted resource when we have a UX design.
216 // not worth translation right now.
217 StringBuilder sb = new StringBuilder();
218 if (prefix != null) {
219 sb.append(prefix);
220 }
221 sb.append(mAttachmentSizeText);
222 sb.append(' ');
223 sb.append(mDisplayType);
224 mSubTitle.setText(sb.toString());
225 }
226
227 @Override
228 protected void onFinishInflate() {
229 super.onFinishInflate();
230
231 mIcon = (ImageView) findViewById(R.id.attachment_icon);
232 mTitle = (TextView) findViewById(R.id.attachment_title);
233 mSubTitle = (TextView) findViewById(R.id.attachment_subtitle);
234 mProgress = (ProgressBar) findViewById(R.id.attachment_progress);
235
236 mPreviewButton = (Button) findViewById(R.id.preview_attachment);
237 mViewButton = (Button) findViewById(R.id.view_attachment);
238 mSaveButton = (Button) findViewById(R.id.save_attachment);
239 mInfoButton = (Button) findViewById(R.id.info_attachment);
240 mPlayButton = (Button) findViewById(R.id.play_attachment);
241 mInstallButton = (Button) findViewById(R.id.install_attachment);
242 mCancelButton = (Button) findViewById(R.id.cancel_attachment);
243
244 setOnClickListener(this);
245 mPreviewButton.setOnClickListener(this);
246 mViewButton.setOnClickListener(this);
247 mSaveButton.setOnClickListener(this);
248 mInfoButton.setOnClickListener(this);
249 mPlayButton.setOnClickListener(this);
250 mInstallButton.setOnClickListener(this);
251 mCancelButton.setOnClickListener(this);
252 }
253
254 @Override
255 public void onClick(View v) {
256 onClick(v.getId(), v);
257 }
258
259 @Override
260 public boolean onMenuItemClick(MenuItem item) {
261 return onClick(item.getItemId(), null);
262 }
263
264 private boolean onClick(int res, View v) {
265 switch (res) {
266 case R.id.preview_attachment:
267 getContext().startActivity(mAttachment.previewIntent);
268 break;
269 case R.id.view_attachment:
270 case R.id.play_attachment:
271 showAttachment(AttachmentDestination.CACHE);
272 break;
273 case R.id.save_attachment:
274 if (mAttachment.canSave()) {
275 startDownloadingAttachment(AttachmentDestination.EXTERNAL);
276 }
277 break;
278 case R.id.info_attachment:
279 AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
280 int dialogMessage = MimeType.isBlocked(mAttachment.mimeType)
281 ? R.string.attachment_type_blocked : R.string.no_application_found;
282 builder.setTitle(R.string.more_info_attachment).setMessage(dialogMessage).show();
283 break;
284 case R.id.install_attachment:
285 showAttachment(AttachmentDestination.EXTERNAL);
286 break;
287 case R.id.cancel_attachment:
288 cancelAttachment();
289 break;
290 default:
291 // entire attachment view is clickable.
292 // TODO: this should execute a default action
293 break;
294 }
295 return true;
296 }
297
298 private void showAttachment(int destination) {
299 if (mAttachment.isPresentLocally()) {
300 sendViewIntent();
301 } else {
302 showDownloadingDialog();
303 startDownloadingAttachment(destination);
304 }
305 }
306
307 @Override
308 public Loader<Cursor> onCreateLoader(int id, Bundle args) {
309 return new AttachmentLoader(getContext(), mAttachment.uri);
310 }
311
312 @Override
313 public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
314 AttachmentCursor cursor = (AttachmentCursor) data;
315 if (cursor == null || cursor.isClosed() || cursor.getCount() == 0) {
316 return;
317 }
318 cursor.moveToFirst();
319 updateStatus(cursor.get());
320 }
321
322 @Override
323 public void onLoaderReset(Loader<Cursor> loader) {
324 // Do nothing.
325 }
326
327 private void startDownloadingAttachment(int destination) {
328 final ContentValues params = new ContentValues(2);
329 params.put(AttachmentColumns.STATE, AttachmentState.DOWNLOADING);
330 params.put(AttachmentColumns.DESTINATION, destination);
331
332 mCommandHandler.sendCommand(params);
333 }
334
335 private void cancelAttachment() {
336 final ContentValues params = new ContentValues(1);
337 params.put(AttachmentColumns.STATE, AttachmentState.NOT_SAVED);
338
339 mCommandHandler.sendCommand(params);
340 }
341
342 private void setButtonVisible(View button, boolean visible) {
343 button.setVisibility(visible ? VISIBLE : GONE);
344 }
345
346 /**
347 * Update all action buttons based on current downloading state.
348 */
349 private void updateActions() {
350 // To avoid visibility state transition bugs, every button's visibility should be touched
351 // once by this routine.
352
353 final boolean isDownloading = mAttachment.isDownloading();
354
355 setButtonVisible(mCancelButton, isDownloading);
356
357 final boolean canInstall = MimeType.isInstallable(mAttachment.mimeType);
358 setButtonVisible(mInstallButton, canInstall && !isDownloading);
359
360 if (!canInstall) {
361
362 final boolean canPreview = (mAttachment.previewIntent != null);
363 final boolean canView = MimeType.isViewable(getContext(), mAttachment.mimeType);
364 final boolean canPlay = MimeType.isPlayable(mAttachment.mimeType);
365
366 setButtonVisible(mPreviewButton, canPreview);
367 setButtonVisible(mPlayButton, canView && canPlay && !isDownloading);
368 setButtonVisible(mViewButton, canView && !canPlay && !isDownloading);
369 setButtonVisible(mSaveButton, canView && mAttachment.canSave() && !isDownloading);
370 setButtonVisible(mInfoButton, !(canPreview || canView));
371
372 } else {
373
374 setButtonVisible(mPreviewButton, false);
375 setButtonVisible(mPlayButton, false);
376 setButtonVisible(mViewButton, false);
377 setButtonVisible(mSaveButton, false);
378 setButtonVisible(mInfoButton, false);
379
380 }
381 }
382
383 /**
384 * View an attachment by an application on device.
385 */
386 private void sendViewIntent() {
387 Intent intent = new Intent(Intent.ACTION_VIEW);
388 intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION
389 | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
390 Utils.setIntentDataAndTypeAndNormalize(intent, Uri.parse(mAttachment.contentUri),
391 mAttachment.mimeType);
392 try {
393 getContext().startActivity(intent);
394 } catch (ActivityNotFoundException e) {
395 // couldn't find activity for View intent
396 LogUtils.e(LOG_TAG, "Coun't find Activity for intent", e);
397 }
398 }
399
400 /**
401 * Displays a loading dialog to be used for downloading attachments.
402 * Must be called on the UI thread.
403 */
404 private void showDownloadingDialog() {
405 mViewProgressDialog = new ProgressDialog(getContext());
406 mViewProgressDialog.setTitle(R.string.fetching_attachment);
407 mViewProgressDialog.setMessage(getResources().getString(R.string.please_wait));
408 mViewProgressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
409 mViewProgressDialog.setMax(mAttachment.size);
410 mViewProgressDialog.setOnDismissListener(this);
411 mViewProgressDialog.setOnCancelListener(this);
412 mViewProgressDialog.show();
413
414 // The progress number format needs to be set after the dialog is shown. See bug: 5149918
415 mViewProgressDialog.setProgressNumberFormat(null);
416 }
417
418 @Override
419 public void onDismiss(DialogInterface dialog) {
420 mViewProgressDialog = null;
421 }
422
423 @Override
424 public void onCancel(DialogInterface dialog) {
425 cancelAttachment();
426 }
427
428}