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.ui;
19
20import android.app.Activity;
21import android.content.ActivityNotFoundException;
22import android.content.ContentResolver;
23import android.content.Context;
24import android.content.Intent;
25import android.content.pm.ActivityInfo;
26import android.content.pm.PackageManager;
27import android.content.pm.ResolveInfo;
28import android.database.Cursor;
29import android.net.Uri;
30import android.os.ParcelFileDescriptor;
31import android.provider.Browser;
32import android.webkit.WebResourceResponse;
33import android.webkit.WebView;
34import android.webkit.WebViewClient;
35
36import com.android.mail.browse.ConversationMessage;
37import com.android.mail.providers.Account;
38import com.android.mail.providers.Attachment;
39import com.android.mail.providers.UIProvider;
40import com.android.mail.utils.LogTag;
41import com.android.mail.utils.LogUtils;
42import com.android.mail.utils.Utils;
43
44import java.io.FileInputStream;
45import java.io.FileNotFoundException;
46import java.io.InputStream;
47import java.util.List;
48
49/**
50 * Base implementation of a web view client for the conversation views.
51 * Handles proxying the view intent so that additional information can
52 * be sent with the intent when links are clicked.
53 */
54public class AbstractConversationWebViewClient extends WebViewClient {
55    private static final String LOG_TAG = LogTag.getLogTag();
56
57    private Account mAccount;
58    private Activity mActivity;
59
60    public AbstractConversationWebViewClient(Account account) {
61        mAccount = account;
62    }
63
64    public void setAccount(Account account) {
65        mAccount = account;
66    }
67
68    public void setActivity(Activity activity) {
69        mActivity = activity;
70    }
71
72    public Activity getActivity() {
73        return mActivity;
74    }
75
76    /**
77     * Translates Content ID urls (CID urls) into provider queries for the associated attachment.
78     * With the attachment in hand, it's trivial to open a stream to the file containing the content
79     * of the attachment.
80     *
81     * @param uri the raw URI from the HTML document in the Webview
82     * @param message the message containing the HTML that is being rendered
83     * @return a response if a stream to the attachment file can be created from the CID URL;
84     *      <tt>null</tt> if it cannot for any reason
85     */
86    protected final WebResourceResponse loadCIDUri(Uri uri, ConversationMessage message) {
87        // if the url is not a CID url, we do nothing
88        if (!"cid".equals(uri.getScheme())) {
89            return null;
90        }
91
92        // cid urls can be translated to content urls
93        final String cid = uri.getSchemeSpecificPart();
94        if (cid == null) {
95            return null;
96        }
97
98        if (message.attachmentByCidUri == null) {
99            return null;
100        }
101
102        final Uri queryUri = Uri.withAppendedPath(message.attachmentByCidUri, cid);
103        if (queryUri == null) {
104            return null;
105        }
106
107        // query for the attachment using its cid
108        final ContentResolver cr = getActivity().getContentResolver();
109        final Cursor c = cr.query(queryUri, UIProvider.ATTACHMENT_PROJECTION, null, null, null);
110        if (c == null) {
111            return null;
112        }
113
114        // create the attachment from the cursor, if one was found
115        final Attachment target;
116        try {
117            if (!c.moveToFirst()) {
118                return null;
119            }
120            target = new Attachment(c);
121        } finally {
122            c.close();
123        }
124
125        // try to return a response that includes a stream to the attachment data
126        try {
127            final ParcelFileDescriptor fd = cr.openFileDescriptor(target.contentUri, "r");
128            final InputStream stream = new FileInputStream(fd.getFileDescriptor());
129            return new WebResourceResponse(target.getContentType(), null, stream);
130        } catch (FileNotFoundException e) {
131            // if no attachment file was found return null to let webview handle it
132            return null;
133        }
134    }
135
136    @Override
137    public boolean shouldOverrideUrlLoading(WebView view, String url) {
138        if (mActivity == null) {
139            return false;
140        }
141
142        final Uri uri = Uri.parse(url);
143        if (Utils.divertMailtoUri(mActivity, uri, mAccount)) {
144            return true;
145        }
146
147        final Intent intent;
148        if (mAccount != null && !Utils.isEmpty(mAccount.viewIntentProxyUri)) {
149            intent = generateProxyIntent(uri);
150        } else {
151            intent = new Intent(Intent.ACTION_VIEW, uri);
152            intent.putExtra(Browser.EXTRA_APPLICATION_ID, mActivity.getPackageName());
153            intent.putExtra(Browser.EXTRA_CREATE_NEW_TAB, true);
154        }
155
156        boolean result = false;
157        try {
158            intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET
159                    | Intent.FLAG_ACTIVITY_NO_ANIMATION);
160            mActivity.startActivity(intent);
161            result = true;
162        } catch (ActivityNotFoundException ex) {
163            // If no application can handle the URL, assume that the
164            // caller can handle it.
165        }
166
167        return result;
168    }
169
170    private Intent generateProxyIntent(Uri uri) {
171        return generateProxyIntent(
172                mActivity, mAccount.viewIntentProxyUri, uri, mAccount.getEmailAddress());
173    }
174
175    public static Intent generateProxyIntent(
176            Context context, Uri proxyUri, Uri uri, String accountName) {
177        final Intent intent = new Intent(Intent.ACTION_VIEW, proxyUri);
178        intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ORIGINAL_URI, uri);
179        intent.putExtra(UIProvider.ViewProxyExtras.EXTRA_ACCOUNT_NAME, accountName);
180
181        PackageManager manager = null;
182        // We need to catch the exception to make CanvasConversationHeaderView
183        // test pass.  Bug: http://b/issue?id=3470653.
184        try {
185            manager = context.getPackageManager();
186        } catch (UnsupportedOperationException e) {
187            LogUtils.e(LOG_TAG, e, "Error getting package manager");
188        }
189
190        if (manager != null) {
191            // Try and resolve the intent, to find an activity from this package
192            final List<ResolveInfo> resolvedActivities = manager.queryIntentActivities(
193                    intent, PackageManager.MATCH_DEFAULT_ONLY);
194
195            final String packageName = context.getPackageName();
196
197            // Now try and find one that came from this package, if one is not found, the UI
198            // provider must have specified an intent that is to be handled by a different apk.
199            // In that case, the class name will not be set on the intent, so the default
200            // intent resolution will be used.
201            for (ResolveInfo resolveInfo: resolvedActivities) {
202                final ActivityInfo activityInfo = resolveInfo.activityInfo;
203                if (packageName.equals(activityInfo.packageName)) {
204                    intent.setClassName(activityInfo.packageName, activityInfo.name);
205                    break;
206                }
207            }
208        }
209
210        return intent;
211    }
212}
213