1// Copyright 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5package org.chromium.android_webview;
6
7import android.content.Context;
8import android.content.res.AssetManager;
9import android.net.Uri;
10import android.util.Log;
11import android.util.TypedValue;
12
13import org.chromium.base.CalledByNative;
14import org.chromium.base.JNINamespace;
15
16import java.io.IOException;
17import java.io.InputStream;
18import java.net.URLConnection;
19import java.util.List;
20
21/**
22 * Implements the Java side of Android URL protocol jobs.
23 * See android_protocol_handler.cc.
24 */
25@JNINamespace("android_webview")
26public class AndroidProtocolHandler {
27    private static final String TAG = "AndroidProtocolHandler";
28
29    // Supported URL schemes. This needs to be kept in sync with
30    // clank/native/framework/chrome/url_request_android_job.cc.
31    private static final String FILE_SCHEME = "file";
32    private static final String CONTENT_SCHEME = "content";
33
34    /**
35     * Open an InputStream for an Android resource.
36     * @param context The context manager.
37     * @param url The url to load.
38     * @return An InputStream to the Android resource.
39     */
40    @CalledByNative
41    public static InputStream open(Context context, String url) {
42        Uri uri = verifyUrl(url);
43        if (uri == null) {
44            return null;
45        }
46        try {
47            String path = uri.getPath();
48            if (uri.getScheme().equals(FILE_SCHEME)) {
49                if (path.startsWith(nativeGetAndroidAssetPath())) {
50                    return openAsset(context, uri);
51                } else if (path.startsWith(nativeGetAndroidResourcePath())) {
52                    return openResource(context, uri);
53                }
54            } else if (uri.getScheme().equals(CONTENT_SCHEME)) {
55                return openContent(context, uri);
56            }
57        } catch (Exception ex) {
58            Log.e(TAG, "Error opening inputstream: " + url);
59        }
60        return null;
61    }
62
63    private static int getFieldId(Context context, String assetType, String assetName)
64        throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
65        Class<?> d = context.getClassLoader()
66            .loadClass(context.getPackageName() + ".R$" + assetType);
67        java.lang.reflect.Field field = d.getField(assetName);
68        int id = field.getInt(null);
69        return id;
70    }
71
72    private static int getValueType(Context context, int fieldId) {
73        TypedValue value = new TypedValue();
74        context.getResources().getValue(fieldId, value, true);
75        return value.type;
76    }
77
78    private static InputStream openResource(Context context, Uri uri) {
79        assert uri.getScheme().equals(FILE_SCHEME);
80        assert uri.getPath() != null;
81        assert uri.getPath().startsWith(nativeGetAndroidResourcePath());
82        // The path must be of the form "/android_res/asset_type/asset_name.ext".
83        List<String> pathSegments = uri.getPathSegments();
84        if (pathSegments.size() != 3) {
85            Log.e(TAG, "Incorrect resource path: " + uri);
86            return null;
87        }
88        String assetPath = pathSegments.get(0);
89        String assetType = pathSegments.get(1);
90        String assetName = pathSegments.get(2);
91        if (!("/" + assetPath + "/").equals(nativeGetAndroidResourcePath())) {
92            Log.e(TAG, "Resource path does not start with " + nativeGetAndroidResourcePath() +
93                  ": " + uri);
94            return null;
95        }
96        // Drop the file extension.
97        assetName = assetName.split("\\.")[0];
98        try {
99            // Use the application context for resolving the resource package name so that we do
100            // not use the browser's own resources. Note that if 'context' here belongs to the
101            // test suite, it does not have a separate application context. In that case we use
102            // the original context object directly.
103            if (context.getApplicationContext() != null) {
104                context = context.getApplicationContext();
105            }
106            int fieldId = getFieldId(context, assetType, assetName);
107            int valueType = getValueType(context, fieldId);
108            if (valueType == TypedValue.TYPE_STRING) {
109                return context.getResources().openRawResource(fieldId);
110            } else {
111                Log.e(TAG, "Asset not of type string: " + uri);
112                return null;
113            }
114        } catch (ClassNotFoundException e) {
115            Log.e(TAG, "Unable to open resource URL: " + uri, e);
116            return null;
117        } catch (NoSuchFieldException e) {
118            Log.e(TAG, "Unable to open resource URL: " + uri, e);
119            return null;
120        } catch (IllegalAccessException e) {
121            Log.e(TAG, "Unable to open resource URL: " + uri, e);
122            return null;
123        }
124    }
125
126    private static InputStream openAsset(Context context, Uri uri) {
127        assert uri.getScheme().equals(FILE_SCHEME);
128        assert uri.getPath() != null;
129        assert uri.getPath().startsWith(nativeGetAndroidAssetPath());
130        String path = uri.getPath().replaceFirst(nativeGetAndroidAssetPath(), "");
131        try {
132            AssetManager assets = context.getAssets();
133            return assets.open(path, AssetManager.ACCESS_STREAMING);
134        } catch (IOException e) {
135            Log.e(TAG, "Unable to open asset URL: " + uri);
136            return null;
137        }
138    }
139
140    private static InputStream openContent(Context context, Uri uri) {
141        assert uri.getScheme().equals(CONTENT_SCHEME);
142        try {
143            return context.getContentResolver().openInputStream(uri);
144        } catch (Exception e) {
145            Log.e(TAG, "Unable to open content URL: " + uri);
146            return null;
147        }
148    }
149
150    /**
151     * Determine the mime type for an Android resource.
152     * @param context The context manager.
153     * @param stream The opened input stream which to examine.
154     * @param url The url from which the stream was opened.
155     * @return The mime type or null if the type is unknown.
156     */
157    @CalledByNative
158    public static String getMimeType(Context context, InputStream stream, String url) {
159        Uri uri = verifyUrl(url);
160        if (uri == null) {
161            return null;
162        }
163        try {
164            String path = uri.getPath();
165            // The content URL type can be queried directly.
166            if (uri.getScheme().equals(CONTENT_SCHEME)) {
167                return context.getContentResolver().getType(uri);
168                // Asset files may have a known extension.
169            } else if (uri.getScheme().equals(FILE_SCHEME) &&
170                       path.startsWith(nativeGetAndroidAssetPath())) {
171                String mimeType = URLConnection.guessContentTypeFromName(path);
172                if (mimeType != null) {
173                    return mimeType;
174                }
175            }
176        } catch (Exception ex) {
177            Log.e(TAG, "Unable to get mime type" + url);
178            return null;
179        }
180        // Fall back to sniffing the type from the stream.
181        try {
182            return URLConnection.guessContentTypeFromStream(stream);
183        } catch (IOException e) {
184            return null;
185        }
186    }
187
188    /**
189     * Make sure the given string URL is correctly formed and parse it into a Uri.
190     * @return a Uri instance, or null if the URL was invalid.
191     */
192    private static Uri verifyUrl(String url) {
193        if (url == null) {
194            return null;
195        }
196        Uri uri = Uri.parse(url);
197        if (uri == null) {
198            Log.e(TAG, "Malformed URL: " + url);
199            return null;
200        }
201        String path = uri.getPath();
202        if (path == null || path.length() == 0) {
203            Log.e(TAG, "URL does not have a path: " + url);
204            return null;
205        }
206        return uri;
207    }
208
209    /**
210     * Set the context to be used for resolving resource queries.
211     * @param context Context to be used, or null for the default application
212     *                context.
213     */
214    public static void setResourceContextForTesting(Context context) {
215        nativeSetResourceContextForTesting(context);
216    }
217
218    private static native void nativeSetResourceContextForTesting(Context context);
219    private static native String nativeGetAndroidAssetPath();
220    private static native String nativeGetAndroidResourcePath();
221}
222