MessageViewFragment.java revision 5dceaa5b5ec6006aced6dfe37a718ea376decf53
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 Drawable mFavoriteIconOn;
66    private Drawable mFavoriteIconOff;
67
68    private int mPreviousMeetingResponse = EmailServiceConstants.MEETING_REQUEST_NOT_RESPONDED;
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        mPreviousMeetingResponse = EmailServiceConstants.MEETING_REQUEST_NOT_RESPONDED;
237    }
238
239    @Override
240    protected boolean isMessageSpecified() {
241        synchronized (mLock) {
242            return mMessageIdToOpen != -1;
243        }
244    }
245
246    /**
247     * NOTE See the comment on the super method.  It's called on a worker thread.
248     */
249    @Override
250    protected Message openMessageSync(Activity activity) {
251        synchronized (mLock) {
252            long messageId = mMessageIdToOpen;
253            if (messageId < 0) {
254                return null; // Called after clearContent().
255            }
256            return Message.restoreMessageWithId(activity, messageId);
257        }
258    }
259
260    @Override
261    protected void onMessageShown(long messageId, int mailboxType) {
262        super.onMessageShown(messageId, mailboxType);
263
264        // Remember the currently shown message ID.
265        mCurrentMessageId = messageId;
266
267        // Disable forward/reply buttons as necessary.
268        enableReplyForwardButtons(Mailbox.isMailboxTypeReplyAndForwardable(mailboxType));
269
270        // Show the menu when it's showing a message.
271        setHasOptionsMenu(true);
272    }
273
274    /**
275     * Toggle favorite status and write back to provider
276     */
277    private void onClickFavorite() {
278        Message message = getMessage();
279
280        // Update UI
281        boolean newFavorite = ! message.mFlagFavorite;
282        mFavoriteIcon.setImageDrawable(newFavorite ? mFavoriteIconOn : mFavoriteIconOff);
283
284        // Update provider
285        message.mFlagFavorite = newFavorite;
286        getController().setMessageFavorite(message.mId, newFavorite);
287    }
288
289    /**
290     * Set message read/unread.
291     */
292    public void onMarkMessageAsRead(boolean isRead) {
293        Message message = getMessage();
294        if (message.mFlagRead != isRead) {
295            message.mFlagRead = isRead;
296            getController().setMessageRead(message.mId, isRead);
297            if (!isRead) { // Became unread.  We need to close the message.
298                mCallback.onMessageSetUnread();
299            }
300        }
301    }
302
303    /**
304     * Send a service message indicating that a meeting invite button has been clicked.
305     */
306    private void onRespondToInvite(int response, int toastResId) {
307        Message message = getMessage();
308        // do not send twice in a row the same response
309        if (mPreviousMeetingResponse != response) {
310            getController().sendMeetingResponse(message.mId, response);
311            mPreviousMeetingResponse = response;
312        }
313        Utility.showToast(getActivity(), toastResId);
314        mCallback.onRespondedToInvite(response);
315    }
316
317    private void onInviteLinkClicked() {
318        Message message = getMessage();
319        String startTime = new PackedString(message.mMeetingInfo).get(MeetingInfo.MEETING_DTSTART);
320        if (startTime != null) {
321            long epochTimeMillis = Utility.parseEmailDateTimeToMillis(startTime);
322            mCallback.onCalendarLinkClicked(epochTimeMillis);
323        } else {
324            Email.log("meetingInfo without DTSTART " + message.mMeetingInfo);
325        }
326    }
327
328    @Override
329    public void onClick(View view) {
330        if (!isMessageOpen()) {
331            return; // Ignore.
332        }
333        switch (view.getId()) {
334            case R.id.reply:
335                mCallback.onReply();
336                return;
337            case R.id.reply_all:
338                mCallback.onReplyAll();
339                return;
340            case R.id.forward:
341                mCallback.onForward();
342                return;
343
344            case R.id.favorite:
345                onClickFavorite();
346                return;
347
348            case R.id.invite_link:
349                onInviteLinkClicked();
350                return;
351        }
352        super.onClick(view);
353    }
354
355    @Override
356    public void onCheckedChanged(CompoundButton view, boolean isChecked) {
357        if (!isChecked) return;
358        switch (view.getId()) {
359            case R.id.accept:
360                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_ACCEPTED,
361                        R.string.message_view_invite_toast_yes);
362                return;
363            case R.id.maybe:
364                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_TENTATIVE,
365                        R.string.message_view_invite_toast_maybe);
366                return;
367            case R.id.decline:
368                onRespondToInvite(EmailServiceConstants.MEETING_REQUEST_DECLINED,
369                        R.string.message_view_invite_toast_no);
370                return;
371        }
372    }
373
374    @Override
375    public boolean onOptionsItemSelected(MenuItem item) {
376        switch (item.getItemId()) {
377            case R.id.move:
378                onMove();
379                return true;
380            case R.id.delete:
381                onDelete();
382                return true;
383            case R.id.mark_as_unread:
384                onMarkAsUnread();
385                return true;
386        }
387        return super.onOptionsItemSelected(item);
388    }
389
390    private void onMove() {
391        mCallback.onMoveMessage();
392    }
393
394    private void onDelete() {
395        mCallback.onBeforeMessageDelete();
396        ActivityHelper.deleteMessage(getActivity(), mCurrentMessageId);
397    }
398
399    private void onMarkAsUnread() {
400        onMarkMessageAsRead(false);
401    }
402
403    /**
404     * {@inheritDoc}
405     *
406     * Mark the current as unread.
407     */
408    @Override
409    protected void onPostLoadBody() {
410        onMarkMessageAsRead(true);
411    }
412
413    @Override
414    protected void updateHeaderView(Message message) {
415        super.updateHeaderView(message);
416
417        mFavoriteIcon.setImageDrawable(message.mFlagFavorite ? mFavoriteIconOn : mFavoriteIconOff);
418
419        // Enable the invite tab if necessary
420        if ((message.mFlags & Message.FLAG_INCOMING_MEETING_INVITE) != 0) {
421            addTabFlags(TAB_FLAGS_HAS_INVITE);
422        }
423    }
424}
425