MessageViewFragment.java revision a826d3fb03f29a07ea12e44237b2c02ea1926c74
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.email.Utility;
22import com.android.email.mail.MeetingInfo;
23import com.android.email.mail.PackedString;
24import com.android.email.provider.EmailContent.Mailbox;
25import com.android.email.provider.EmailContent.Message;
26import com.android.email.service.EmailServiceConstants;
27
28import android.app.Activity;
29import android.content.res.Resources;
30import android.graphics.drawable.Drawable;
31import android.os.Bundle;
32import android.util.Log;
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
43import java.security.InvalidParameterException;
44
45/**
46 * A {@link MessageViewFragmentBase} subclass for regular email messages.  (regular as in "not eml
47 * files").
48 *
49 * See {@link MessageViewBase} for the class relation diagram.
50 */
51public class MessageViewFragment extends MessageViewFragmentBase
52        implements CheckBox.OnCheckedChangeListener {
53    private ImageView mFavoriteIcon;
54    private View mInviteSection;
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 MessageCommandButtonView mCommandButtons;
65    private int mPreviousMeetingResponse = -1;
66
67    private Drawable mFavoriteIconOn;
68    private Drawable mFavoriteIconOff;
69
70    /**
71     * ID of the message that will be loaded.  Protect with {@link #mLock}.
72     */
73    private long mMessageIdToOpen = -1;
74
75    /** Lock object to protect {@link #mMessageIdToOpen} */
76    private final Object mLock = new Object();
77
78    /** ID of the currently shown message */
79    private long mCurrentMessageId = -1;
80
81    private final CommandButtonCallback mCommandButtonCallback = new CommandButtonCallback();
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        /** Called when "move to newer" button is pressed. */
110        public void onMoveToNewer();
111
112        /** Called when "move to older" button is pressed. */
113        public void onMoveToOlder();
114
115        /**
116         * Called right before the current message will be deleted.
117         * Callees don't have to delete messages.  The fragment does.
118         */
119        public void onBeforeMessageDelete();
120
121        /** Called when the move button is pressed. */
122        public void onMoveMessage();
123        /** Called when the forward button is pressed. */
124        public void onForward();
125        /** Called when the reply button is pressed. */
126        public void onReply();
127        /** Called when the reply-all button is pressed. */
128        public void onReplyAll();
129    }
130
131    public static final class EmptyCallback extends MessageViewFragmentBase.EmptyCallback
132            implements Callback {
133        public static final Callback INSTANCE = new EmptyCallback();
134
135        @Override public void onCalendarLinkClicked(long epochEventStartTime) { }
136        @Override public void onMessageSetUnread() { }
137        @Override public void onRespondedToInvite(int response) { }
138        @Override public void onMoveToNewer() { }
139        @Override public void onMoveToOlder() { }
140        @Override public void onBeforeMessageDelete() { }
141        @Override public void onMoveMessage() { }
142        @Override public void onForward() { }
143        @Override public void onReply() { }
144        @Override public void onReplyAll() { }
145    }
146
147    private Callback mCallback = EmptyCallback.INSTANCE;
148
149    @Override
150    public void onCreate(Bundle savedInstanceState) {
151        super.onCreate(savedInstanceState);
152
153        final Resources res = getActivity().getResources();
154        mFavoriteIconOn = res.getDrawable(R.drawable.btn_star_big_buttonless_on);
155        mFavoriteIconOff = res.getDrawable(R.drawable.ic_star_none_holo_light);
156    }
157
158    @Override
159    public View onCreateView(
160            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
161        final View view = super.onCreateView(inflater, container, savedInstanceState);
162
163        mFavoriteIcon = (ImageView) view.findViewById(R.id.favorite);
164        mInviteSection = view.findViewById(R.id.invite_section);
165        mReplyButton = view.findViewById(R.id.reply);
166        mReplyAllButton = view.findViewById(R.id.reply_all);
167        mForwardButton = view.findViewById(R.id.forward);
168        mMeetingYes = (CheckBox) view.findViewById(R.id.accept);
169        mMeetingMaybe = (CheckBox) view.findViewById(R.id.maybe);
170        mMeetingNo = (CheckBox) view.findViewById(R.id.decline);
171
172        // Star is only visible on this fragment (as opposed to MessageFileViewFragment.)
173        view.findViewById(R.id.favorite).setVisibility(View.VISIBLE);
174
175        mFavoriteIcon.setOnClickListener(this);
176        mReplyButton.setOnClickListener(this);
177        mReplyAllButton.setOnClickListener(this);
178        mForwardButton.setOnClickListener(this);
179        mMeetingYes.setOnCheckedChangeListener(this);
180        mMeetingMaybe.setOnCheckedChangeListener(this);
181        mMeetingNo.setOnCheckedChangeListener(this);
182        view.findViewById(R.id.invite_link).setOnClickListener(this);
183
184        // Show the command buttons at the bottom.
185        mCommandButtons =
186                (MessageCommandButtonView) view.findViewById(R.id.message_command_buttons);
187        mCommandButtons.setVisibility(View.VISIBLE);
188        mCommandButtons.setCallback(mCommandButtonCallback);
189
190        enableReplyForwardButtons(false);
191
192        return view;
193    }
194
195    @Override
196    public void onResume() {
197        super.onResume();
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    /** Called by activities to set an id of a message to open. */
219    public void openMessage(long messageId) {
220        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
221            Log.d(Email.LOG_TAG, "MessageViewFragment openMessage");
222        }
223        if (messageId == -1) {
224            throw new InvalidParameterException();
225        }
226        synchronized (mLock) {
227            mMessageIdToOpen = messageId;
228        }
229        loadMessageIfResumed();
230    }
231
232    @Override
233    public void clearContent() {
234        synchronized (mLock) {
235            super.clearContent();
236            mMessageIdToOpen = -1;
237
238            // Hide the menu.
239            // This isn't really necessary if we're really hiding the fragment.  However,
240            // for now, we're actually *not* hiding the fragment (just hiding the root view of it),
241            // so need to remove menus manually.
242            setHasOptionsMenu(false);
243        }
244    }
245
246    @Override
247    protected void resetView() {
248        super.resetView();
249        mMeetingYes.setChecked(false);
250        mMeetingNo.setChecked(false);
251        mMeetingMaybe.setChecked(false);
252    }
253
254    @Override
255    protected boolean isMessageSpecified() {
256        synchronized (mLock) {
257            return mMessageIdToOpen != -1;
258        }
259    }
260
261    /**
262     * NOTE See the comment on the super method.  It's called on a worker thread.
263     */
264    @Override
265    protected Message openMessageSync(Activity activity) {
266        synchronized (mLock) {
267            long messageId = mMessageIdToOpen;
268            if (messageId < 0) {
269                return null; // Called after clearContent().
270            }
271            return Message.restoreMessageWithId(activity, messageId);
272        }
273    }
274
275    @Override
276    protected void onMessageShown(long messageId, int mailboxType) {
277        super.onMessageShown(messageId, mailboxType);
278
279        // Remember the currently shown message ID.
280        mCurrentMessageId = messageId;
281
282        // Disable forward/reply buttons as necessary.
283        enableReplyForwardButtons(Mailbox.isMailboxTypeReplyAndForwardable(mailboxType));
284
285        // Show the menu when it's showing a message.
286        setHasOptionsMenu(true);
287    }
288
289    public void enableNavigationButons(boolean enableMoveToNewer, boolean enableMoveToOlder,
290            int currentPosition, int countMessages) {
291        mCommandButtons.enableNavigationButtons(enableMoveToNewer, enableMoveToOlder,
292                currentPosition, countMessages);
293    }
294
295    /**
296     * Toggle favorite status and write back to provider
297     */
298    private void onClickFavorite() {
299        Message message = getMessage();
300
301        // Update UI
302        boolean newFavorite = ! message.mFlagFavorite;
303        mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
304
305        // Update provider
306        message.mFlagFavorite = newFavorite;
307        getController().setMessageFavorite(message.mId, newFavorite);
308    }
309
310    /**
311     * Set message read/unread.
312     */
313    public void onMarkMessageAsRead(boolean isRead) {
314        Message message = getMessage();
315        if (message.mFlagRead != isRead) {
316            message.mFlagRead = isRead;
317            getController().setMessageRead(message.mId, isRead);
318            if (!isRead) { // Became unread.  We need to close the message.
319                mCallback.onMessageSetUnread();
320            }
321        }
322    }
323
324    /**
325     * Send a service message indicating that a meeting invite button has been clicked.
326     */
327    private void onRespondToInvite(int response, int toastResId) {
328        Message message = getMessage();
329        // do not send twice in a row the same response
330        if (mPreviousMeetingResponse != response) {
331            getController().sendMeetingResponse(message.mId, response);
332            mPreviousMeetingResponse = response;
333        }
334        Utility.showToast(getActivity(), toastResId);
335        mCallback.onRespondedToInvite(response);
336    }
337
338    private void onInviteLinkClicked() {
339        Message message = getMessage();
340        String startTime = new PackedString(message.mMeetingInfo).get(MeetingInfo.MEETING_DTSTART);
341        if (startTime != null) {
342            long epochTimeMillis = Utility.parseEmailDateTimeToMillis(startTime);
343            mCallback.onCalendarLinkClicked(epochTimeMillis);
344        } else {
345            Email.log("meetingInfo without DTSTART " + message.mMeetingInfo);
346        }
347    }
348
349    @Override
350    public void onClick(View view) {
351        if (!isMessageOpen()) {
352            return; // Ignore.
353        }
354        switch (view.getId()) {
355            case R.id.reply:
356                mCallback.onReply();
357                return;
358            case R.id.reply_all:
359                mCallback.onReplyAll();
360                return;
361            case R.id.forward:
362                mCallback.onForward();
363                return;
364
365            case R.id.favorite:
366                onClickFavorite();
367                return;
368
369            case R.id.invite_link:
370                onInviteLinkClicked();
371                return;
372        }
373        super.onClick(view);
374    }
375
376    @Override
377    public void onCheckedChanged(CompoundButton view, boolean isChecked) {
378        if (!isChecked) return;
379        switch (view.getId()) {
380            case R.id.accept:
381                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_ACCEPTED,
382                        R.string.message_view_invite_toast_yes);
383                return;
384            case R.id.maybe:
385                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_TENTATIVE,
386                        R.string.message_view_invite_toast_maybe);
387                return;
388            case R.id.decline:
389                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_DECLINED,
390                        R.string.message_view_invite_toast_no);
391                return;
392        }
393    }
394
395    @Override
396    public boolean onOptionsItemSelected(MenuItem item) {
397        switch (item.getItemId()) {
398            case R.id.move:
399                onMove();
400                return true;
401            case R.id.delete:
402                onDelete();
403                return true;
404            case R.id.mark_as_unread:
405                onMarkAsUnread();
406                return true;
407        }
408        return super.onOptionsItemSelected(item);
409    }
410
411    private void onMove() {
412        mCallback.onMoveMessage();
413    }
414
415    private void onDelete() {
416        mCallback.onBeforeMessageDelete();
417        ActivityHelper.deleteMessage(getActivity(), mCurrentMessageId);
418    }
419
420    private void onMarkAsUnread() {
421        onMarkMessageAsRead(false);
422    }
423
424    /**
425     * {@inheritDoc}
426     *
427     * Mark the current as unread.
428     */
429    @Override
430    protected void onPostLoadBody() {
431        onMarkMessageAsRead(true);
432    }
433
434    @Override
435    protected void updateHeaderView(Message message) {
436        super.updateHeaderView(message);
437
438        mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff);
439
440        // Enable the invite tab if necessary
441        if ((message.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
442            addTabFlags(TAB_FLAGS_HAS_INVITE);
443        }
444    }
445
446    private class CommandButtonCallback implements MessageCommandButtonView.Callback {
447        @Override
448        public void onMoveToNewer() {
449            mCallback.onMoveToNewer();
450        }
451
452        @Override
453        public void onMoveToOlder() {
454            mCallback.onMoveToOlder();
455        }
456    }
457}
458