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