1 /*
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 
18 package com.android.mail.browse;
19 
20 import android.app.FragmentManager;
21 import android.app.LoaderManager;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.Loader;
25 import android.database.Cursor;
26 import android.net.Uri;
27 import android.os.Bundle;
28 import android.support.v4.text.BidiFormatter;
29 import android.text.TextUtils;
30 import android.util.AttributeSet;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.widget.LinearLayout;
34 
35 import com.android.mail.R;
36 import com.android.mail.analytics.Analytics;
37 import com.android.mail.browse.AttachmentLoader.AttachmentCursor;
38 import com.android.mail.browse.ConversationContainer.DetachListener;
39 import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
40 import com.android.mail.providers.Account;
41 import com.android.mail.providers.Attachment;
42 import com.android.mail.providers.Message;
43 import com.android.mail.ui.AccountFeedbackActivity;
44 import com.android.mail.ui.AttachmentTile;
45 import com.android.mail.ui.AttachmentTileGrid;
46 import com.android.mail.utils.LogTag;
47 import com.android.mail.utils.LogUtils;
48 import com.google.common.base.Objects;
49 import com.google.common.collect.Lists;
50 
51 import java.util.ArrayList;
52 import java.util.List;
53 
54 public class MessageFooterView extends LinearLayout implements DetachListener,
55         LoaderManager.LoaderCallbacks<Cursor>, View.OnClickListener {
56 
57     private MessageHeaderItem mMessageHeaderItem;
58     private LoaderManager mLoaderManager;
59     private FragmentManager mFragmentManager;
60     private AttachmentCursor mAttachmentsCursor;
61     private View mViewEntireMessagePrompt;
62     private AttachmentTileGrid mAttachmentGrid;
63     private LinearLayout mAttachmentBarList;
64 
65     private final LayoutInflater mInflater;
66 
67     private static final String LOG_TAG = LogTag.getLogTag();
68 
69     private ConversationAccountController mAccountController;
70 
71     private BidiFormatter mBidiFormatter;
72 
73     private MessageFooterCallbacks mCallbacks;
74 
75     private Integer mOldAttachmentLoaderId;
76 
77     /**
78      * Callbacks for the MessageFooterView to enable resizing the height.
79      */
80     public interface MessageFooterCallbacks {
81         /**
82          * @return <tt>true</tt> if this footer is contained within a SecureConversationViewFragment
83          * and cannot assume the content is <strong>not</strong> malicious
84          */
isSecure()85         boolean isSecure();
86     }
87 
MessageFooterView(Context context)88     public MessageFooterView(Context context) {
89         this(context, null);
90     }
91 
MessageFooterView(Context context, AttributeSet attrs)92     public MessageFooterView(Context context, AttributeSet attrs) {
93         super(context, attrs);
94 
95         mInflater = LayoutInflater.from(context);
96     }
97 
98     @Override
onFinishInflate()99     protected void onFinishInflate() {
100         super.onFinishInflate();
101 
102         mViewEntireMessagePrompt = findViewById(R.id.view_entire_message_prompt);
103         mAttachmentGrid = (AttachmentTileGrid) findViewById(R.id.attachment_tile_grid);
104         mAttachmentBarList = (LinearLayout) findViewById(R.id.attachment_bar_list);
105 
106         mViewEntireMessagePrompt.setOnClickListener(this);
107     }
108 
initialize(LoaderManager loaderManager, FragmentManager fragmentManager, ConversationAccountController accountController, MessageFooterCallbacks callbacks)109     public void initialize(LoaderManager loaderManager, FragmentManager fragmentManager,
110             ConversationAccountController accountController, MessageFooterCallbacks callbacks) {
111         mLoaderManager = loaderManager;
112         mFragmentManager = fragmentManager;
113         mAccountController = accountController;
114         mCallbacks = callbacks;
115     }
116 
bind( MessageHeaderItem headerItem, boolean measureOnly)117     public void bind(
118             MessageHeaderItem headerItem, boolean measureOnly) {
119         mMessageHeaderItem = headerItem;
120 
121         final Integer attachmentLoaderId = getAttachmentLoaderId();
122 
123         // Destroy the loader if we are attempting to load a different attachment
124         if (mOldAttachmentLoaderId != null &&
125                 !Objects.equal(mOldAttachmentLoaderId, attachmentLoaderId)) {
126             mLoaderManager.destroyLoader(mOldAttachmentLoaderId);
127 
128             // Resets the footer view. This step is only done if the
129             // attachmentsListUri changes so that we don't
130             // repeat the work of layout and measure when
131             // we're only updating the attachments.
132             mAttachmentGrid.removeAllViewsInLayout();
133             mAttachmentBarList.removeAllViewsInLayout();
134             mViewEntireMessagePrompt.setVisibility(View.GONE);
135             mAttachmentGrid.setVisibility(View.GONE);
136             mAttachmentBarList.setVisibility(View.GONE);
137         }
138         mOldAttachmentLoaderId = attachmentLoaderId;
139 
140         // kick off load of Attachment objects in background thread
141         // but don't do any Loader work if we're only measuring
142         if (!measureOnly && attachmentLoaderId != null) {
143             LogUtils.i(LOG_TAG, "binding footer view, calling initLoader for message %d",
144                     attachmentLoaderId);
145             mLoaderManager.initLoader(attachmentLoaderId, Bundle.EMPTY, this);
146         }
147 
148         // Do an initial render if initLoader didn't already do one
149         if (mAttachmentGrid.getChildCount() == 0 &&
150                 mAttachmentBarList.getChildCount() == 0) {
151             renderAttachments(false);
152         }
153 
154         final ConversationMessage message = mMessageHeaderItem.getMessage();
155         mViewEntireMessagePrompt.setVisibility(
156                 message.clipped && !TextUtils.isEmpty(message.permalink) ? VISIBLE : GONE);
157         setVisibility(mMessageHeaderItem.isExpanded() ? VISIBLE : GONE);
158     }
159 
renderAttachments(boolean loaderResult)160     private void renderAttachments(boolean loaderResult) {
161         final List<Attachment> attachments;
162         if (mAttachmentsCursor != null && !mAttachmentsCursor.isClosed()) {
163             int i = -1;
164             attachments = Lists.newArrayList();
165             while (mAttachmentsCursor.moveToPosition(++i)) {
166                 attachments.add(mAttachmentsCursor.get());
167             }
168         } else {
169             // before the attachment loader results are in, we can still render immediately using
170             // the basic info in the message's attachmentsJSON
171             attachments = mMessageHeaderItem.getMessage().getAttachments();
172         }
173         renderAttachments(attachments, loaderResult);
174     }
175 
renderAttachments(List<Attachment> attachments, boolean loaderResult)176     private void renderAttachments(List<Attachment> attachments, boolean loaderResult) {
177         if (attachments == null || attachments.isEmpty()) {
178             return;
179         }
180 
181         // filter the attachments into tiled and non-tiled
182         final int maxSize = attachments.size();
183         final List<Attachment> tiledAttachments = new ArrayList<Attachment>(maxSize);
184         final List<Attachment> barAttachments = new ArrayList<Attachment>(maxSize);
185 
186         for (Attachment attachment : attachments) {
187             // attachments in secure views are displayed in the footer so the user may interact with
188             // them; for normal views there is no need to show inline attachments in the footer
189             // since users can interact with them in place
190             if (!attachment.isInlineAttachment() || mCallbacks.isSecure()) {
191                 if (AttachmentTile.isTiledAttachment(attachment)) {
192                     tiledAttachments.add(attachment);
193                 } else {
194                     barAttachments.add(attachment);
195                 }
196             }
197         }
198 
199         mMessageHeaderItem.getMessage().attachmentsJson = Attachment.toJSONArray(attachments);
200 
201         // All attachments are inline, don't display anything.
202         if (tiledAttachments.isEmpty() && barAttachments.isEmpty()) {
203             return;
204         }
205 
206         if (!tiledAttachments.isEmpty()) {
207             renderTiledAttachments(tiledAttachments, loaderResult);
208         }
209         if (!barAttachments.isEmpty()) {
210             renderBarAttachments(barAttachments, loaderResult);
211         }
212     }
213 
renderTiledAttachments(List<Attachment> tiledAttachments, boolean loaderResult)214     private void renderTiledAttachments(List<Attachment> tiledAttachments, boolean loaderResult) {
215         mAttachmentGrid.setVisibility(View.VISIBLE);
216 
217         // Setup the tiles.
218         mAttachmentGrid.configureGrid(mFragmentManager, getAccount(),
219                 mMessageHeaderItem.getMessage(), tiledAttachments, loaderResult);
220     }
221 
renderBarAttachments(List<Attachment> barAttachments, boolean loaderResult)222     private void renderBarAttachments(List<Attachment> barAttachments, boolean loaderResult) {
223         mAttachmentBarList.setVisibility(View.VISIBLE);
224 
225         final Account account = getAccount();
226         for (Attachment attachment : barAttachments) {
227             final Uri id = attachment.getIdentifierUri();
228             MessageAttachmentBar barAttachmentView =
229                     (MessageAttachmentBar) mAttachmentBarList.findViewWithTag(id);
230 
231             if (barAttachmentView == null) {
232                 barAttachmentView = MessageAttachmentBar.inflate(mInflater, this);
233                 barAttachmentView.setTag(id);
234                 barAttachmentView.initialize(mFragmentManager);
235                 mAttachmentBarList.addView(barAttachmentView);
236             }
237 
238             barAttachmentView.render(attachment, account, mMessageHeaderItem.getMessage(),
239                     loaderResult, getBidiFormatter());
240         }
241     }
242 
getAttachmentLoaderId()243     private Integer getAttachmentLoaderId() {
244         Integer id = null;
245         final Message msg = mMessageHeaderItem == null ? null : mMessageHeaderItem.getMessage();
246         if (msg != null && msg.hasAttachments && msg.attachmentListUri != null) {
247             id = msg.attachmentListUri.hashCode();
248         }
249         return id;
250     }
251 
252     @Override
onDetachedFromParent()253     public void onDetachedFromParent() {
254         // Do nothing.
255     }
256 
257     @Override
onCreateLoader(int id, Bundle args)258     public Loader<Cursor> onCreateLoader(int id, Bundle args) {
259         return new AttachmentLoader(getContext(),
260                 mMessageHeaderItem.getMessage().attachmentListUri);
261     }
262 
263     @Override
onLoadFinished(Loader<Cursor> loader, Cursor data)264     public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
265         mAttachmentsCursor = (AttachmentCursor) data;
266 
267         if (mAttachmentsCursor == null || mAttachmentsCursor.isClosed()) {
268             return;
269         }
270 
271         renderAttachments(true);
272     }
273 
274     @Override
onLoaderReset(Loader<Cursor> loader)275     public void onLoaderReset(Loader<Cursor> loader) {
276         mAttachmentsCursor = null;
277     }
278 
getBidiFormatter()279     private BidiFormatter getBidiFormatter() {
280         if (mBidiFormatter == null) {
281             final ConversationViewAdapter adapter = mMessageHeaderItem != null
282                     ? mMessageHeaderItem.getAdapter() : null;
283             if (adapter == null) {
284                 mBidiFormatter = BidiFormatter.getInstance();
285             } else {
286                 mBidiFormatter = adapter.getBidiFormatter();
287             }
288         }
289         return mBidiFormatter;
290     }
291 
292     @Override
onClick(View v)293     public void onClick(View v) {
294         viewEntireMessage();
295     }
296 
viewEntireMessage()297     private void viewEntireMessage() {
298         Analytics.getInstance().sendEvent("view_entire_message", "clicked", null, 0);
299 
300         final Context context = getContext();
301         final Intent intent = new Intent();
302         final String activityName =
303                 context.getResources().getString(R.string.full_message_activity);
304         if (TextUtils.isEmpty(activityName)) {
305             LogUtils.wtf(LOG_TAG, "Trying to open clipped message with no activity defined");
306             return;
307         }
308         intent.setClassName(context, activityName);
309         final Account account = getAccount();
310         final ConversationMessage message = mMessageHeaderItem.getMessage();
311         if (account != null && !TextUtils.isEmpty(message.permalink)) {
312             intent.putExtra(AccountFeedbackActivity.EXTRA_ACCOUNT_URI, account.uri);
313             intent.putExtra(FullMessageContract.EXTRA_PERMALINK, message.permalink);
314             intent.putExtra(FullMessageContract.EXTRA_ACCOUNT_NAME, account.getEmailAddress());
315             intent.putExtra(FullMessageContract.EXTRA_SERVER_MESSAGE_ID, message.serverId);
316             context.startActivity(intent);
317         }
318     }
319 
getAccount()320     private Account getAccount() {
321         return mAccountController != null ? mAccountController.getAccount() : null;
322     }
323 }
324