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