AbstractConversationViewFragment.java revision ca895f1f844c483f796a992ea0589a0b619e640f
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 android.animation.Animator;
21import android.animation.AnimatorInflater;
22import android.animation.AnimatorListenerAdapter;
23import android.app.Activity;
24import android.app.Fragment;
25import android.app.LoaderManager;
26import android.content.ActivityNotFoundException;
27import android.content.Context;
28import android.content.CursorLoader;
29import android.content.Intent;
30import android.content.Loader;
31import android.content.pm.ActivityInfo;
32import android.content.pm.PackageManager;
33import android.content.pm.ResolveInfo;
34import android.content.res.Resources;
35import android.database.Cursor;
36import android.database.DataSetObservable;
37import android.database.DataSetObserver;
38import android.net.Uri;
39import android.os.Bundle;
40import android.os.Handler;
41import android.provider.Browser;
42import android.view.Menu;
43import android.view.MenuInflater;
44import android.view.MenuItem;
45import android.view.View;
46import android.webkit.WebView;
47import android.webkit.WebViewClient;
48
49import com.android.mail.ContactInfo;
50import com.android.mail.ContactInfoSource;
51import com.android.mail.FormattedDateBuilder;
52import com.android.mail.R;
53import com.android.mail.SenderInfoLoader;
54import com.android.mail.browse.ConversationAccountController;
55import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
56import com.android.mail.browse.MessageCursor;
57import com.android.mail.browse.MessageCursor.ConversationController;
58import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
59import com.android.mail.providers.Account;
60import com.android.mail.providers.AccountObserver;
61import com.android.mail.providers.Address;
62import com.android.mail.providers.Conversation;
63import com.android.mail.providers.Folder;
64import com.android.mail.providers.ListParams;
65import com.android.mail.providers.UIProvider;
66import com.android.mail.providers.UIProvider.CursorStatus;
67import com.android.mail.utils.LogTag;
68import com.android.mail.utils.LogUtils;
69import com.android.mail.utils.Utils;
70import com.google.common.collect.ImmutableMap;
71import com.google.common.collect.Maps;
72
73import java.util.Arrays;
74import java.util.List;
75import java.util.Map;
76import java.util.Set;
77
78public abstract class AbstractConversationViewFragment extends Fragment implements
79        ConversationController, ConversationAccountController, MessageHeaderViewCallbacks,
80        ConversationViewHeaderCallbacks {
81
82    private static final String ARG_ACCOUNT = "account";
83    public static final String ARG_CONVERSATION = "conversation";
84    private static final String ARG_FOLDER = "folder";
85    private static final String LOG_TAG = LogTag.getLogTag();
86    protected static final int MESSAGE_LOADER = 0;
87    protected static final int CONTACT_LOADER = 1;
88    private static int sMinDelay = -1;
89    private static int sMinShowTime = -1;
90    protected ControllableActivity mActivity;
91    private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
92    protected FormattedDateBuilder mDateBuilder;
93    private final ContactLoaderCallbacks mContactLoaderCallbacks = new ContactLoaderCallbacks();
94    private MenuItem mChangeFoldersMenuItem;
95    protected Conversation mConversation;
96    protected Folder mFolder;
97    protected String mBaseUri;
98    protected Account mAccount;
99    protected final Map<String, Address> mAddressCache = Maps.newHashMap();
100    protected boolean mEnableContentReadySignal;
101    private MessageCursor mCursor;
102    private Context mContext;
103    /**
104     * A backwards-compatible version of {{@link #getUserVisibleHint()}. Like the framework flag,
105     * this flag is saved and restored.
106     */
107    private boolean mUserVisible;
108    private View mProgressView;
109    private View mBackgroundView;
110    private final Handler mHandler = new Handler();
111    /** True if we want to avoid marking the conversation as viewed and read. */
112    private boolean mSuppressMarkingViewed;
113    /**
114     * Parcelable state of the conversation view. Can safely be used without null checking any time
115     * after {@link #onCreate(Bundle)}.
116     */
117    protected ConversationViewState mViewState;
118
119    private long mLoadingShownTime = -1;
120
121    private boolean mIsDetached;
122
123    private final Runnable mDelayedShow = new FragmentRunnable("mDelayedShow") {
124        @Override
125        public void go() {
126            mLoadingShownTime = System.currentTimeMillis();
127            mProgressView.setVisibility(View.VISIBLE);
128        }
129    };
130
131    private final AccountObserver mAccountObserver = new AccountObserver() {
132        @Override
133        public void onChanged(Account newAccount) {
134            final Account oldAccount = mAccount;
135            mAccount = newAccount;
136            onAccountChanged(newAccount, oldAccount);
137        }
138    };
139
140    private static final String BUNDLE_VIEW_STATE =
141            AbstractConversationViewFragment.class.getName() + "viewstate";
142    /**
143     * We save the user visible flag so the various transitions that occur during rotation do not
144     * cause unnecessary visibility change.
145     */
146    private static final String BUNDLE_USER_VISIBLE =
147            AbstractConversationViewFragment.class.getName() + "uservisible";
148
149    private static final String BUNDLE_DETACHED =
150            AbstractConversationViewFragment.class.getName() + "detached";
151
152    public static Bundle makeBasicArgs(Account account, Folder folder) {
153        Bundle args = new Bundle();
154        args.putParcelable(ARG_ACCOUNT, account);
155        args.putParcelable(ARG_FOLDER, folder);
156        return args;
157    }
158
159    /**
160     * Constructor needs to be public to handle orientation changes and activity
161     * lifecycle events.
162     */
163    public AbstractConversationViewFragment() {
164        super();
165    }
166
167    /**
168     * Subclasses must override, since this depends on how many messages are
169     * shown in the conversation view.
170     */
171    protected void markUnread() {
172        // Do not automatically mark this conversation viewed and read.
173        mSuppressMarkingViewed = true;
174    }
175
176    /**
177     * Subclasses must override this, since they may want to display a single or
178     * many messages related to this conversation.
179     */
180    protected abstract void onMessageCursorLoadFinished(Loader<Cursor> loader,
181            MessageCursor newCursor, MessageCursor oldCursor);
182
183    /**
184     * Subclasses must override this, since they may want to display a single or
185     * many messages related to this conversation.
186     */
187    @Override
188    public abstract void onConversationViewHeaderHeightChange(int newHeight);
189
190    public abstract void onUserVisibleHintChanged();
191
192    /**
193     * Subclasses must override this.
194     */
195    protected abstract void onAccountChanged(Account newAccount, Account oldAccount);
196
197    @Override
198    public void onCreate(Bundle savedState) {
199        super.onCreate(savedState);
200
201        final Bundle args = getArguments();
202        mAccount = args.getParcelable(ARG_ACCOUNT);
203        mConversation = args.getParcelable(ARG_CONVERSATION);
204        mFolder = args.getParcelable(ARG_FOLDER);
205
206        // Since the uri specified in the conversation base uri may not be unique, we specify a
207        // base uri that us guaranteed to be unique for this conversation.
208        mBaseUri = "x-thread://" + mAccount.name + "/" + mConversation.id;
209
210        // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
211        // Below JB, try to speed up initial render by having the webview do supplemental draws to
212        // custom a software canvas.
213        // TODO(mindyp):
214        //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
215        // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
216        // animation that immediately runs on page load. The app uses this as a signal that the
217        // content is loaded and ready to draw, since WebView delays firing this event until the
218        // layers are composited and everything is ready to draw.
219        // This signal does not seem to be reliable, so just use the old method for now.
220        mEnableContentReadySignal = false; //Utils.isRunningJellybeanOrLater();
221        LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
222        // Not really, we just want to get a crack to store a reference to the change_folder item
223        setHasOptionsMenu(true);
224
225        if (savedState != null) {
226            mViewState = savedState.getParcelable(BUNDLE_VIEW_STATE);
227            mUserVisible = savedState.getBoolean(BUNDLE_USER_VISIBLE);
228            mIsDetached = savedState.getBoolean(BUNDLE_DETACHED, false);
229        } else {
230            mViewState = getNewViewState();
231        }
232    }
233
234    @Override
235    public String toString() {
236        // log extra info at DEBUG level or finer
237        final String s = super.toString();
238        if (!LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG) || mConversation == null) {
239            return s;
240        }
241        return "(" + s + " conv=" + mConversation + ")";
242    }
243
244    protected abstract WebView getWebView();
245
246    public void instantiateProgressIndicators(View rootView) {
247        mBackgroundView = rootView.findViewById(R.id.background_view);
248        mProgressView = rootView.findViewById(R.id.loading_progress);
249    }
250
251    protected void dismissLoadingStatus() {
252        dismissLoadingStatus(null);
253    }
254
255    /**
256     * Begin the fade-out animation to hide the Progress overlay, either immediately or after some
257     * timeout (to ensure that the progress minimum time elapses).
258     *
259     * @param doAfter an optional Runnable action to execute after the animation completes
260     */
261    protected void dismissLoadingStatus(final Runnable doAfter) {
262        if (mLoadingShownTime == -1) {
263            // The runnable hasn't run yet, so just remove it.
264            mHandler.removeCallbacks(mDelayedShow);
265            dismiss(doAfter);
266            return;
267        }
268        final long diff = Math.abs(System.currentTimeMillis() - mLoadingShownTime);
269        if (diff > sMinShowTime) {
270            dismiss(doAfter);
271        } else {
272            mHandler.postDelayed(new FragmentRunnable("dismissLoadingStatus") {
273                @Override
274                public void go() {
275                    dismiss(doAfter);
276                }
277            }, Math.abs(sMinShowTime - diff));
278        }
279    }
280
281    private void dismiss(final Runnable doAfter) {
282        // Reset loading shown time.
283        mLoadingShownTime = -1;
284        mProgressView.setVisibility(View.GONE);
285        if (mBackgroundView.getVisibility() == View.VISIBLE) {
286            animateDismiss(doAfter);
287        } else {
288            if (doAfter != null) {
289                doAfter.run();
290            }
291        }
292    }
293
294    private void animateDismiss(final Runnable doAfter) {
295        // the animation can only work (and is only worth doing) if this fragment is added
296        // reasons it may not be added: fragment is being destroyed, or in the process of being
297        // restored
298        if (!isAdded()) {
299            mBackgroundView.setVisibility(View.GONE);
300            return;
301        }
302
303        Utils.enableHardwareLayer(mBackgroundView);
304        final Animator animator = AnimatorInflater.loadAnimator(getContext(), R.anim.fade_out);
305        animator.setTarget(mBackgroundView);
306        animator.addListener(new AnimatorListenerAdapter() {
307            @Override
308            public void onAnimationEnd(Animator animation) {
309                mBackgroundView.setVisibility(View.GONE);
310                mBackgroundView.setLayerType(View.LAYER_TYPE_NONE, null);
311                if (doAfter != null) {
312                    doAfter.run();
313                }
314            }
315        });
316        animator.start();
317    }
318
319    @Override
320    public void onActivityCreated(Bundle savedInstanceState) {
321        super.onActivityCreated(savedInstanceState);
322        final Activity activity = getActivity();
323        if (!(activity instanceof ControllableActivity)) {
324            LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
325                    + "create it. Cannot proceed.");
326        }
327        if (activity == null || activity.isFinishing()) {
328            // Activity is finishing, just bail.
329            return;
330        }
331        mActivity = (ControllableActivity) activity;
332        mContext = activity.getApplicationContext();
333        mDateBuilder = new FormattedDateBuilder((Context) mActivity);
334        mAccount = mAccountObserver.initialize(mActivity.getAccountController());
335    }
336
337    @Override
338    public ConversationUpdater getListController() {
339        final ControllableActivity activity = (ControllableActivity) getActivity();
340        return activity != null ? activity.getConversationUpdater() : null;
341    }
342
343
344    protected void showLoadingStatus() {
345        if (!mUserVisible) {
346            return;
347        }
348        if (sMinDelay == -1) {
349            Resources res = getContext().getResources();
350            sMinDelay = res.getInteger(R.integer.conversationview_show_loading_delay);
351            sMinShowTime = res.getInteger(R.integer.conversationview_min_show_loading);
352        }
353        // If the loading view isn't already showing, show it and remove any
354        // pending calls to show the loading screen.
355        mBackgroundView.setVisibility(View.VISIBLE);
356        mHandler.removeCallbacks(mDelayedShow);
357        mHandler.postDelayed(mDelayedShow, sMinDelay);
358    }
359
360    public Context getContext() {
361        return mContext;
362    }
363
364    @Override
365    public Conversation getConversation() {
366        return mConversation;
367    }
368
369    @Override
370    public MessageCursor getMessageCursor() {
371        return mCursor;
372    }
373
374    public Handler getHandler() {
375        return mHandler;
376    }
377
378    public MessageLoaderCallbacks getMessageLoaderCallbacks() {
379        return mMessageLoaderCallbacks;
380    }
381
382    public ContactLoaderCallbacks getContactInfoSource() {
383        return mContactLoaderCallbacks;
384    }
385
386    @Override
387    public Account getAccount() {
388        return mAccount;
389    }
390
391    @Override
392    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
393        super.onCreateOptionsMenu(menu, inflater);
394        mChangeFoldersMenuItem = menu.findItem(R.id.change_folder);
395    }
396
397    @Override
398    public boolean onOptionsItemSelected(MenuItem item) {
399        boolean handled = false;
400        switch (item.getItemId()) {
401            case R.id.inside_conversation_unread:
402                markUnread();
403                handled = true;
404                break;
405        }
406        return handled;
407    }
408
409    // BEGIN conversation header callbacks
410    @Override
411    public void onFoldersClicked() {
412        if (mChangeFoldersMenuItem == null) {
413            LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
414            return;
415        }
416        mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
417    }
418
419    @Override
420    public String getSubjectRemainder(String subject) {
421        final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger();
422        if (sdc == null) {
423            return subject;
424        }
425        return sdc.getUnshownSubject(subject);
426    }
427    // END conversation header callbacks
428
429    @Override
430    public void onSaveInstanceState(Bundle outState) {
431        if (mViewState != null) {
432            outState.putParcelable(BUNDLE_VIEW_STATE, mViewState);
433        }
434        outState.putBoolean(BUNDLE_USER_VISIBLE, mUserVisible);
435        outState.putBoolean(BUNDLE_DETACHED, mIsDetached);
436    }
437
438    @Override
439    public void onDestroyView() {
440        super.onDestroyView();
441        mAccountObserver.unregisterAndDestroy();
442    }
443
444    /**
445     * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
446     * reliability on older platforms.
447     */
448    public void setExtraUserVisibleHint(boolean isVisibleToUser) {
449        LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
450        if (mUserVisible != isVisibleToUser) {
451            mUserVisible = isVisibleToUser;
452            MessageCursor cursor = getMessageCursor();
453            if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) {
454                // Pop back to conversation list and show error.
455                onError();
456                return;
457            }
458            onUserVisibleHintChanged();
459        }
460    }
461
462    public boolean isUserVisible() {
463        return mUserVisible;
464    }
465
466    private class MessageLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
467
468        @Override
469        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
470            return new MessageLoader(mActivity.getActivityContext(), mConversation.messageListUri);
471        }
472
473        @Override
474        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
475            // ignore truly duplicate results
476            // this can happen when restoring after rotation
477            if (mCursor == data) {
478                return;
479            } else {
480                final MessageCursor messageCursor = (MessageCursor) data;
481
482                // bind the cursor to this fragment so it can access to the current list controller
483                messageCursor.setController(AbstractConversationViewFragment.this);
484
485                if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
486                    LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
487                }
488
489                // We have no messages: exit conversation view.
490                if (messageCursor.getCount() == 0
491                        && (!CursorStatus.isWaitingForResults(messageCursor.getStatus())
492                                || mIsDetached)) {
493                    if (mUserVisible) {
494                        onError();
495                    } else {
496                        // we expect that the pager adapter will remove this
497                        // conversation fragment on its own due to a separate
498                        // conversation cursor update (we might get here if the
499                        // message list update fires first. nothing to do
500                        // because we expect to be torn down soon.)
501                        LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update"
502                                + " in anticipation of conv cursor update. c=%s", mConversation.uri);
503                    }
504                    // existing mCursor will imminently be closed, must stop referencing it
505                    // since we expect to be kicked out soon, it doesn't matter what mCursor
506                    // becomes
507                    mCursor = null;
508                    return;
509                }
510
511                // ignore cursors that are still loading results
512                if (!messageCursor.isLoaded()) {
513                    // existing mCursor will imminently be closed, must stop referencing it
514                    // in this case, the new cursor is also no good, and since don't expect to get
515                    // here except in initial load situations, it's safest to just ensure the
516                    // reference is null
517                    mCursor = null;
518                    return;
519                }
520                final MessageCursor oldCursor = mCursor;
521                mCursor = messageCursor;
522                onMessageCursorLoadFinished(loader, mCursor, oldCursor);
523            }
524        }
525
526        @Override
527        public void onLoaderReset(Loader<Cursor> loader) {
528            mCursor = null;
529        }
530
531    }
532
533    private void onError() {
534        // need to exit this view- conversation may have been
535        // deleted, or for whatever reason is now invalid (e.g.
536        // discard single draft)
537        //
538        // N.B. this may involve a fragment transaction, which
539        // FragmentManager will refuse to execute directly
540        // within onLoadFinished. Make sure the controller knows.
541        LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode");
542        // TODO(mindyp): handle ERROR status by showing an error
543        // message to the user that there are no messages in
544        // this conversation
545        popOut();
546    }
547
548    private void popOut() {
549        mHandler.post(new FragmentRunnable("popOut") {
550            @Override
551            public void go() {
552                mActivity.getListHandler()
553                .onConversationSelected(null, true /* inLoaderCallbacks */);
554            }
555        });
556    }
557
558    protected void onConversationSeen() {
559        // Ignore unsafe calls made after a fragment is detached from an activity
560        final ControllableActivity activity = (ControllableActivity) getActivity();
561        if (activity == null) {
562            LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id);
563            return;
564        }
565
566        mViewState.setInfoForConversation(mConversation);
567
568        // In most circumstances we want to mark the conversation as viewed and read, since the
569        // user has read it.  However, if the user has already marked the conversation unread, we
570        // do not want a  later mark-read operation to undo this.  So we check this variable which
571        // is set in #markUnread() which suppresses automatic mark-read.
572        if (!mSuppressMarkingViewed) {
573            // mark viewed/read if not previously marked viewed by this conversation view,
574            // or if unread messages still exist in the message list cursor
575            // we don't want to keep marking viewed on rotation or restore
576            // but we do want future re-renders to mark read (e.g. "New message from X" case)
577            MessageCursor cursor = getMessageCursor();
578            if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) {
579                // Mark the conversation viewed and read.
580                activity.getConversationUpdater()
581                        .markConversationsRead(Arrays.asList(mConversation), true, true);
582
583                // and update the Message objects in the cursor so the next time a cursor update
584                // happens with these messages marked read, we know to ignore it
585                if (cursor != null) {
586                    cursor.markMessagesRead();
587                }
588            }
589        }
590        activity.getListHandler().onConversationSeen(mConversation);
591
592        showAutoFitPrompt();
593    }
594
595    protected ConversationViewState getNewViewState() {
596        return new ConversationViewState();
597    }
598
599    private static class MessageLoader extends CursorLoader {
600        private boolean mDeliveredFirstResults = false;
601
602        public MessageLoader(Context c, Uri messageListUri) {
603            super(c, messageListUri, UIProvider.MESSAGE_PROJECTION, null, null, null);
604        }
605
606        @Override
607        public Cursor loadInBackground() {
608            return new MessageCursor(super.loadInBackground());
609        }
610
611        @Override
612        public void deliverResult(Cursor result) {
613            // We want to deliver these results, and then we want to make sure
614            // that any subsequent
615            // queries do not hit the network
616            super.deliverResult(result);
617
618            if (!mDeliveredFirstResults) {
619                mDeliveredFirstResults = true;
620                Uri uri = getUri();
621
622                // Create a ListParams that tells the provider to not hit the
623                // network
624                final ListParams listParams = new ListParams(ListParams.NO_LIMIT,
625                        false /* useNetwork */);
626
627                // Build the new uri with this additional parameter
628                uri = uri
629                        .buildUpon()
630                        .appendQueryParameter(UIProvider.LIST_PARAMS_QUERY_PARAMETER,
631                                listParams.serialize()).build();
632                setUri(uri);
633            }
634        }
635    }
636
637    /**
638     * Inner class to to asynchronously load contact data for all senders in the conversation,
639     * and notify observers when the data is ready.
640     *
641     */
642    protected class ContactLoaderCallbacks implements ContactInfoSource,
643            LoaderManager.LoaderCallbacks<ImmutableMap<String, ContactInfo>> {
644
645        private Set<String> mSenders;
646        private ImmutableMap<String, ContactInfo> mContactInfoMap;
647        private DataSetObservable mObservable = new DataSetObservable();
648
649        public void setSenders(Set<String> emailAddresses) {
650            mSenders = emailAddresses;
651        }
652
653        @Override
654        public Loader<ImmutableMap<String, ContactInfo>> onCreateLoader(int id, Bundle args) {
655            return new SenderInfoLoader(mActivity.getActivityContext(), mSenders);
656        }
657
658        @Override
659        public void onLoadFinished(Loader<ImmutableMap<String, ContactInfo>> loader,
660                ImmutableMap<String, ContactInfo> data) {
661            mContactInfoMap = data;
662            mObservable.notifyChanged();
663        }
664
665        @Override
666        public void onLoaderReset(Loader<ImmutableMap<String, ContactInfo>> loader) {
667        }
668
669        @Override
670        public ContactInfo getContactInfo(String email) {
671            if (mContactInfoMap == null) {
672                return null;
673            }
674            return mContactInfoMap.get(email);
675        }
676
677        @Override
678        public void registerObserver(DataSetObserver observer) {
679            mObservable.registerObserver(observer);
680        }
681
682        @Override
683        public void unregisterObserver(DataSetObserver observer) {
684            mObservable.unregisterObserver(observer);
685        }
686    }
687
688    protected class AbstractConversationWebViewClient extends WebViewClient {
689        @Override
690        public boolean shouldOverrideUrlLoading(WebView view, String url) {
691            final Activity activity = getActivity();
692            if (activity == null) {
693                return false;
694            }
695
696            boolean result = false;
697            final Intent intent;
698            Uri uri = Uri.parse(url);
699            if (!Utils.isEmpty(mAccount.viewIntentProxyUri)) {
700                intent = generateProxyIntent(uri);
701            } else {
702                intent = new Intent(Intent.ACTION_VIEW, uri);
703                intent.putExtra(Browser.EXTRA_APPLICATION_ID, activity.getPackageName());
704            }
705
706            try {
707                intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
708                activity.startActivity(intent);
709                result = true;
710            } catch (ActivityNotFoundException ex) {
711                // If no application can handle the URL, assume that the
712                // caller can handle it.
713            }
714
715            return result;
716        }
717
718        private Intent generateProxyIntent(Uri uri) {
719            final Intent intent = new Intent(Intent.ACTION_VIEW, mAccount.viewIntentProxyUri);
720            intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ORIGINAL_URI, uri);
721            intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ACCOUNT, mAccount);
722
723            final Context context = getContext();
724            PackageManager manager = null;
725            // We need to catch the exception to make CanvasConversationHeaderView
726            // test pass.  Bug: http://b/issue?id=3470653.
727            try {
728                manager = context.getPackageManager();
729            } catch (UnsupportedOperationException e) {
730                LogUtils.e(LOG_TAG, e, "Error getting package manager");
731            }
732
733            if (manager != null) {
734                // Try and resolve the intent, to find an activity from this package
735                final List<ResolveInfo> resolvedActivities = manager.queryIntentActivities(
736                        intent, PackageManager.MATCH_DEFAULT_ONLY);
737
738                final String packageName = context.getPackageName();
739
740                // Now try and find one that came from this package, if one is not found, the UI
741                // provider must have specified an intent that is to be handled by a different apk.
742                // In that case, the class name will not be set on the intent, so the default
743                // intent resolution will be used.
744                for (ResolveInfo resolveInfo: resolvedActivities) {
745                    final ActivityInfo activityInfo = resolveInfo.activityInfo;
746                    if (packageName.equals(activityInfo.packageName)) {
747                        intent.setClassName(activityInfo.packageName, activityInfo.name);
748                        break;
749                    }
750                }
751            }
752
753            return intent;
754        }
755    }
756
757    public abstract void onConversationUpdated(Conversation conversation);
758
759    /**
760     * Small Runnable-like wrapper that first checks that the Fragment is in a good state before
761     * doing any work. Ideal for use with a {@link Handler}.
762     */
763    protected abstract class FragmentRunnable implements Runnable {
764
765        private final String mOpName;
766
767        public FragmentRunnable(String opName) {
768            mOpName = opName;
769        }
770
771        public abstract void go();
772
773        @Override
774        public void run() {
775            if (!isAdded()) {
776                LogUtils.i(LOG_TAG, "Unable to run op='%s' b/c fragment is not attached: %s",
777                        mOpName, AbstractConversationViewFragment.this);
778                return;
779            }
780            go();
781        }
782
783    }
784
785    private static boolean isConversationViewModeSet(final Account acct) {
786        return acct.settings.conversationViewMode != UIProvider.ConversationViewMode.UNDEFINED;
787    }
788
789    private void showAutoFitPrompt() {
790        // If the user has never set a conversation view mode, and they view a wide message, we
791        // should prompt them to turn on auto-fit
792        final boolean enablePrompt =
793                getResources().getInteger(R.integer.prompt_auto_fit_on_first_wide_message) == 1;
794        // TODO: Enable this dialog for Email and ensure it saves the setting properly, and remove
795        // R.integer.prompt_auto_fit_on_first_wide_message
796        if (enablePrompt && isUserVisible() && !isConversationViewModeSet(mAccount)) {
797            final boolean isWideContent =
798                    getWebView().canScrollHorizontally(1) || getWebView().canScrollHorizontally(-1);
799
800            final boolean dialogShowing =
801                    getFragmentManager().findFragmentByTag(AutoFitPromptDialogFragment.FRAGMENT_TAG)
802                    != null;
803
804            if (isWideContent && !dialogShowing) {
805                // Not everything fits, so let's prompt them to set an auto-fit value
806                AutoFitPromptDialogFragment.newInstance(mAccount.updateSettingsUri)
807                        .show(getFragmentManager(), AutoFitPromptDialogFragment.FRAGMENT_TAG);
808            }
809        }
810    }
811
812    public void onDetachedModeEntered() {
813        // If we have no messages, then we have nothing to display, so leave this view.
814        // Otherwise, just set the detached flag.
815        final Cursor messageCursor = getMessageCursor();
816
817        if (messageCursor == null || messageCursor.getCount() == 0) {
818            popOut();
819        } else {
820            mIsDetached = true;
821        }
822    }
823}
824