MessageViewFragment.java revision f5418f1f93b02e7fab9f15eb201800b65510998e
1/*
2 * Copyright (C) 2010 The Android Open Source Project
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
17package com.android.email.activity;
18
19import com.android.email.Email;
20import com.android.email.R;
21import com.android.emailcommon.mail.MeetingInfo;
22import com.android.emailcommon.mail.PackedString;
23import com.android.emailcommon.provider.Account;
24import com.android.emailcommon.provider.EmailContent.Message;
25import com.android.emailcommon.provider.Mailbox;
26import com.android.emailcommon.service.EmailServiceConstants;
27import com.android.emailcommon.utility.Utility;
28
29import android.app.Activity;
30import android.content.res.Resources;
31import android.graphics.drawable.Drawable;
32import android.os.Bundle;
33import android.view.LayoutInflater;
34import android.view.Menu;
35import android.view.MenuInflater;
36import android.view.MenuItem;
37import android.view.View;
38import android.view.ViewGroup;
39import android.widget.CheckBox;
40import android.widget.CompoundButton;
41import android.widget.ImageView;
42
43/**
44 * A {@link MessageViewFragmentBase} subclass for regular email messages.  (regular as in "not eml
45 * files").
46 */
47public class MessageViewFragment extends MessageViewFragmentBase
48        implements CheckBox.OnCheckedChangeListener, MoveMessageToDialog.Callback {
49    /** Argument name(s) */
50    private static final String ARG_OPENER_ACCOUNT_ID = "accountId";
51    private static final String ARG_OPENER_MAILBOX_ID = "mailboxId";
52    private static final String ARG_MESSAGE_ID = "messageId";
53
54    private ImageView mFavoriteIcon;
55
56    private View mReplyButton;
57    private View mReplyAllButton;
58    private View mForwardButton;
59
60    // calendar meeting invite answers
61    private CheckBox mMeetingYes;
62    private CheckBox mMeetingMaybe;
63    private CheckBox mMeetingNo;
64    private Drawable mFavoriteIconOn;
65    private Drawable mFavoriteIconOff;
66
67    private int mPreviousMeetingResponse = EmailServiceConstants.MEETING_REQUEST_NOT_RESPONDED;
68
69    /**
70     * This class has more call backs than {@link MessageViewFragmentBase}.
71     *
72     * - EML files can't be "mark unread".
73     * - EML files can't have the invite buttons or the view in calender link.
74     *   Note EML files can have ICS (calendar invitation) files, but we don't treat them as
75     *   invites.  (Only exchange provider sets the FLAG_INCOMING_MEETING_INVITE
76     *   flag.)
77     *   It'd be weird to respond to an invitation in an EML that might not be addressed to you...
78     */
79    public interface Callback extends MessageViewFragmentBase.Callback {
80        /** Called when the "view in calendar" link is clicked. */
81        public void onCalendarLinkClicked(long epochEventStartTime);
82
83        /**
84         * Called when a calender response button is clicked.
85         *
86         * @param response one of {@link EmailServiceConstants#MEETING_REQUEST_ACCEPTED},
87         * {@link EmailServiceConstants#MEETING_REQUEST_DECLINED}, or
88         * {@link EmailServiceConstants#MEETING_REQUEST_TENTATIVE}.
89         */
90        public void onRespondedToInvite(int response);
91
92        /** Called when the current message is set unread. */
93        public void onMessageSetUnread();
94
95        /**
96         * Called right before the current message will be deleted or moved to another mailbox.
97         *
98         * Callees will usually close the fragment.
99         */
100        public void onBeforeMessageGone();
101
102        /** Called when the forward button is pressed. */
103        public void onForward();
104        /** Called when the reply button is pressed. */
105        public void onReply();
106        /** Called when the reply-all button is pressed. */
107        public void onReplyAll();
108    }
109
110    public static final class EmptyCallback extends MessageViewFragmentBase.EmptyCallback
111            implements Callback {
112        @SuppressWarnings("hiding")
113        public static final Callback INSTANCE = new EmptyCallback();
114
115        @Override public void onCalendarLinkClicked(long epochEventStartTime) { }
116        @Override public void onMessageSetUnread() { }
117        @Override public void onRespondedToInvite(int response) { }
118        @Override public void onBeforeMessageGone() { }
119        @Override public void onForward() { }
120        @Override public void onReply() { }
121        @Override public void onReplyAll() { }
122    }
123
124    private Callback mCallback = EmptyCallback.INSTANCE;
125
126    /**
127     * Create a new instance with initialization parameters.
128     *
129     * This fragment should be created only with this method.  (Arguments should always be set.)
130     *
131     * @param openerAccountId account ID that's used in the UI that opened this fragment.
132     *        The primary use is for the back navigation to determine which mailbox to show.
133     *
134     *        Note this is not necessarily the same ID as the actual account ID for the message.
135     *        If a message is opened on the combined view, the caller probably want to pass
136     *        {@link Account#ACCOUNT_ID_COMBINED_VIEW} so that back will navigate to the
137     *        combined view.
138     *
139     * @param openerMailboxId mailbox ID that's used in the UI that opened this fragment.
140     *        The primary use is for the back navigation to determine which mailbox to show.
141     *
142     *        Note this is not necessarily the same ID as the actual mailbox ID for the message.
143     *        If a message is opened on the combined view, the caller probably want to pass
144     *        a combined mailbox ID so that back will navigate to it.
145     *
146     * @param messageId ID of the message to open
147     */
148    public static MessageViewFragment newInstance(long openerAccountId, long openerMailboxId,
149            long messageId) {
150        if (messageId == Message.NO_MESSAGE) {
151            throw new IllegalArgumentException();
152        }
153        final MessageViewFragment instance = new MessageViewFragment();
154        final Bundle args = new Bundle();
155        args.putLong(ARG_OPENER_ACCOUNT_ID, openerAccountId);
156        args.putLong(ARG_OPENER_MAILBOX_ID, openerMailboxId);
157        args.putLong(ARG_MESSAGE_ID, messageId);
158        instance.setArguments(args);
159        return instance;
160    }
161
162    /**
163     * We will display the message for this ID. This must never be a special message ID such as
164     * {@link Message#NO_MESSAGE}. Do NOT use directly; instead, use {@link #getMessageId()}.
165     * <p><em>NOTE:</em> Although we cannot force these to be immutable using Java language
166     * constructs, this <em>must</em> be considered immutable.
167     */
168    private Long mImmutableMessageId;
169    private Long mImmutableOpenerAccountId;
170    private Long mImmutableOpenerMailboxId;
171
172    private void initializeArgCache() {
173        if (mImmutableMessageId != null) return;
174        mImmutableMessageId = getArguments().getLong(ARG_MESSAGE_ID);
175        mImmutableOpenerAccountId = getArguments().getLong(ARG_OPENER_ACCOUNT_ID);
176        mImmutableOpenerMailboxId = getArguments().getLong(ARG_OPENER_MAILBOX_ID);
177    }
178
179    /**
180     * @return the message ID passed to {@link #newInstance}.  Safe to call even before onCreate.
181     */
182    public long getMessageId() {
183        initializeArgCache();
184        return mImmutableMessageId;
185    }
186
187    /**
188     * @return the account ID passed to {@link #newInstance}.  Safe to call even before onCreate.
189     */
190    public long getOpenerAccountId() {
191        initializeArgCache();
192        return mImmutableOpenerAccountId;
193    }
194
195    /**
196     * @return the mailbox ID passed to {@link #newInstance}.  Safe to call even before onCreate.
197     */
198    public long getOpenerMailboxId() {
199        initializeArgCache();
200        return mImmutableOpenerMailboxId;
201    }
202
203    @Override
204    public void onCreate(Bundle savedInstanceState) {
205        super.onCreate(savedInstanceState);
206
207        setHasOptionsMenu(true);
208
209        final Resources res = getActivity().getResources();
210        mFavoriteIconOn = res.getDrawable(R.drawable.btn_star_on_normal_email_holo_light);
211        mFavoriteIconOff = res.getDrawable(R.drawable.btn_star_off_normal_email_holo_light);
212    }
213
214    @Override
215    public View onCreateView(
216            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
217        final View view = super.onCreateView(inflater, container, savedInstanceState);
218
219        mFavoriteIcon = (ImageView) UiUtilities.getView(view, R.id.favorite);
220        mReplyButton = UiUtilities.getView(view, R.id.reply);
221        mReplyAllButton = UiUtilities.getView(view, R.id.reply_all);
222        mForwardButton = UiUtilities.getView(view, R.id.forward);
223        mMeetingYes = (CheckBox) UiUtilities.getView(view, R.id.accept);
224        mMeetingMaybe = (CheckBox) UiUtilities.getView(view, R.id.maybe);
225        mMeetingNo = (CheckBox) UiUtilities.getView(view, R.id.decline);
226
227        // Star is only visible on this fragment (as opposed to MessageFileViewFragment.)
228        UiUtilities.getView(view, R.id.favorite).setVisibility(View.VISIBLE);
229
230        mFavoriteIcon.setOnClickListener(this);
231        mReplyButton.setOnClickListener(this);
232        mReplyAllButton.setOnClickListener(this);
233        mForwardButton.setOnClickListener(this);
234        mMeetingYes.setOnCheckedChangeListener(this);
235        mMeetingMaybe.setOnCheckedChangeListener(this);
236        mMeetingNo.setOnCheckedChangeListener(this);
237        UiUtilities.getView(view, R.id.invite_link).setOnClickListener(this);
238
239        enableReplyForwardButtons(false);
240
241        return view;
242    }
243
244    @Override
245    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
246        inflater.inflate(R.menu.message_view_fragment_option, menu);
247    }
248
249    private void enableReplyForwardButtons(boolean enabled) {
250        // We don't have disabled button assets, so let's hide them for now
251        final int visibility = enabled ? View.VISIBLE : View.GONE;
252        mReplyButton.setVisibility(visibility);
253        mReplyAllButton.setVisibility(visibility);
254        mForwardButton.setVisibility(visibility);
255    }
256
257    public void setCallback(Callback callback) {
258        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
259        super.setCallback(mCallback);
260    }
261
262    @Override
263    protected void resetView() {
264        super.resetView();
265        mMeetingYes.setChecked(false);
266        mMeetingNo.setChecked(false);
267        mMeetingMaybe.setChecked(false);
268        mPreviousMeetingResponse = EmailServiceConstants.MEETING_REQUEST_NOT_RESPONDED;
269    }
270
271    /**
272     * NOTE See the comment on the super method.  It's called on a worker thread.
273     */
274    @Override
275    protected Message openMessageSync(Activity activity) {
276        return Message.restoreMessageWithId(activity, getMessageId());
277    }
278
279    @Override
280    protected void onMessageShown(long messageId, int mailboxType) {
281        super.onMessageShown(messageId, mailboxType);
282
283        // Disable forward/reply buttons as necessary.
284        enableReplyForwardButtons(Mailbox.isMailboxTypeReplyAndForwardable(mailboxType));
285    }
286
287    /**
288     * Toggle favorite status and write back to provider
289     */
290    private void onClickFavorite() {
291        if (!isMessageOpen()) return;
292        Message message = getMessage();
293
294        // Update UI
295        boolean newFavorite = ! message.mFlagFavorite;
296        mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
297
298        // Update provider
299        message.mFlagFavorite = newFavorite;
300        getController().setMessageFavorite(message.mId, newFavorite);
301    }
302
303    /**
304     * Set message read/unread.
305     */
306    public void onMarkMessageAsRead(boolean isRead) {
307        if (!isMessageOpen()) return;
308        Message message = getMessage();
309        if (message.mFlagRead != isRead) {
310            message.mFlagRead = isRead;
311            getController().setMessageRead(message.mId, isRead);
312            if (!isRead) { // Became unread.  We need to close the message.
313                mCallback.onMessageSetUnread();
314            }
315        }
316    }
317
318    /**
319     * Send a service message indicating that a meeting invite button has been clicked.
320     */
321    private void onRespondToInvite(int response, int toastResId) {
322        if (!isMessageOpen()) return;
323        Message message = getMessage();
324        // do not send twice in a row the same response
325        if (mPreviousMeetingResponse != response) {
326            getController().sendMeetingResponse(message.mId, response);
327            mPreviousMeetingResponse = response;
328        }
329        Utility.showToast(getActivity(), toastResId);
330        mCallback.onRespondedToInvite(response);
331    }
332
333    private void onInviteLinkClicked() {
334        if (!isMessageOpen()) return;
335        Message message = getMessage();
336        String startTime = new PackedString(message.mMeetingInfo).get(MeetingInfo.MEETING_DTSTART);
337        if (startTime != null) {
338            long epochTimeMillis = Utility.parseEmailDateTimeToMillis(startTime);
339            mCallback.onCalendarLinkClicked(epochTimeMillis);
340        } else {
341            Email.log("meetingInfo without DTSTART " + message.mMeetingInfo);
342        }
343    }
344
345    @Override
346    public void onClick(View view) {
347        if (!isMessageOpen()) {
348            return; // Ignore.
349        }
350        switch (view.getId()) {
351            case R.id.reply:
352                mCallback.onReply();
353                return;
354            case R.id.reply_all:
355                mCallback.onReplyAll();
356                return;
357            case R.id.forward:
358                mCallback.onForward();
359                return;
360
361            case R.id.favorite:
362                onClickFavorite();
363                return;
364
365            case R.id.invite_link:
366                onInviteLinkClicked();
367                return;
368        }
369        super.onClick(view);
370    }
371
372    @Override
373    public void onCheckedChanged(CompoundButton view, boolean isChecked) {
374        if (!isChecked) return;
375        switch (view.getId()) {
376            case R.id.accept:
377                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_ACCEPTED,
378                        R.string.message_view_invite_toast_yes);
379                return;
380            case R.id.maybe:
381                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_TENTATIVE,
382                        R.string.message_view_invite_toast_maybe);
383                return;
384            case R.id.decline:
385                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_DECLINED,
386                        R.string.message_view_invite_toast_no);
387                return;
388        }
389    }
390
391    @Override
392    public boolean onOptionsItemSelected(MenuItem item) {
393        switch (item.getItemId()) {
394            case R.id.move:
395                onMove();
396                return true;
397            case R.id.delete:
398                onDelete();
399                return true;
400            case R.id.mark_as_unread:
401                onMarkAsUnread();
402                return true;
403        }
404        return super.onOptionsItemSelected(item);
405    }
406
407    private void onMove() {
408        MoveMessageToDialog dialog = MoveMessageToDialog.newInstance(new long[] {getMessageId()},
409                this);
410        dialog.show(getFragmentManager(), "dialog");
411    }
412
413    // MoveMessageToDialog$Callback
414    @Override
415    public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds) {
416        mCallback.onBeforeMessageGone();
417        ActivityHelper.moveMessages(mContext, newMailboxId, messageIds);
418    }
419
420    private void onDelete() {
421        mCallback.onBeforeMessageGone();
422        ActivityHelper.deleteMessage(mContext, getMessageId());
423    }
424
425    private void onMarkAsUnread() {
426        onMarkMessageAsRead(false);
427    }
428
429    /**
430     * {@inheritDoc}
431     *
432     * Mark the current as unread.
433     */
434    @Override
435    protected void onPostLoadBody() {
436        onMarkMessageAsRead(true);
437    }
438
439    @Override
440    protected void updateHeaderView(Message message) {
441        super.updateHeaderView(message);
442
443        mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff);
444
445        // Enable the invite tab if necessary
446        if ((message.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
447            addTabFlags(TAB_FLAGS_HAS_INVITE);
448        }
449    }
450}
451