ConversationViewFragment.java revision f70fc4052b72a850bbb9be585d0f5a4877ee9448
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.widget.ResourceCursorAdapter;
41import android.widget.TextView;
42
43import com.android.mail.FormattedDateBuilder;
44import com.android.mail.R;
45import com.android.mail.browse.MessageHeaderView;
46import com.android.mail.providers.Account;
47import com.android.mail.providers.Conversation;
48import com.android.mail.providers.Message;
49import com.android.mail.providers.UIProvider;
50import com.android.mail.utils.LogUtils;
51
52import java.util.Map;
53
54/**
55 * The conversation view UI component.
56 */
57public final class ConversationViewFragment extends Fragment implements
58        LoaderManager.LoaderCallbacks<Cursor> {
59
60    private static final String LOG_TAG = new LogUtils().getLogTag();
61
62    private static final int MESSAGE_LOADER_ID = 0;
63
64    private ControllableActivity mActivity;
65
66    private Context mContext;
67
68    private Conversation mConversation;
69
70    private TextView mSubject;
71
72    private ConversationContainer mConversationContainer;
73
74    private Account mAccount;
75
76    private ConversationWebView mWebView;
77
78    private HtmlConversationTemplates mTemplates;
79
80    private String mBaseUri;
81
82    private final Handler mHandler = new Handler();
83
84    private final MailJsBridge mJsBridge = new MailJsBridge();
85
86    private static final String ARG_ACCOUNT = "account";
87    private static final String ARG_CONVERSATION = "conversation";
88
89    public ConversationViewFragment() {
90    }
91
92    /**
93     * Creates a new instance of {@link ConversationViewFragment}, initialized
94     * to display conversation.
95     */
96    public static ConversationViewFragment newInstance(Account account,
97            Conversation conversation) {
98       ConversationViewFragment f = new ConversationViewFragment();
99       Bundle args = new Bundle();
100       args.putParcelable(ARG_ACCOUNT, account);
101       args.putParcelable(ARG_CONVERSATION, conversation);
102       f.setArguments(args);
103       return f;
104    }
105
106    @Override
107    public void onActivityCreated(Bundle savedInstanceState) {
108        super.onActivityCreated(savedInstanceState);
109        // Strictly speaking, we get back an android.app.Activity from getActivity. However, the
110        // only activity creating a ConversationListContext is a MailActivity which is of type
111        // ControllableActivity, so this cast should be safe. If this cast fails, some other
112        // activity is creating ConversationListFragments. This activity must be of type
113        // ControllableActivity.
114        final Activity activity = getActivity();
115        if (! (activity instanceof ControllableActivity)){
116            LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
117                    + "create it. Cannot proceed.");
118        }
119        mActivity = (ControllableActivity) activity;
120        mContext = mActivity.getApplicationContext();
121        if (mActivity.isFinishing()) {
122            // Activity is finishing, just bail.
123            return;
124        }
125        mActivity.attachConversationView(this);
126        mTemplates = new HtmlConversationTemplates(mContext);
127        // Show conversation and start loading messages.
128        showConversation();
129    }
130
131    @Override
132    public void onCreate(Bundle savedState) {
133        LogUtils.v(LOG_TAG, "onCreate in FolderListFragment(this=%s)", this);
134        super.onCreate(savedState);
135
136        Bundle args = getArguments();
137        mAccount = args.getParcelable(ARG_ACCOUNT);
138        mConversation = args.getParcelable(ARG_CONVERSATION);
139        mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id;
140    }
141
142    @Override
143    public View onCreateView(LayoutInflater inflater,
144            ViewGroup container, Bundle savedInstanceState) {
145        View rootView = inflater.inflate(R.layout.conversation_view, null);
146        mSubject = (TextView) rootView.findViewById(R.id.subject);
147        mConversationContainer = (ConversationContainer) rootView
148                .findViewById(R.id.conversation_container);
149        mWebView = (ConversationWebView) rootView.findViewById(R.id.webview);
150
151        mWebView.addScrollListener(mConversationContainer);
152
153        mWebView.addJavascriptInterface(mJsBridge, "mail");
154
155        mWebView.setWebChromeClient(new WebChromeClient() {
156            @Override
157            public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
158                LogUtils.i(LOG_TAG, "JS: %s (%s:%d)", consoleMessage.message(),
159                        consoleMessage.sourceId(), consoleMessage.lineNumber());
160                return true;
161            }
162        });
163
164        WebSettings settings = mWebView.getSettings();
165
166        settings.setBlockNetworkImage(true);
167
168        settings.setJavaScriptEnabled(true);
169        settings.setUseWideViewPort(true);
170
171        settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
172
173        settings.setSupportZoom(true);
174        settings.setBuiltInZoomControls(true);
175        settings.setDisplayZoomControls(false);
176
177        return rootView;
178    }
179
180    @Override
181    public void onDestroyView() {
182        // Clear the adapter.
183        mConversationContainer.setOverlayAdapter(null);
184        mActivity.attachConversationView(null);
185
186        super.onDestroyView();
187    }
188
189    /**
190     * Handles a request to show a new conversation list, either from a search query or for viewing
191     * a label. This will initiate a data load, and hence must be called on the UI thread.
192     */
193    private void showConversation() {
194        mSubject.setText(mConversation.subject);
195        getLoaderManager().initLoader(MESSAGE_LOADER_ID, Bundle.EMPTY, this);
196    }
197
198    @Override
199    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
200        return new CursorLoader(mContext, Uri.parse(mConversation.messageListUri),
201                UIProvider.MESSAGE_PROJECTION, null, null, null);
202    }
203
204    @Override
205    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
206        MessageCursor messageCursor = new MessageCursor(data);
207        mWebView.loadDataWithBaseURL(mBaseUri, renderMessageBodies(messageCursor), "text/html",
208                "utf-8", null);
209        mConversationContainer.setOverlayAdapter(
210                new MessageListAdapter(mContext, messageCursor, mAccount));
211    }
212
213    @Override
214    public void onLoaderReset(Loader<Cursor> loader) {
215        // Do nothing.
216    }
217
218    private String renderMessageBodies(MessageCursor messageCursor) {
219        int pos = -1;
220        mTemplates.startConversation(0);
221        while (messageCursor.moveToPosition(++pos)) {
222            mTemplates.appendMessageHtml(messageCursor.get(), true, false, 1.0f, 96);
223        }
224        return mTemplates.endConversation(mBaseUri, 320);
225    }
226
227    public void onTouchEvent(MotionEvent event) {
228        // TODO: (mindyp) when there is an undo bar, check for event !in undo bar
229        // if its not in undo bar, dismiss the undo bar.
230    }
231
232    private static class MessageCursor extends CursorWrapper {
233
234        private Map<Long, Message> mCache = Maps.newHashMap();
235
236        public MessageCursor(Cursor inner) {
237            super(inner);
238        }
239
240        public Message get() {
241            long id = getWrappedCursor().getLong(0);
242            Message m = mCache.get(id);
243            if (m == null) {
244                m = new Message(this);
245                mCache.put(id, m);
246            }
247            return m;
248        }
249    }
250
251    private static class MessageListAdapter extends ResourceCursorAdapter {
252
253        private final FormattedDateBuilder mDateBuilder;
254        private final Account mAccount;
255
256        public MessageListAdapter(Context context, Cursor cursor, Account account) {
257            super(context, R.layout.conversation_message_header, cursor, 0);
258            mDateBuilder = new FormattedDateBuilder(context);
259            mAccount = account;
260        }
261
262        @Override
263        public void bindView(View view, Context context, Cursor cursor) {
264            Message m = ((MessageCursor) cursor).get();
265            MessageHeaderView header = (MessageHeaderView) view;
266            header.initialize(mDateBuilder, mAccount, true, false, false);
267            header.bind(m);
268        }
269    }
270
271    /**
272     * NOTE: all public methods must be listed in the proguard flags so that they can be accessed
273     * via reflection and not stripped.
274     *
275     */
276    private class MailJsBridge {
277
278        @SuppressWarnings("unused")
279        public void onWebContentGeometryChange(final String[] messageTopStrs) {
280            mHandler.post(new Runnable() {
281                @Override
282                public void run() {
283                    final int len = messageTopStrs.length;
284                    int[] messageTops = new int[len];
285                    for (int i = 0; i < len; i++) {
286                        messageTops[i] = Integer.parseInt(messageTopStrs[i]);
287                    }
288                    mConversationContainer.onGeometryChange(messageTops);
289                }
290            });
291        }
292
293    }
294
295}
296