AbstractConversationViewFragment.java revision 4d8cad5e37ade03903a23cca8ea3e782af21170f
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.Animator.AnimatorListener;
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.text.Spannable;
43import android.text.SpannableStringBuilder;
44import android.text.TextUtils;
45import android.text.style.ForegroundColorSpan;
46import android.view.Menu;
47import android.view.MenuInflater;
48import android.view.MenuItem;
49import android.view.View;
50import android.webkit.WebView;
51import android.webkit.WebViewClient;
52import android.widget.TextView;
53
54import com.android.mail.ContactInfo;
55import com.android.mail.ContactInfoSource;
56import com.android.mail.FormattedDateBuilder;
57import com.android.mail.R;
58import com.android.mail.SenderInfoLoader;
59import com.android.mail.browse.MessageCursor;
60import com.android.mail.browse.ConversationViewAdapter.ConversationAccountController;
61import com.android.mail.browse.ConversationViewHeader.ConversationViewHeaderCallbacks;
62import com.android.mail.browse.MessageCursor.ConversationController;
63import com.android.mail.browse.MessageHeaderView.MessageHeaderViewCallbacks;
64import com.android.mail.providers.Account;
65import com.android.mail.providers.AccountObserver;
66import com.android.mail.providers.Address;
67import com.android.mail.providers.Conversation;
68import com.android.mail.providers.Folder;
69import com.android.mail.providers.ListParams;
70import com.android.mail.providers.UIProvider;
71import com.android.mail.providers.UIProvider.AccountCapabilities;
72import com.android.mail.providers.UIProvider.FolderCapabilities;
73import com.android.mail.utils.LogTag;
74import com.android.mail.utils.LogUtils;
75import com.android.mail.utils.Utils;
76import com.google.common.collect.ImmutableMap;
77import com.google.common.collect.Maps;
78import com.google.common.collect.Sets;
79
80import java.util.Arrays;
81import java.util.List;
82import java.util.Map;
83import java.util.Set;
84
85public abstract class AbstractConversationViewFragment extends Fragment implements
86        ConversationController, ConversationAccountController, MessageHeaderViewCallbacks,
87        ConversationViewHeaderCallbacks {
88
89    private static final String ARG_ACCOUNT = "account";
90    public static final String ARG_CONVERSATION = "conversation";
91    private static final String ARG_FOLDER = "folder";
92    private static final String LOG_TAG = LogTag.getLogTag();
93    protected static final int MESSAGE_LOADER = 0;
94    protected static final int CONTACT_LOADER = 1;
95    private static int sSubjectColor = Integer.MIN_VALUE;
96    private static int sSnippetColor = Integer.MIN_VALUE;
97    private static long sMinDelay = -1;
98    protected ControllableActivity mActivity;
99    private final MessageLoaderCallbacks mMessageLoaderCallbacks = new MessageLoaderCallbacks();
100    protected FormattedDateBuilder mDateBuilder;
101    private final ContactLoaderCallbacks mContactLoaderCallbacks = new ContactLoaderCallbacks();
102    private MenuItem mChangeFoldersMenuItem;
103    protected Conversation mConversation;
104    protected Folder mFolder;
105    protected String mBaseUri;
106    protected Account mAccount;
107    protected final Map<String, Address> mAddressCache = Maps.newHashMap();
108    protected boolean mEnableContentReadySignal;
109    private MessageCursor mCursor;
110    private Context mContext;
111    public boolean mUserVisible;
112    private View mProgressView;
113    private View mBackgroundView;
114    private View mInfoView;
115    private final Handler mHandler = new Handler();
116
117    /**
118     * Parcelable state of the conversation view. Can safely be used without null checking any time
119     * after {@link #onCreateView(android.view.LayoutInflater, android.view.ViewGroup, Bundle)}.
120     */
121    protected ConversationViewState mViewState;
122
123    /**
124     * Handles a deferred 'mark read' operation, necessary when the conversation view has finished
125     * loading before the conversation cursor. Normally null unless this situation occurs.
126     * When finally able to 'mark read', this observer will also be unregistered and cleaned up.
127     */
128    private MarkReadObserver mMarkReadObserver;
129
130    private Runnable mDelayedShow = new Runnable() {
131        @Override
132        public void run() {
133            mBackgroundView.setVisibility(View.VISIBLE);
134            String senders = mConversation.getSenders(getContext());
135            if (!TextUtils.isEmpty(senders) && mConversation.subject != null) {
136                mInfoView.setVisibility(View.VISIBLE);
137                mSendersView.setText(senders);
138                mSubjectView.setText(createSubjectSnippet(mConversation.subject,
139                        mConversation.getSnippet()));
140            } else {
141                mProgressView.setVisibility(View.VISIBLE);
142            }
143        }
144    };
145
146    private final AccountObserver mAccountObserver = new AccountObserver() {
147        @Override
148        public void onChanged(Account newAccount) {
149            mAccount = newAccount;
150            onAccountChanged();
151        }
152    };
153    private TextView mSendersView;
154    private TextView mSubjectView;
155
156    public static Bundle makeBasicArgs(Account account, Folder folder) {
157        Bundle args = new Bundle();
158        args.putParcelable(ARG_ACCOUNT, account);
159        args.putParcelable(ARG_FOLDER, folder);
160        return args;
161    }
162
163    /**
164     * Constructor needs to be public to handle orientation changes and activity
165     * lifecycle events.
166     */
167    public AbstractConversationViewFragment() {
168        super();
169    }
170
171    /**
172     * Subclasses must override, since this depends on how many messages are
173     * shown in the conversation view.
174     */
175    protected abstract void markUnread();
176
177    /**
178     * Subclasses must override this, since they may want to display a single or
179     * many messages related to this conversation.
180     */
181    protected abstract void onMessageCursorLoadFinished(Loader<Cursor> loader, Cursor data,
182            boolean wasNull, boolean messageCursorChanged);
183
184    /**
185     * Subclasses must override this, since they may want to display a single or
186     * many messages related to this conversation.
187     */
188    @Override
189    public abstract void onConversationViewHeaderHeightChange(int newHeight);
190
191    public abstract void onUserVisibleHintChanged();
192
193    /**
194     * Subclasses must override this.
195     */
196    protected abstract void onAccountChanged();
197
198    @Override
199    public void onCreate(Bundle savedState) {
200        super.onCreate(savedState);
201
202        final Bundle args = getArguments();
203        mAccount = args.getParcelable(ARG_ACCOUNT);
204        mConversation = args.getParcelable(ARG_CONVERSATION);
205        mFolder = args.getParcelable(ARG_FOLDER);
206        // On JB or newer, we use the 'webkitAnimationStart' DOM event to signal load complete
207        // Below JB, try to speed up initial render by having the webview do supplemental draws to
208        // custom a software canvas.
209        // TODO(mindyp):
210        //PAGE READINESS SIGNAL FOR JELLYBEAN AND NEWER
211        // Notify the app on 'webkitAnimationStart' of a simple dummy element with a simple no-op
212        // animation that immediately runs on page load. The app uses this as a signal that the
213        // content is loaded and ready to draw, since WebView delays firing this event until the
214        // layers are composited and everything is ready to draw.
215        // This signal does not seem to be reliable, so just use the old method for now.
216        mEnableContentReadySignal = false; //Utils.isRunningJellybeanOrLater();
217        LogUtils.d(LOG_TAG, "onCreate in ConversationViewFragment (this=%s)", this);
218        // Not really, we just want to get a crack to store a reference to the change_folder item
219        setHasOptionsMenu(true);
220    }
221
222    public void instantiateProgressIndicators(View rootView) {
223        mSendersView = (TextView) rootView.findViewById(R.id.senders_view);
224        mSubjectView = (TextView) rootView.findViewById(R.id.info_subject_view);
225        mBackgroundView = rootView.findViewById(R.id.background_view);
226        mInfoView = rootView.findViewById(R.id.info_view);
227        mProgressView = rootView.findViewById(R.id.loading_progress);
228    }
229
230    protected void dismissLoadingStatus() {
231        if (mBackgroundView.getVisibility() != View.VISIBLE) {
232            // The runnable hasn't run yet, so just remove it.
233            mHandler.removeCallbacks(mDelayedShow);
234            return;
235        }
236        // Fade out the info view.
237        if (mBackgroundView.getVisibility() == View.VISIBLE) {
238            Animator animator = AnimatorInflater.loadAnimator(getContext(), R.anim.fade_out);
239            animator.setTarget(mBackgroundView);
240            animator.addListener(new AnimatorListener() {
241                @Override
242                public void onAnimationStart(Animator animation) {
243                    if (mProgressView.getVisibility() != View.VISIBLE) {
244                        mProgressView.setVisibility(View.GONE);
245                    }
246                }
247
248                @Override
249                public void onAnimationEnd(Animator animation) {
250                    mBackgroundView.setVisibility(View.GONE);
251                    mInfoView.setVisibility(View.GONE);
252                    mProgressView.setVisibility(View.GONE);
253                }
254
255                @Override
256                public void onAnimationCancel(Animator animation) {
257                    // Do nothing.
258                }
259
260                @Override
261                public void onAnimationRepeat(Animator animation) {
262                    // Do nothing.
263                }
264            });
265            animator.start();
266        } else {
267            mBackgroundView.setVisibility(View.GONE);
268            mInfoView.setVisibility(View.GONE);
269            mProgressView.setVisibility(View.GONE);
270        }
271    }
272
273    @Override
274    public void onActivityCreated(Bundle savedInstanceState) {
275        super.onActivityCreated(savedInstanceState);
276        final Activity activity = getActivity();
277        if (!(activity instanceof ControllableActivity)) {
278            LogUtils.wtf(LOG_TAG, "ConversationViewFragment expects only a ControllableActivity to"
279                    + "create it. Cannot proceed.");
280        }
281        if (activity == null || activity.isFinishing()) {
282            // Activity is finishing, just bail.
283            return;
284        }
285        mActivity = (ControllableActivity) activity;
286        mContext = activity.getApplicationContext();
287        mDateBuilder = new FormattedDateBuilder((Context) mActivity);
288        mAccount = mAccountObserver.initialize(mActivity.getAccountController());
289    }
290
291    @Override
292    public ConversationUpdater getListController() {
293        final ControllableActivity activity = (ControllableActivity) getActivity();
294        return activity != null ? activity.getConversationUpdater() : null;
295    }
296
297
298    protected void showLoadingStatus() {
299        if (sMinDelay == -1) {
300            sMinDelay = getContext().getResources()
301                    .getInteger(R.integer.conversationview_show_loading_delay);
302        }
303        // In case there were any other instances around, get rid of them.
304        mHandler.removeCallbacks(mDelayedShow);
305        mHandler.postDelayed(mDelayedShow, sMinDelay);
306    }
307
308    private CharSequence createSubjectSnippet(CharSequence subject, CharSequence snippet) {
309        if (TextUtils.isEmpty(subject) && TextUtils.isEmpty(snippet)) {
310            return "";
311        }
312        if (subject == null) {
313            subject = "";
314        }
315        if (snippet == null) {
316            snippet = "";
317        }
318        SpannableStringBuilder subjectText = new SpannableStringBuilder(getContext().getString(
319                R.string.subject_and_snippet, subject, snippet));
320        ensureSubjectSnippetColors();
321        int snippetStart = 0;
322        int fontColor = sSubjectColor;
323        subjectText.setSpan(new ForegroundColorSpan(fontColor), 0, subject.length(),
324                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
325        snippetStart = subject.length() + 1;
326        fontColor = sSnippetColor;
327        subjectText.setSpan(new ForegroundColorSpan(fontColor), snippetStart, subjectText.length(),
328                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
329        return subjectText;
330    }
331
332    private void ensureSubjectSnippetColors() {
333        if (sSubjectColor == Integer.MIN_VALUE) {
334            Resources res = getContext().getResources();
335            sSubjectColor = res.getColor(R.color.subject_text_color_read);
336            sSnippetColor = res.getColor(R.color.snippet_text_color_read);
337        }
338    }
339
340    public Context getContext() {
341        return mContext;
342    }
343
344    public Conversation getConversation() {
345        return mConversation;
346    }
347
348    @Override
349    public MessageCursor getMessageCursor() {
350        return mCursor;
351    }
352
353    public Handler getHandler() {
354        return mHandler;
355    }
356
357    public MessageLoaderCallbacks getMessageLoaderCallbacks() {
358        return mMessageLoaderCallbacks;
359    }
360
361    public ContactLoaderCallbacks getContactInfoSource() {
362        return mContactLoaderCallbacks;
363    }
364
365    @Override
366    public Account getAccount() {
367        return mAccount;
368    }
369
370    @Override
371    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
372        super.onCreateOptionsMenu(menu, inflater);
373        mChangeFoldersMenuItem = menu.findItem(R.id.change_folder);
374    }
375
376    @Override
377    public boolean onOptionsItemSelected(MenuItem item) {
378        boolean handled = false;
379        switch (item.getItemId()) {
380            case R.id.inside_conversation_unread:
381                markUnread();
382                handled = true;
383                break;
384        }
385        return handled;
386    }
387
388    // BEGIN conversation header callbacks
389    @Override
390    public void onFoldersClicked() {
391        if (mChangeFoldersMenuItem == null) {
392            LogUtils.e(LOG_TAG, "unable to open 'change folders' dialog for a conversation");
393            return;
394        }
395        mActivity.onOptionsItemSelected(mChangeFoldersMenuItem);
396    }
397
398    @Override
399    public String getSubjectRemainder(String subject) {
400        final SubjectDisplayChanger sdc = mActivity.getSubjectDisplayChanger();
401        if (sdc == null) {
402            return subject;
403        }
404        return sdc.getUnshownSubject(subject);
405    }
406    // END conversation header callbacks
407
408    @Override
409    public void onDestroyView() {
410        super.onDestroyView();
411        mAccountObserver.unregisterAndDestroy();
412        if (mMarkReadObserver != null) {
413            mActivity.getConversationUpdater().unregisterConversationListObserver(
414                    mMarkReadObserver);
415            mMarkReadObserver = null;
416        }
417    }
418
419    /**
420     * {@link #setUserVisibleHint(boolean)} only works on API >= 15, so implement our own for
421     * reliability on older platforms.
422     */
423    public void setExtraUserVisibleHint(boolean isVisibleToUser) {
424        LogUtils.v(LOG_TAG, "in CVF.setHint, val=%s (%s)", isVisibleToUser, this);
425        if (mUserVisible != isVisibleToUser) {
426            mUserVisible = isVisibleToUser;
427            MessageCursor cursor = getMessageCursor();
428            if (mUserVisible && (cursor != null && cursor.isLoaded() && cursor.getCount() == 0)) {
429                // Pop back to conversation list and show error.
430                onError();
431                return;
432            }
433            onUserVisibleHintChanged();
434        }
435    }
436
437    private class MessageLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
438
439        @Override
440        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
441            return new MessageLoader(mActivity.getActivityContext(), mConversation,
442                    AbstractConversationViewFragment.this);
443        }
444
445        @Override
446        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
447            // ignore truly duplicate results
448            // this can happen when restoring after rotation
449            if (mCursor == data) {
450                return;
451            } else {
452                MessageCursor messageCursor = (MessageCursor) data;
453
454                if (LogUtils.isLoggable(LOG_TAG, LogUtils.DEBUG)) {
455                    LogUtils.d(LOG_TAG, "LOADED CONVERSATION= %s", messageCursor.getDebugDump());
456                }
457
458                // When the last cursor had message(s), and the new version has
459                // no messages, we need to exit conversation view.
460                if (messageCursor.getCount() == 0 && mCursor != null) {
461                    if (mUserVisible) {
462                        onError();
463                    } else {
464                        // we expect that the pager adapter will remove this
465                        // conversation fragment on its own due to a separate
466                        // conversation cursor update (we might get here if the
467                        // message list update fires first. nothing to do
468                        // because we expect to be torn down soon.)
469                        LogUtils.i(LOG_TAG, "CVF: offscreen conv has no messages, ignoring update"
470                                + " in anticipation of conv cursor update. c=%s", mConversation.uri);
471                    }
472                    return;
473                }
474
475                // ignore cursors that are still loading results
476                if (!messageCursor.isLoaded()) {
477                    return;
478                }
479                boolean wasNull = mCursor == null;
480                boolean messageCursorChanged = mCursor != null
481                        && messageCursor.hashCode() != mCursor.hashCode();
482                mCursor = (MessageCursor) data;
483                onMessageCursorLoadFinished(loader, data, wasNull, messageCursorChanged);
484            }
485        }
486
487        @Override
488        public void onLoaderReset(Loader<Cursor> loader) {
489            mCursor = null;
490        }
491
492    }
493
494    private void onError() {
495        // need to exit this view- conversation may have been
496        // deleted, or for whatever reason is now invalid (e.g.
497        // discard single draft)
498        //
499        // N.B. this may involve a fragment transaction, which
500        // FragmentManager will refuse to execute directly
501        // within onLoadFinished. Make sure the controller knows.
502        LogUtils.i(LOG_TAG, "CVF: visible conv has no messages, exiting conv mode");
503        // TODO(mindyp): handle ERROR status by showing an error
504        // message to the user that there are no messages in
505        // this conversation
506        mHandler.post(new Runnable() {
507
508            @Override
509            public void run() {
510                mActivity.getListHandler()
511                .onConversationSelected(null, true /* inLoaderCallbacks */);
512            }
513
514        });
515    }
516
517    protected void onConversationSeen() {
518        // Ignore unsafe calls made after a fragment is detached from an activity
519        final ControllableActivity activity = (ControllableActivity) getActivity();
520        if (activity == null) {
521            LogUtils.w(LOG_TAG, "ignoring onConversationSeen for conv=%s", mConversation.id);
522            return;
523        }
524
525        mViewState.setInfoForConversation(mConversation);
526
527        // mark viewed/read if not previously marked viewed by this conversation view,
528        // or if unread messages still exist in the message list cursor
529        // we don't want to keep marking viewed on rotation or restore
530        // but we do want future re-renders to mark read (e.g. "New message from X" case)
531        MessageCursor cursor = getMessageCursor();
532        if (!mConversation.isViewed() || (cursor != null && !cursor.isConversationRead())) {
533            final ConversationUpdater listController = activity.getConversationUpdater();
534            // The conversation cursor may not have finished loading by now (when launched via
535            // notification), so watch for when it finishes and mark it read then.
536            if (listController.getConversationListCursor() == null) {
537                LogUtils.i(LOG_TAG, "deferring conv mark read on open for id=%d",
538                        mConversation.id);
539                mMarkReadObserver = new MarkReadObserver(listController);
540                listController.registerConversationListObserver(mMarkReadObserver);
541            } else {
542                markReadOnSeen(listController);
543            }
544        }
545
546        activity.getListHandler().onConversationSeen(mConversation);
547    }
548
549    protected void markReadOnSeen(ConversationUpdater listController) {
550        // Mark the conversation viewed and read.
551        listController.markConversationsRead(Arrays.asList(mConversation), true /* read */,
552                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        MessageCursor cursor = getMessageCursor();
557        if (cursor != null) {
558            cursor.markMessagesRead();
559        }
560    }
561
562    protected ConversationViewState getNewViewState() {
563        return new ConversationViewState();
564    }
565
566    private static class MessageLoader extends CursorLoader {
567        private boolean mDeliveredFirstResults = false;
568        private final Conversation mConversation;
569        private final ConversationController mController;
570
571        public MessageLoader(Context c, Conversation conv, ConversationController controller) {
572            super(c, conv.messageListUri, UIProvider.MESSAGE_PROJECTION, null, null, null);
573            mConversation = conv;
574            mController = controller;
575        }
576
577        @Override
578        public Cursor loadInBackground() {
579            return new MessageCursor(super.loadInBackground(), mConversation, mController);
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    private class MarkReadObserver extends DataSetObserver {
729        private final ConversationUpdater mListController;
730
731        private MarkReadObserver(ConversationUpdater listController) {
732            mListController = listController;
733        }
734
735        @Override
736        public void onChanged() {
737            if (mListController.getConversationListCursor() == null) {
738                // nothing yet, keep watching
739                return;
740            }
741            // done loading, safe to mark read now
742            mListController.unregisterConversationListObserver(this);
743            mMarkReadObserver = null;
744            LogUtils.i(LOG_TAG, "running deferred conv mark read on open, id=%d", mConversation.id);
745            markReadOnSeen(mListController);
746        }
747    }
748
749    public abstract void onConversationUpdated(Conversation conversation);
750
751}
752