1/*
2 * Copyright (C) 2013 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.browse;
19
20import android.app.Activity;
21import android.app.Fragment;
22import android.app.LoaderManager;
23import android.content.CursorLoader;
24import android.content.Loader;
25import android.database.Cursor;
26import android.net.Uri;
27import android.os.Bundle;
28import android.os.Handler;
29import android.provider.OpenableColumns;
30import android.support.v7.app.AppCompatActivity;
31import android.view.LayoutInflater;
32import android.view.Menu;
33import android.view.MenuInflater;
34import android.view.MenuItem;
35import android.view.View;
36import android.view.ViewGroup;
37import android.webkit.WebResourceResponse;
38import android.webkit.WebView;
39import android.widget.Toast;
40
41import com.android.emailcommon.mail.Address;
42import com.android.mail.R;
43import com.android.mail.providers.Account;
44import com.android.mail.ui.AbstractConversationWebViewClient;
45import com.android.mail.ui.ContactLoaderCallbacks;
46import com.android.mail.ui.SecureConversationViewController;
47import com.android.mail.ui.SecureConversationViewControllerCallbacks;
48import com.android.mail.utils.LogTag;
49import com.android.mail.utils.LogUtils;
50import com.android.mail.utils.Utils;
51import com.google.common.collect.ImmutableList;
52import com.google.common.collect.Sets;
53
54import java.util.Collections;
55import java.util.HashMap;
56import java.util.List;
57import java.util.Map;
58import java.util.Set;
59
60/**
61 * Fragment that is used to view EML files. It is mostly stubs
62 * that calls {@link SecureConversationViewController} to do most
63 * of the rendering work.
64 */
65public class EmlMessageViewFragment extends Fragment
66        implements SecureConversationViewControllerCallbacks {
67    private static final String ARG_EML_FILE_URI = "eml_file_uri";
68    private static final String BASE_URI = "x-thread://message/rfc822/";
69
70    private static final int MESSAGE_LOADER = 0;
71    private static final int CONTACT_LOADER = 1;
72    private static final int FILENAME_LOADER = 2;
73
74    private static final String LOG_TAG = LogTag.getLogTag();
75
76    private final Handler mHandler = new Handler();
77
78    private EmlWebViewClient mWebViewClient;
79    private SecureConversationViewController mViewController;
80    private ContactLoaderCallbacks mContactLoaderCallbacks;
81
82    private final MessageLoadCallbacks mMessageLoadCallbacks = new MessageLoadCallbacks();
83    private final FilenameLoadCallbacks mFilenameLoadCallbacks = new FilenameLoadCallbacks();
84
85    private Uri mEmlFileUri;
86
87    private boolean mMessageLoadFailed;
88
89    /**
90     * Cache of email address strings to parsed Address objects.
91     * <p>
92     * Remember to synchronize on the map when reading or writing to this cache, because some
93     * instances use it off the UI thread (e.g. from WebView).
94     */
95    protected final Map<String, Address> mAddressCache = Collections.synchronizedMap(
96            new HashMap<String, Address>());
97
98    private class EmlWebViewClient extends AbstractConversationWebViewClient {
99        public EmlWebViewClient(Account account) {
100            super(account);
101        }
102
103        @Override
104        public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
105            // try to load the url assuming it is a cid url
106            final Uri uri = Uri.parse(url);
107            final WebResourceResponse response = loadCIDUri(uri, mViewController.getMessage());
108            if (response != null) {
109                return response;
110            }
111
112            return super.shouldInterceptRequest(view, url);
113        }
114
115        @Override
116        public void onPageFinished(WebView view, String url) {
117            // Ignore unsafe calls made after a fragment is detached from an activity.
118            // This method needs to, for example, get at the loader manager, which needs
119            // the fragment to be added.
120            if (!isAdded()) {
121                LogUtils.d(LOG_TAG, "ignoring EMLVF.onPageFinished, url=%s fragment=%s", url,
122                        EmlMessageViewFragment.this);
123                return;
124            }
125            mViewController.dismissLoadingStatus();
126
127            final Set<String> emailAddresses = Sets.newHashSet();
128            final List<Address> cacheCopy;
129            synchronized (mAddressCache) {
130                cacheCopy = ImmutableList.copyOf(mAddressCache.values());
131            }
132            for (Address addr : cacheCopy) {
133                emailAddresses.add(addr.getAddress());
134            }
135            final ContactLoaderCallbacks callbacks = getContactInfoSource();
136            callbacks.setSenders(emailAddresses);
137            getLoaderManager().restartLoader(CONTACT_LOADER, Bundle.EMPTY, callbacks);
138        }
139    }
140
141    /**
142     * Creates a new instance of {@link EmlMessageViewFragment},
143     * initialized to display an eml file from the specified {@link Uri}.
144     */
145    public static EmlMessageViewFragment newInstance(Uri emlFileUri, Uri accountUri) {
146        EmlMessageViewFragment f = new EmlMessageViewFragment();
147        Bundle args = new Bundle(1);
148        args.putParcelable(ARG_EML_FILE_URI, emlFileUri);
149        f.setArguments(args);
150        return f;
151    }
152
153    /**
154     * Constructor needs to be public to handle orientation changes and activity
155     * lifecycle events.
156     */
157    public EmlMessageViewFragment() {}
158
159    @Override
160    public void onCreate(Bundle savedState) {
161        super.onCreate(savedState);
162
163        Bundle args = getArguments();
164        mEmlFileUri = args.getParcelable(ARG_EML_FILE_URI);
165
166        mWebViewClient = new EmlWebViewClient(null);
167        mViewController = new SecureConversationViewController(this);
168
169        setHasOptionsMenu(true);
170    }
171
172    @Override
173    public View onCreateView(LayoutInflater inflater, ViewGroup container,
174            Bundle savedInstanceState) {
175        return mViewController.onCreateView(inflater, container, savedInstanceState);
176    }
177
178    @Override
179    public void onActivityCreated(Bundle savedInstanceState) {
180        super.onActivityCreated(savedInstanceState);
181        if (mMessageLoadFailed) {
182            bailOutOnLoadError();
183            return;
184        }
185        mWebViewClient.setActivity(getActivity());
186        mViewController.onActivityCreated(savedInstanceState);
187    }
188
189    @Override
190    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
191        if (Utils.isRunningKitkatOrLater()) {
192            inflater.inflate(R.menu.eml_fragment_menu, menu);
193        }
194    }
195
196    @Override
197    public boolean onOptionsItemSelected(MenuItem item) {
198        final int itemId = item.getItemId();
199        if (itemId == R.id.print_message) {
200            mViewController.printMessage();
201        } else {
202            return super.onOptionsItemSelected(item);
203        }
204
205        return true;
206    }
207
208    private void bailOutOnLoadError() {
209        final Activity activity = getActivity();
210        Toast.makeText(activity, R.string.eml_loader_error_toast, Toast.LENGTH_LONG).show();
211        activity.finish();
212    }
213
214    // Start SecureConversationViewControllerCallbacks
215
216    @Override
217    public Handler getHandler() {
218        return mHandler;
219    }
220
221    @Override
222    public AbstractConversationWebViewClient getWebViewClient() {
223        return mWebViewClient;
224    }
225
226    @Override
227    public Fragment getFragment() {
228        return this;
229    }
230
231    @Override
232    public void setupConversationHeaderView(ConversationViewHeader headerView) {
233        // DO NOTHING
234    }
235
236    @Override
237    public boolean isViewVisibleToUser() {
238        return true;
239    }
240
241    @Override
242    public ContactLoaderCallbacks getContactInfoSource() {
243        if (mContactLoaderCallbacks == null) {
244            mContactLoaderCallbacks = new ContactLoaderCallbacks(getActivity());
245        }
246        return mContactLoaderCallbacks;
247    }
248
249    @Override
250    public ConversationAccountController getConversationAccountController() {
251        return (EmlViewerActivity) getActivity();
252    }
253
254    @Override
255    public Map<String, Address> getAddressCache() {
256        return mAddressCache;
257    }
258
259    @Override
260    public void setupMessageHeaderVeiledMatcher(MessageHeaderView messageHeaderView) {
261        // DO NOTHING
262    }
263
264    @Override
265    public void startMessageLoader() {
266        final LoaderManager manager = getLoaderManager();
267        manager.initLoader(MESSAGE_LOADER, null, mMessageLoadCallbacks);
268        manager.initLoader(FILENAME_LOADER, null, mFilenameLoadCallbacks);
269    }
270
271    @Override
272    public String getBaseUri() {
273        return BASE_URI;
274    }
275
276    @Override
277    public boolean isViewOnlyMode() {
278        return true;
279    }
280
281    @Override
282    public boolean shouldAlwaysShowImages() {
283        return false;
284    }
285
286    // End SecureConversationViewControllerCallbacks
287
288    private class MessageLoadCallbacks
289            implements LoaderManager.LoaderCallbacks<ConversationMessage> {
290        @Override
291        public Loader<ConversationMessage> onCreateLoader(int id, Bundle args) {
292            switch (id) {
293                case MESSAGE_LOADER:
294                    return new EmlMessageLoader(getActivity(), mEmlFileUri);
295                default:
296                    return null;
297            }
298        }
299
300        @Override
301        public void onLoadFinished(Loader<ConversationMessage> loader, ConversationMessage data) {
302            if (data == null) {
303                final Activity activity = getActivity();
304                if (activity != null) {
305                    bailOutOnLoadError();
306                } else {
307                    mMessageLoadFailed = true;
308                }
309                return;
310            }
311            mViewController.setSubject(data.subject);
312            mViewController.renderMessage(data);
313        }
314
315        @Override
316        public void onLoaderReset(Loader<ConversationMessage> loader) {
317            // Do nothing
318        }
319    }
320
321    private class FilenameLoadCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
322        @Override
323        public Loader<Cursor> onCreateLoader(int id, Bundle args) {
324            switch (id) {
325                case FILENAME_LOADER:
326                    return new CursorLoader(getActivity(), mEmlFileUri,
327                            new String[] { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE },
328                            null, null, null);
329                default:
330                    return null;
331            }
332        }
333
334        @Override
335        public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
336            if (data == null || !data.moveToFirst()) {
337                return;
338            }
339
340            ((AppCompatActivity) getActivity()).getSupportActionBar().setTitle(
341                    data.getString(data.getColumnIndex(OpenableColumns.DISPLAY_NAME)));
342        }
343
344        @Override
345        public void onLoaderReset(Loader<Cursor> loader) {
346        }
347    }
348
349}
350