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
18package com.android.mail.browse;
19
20import android.app.FragmentManager;
21import android.app.LoaderManager;
22import android.content.Context;
23import android.content.Intent;
24import android.content.Loader;
25import android.database.Cursor;
26import android.net.Uri;
27import android.os.Bundle;
28import android.support.v4.text.BidiFormatter;
29import android.text.TextUtils;
30import android.util.AttributeSet;
31import android.view.LayoutInflater;
32import android.view.View;
33import android.widget.LinearLayout;
34
35import com.android.mail.R;
36import com.android.mail.analytics.Analytics;
37import com.android.mail.browse.AttachmentLoader.AttachmentCursor;
38import com.android.mail.browse.ConversationContainer.DetachListener;
39import com.android.mail.browse.ConversationViewAdapter.MessageHeaderItem;
40import com.android.mail.providers.Account;
41import com.android.mail.providers.Attachment;
42import com.android.mail.providers.Message;
43import com.android.mail.ui.AccountFeedbackActivity;
44import com.android.mail.ui.AttachmentTile;
45import com.android.mail.ui.AttachmentTileGrid;
46import com.android.mail.utils.LogTag;
47import com.android.mail.utils.LogUtils;
48import com.google.common.base.Objects;
49import com.google.common.collect.Lists;
50
51import java.util.ArrayList;
52import java.util.List;
53
54public 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         */
85        boolean isSecure();
86    }
87
88    public MessageFooterView(Context context) {
89        this(context, null);
90    }
91
92    public MessageFooterView(Context context, AttributeSet attrs) {
93        super(context, attrs);
94
95        mInflater = LayoutInflater.from(context);
96    }
97
98    @Override
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
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
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
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
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
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
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
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
253    public void onDetachedFromParent() {
254        // Do nothing.
255    }
256
257    @Override
258    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
259        return new AttachmentLoader(getContext(),
260                mMessageHeaderItem.getMessage().attachmentListUri);
261    }
262
263    @Override
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
275    public void onLoaderReset(Loader<Cursor> loader) {
276        mAttachmentsCursor = null;
277    }
278
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
293    public void onClick(View v) {
294        viewEntireMessage();
295    }
296
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
320    private Account getAccount() {
321        return mAccountController != null ? mAccountController.getAccount() : null;
322    }
323}
324