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