ConversationViewFragment.java revision 2d82d61d85cd5ed9e9e23c0968b492f11f3ee9bd
1/*
2 * Copyright (C) 2012 Google Inc.
3 * Licensed to The Android Open Source Project.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18package com.android.mail.ui;
19
20import com.google.common.collect.Maps;
21
22import android.app.Activity;
23import android.app.Fragment;
24import android.app.LoaderManager;
25import android.content.Context;
26import android.content.CursorLoader;
27import android.content.Loader;
28import android.database.Cursor;
29import android.database.CursorWrapper;
30import android.net.Uri;
31import android.os.Bundle;
32import android.os.Handler;
33import android.view.LayoutInflater;
34import android.view.MotionEvent;
35import android.view.View;
36import android.view.ViewGroup;
37import android.webkit.ConsoleMessage;
38import android.webkit.WebChromeClient;
39import android.webkit.WebSettings;
40import android.webkit.WebView;
41import android.webkit.WebViewClient;
42import android.widget.Adapter;
43import android.widget.ResourceCursorAdapter;
44import android.widget.TextView;
45
46import com.android.mail.FormattedDateBuilder;
47import com.android.mail.R;
48import com.android.mail.browse.MessageHeaderView;
49import com.android.mail.providers.Account;
50import com.android.mail.providers.Conversation;
51import com.android.mail.providers.ListParams;
52import com.android.mail.providers.Message;
53import com.android.mail.providers.UIProvider;
54import com.android.mail.utils.LogUtils;
55
56import java.util.Map;
57
58/**
59 * The conversation view UI component.
60 */
61public final class ConversationViewFragment extends Fragment implements
62        LoaderManager.LoaderCallbacks<Cursor> {
63
64    private static final String LOG_TAG = new LogUtils().getLogTag();
65
66    private static final int MESSAGE_LOADER_ID = 0;
67
68    private ControllableActivity mActivity;
69
70    private Context mContext;
71
72    private Conversation mConversation;
73
74    private TextView mSubject;
75
76    private ConversationContainer mConversationContainer;
77
78    private Account mAccount;
79
80    private ConversationWebView mWebView;
81
82    private HtmlConversationTemplates mTemplates;
83
84    private String mBaseUri;
85
86    private final Handler mHandler = new Handler();
87
88    private final MailJsBridge mJsBridge = new MailJsBridge();
89
90    private final WebViewClient mWebViewClient = new ConversationWebViewClient();
91
92    private MessageListAdapter mAdapter;
93
94    private boolean mViewsCreated;
95
96    private static final String ARG_ACCOUNT = "account";
97    private static final String ARG_CONVERSATION = "conversation";
98
99    /**
100     * Constructor needs to be public to handle orientation changes and activity lifecycle events.
101     */
102    public ConversationViewFragment() {
103        super();
104    }
105
106    /**
107     * Creates a new instance of {@link ConversationViewFragment}, initialized
108     * to display conversation.
109     */
110    public static ConversationViewFragment newInstance(Account account,
111            Conversation conversation) {
112       ConversationViewFragment f = new ConversationViewFragment();
113       Bundle args = new Bundle();
114       args.putParcelable(ARG_ACCOUNT, account);
115       args.putParcelable(ARG_CONVERSATION, conversation);
116       f.setArguments(args);
117       return f;
118    }
119
120    @Override
121    public void onActivityCreated(Bundle savedInstanceState) {
122        super.onActivityCreated(savedInstanceState);
123        // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
124        // only activity creating a ConversationListContext is a MailActivity which is of type
125        // ControllableActivity, so this cast should be safe. If this cast fails, some other
126        // activity is creating ConversationListFragments. This activity must be of type
127        // ControllableActivity.
128        final Activity activity = getActivity();
129        if (! (activity instanceof ControllableActivity)){
130            LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
131                    + "create it. Cannot proceed.");
132        }
133        mActivity = (ControllableActivity) activity;
134        mContext = mActivity.getApplicationContext();
135        if (mActivity.isFinishing()) {
136            // Activity is finishing, just bail.
137            return;
138        }
139        mActivity.attachConversationView(this);
140        mTemplates = new HtmlConversationTemplates(mContext);
141
142        mAdapter = new MessageListAdapter(mActivity.getActivityContext(),
143                null /* cursor */, mAccount, getLoaderManager());
144        mConversationContainer.setOverlayAdapter(mAdapter);
145
146        // Show conversation and start loading messages.
147        showConversation();
148    }
149
150    @Override
151    public void onCreate(Bundle savedState) {
152        LogUtils.v(LOG_TAG, "onCreate in FolderListFragment(this=%s)", this);
153        super.onCreate(savedState);
154
155        Bundle args = getArguments();
156        mAccount = args.getParcelable(ARG_ACCOUNT);
157        mConversation = args.getParcelable(ARG_CONVERSATION);
158        mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id;
159    }
160
161    @Override
162    public View onCreateView(LayoutInflater inflater,
163            ViewGroup container, Bundle savedInstanceState) {
164        View rootView = inflater.inflate(R.layout.conversation_view, null);
165        mSubject = (TextView) rootView.findViewById(R.id.subject);
166        mConversationContainer = (ConversationContainer) rootView
167                .findViewById(R.id.conversation_container);
168        mWebView = (ConversationWebView) rootView.findViewById(R.id.webview);
169
170        mWebView.addJavascriptInterface(mJsBridge, "mail");
171        mWebView.setWebViewClient(mWebViewClient);
172        mWebView.setWebChromeClient(new WebChromeClient() {
173            @Override
174            public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
175                LogUtils.i(LOG_TAG, "JS: %s (%s:%d)", consoleMessage.message(),
176                        consoleMessage.sourceId(), consoleMessage.lineNumber());
177                return true;
178            }
179        });
180
181        WebSettings settings = mWebView.getSettings();
182
183        settings.setBlockNetworkImage(true);
184
185        settings.setJavaScriptEnabled(true);
186        settings.setUseWideViewPort(true);
187
188        settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
189
190        settings.setSupportZoom(true);
191        settings.setBuiltInZoomControls(true);
192        settings.setDisplayZoomControls(false);
193
194        mViewsCreated = true;
195
196        return rootView;
197    }
198
199    @Override
200    public void onDestroyView() {
201        super.onDestroyView();
202        mViewsCreated = false;
203        mActivity.attachConversationView(null);
204    }
205
206    /**
207     * Handles a request to show a new conversation list, either from a search query or for viewing
208     * a folder. This will initiate a data load, and hence must be called on the UI thread.
209     */
210    private void showConversation() {
211        mSubject.setText(mConversation.subject);
212        getLoaderManager().initLoader(MESSAGE_LOADER_ID, Bundle.EMPTY, this);
213    }
214
215    @Override
216    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
217        return new MessageLoader(mContext, mConversation.messageListUri);
218    }
219
220    @Override
221    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
222        MessageCursor messageCursor = (MessageCursor) data;
223
224        if (mAdapter.getCursor() == null) {
225            renderConversation(messageCursor);
226        } else {
227            updateConversation(messageCursor);
228        }
229    }
230
231    @Override
232    public void onLoaderReset(Loader<Cursor> loader) {
233        mAdapter.swapCursor(null);
234    }
235
236    private void renderConversation(MessageCursor messageCursor) {
237        mWebView.loadDataWithBaseURL(mBaseUri, renderMessageBodies(messageCursor), "text/html",
238                "utf-8", null);
239        mAdapter.swapCursor(messageCursor);
240    }
241
242    private void updateConversation(MessageCursor messageCursor) {
243        // TODO: handle server-side conversation updates
244        // for simple things like header data changes, just re-render the affected headers
245        // if a new message is present, save off the pending cursor and show a notification to
246        // re-render
247
248        final MessageCursor oldCursor = (MessageCursor) mAdapter.getCursor();
249        mAdapter.swapCursor(messageCursor);
250    }
251
252    private String renderMessageBodies(MessageCursor messageCursor) {
253        int pos = -1;
254        mTemplates.startConversation(0);
255        while (messageCursor.moveToPosition(++pos)) {
256            mTemplates.appendMessageHtml(messageCursor.get(), true, false, 1.0f, 96);
257        }
258        return mTemplates.endConversation(mBaseUri, 320);
259    }
260
261    public void onTouchEvent(MotionEvent event) {
262        // TODO: (mindyp) when there is an undo bar, check for event !in undo bar
263        // if its not in undo bar, dismiss the undo bar.
264    }
265
266    private static class MessageLoader extends CursorLoader {
267        private boolean mDeliveredFirstResults = false;
268
269        public MessageLoader(Context c, Uri uri) {
270            super(c, uri, UIProvider.MESSAGE_PROJECTION, null, null, null);
271        }
272
273        @Override
274        public Cursor loadInBackground() {
275            return new MessageCursor(super.loadInBackground());
276
277        }
278
279        @Override
280        public void deliverResult(Cursor result) {
281            // We want to deliver these results, and then we want to make sure that any subsequent
282            // queries do not hit the network
283            super.deliverResult(result);
284
285            if (!mDeliveredFirstResults) {
286                mDeliveredFirstResults = true;
287                Uri uri = getUri();
288
289                // Create a ListParams that tells the provider to not hit the network
290                final ListParams listParams =
291                        new ListParams(ListParams.NO_LIMIT, false /* useNetwork */);
292
293                // Build the new uri with this additional parameter
294                uri = uri.buildUpon().appendQueryParameter(
295                        UIProvider.LIST_PARAMS_QUERY_PARAMETER, listParams.serialize()).build();
296                setUri(uri);
297            }
298        }
299    }
300
301    private static class MessageCursor extends CursorWrapper {
302
303        private Map<Long, Message> mCache = Maps.newHashMap();
304
305        public MessageCursor(Cursor inner) {
306            super(inner);
307        }
308
309        public Message get() {
310            final long id = getWrappedCursor().getLong(UIProvider.MESSAGE_ID_COLUMN);
311            Message m = mCache.get(id);
312            if (m == null) {
313                m = new Message(this);
314                mCache.put(id, m);
315            }
316            return m;
317        }
318    }
319
320    private static class MessageListAdapter extends ResourceCursorAdapter {
321
322        private final FormattedDateBuilder mDateBuilder;
323        private final Account mAccount;
324        private final LoaderManager mLoaderManager;
325
326        public MessageListAdapter(Context context, Cursor messageCursor, Account account,
327                LoaderManager loaderManager) {
328            super(context, R.layout.conversation_message_header, messageCursor, 0);
329            mDateBuilder = new FormattedDateBuilder(context);
330            mAccount = account;
331            mLoaderManager = loaderManager;
332        }
333
334        @Override
335        public void bindView(View view, Context context, Cursor cursor) {
336            Message m = ((MessageCursor) cursor).get();
337            MessageHeaderView header = (MessageHeaderView) view;
338            header.initialize(mDateBuilder, mAccount, mLoaderManager, true, false, false);
339            header.bind(m);
340        }
341    }
342
343    private static int[] parseInts(final String[] stringArray) {
344        final int len = stringArray.length;
345        final int[] ints = new int[len];
346        for (int i = 0; i < len; i++) {
347            ints[i] = Integer.parseInt(stringArray[i]);
348        }
349        return ints;
350    }
351
352    private class ConversationWebViewClient extends WebViewClient {
353
354        @Override
355        public void onPageFinished(WebView view, String url) {
356            super.onPageFinished(view, url);
357
358            // TODO: save off individual message unread state (here, or in onLoadFinished?) so
359            // 'mark unread' restores the original unread state for each individual message
360
361            // mark as read upon open
362            if (!mConversation.read) {
363                mConversation.markRead(mContext, true /* read */);
364            }
365        }
366
367    }
368
369    /**
370     * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
371     * via reflection and not stripped.
372     *
373     */
374    private class MailJsBridge {
375
376        @SuppressWarnings("unused")
377        public void onWebContentGeometryChange(final String[] headerBottomStrs,
378                final String[] headerHeightStrs) {
379            mHandler.post(new Runnable() {
380                @Override
381                public void run() {
382                    if (!mViewsCreated) {
383                        LogUtils.d(LOG_TAG, "ignoring webContentGeometryChange because views" +
384                                " are gone, %s", ConversationViewFragment.this);
385                        return;
386                    }
387
388                    mConversationContainer.onGeometryChange(parseInts(headerBottomStrs),
389                            parseInts(headerHeightStrs));
390                }
391            });
392        }
393
394    }
395
396}
397