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