MessageViewFragment.java revision 3d9b8e76f0a396370a5c0be99a34bf7c24bd20dd
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.EmailContent.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        final Resources res = getActivity().getResources();
208        mFavoriteIconOn = res.getDrawable(R.drawable.btn_star_on_normal_email_holo_light);
209        mFavoriteIconOff = res.getDrawable(R.drawable.btn_star_off_normal_email_holo_light);
210    }
211
212    @Override
213    public View onCreateView(
214            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
215        final View view = super.onCreateView(inflater, container, savedInstanceState);
216
217        mFavoriteIcon = (ImageView) UiUtilities.getView(view, R.id.favorite);
218        mReplyButton = UiUtilities.getView(view, R.id.reply);
219        mReplyAllButton = UiUtilities.getView(view, R.id.reply_all);
220        mForwardButton = UiUtilities.getView(view, R.id.forward);
221        mMeetingYes = (CheckBox) UiUtilities.getView(view, R.id.accept);
222        mMeetingMaybe = (CheckBox) UiUtilities.getView(view, R.id.maybe);
223        mMeetingNo = (CheckBox) UiUtilities.getView(view, R.id.decline);
224
225        // Star is only visible on this fragment (as opposed to MessageFileViewFragment.)
226        UiUtilities.getView(view, R.id.favorite).setVisibility(View.VISIBLE);
227
228        mFavoriteIcon.setOnClickListener(this);
229        mReplyButton.setOnClickListener(this);
230        mReplyAllButton.setOnClickListener(this);
231        mForwardButton.setOnClickListener(this);
232        mMeetingYes.setOnCheckedChangeListener(this);
233        mMeetingMaybe.setOnCheckedChangeListener(this);
234        mMeetingNo.setOnCheckedChangeListener(this);
235        UiUtilities.getView(view, R.id.invite_link).setOnClickListener(this);
236
237        enableReplyForwardButtons(false);
238
239        return view;
240    }
241
242    @Override
243    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
244        inflater.inflate(R.menu.message_view_fragment_option, menu);
245    }
246
247    private void enableReplyForwardButtons(boolean enabled) {
248        // We don't have disabled button assets, so let's hide them for now
249        final int visibility = enabled ? View.VISIBLE : View.GONE;
250        mReplyButton.setVisibility(visibility);
251        mReplyAllButton.setVisibility(visibility);
252        mForwardButton.setVisibility(visibility);
253    }
254
255    public void setCallback(Callback callback) {
256        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
257        super.setCallback(mCallback);
258    }
259
260    @Override
261    protected void resetView() {
262        super.resetView();
263        mMeetingYes.setChecked(false);
264        mMeetingNo.setChecked(false);
265        mMeetingMaybe.setChecked(false);
266        mPreviousMeetingResponse = EmailServiceConstants.MEETING_REQUEST_NOT_RESPONDED;
267    }
268
269    /**
270     * NOTE See the comment on the super method.  It's called on a worker thread.
271     */
272    @Override
273    protected Message openMessageSync(Activity activity) {
274        return Message.restoreMessageWithId(activity, getMessageId());
275    }
276
277    @Override
278    protected void onMessageShown(long messageId, int mailboxType) {
279        super.onMessageShown(messageId, mailboxType);
280
281        // Disable forward/reply buttons as necessary.
282        enableReplyForwardButtons(Mailbox.isMailboxTypeReplyAndForwardable(mailboxType));
283    }
284
285    /**
286     * Toggle favorite status and write back to provider
287     */
288    private void onClickFavorite() {
289        if (!isMessageOpen()) return;
290        Message message = getMessage();
291
292        // Update UI
293        boolean newFavorite = ! message.mFlagFavorite;
294        mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
295
296        // Update provider
297        message.mFlagFavorite = newFavorite;
298        getController().setMessageFavorite(message.mId, newFavorite);
299    }
300
301    /**
302     * Set message read/unread.
303     */
304    public void onMarkMessageAsRead(boolean isRead) {
305        if (!isMessageOpen()) return;
306        Message message = getMessage();
307        if (message.mFlagRead != isRead) {
308            message.mFlagRead = isRead;
309            getController().setMessageRead(message.mId, isRead);
310            if (!isRead) { // Became unread.  We need to close the message.
311                mCallback.onMessageSetUnread();
312            }
313        }
314    }
315
316    /**
317     * Send a service message indicating that a meeting invite button has been clicked.
318     */
319    private void onRespondToInvite(int response, int toastResId) {
320        if (!isMessageOpen()) return;
321        Message message = getMessage();
322        // do not send twice in a row the same response
323        if (mPreviousMeetingResponse != response) {
324            getController().sendMeetingResponse(message.mId, response);
325            mPreviousMeetingResponse = response;
326        }
327        Utility.showToast(getActivity(), toastResId);
328        mCallback.onRespondedToInvite(response);
329    }
330
331    private void onInviteLinkClicked() {
332        if (!isMessageOpen()) return;
333        Message message = getMessage();
334        String startTime = new PackedString(message.mMeetingInfo).get(MeetingInfo.MEETING_DTSTART);
335        if (startTime != null) {
336            long epochTimeMillis = Utility.parseEmailDateTimeToMillis(startTime);
337            mCallback.onCalendarLinkClicked(epochTimeMillis);
338        } else {
339            Email.log("meetingInfo without DTSTART " + message.mMeetingInfo);
340        }
341    }
342
343    @Override
344    public void onClick(View view) {
345        if (!isMessageOpen()) {
346            return; // Ignore.
347        }
348        switch (view.getId()) {
349            case R.id.reply:
350                mCallback.onReply();
351                return;
352            case R.id.reply_all:
353                mCallback.onReplyAll();
354                return;
355            case R.id.forward:
356                mCallback.onForward();
357                return;
358
359            case R.id.favorite:
360                onClickFavorite();
361                return;
362
363            case R.id.invite_link:
364                onInviteLinkClicked();
365                return;
366        }
367        super.onClick(view);
368    }
369
370    @Override
371    public void onCheckedChanged(CompoundButton view, boolean isChecked) {
372        if (!isChecked) return;
373        switch (view.getId()) {
374            case R.id.accept:
375                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_ACCEPTED,
376                        R.string.message_view_invite_toast_yes);
377                return;
378            case R.id.maybe:
379                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_TENTATIVE,
380                        R.string.message_view_invite_toast_maybe);
381                return;
382            case R.id.decline:
383                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_DECLINED,
384                        R.string.message_view_invite_toast_no);
385                return;
386        }
387    }
388
389    @Override
390    public boolean onOptionsItemSelected(MenuItem item) {
391        switch (item.getItemId()) {
392            case R.id.move:
393                onMove();
394                return true;
395            case R.id.delete:
396                onDelete();
397                return true;
398            case R.id.mark_as_unread:
399                onMarkAsUnread();
400                return true;
401        }
402        return super.onOptionsItemSelected(item);
403    }
404
405    private void onMove() {
406        MoveMessageToDialog dialog = MoveMessageToDialog.newInstance(new long[] {getMessageId()},
407                this);
408        dialog.show(getFragmentManager(), "dialog");
409    }
410
411    // MoveMessageToDialog$Callback
412    @Override
413    public void onMoveToMailboxSelected(long newMailboxId, long[] messageIds) {
414        mCallback.onBeforeMessageGone();
415        ActivityHelper.moveMessages(mContext, newMailboxId, messageIds);
416    }
417
418    private void onDelete() {
419        mCallback.onBeforeMessageGone();
420        ActivityHelper.deleteMessage(mContext, getMessageId());
421    }
422
423    private void onMarkAsUnread() {
424        onMarkMessageAsRead(false);
425    }
426
427    /**
428     * {@inheritDoc}
429     *
430     * Mark the current as unread.
431     */
432    @Override
433    protected void onPostLoadBody() {
434        onMarkMessageAsRead(true);
435    }
436
437    @Override
438    protected void updateHeaderView(Message message) {
439        super.updateHeaderView(message);
440
441        mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff);
442
443        // Enable the invite tab if necessary
444        if ((message.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
445            addTabFlags(TAB_FLAGS_HAS_INVITE);
446        }
447    }
448}
449