MessageViewFragment.java revision 96332096cae2b35a299c2466ddff75465a42b3d8
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 {
54    private ImageView mFavoriteIcon;
55    private View mInviteSection;
56
57    private View mReplyButton;
58    private View mReplyAllButton;
59    private View mForwardButton;
60
61    // calendar meeting invite answers
62    private CheckBox mMeetingYes;
63    private CheckBox mMeetingMaybe;
64    private CheckBox mMeetingNo;
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    /**
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        /**
108         * Called right before the current message will be deleted.
109         * Callees don't have to delete messages.  The fragment does.
110         */
111        public void onBeforeMessageDelete();
112
113        /** Called when the move button is pressed. */
114        public void onMoveMessage();
115        /** Called when the forward button is pressed. */
116        public void onForward();
117        /** Called when the reply button is pressed. */
118        public void onReply();
119        /** Called when the reply-all button is pressed. */
120        public void onReplyAll();
121    }
122
123    public static final class EmptyCallback extends MessageViewFragmentBase.EmptyCallback
124            implements Callback {
125        public static final Callback INSTANCE = new EmptyCallback();
126
127        @Override public void onCalendarLinkClicked(long epochEventStartTime) { }
128        @Override public void onMessageSetUnread() { }
129        @Override public void onRespondedToInvite(int response) { }
130        @Override public void onBeforeMessageDelete() { }
131        @Override public void onMoveMessage() { }
132        @Override public void onForward() { }
133        @Override public void onReply() { }
134        @Override public void onReplyAll() { }
135    }
136
137    private Callback mCallback = EmptyCallback.INSTANCE;
138
139    @Override
140    public void onCreate(Bundle savedInstanceState) {
141        super.onCreate(savedInstanceState);
142
143        final Resources res = getActivity().getResources();
144        mFavoriteIconOn = res.getDrawable(R.drawable.btn_star_on_normal_email_holo_light);
145        mFavoriteIconOff = res.getDrawable(R.drawable.btn_star_off_normal_email_holo_light);
146    }
147
148    @Override
149    public View onCreateView(
150            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
151        final View view = super.onCreateView(inflater, container, savedInstanceState);
152
153        mFavoriteIcon = (ImageView) UiUtilities.getView(view, R.id.favorite);
154        mInviteSection = UiUtilities.getView(view, R.id.invite_section);
155        mReplyButton = UiUtilities.getView(view, R.id.reply);
156        mReplyAllButton = UiUtilities.getView(view, R.id.reply_all);
157        mForwardButton = UiUtilities.getView(view, R.id.forward);
158        mMeetingYes = (CheckBox) UiUtilities.getView(view, R.id.accept);
159        mMeetingMaybe = (CheckBox) UiUtilities.getView(view, R.id.maybe);
160        mMeetingNo = (CheckBox) UiUtilities.getView(view, R.id.decline);
161
162        // Star is only visible on this fragment (as opposed to MessageFileViewFragment.)
163        UiUtilities.getView(view, R.id.favorite).setVisibility(View.VISIBLE);
164
165        mFavoriteIcon.setOnClickListener(this);
166        mReplyButton.setOnClickListener(this);
167        mReplyAllButton.setOnClickListener(this);
168        mForwardButton.setOnClickListener(this);
169        mMeetingYes.setOnCheckedChangeListener(this);
170        mMeetingMaybe.setOnCheckedChangeListener(this);
171        mMeetingNo.setOnCheckedChangeListener(this);
172        UiUtilities.getView(view, R.id.invite_link).setOnClickListener(this);
173
174        enableReplyForwardButtons(false);
175
176        return view;
177    }
178
179    @Override
180    public void onResume() {
181        super.onResume();
182    }
183
184    @Override
185    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
186        inflater.inflate(R.menu.message_view_fragment_option, menu);
187    }
188
189    private void enableReplyForwardButtons(boolean enabled) {
190        // We don't have disabled button assets, so let's hide them for now
191        final int visibility = enabled ? View.VISIBLE : View.GONE;
192        mReplyButton.setVisibility(visibility);
193        mReplyAllButton.setVisibility(visibility);
194        mForwardButton.setVisibility(visibility);
195    }
196
197    public void setCallback(Callback callback) {
198        mCallback = (callback == null) ? EmptyCallback.INSTANCE : callback;
199        super.setCallback(mCallback);
200    }
201
202    /** Called by activities to set an id of a message to open. */
203    public void openMessage(long messageId) {
204        if (Email.DEBUG_LIFECYCLE && Email.DEBUG) {
205            Log.d(Logging.LOG_TAG, "MessageViewFragment openMessage");
206        }
207        if (messageId == -1) {
208            throw new InvalidParameterException();
209        }
210        synchronized (mLock) {
211            mMessageIdToOpen = messageId;
212        }
213        loadMessageIfResumed();
214    }
215
216    @Override
217    public void clearContent() {
218        synchronized (mLock) {
219            super.clearContent();
220            mMessageIdToOpen = -1;
221
222            // Hide the menu.
223            // This isn't really necessary if we're really hiding the fragment.  However,
224            // for now, we're actually *not* hiding the fragment (just hiding the root view of it),
225            // so need to remove menus manually.
226            setHasOptionsMenu(false);
227        }
228    }
229
230    @Override
231    protected void resetView() {
232        super.resetView();
233        mMeetingYes.setChecked(false);
234        mMeetingNo.setChecked(false);
235        mMeetingMaybe.setChecked(false);
236    }
237
238    @Override
239    protected boolean isMessageSpecified() {
240        synchronized (mLock) {
241            return mMessageIdToOpen != -1;
242        }
243    }
244
245    /**
246     * NOTE See the comment on the super method.  It's called on a worker thread.
247     */
248    @Override
249    protected Message openMessageSync(Activity activity) {
250        synchronized (mLock) {
251            long messageId = mMessageIdToOpen;
252            if (messageId < 0) {
253                return null; // Called after clearContent().
254            }
255            return Message.restoreMessageWithId(activity, messageId);
256        }
257    }
258
259    @Override
260    protected void onMessageShown(long messageId, int mailboxType) {
261        super.onMessageShown(messageId, mailboxType);
262
263        // Remember the currently shown message ID.
264        mCurrentMessageId = messageId;
265
266        // Disable forward/reply buttons as necessary.
267        enableReplyForwardButtons(Mailbox.isMailboxTypeReplyAndForwardable(mailboxType));
268
269        // Show the menu when it's showing a message.
270        setHasOptionsMenu(true);
271    }
272
273    /**
274     * Toggle favorite status and write back to provider
275     */
276    private void onClickFavorite() {
277        Message message = getMessage();
278
279        // Update UI
280        boolean newFavorite = ! message.mFlagFavorite;
281        mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
282
283        // Update provider
284        message.mFlagFavorite = newFavorite;
285        getController().setMessageFavorite(message.mId, newFavorite);
286    }
287
288    /**
289     * Set message read/unread.
290     */
291    public void onMarkMessageAsRead(boolean isRead) {
292        Message message = getMessage();
293        if (message.mFlagRead != isRead) {
294            message.mFlagRead = isRead;
295            getController().setMessageRead(message.mId, isRead);
296            if (!isRead) { // Became unread.  We need to close the message.
297                mCallback.onMessageSetUnread();
298            }
299        }
300    }
301
302    /**
303     * Send a service message indicating that a meeting invite button has been clicked.
304     */
305    private void onRespondToInvite(int response, int toastResId) {
306        Message message = getMessage();
307        // do not send twice in a row the same response
308        if (mPreviousMeetingResponse != response) {
309            getController().sendMeetingResponse(message.mId, response);
310            mPreviousMeetingResponse = response;
311        }
312        Utility.showToast(getActivity(), toastResId);
313        mCallback.onRespondedToInvite(response);
314    }
315
316    private void onInviteLinkClicked() {
317        Message message = getMessage();
318        String startTime = new PackedString(message.mMeetingInfo).get(MeetingInfo.MEETING_DTSTART);
319        if (startTime != null) {
320            long epochTimeMillis = Utility.parseEmailDateTimeToMillis(startTime);
321            mCallback.onCalendarLinkClicked(epochTimeMillis);
322        } else {
323            Email.log("meetingInfo without DTSTART " + message.mMeetingInfo);
324        }
325    }
326
327    @Override
328    public void onClick(View view) {
329        if (!isMessageOpen()) {
330            return; // Ignore.
331        }
332        switch (view.getId()) {
333            case R.id.reply:
334                mCallback.onReply();
335                return;
336            case R.id.reply_all:
337                mCallback.onReplyAll();
338                return;
339            case R.id.forward:
340                mCallback.onForward();
341                return;
342
343            case R.id.favorite:
344                onClickFavorite();
345                return;
346
347            case R.id.invite_link:
348                onInviteLinkClicked();
349                return;
350        }
351        super.onClick(view);
352    }
353
354    @Override
355    public void onCheckedChanged(CompoundButton view, boolean isChecked) {
356        if (!isChecked) return;
357        switch (view.getId()) {
358            case R.id.accept:
359                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_ACCEPTED,
360                        R.string.message_view_invite_toast_yes);
361                return;
362            case R.id.maybe:
363                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_TENTATIVE,
364                        R.string.message_view_invite_toast_maybe);
365                return;
366            case R.id.decline:
367                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_DECLINED,
368                        R.string.message_view_invite_toast_no);
369                return;
370        }
371    }
372
373    @Override
374    public boolean onOptionsItemSelected(MenuItem item) {
375        switch (item.getItemId()) {
376            case R.id.move:
377                onMove();
378                return true;
379            case R.id.delete:
380                onDelete();
381                return true;
382            case R.id.mark_as_unread:
383                onMarkAsUnread();
384                return true;
385        }
386        return super.onOptionsItemSelected(item);
387    }
388
389    private void onMove() {
390        mCallback.onMoveMessage();
391    }
392
393    private void onDelete() {
394        mCallback.onBeforeMessageDelete();
395        ActivityHelper.deleteMessage(getActivity(), mCurrentMessageId);
396    }
397
398    private void onMarkAsUnread() {
399        onMarkMessageAsRead(false);
400    }
401
402    /**
403     * {@inheritDoc}
404     *
405     * Mark the current as unread.
406     */
407    @Override
408    protected void onPostLoadBody() {
409        onMarkMessageAsRead(true);
410    }
411
412    @Override
413    protected void updateHeaderView(Message message) {
414        super.updateHeaderView(message);
415
416        mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff);
417
418        // Enable the invite tab if necessary
419        if ((message.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
420            addTabFlags(TAB_FLAGS_HAS_INVITE);
421        }
422    }
423}
424