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