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