MessageViewFragment.java revision 64ac7a6cc81ef6dc84354153b978bd5db944e8b0
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.ImageView;
40import android.widget.TextView;
41
42import java.security.InvalidParameterException;
43
44/**
45 * A {@link MessageViewFragmentBase} subclass for regular email messages.  (regular as in "not eml
46 * files").
47 *
48 * See {@link MessageViewBase} for the class relation diagram.
49 */
50public class MessageViewFragment extends MessageViewFragmentBase {
51    private ImageView mFavoriteIcon;
52    private View mInviteSection;
53
54    private View mReplyButton;
55    private View mReplyAllButton;
56    private View mForwardButton;
57
58    // calendar meeting invite answers
59    private TextView mMeetingYes;
60    private TextView mMeetingMaybe;
61    private TextView mMeetingNo;
62    private MessageCommandButtonView mCommandButtons;
63    private int mPreviousMeetingResponse = -1;
64
65    private Drawable mFavoriteIconOn;
66    private Drawable mFavoriteIconOff;
67
68    /**
69     * ID of the message that will be loaded.  Protect with {@link #mLock}.
70     */
71    private long mMessageIdToOpen = -1;
72
73    /** Lock object to protect {@link #mMessageIdToOpen} */
74    private final Object mLock = new Object();
75
76    /** ID of the currently shown message */
77    private long mCurrentMessageId = -1;
78
79    private final CommandButtonCallback mCommandButtonCallback = new CommandButtonCallback();
80
81    /**
82     * This class has more call backs than {@link MessageViewFragmentBase}.
83     *
84     * - EML files can't be "mark unread".
85     * - EML files can't have the invite buttons or the view in calender link.
86     *   Note EML files can have ICS (calendar invitation) files, but we don't treat them as
87     *   invites.  (Only exchange provider sets the FLAG_INCOMING_MEETING_INVITE
88     *   flag.)
89     *   It'd be weird to respond to an invitation in an EML that might not be addressed to you...
90     */
91    public interface Callback extends MessageViewFragmentBase.Callback {
92        /** Called when the "view in calendar" link is clicked. */
93        public void onCalendarLinkClicked(long epochEventStartTime);
94
95        /**
96         * Called when a calender response button is clicked.
97         *
98         * @param response one of {@link EmailServiceConstants#MEETING_REQUEST_ACCEPTED},
99         * {@link EmailServiceConstants#MEETING_REQUEST_DECLINED}, or
100         * {@link EmailServiceConstants#MEETING_REQUEST_TENTATIVE}.
101         */
102        public void onRespondedToInvite(int response);
103
104        /** Called when the current message is set unread. */
105        public void onMessageSetUnread();
106
107        /** Called when "move to newer" button is pressed. */
108        public void onMoveToNewer();
109
110        /** Called when "move to older" button is pressed. */
111        public void onMoveToOlder();
112
113        /**
114         * Called right before the current message will be deleted.
115         * Callees don't have to delete messages.  The fragment does.
116         */
117        public void onBeforeMessageDelete();
118
119        /** Called when the move button is pressed. */
120        public void onMoveMessage();
121        /** Called when the forward button is pressed. */
122        public void onForward();
123        /** Called when the reply button is pressed. */
124        public void onReply();
125        /** Called when the reply-all button is pressed. */
126        public void onReplyAll();
127    }
128
129    public static final class EmptyCallback extends MessageViewFragmentBase.EmptyCallback
130            implements Callback {
131        public static final Callback INSTANCE = new EmptyCallback();
132
133        @Override public void onCalendarLinkClicked(long epochEventStartTime) { }
134        @Override public void onMessageSetUnread() { }
135        @Override public void onRespondedToInvite(int response) { }
136        @Override public void onMoveToNewer() { }
137        @Override public void onMoveToOlder() { }
138        @Override public void onBeforeMessageDelete() { }
139        @Override public void onMoveMessage() { }
140        @Override public void onForward() { }
141        @Override public void onReply() { }
142        @Override public void onReplyAll() { }
143    }
144
145    private Callback mCallback = EmptyCallback.INSTANCE;
146
147    @Override
148    public void onCreate(Bundle savedInstanceState) {
149        super.onCreate(savedInstanceState);
150
151        final Resources res = getActivity().getResources();
152        mFavoriteIconOn = res.getDrawable(R.drawable.btn_star_big_buttonless_on);
153        mFavoriteIconOff = res.getDrawable(R.drawable.ic_star_none_holo_light);
154    }
155
156    @Override
157    public View onCreateView(
158            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
159        final View view = super.onCreateView(inflater, container, savedInstanceState);
160
161        mFavoriteIcon = (ImageView) view.findViewById(R.id.favorite);
162        mInviteSection = view.findViewById(R.id.invite_section);
163        mReplyButton = view.findViewById(R.id.reply);
164        mReplyAllButton = view.findViewById(R.id.reply_all);
165        mForwardButton = view.findViewById(R.id.forward);
166        mMeetingYes = (TextView) view.findViewById(R.id.accept);
167        mMeetingMaybe = (TextView) view.findViewById(R.id.maybe);
168        mMeetingNo = (TextView) view.findViewById(R.id.decline);
169
170        // Star is only visible on this fragment (as opposed to MessageFileViewFragment.)
171        view.findViewById(R.id.favorite).setVisibility(View.VISIBLE);
172
173        mFavoriteIcon.setOnClickListener(this);
174        mReplyButton.setOnClickListener(this);
175        mReplyAllButton.setOnClickListener(this);
176        mForwardButton.setOnClickListener(this);
177        mMeetingYes.setOnClickListener(this);
178        mMeetingMaybe.setOnClickListener(this);
179        mMeetingNo.setOnClickListener(this);
180        view.findViewById(R.id.invite_link).setOnClickListener(this);
181
182        // Show the command buttons at the bottom.
183        mCommandButtons =
184                (MessageCommandButtonView) view.findViewById(R.id.message_command_buttons);
185        mCommandButtons.setVisibility(View.VISIBLE);
186        mCommandButtons.setCallback(mCommandButtonCallback);
187
188        enableReplyForwardButtons(false);
189
190        return view;
191    }
192
193    @Override
194    public void onResume() {
195        super.onResume();
196    }
197
198    @Override
199    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
200        inflater.inflate(R.menu.message_view_fragment_option, menu);
201    }
202
203    private void enableReplyForwardButtons(boolean enabled) {
204        // We don't have disabled button assets, so let's hide them for now
205        final int visibility = enabled ? View.VISIBLE : View.GONE;
206        mReplyButton.setVisibility(visibility);
207        mReplyAllButton.setVisibility(visibility);
208        mForwardButton.setVisibility(visibility);
209    }
210
211    public void setCallback(Callback callback) {
212        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
213        super.setCallback(mCallback);
214    }
215
216    /** Called by activities to set an id of a message to open. */
217    public void openMessage(long messageId) {
218        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
219            Log.d(Email.LOG_TAG, "MessageViewFragment openMessage");
220        }
221        if (messageId == -1) {
222            throw new InvalidParameterException();
223        }
224        synchronized (mLock) {
225            mMessageIdToOpen = messageId;
226        }
227        loadMessageIfResumed();
228    }
229
230    @Override
231    public void clearContent() {
232        synchronized (mLock) {
233            super.clearContent();
234            mMessageIdToOpen = -1;
235
236            // Hide the menu.
237            // This isn't really necessary if we're really hiding the fragment.  However,
238            // for now, we're actually *not* hiding the fragment (just hiding the root view of it),
239            // so need to remove menus manually.
240            setHasOptionsMenu(false);
241        }
242    }
243
244    @Override
245    protected void resetView() {
246        super.resetView();
247        // TODO Hide command buttons.  (Careful when to re-show it)
248    }
249
250    @Override
251    protected boolean isMessageSpecified() {
252        synchronized (mLock) {
253            return mMessageIdToOpen != -1;
254        }
255    }
256
257    /**
258     * NOTE See the comment on the super method.  It's called on a worker thread.
259     */
260    @Override
261    protected Message openMessageSync(Activity activity) {
262        synchronized (mLock) {
263            long messageId = mMessageIdToOpen;
264            if (messageId < 0) {
265                return null; // Called after clearContent().
266            }
267            return Message.restoreMessageWithId(activity, messageId);
268        }
269    }
270
271    @Override
272    protected void onMessageShown(long messageId, int mailboxType) {
273        super.onMessageShown(messageId, mailboxType);
274
275        // Remember the currently shown message ID.
276        mCurrentMessageId = messageId;
277
278        // Disable forward/reply buttons as necessary.
279        enableReplyForwardButtons(Mailbox.isMailboxTypeReplyAndForwardable(mailboxType));
280
281        // Show the menu when it's showing a message.
282        setHasOptionsMenu(true);
283    }
284
285    public void enableNavigationButons(boolean enableMoveToNewer, boolean enableMoveToOlder,
286            int currentPosition, int countMessages) {
287        mCommandButtons.enableNavigationButtons(enableMoveToNewer, enableMoveToOlder,
288                currentPosition, countMessages);
289    }
290
291    /**
292     * Toggle favorite status and write back to provider
293     */
294    private void onClickFavorite() {
295        Message message = getMessage();
296
297        // Update UI
298        boolean newFavorite = ! message.mFlagFavorite;
299        mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
300
301        // Update provider
302        message.mFlagFavorite = newFavorite;
303        getController().setMessageFavorite(message.mId, newFavorite);
304    }
305
306    /**
307     * Set message read/unread.
308     */
309    public void onMarkMessageAsRead(boolean isRead) {
310        Message message = getMessage();
311        if (message.mFlagRead != isRead) {
312            message.mFlagRead = isRead;
313            getController().setMessageRead(message.mId, isRead);
314            if (!isRead) { // Became unread.  We need to close the message.
315                mCallback.onMessageSetUnread();
316            }
317        }
318    }
319
320    /**
321     * Send a service message indicating that a meeting invite button has been clicked.
322     */
323    private void onRespondToInvite(int response, int toastResId) {
324        Message message = getMessage();
325        // do not send twice in a row the same response
326        if (mPreviousMeetingResponse != response) {
327            getController().sendMeetingResponse(message.mId, response);
328            mPreviousMeetingResponse = response;
329        }
330        Utility.showToast(getActivity(), toastResId);
331        mCallback.onRespondedToInvite(response);
332    }
333
334    private void onInviteLinkClicked() {
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.accept:
366                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_ACCEPTED,
367                         R.string.message_view_invite_toast_yes);
368                return;
369            case R.id.maybe:
370                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_TENTATIVE,
371                         R.string.message_view_invite_toast_maybe);
372                return;
373            case R.id.decline:
374                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_DECLINED,
375                         R.string.message_view_invite_toast_no);
376                return;
377            case R.id.invite_link:
378                onInviteLinkClicked();
379                return;
380        }
381        super.onClick(view);
382    }
383
384    @Override
385    public boolean onOptionsItemSelected(MenuItem item) {
386        switch (item.getItemId()) {
387            case R.id.move:
388                onMove();
389                return true;
390            case R.id.delete:
391                onDelete();
392                return true;
393            case R.id.mark_as_unread:
394                onMarkAsUnread();
395                return true;
396        }
397        return super.onOptionsItemSelected(item);
398    }
399
400    private void onMove() {
401        mCallback.onMoveMessage();
402    }
403
404    private void onDelete() {
405        mCallback.onBeforeMessageDelete();
406        ActivityHelper.deleteMessage(getActivity(), mCurrentMessageId);
407    }
408
409    private void onMarkAsUnread() {
410        onMarkMessageAsRead(false);
411    }
412
413    /**
414     * {@inheritDoc}
415     *
416     * Mark the current as unread.
417     */
418    @Override
419    protected void onPostLoadBody() {
420        onMarkMessageAsRead(true);
421    }
422
423    @Override
424    protected void updateHeaderView(Message message) {
425        super.updateHeaderView(message);
426
427        mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff);
428
429        // Enable the invite tab if necessary
430        if ((message.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
431            addTabFlags(TAB_FLAGS_HAS_INVITE);
432        }
433    }
434
435    private class CommandButtonCallback implements MessageCommandButtonView.Callback {
436        @Override
437        public void onMoveToNewer() {
438            mCallback.onMoveToNewer();
439        }
440
441        @Override
442        public void onMoveToOlder() {
443            mCallback.onMoveToOlder();
444        }
445    }
446}
447