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