// Copyright 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.android_webview; import android.content.Context; import android.content.res.AssetManager; import android.net.Uri; import android.util.Log; import android.util.TypedValue; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; import java.io.IOException; import java.io.InputStream; import java.net.URLConnection; import java.util.List; /** * Implements the Java side of Android URL protocol jobs. * See android_protocol_handler.cc. */ @JNINamespace("android_webview") public class AndroidProtocolHandler { private static final String TAG = "AndroidProtocolHandler"; // Supported URL schemes. This needs to be kept in sync with // clank/native/framework/chrome/url_request_android_job.cc. private static final String FILE_SCHEME = "file"; private static final String CONTENT_SCHEME = "content"; /** * Open an InputStream for an Android resource. * @param context The context manager. * @param url The url to load. * @return An InputStream to the Android resource. */ @CalledByNative public static InputStream open(Context context, String url) { Uri uri = verifyUrl(url); if (uri == null) { return null; } try { String path = uri.getPath(); if (uri.getScheme().equals(FILE_SCHEME)) { if (path.startsWith(nativeGetAndroidAssetPath())) { return openAsset(context, uri); } else if (path.startsWith(nativeGetAndroidResourcePath())) { return openResource(context, uri); } } else if (uri.getScheme().equals(CONTENT_SCHEME)) { return openContent(context, uri); } } catch (Exception ex) { Log.e(TAG, "Error opening inputstream: " + url); } return null; } private static int getFieldId(Context context, String assetType, String assetName) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException { Class<?> d = context.getClassLoader() .loadClass(context.getPackageName() + ".R$" + assetType); java.lang.reflect.Field field = d.getField(assetName); int id = field.getInt(null); return id; } private static int getValueType(Context context, int fieldId) { TypedValue value = new TypedValue(); context.getResources().getValue(fieldId, value, true); return value.type; } private static InputStream openResource(Context context, Uri uri) { assert uri.getScheme().equals(FILE_SCHEME); assert uri.getPath() != null; assert uri.getPath().startsWith(nativeGetAndroidResourcePath()); // The path must be of the form "/android_res/asset_type/asset_name.ext". List<String> pathSegments = uri.getPathSegments(); if (pathSegments.size() != 3) { Log.e(TAG, "Incorrect resource path: " + uri); return null; } String assetPath = pathSegments.get(0); String assetType = pathSegments.get(1); String assetName = pathSegments.get(2); if (!("/" + assetPath + "/").equals(nativeGetAndroidResourcePath())) { Log.e(TAG, "Resource path does not start with " + nativeGetAndroidResourcePath() + ": " + uri); return null; } // Drop the file extension. assetName = assetName.split("\\.")[0]; try { // Use the application context for resolving the resource package name so that we do // not use the browser's own resources. Note that if 'context' here belongs to the // test suite, it does not have a separate application context. In that case we use // the original context object directly. if (context.getApplicationContext() != null) { context = context.getApplicationContext(); } int fieldId = getFieldId(context, assetType, assetName); int valueType = getValueType(context, fieldId); if (valueType == TypedValue.TYPE_STRING) { return context.getResources().openRawResource(fieldId); } else { Log.e(TAG, "Asset not of type string: " + uri); return null; } } catch (ClassNotFoundException e) { Log.e(TAG, "Unable to open resource URL: " + uri, e); return null; } catch (NoSuchFieldException e) { Log.e(TAG, "Unable to open resource URL: " + uri, e); return null; } catch (IllegalAccessException e) { Log.e(TAG, "Unable to open resource URL: " + uri, e); return null; } } private static InputStream openAsset(Context context, Uri uri) { assert uri.getScheme().equals(FILE_SCHEME); assert uri.getPath() != null; assert uri.getPath().startsWith(nativeGetAndroidAssetPath()); String path = uri.getPath().replaceFirst(nativeGetAndroidAssetPath(), ""); try { AssetManager assets = context.getAssets(); return assets.open(path, AssetManager.ACCESS_STREAMING); } catch (IOException e) { Log.e(TAG, "Unable to open asset URL: " + uri); return null; } } private static InputStream openContent(Context context, Uri uri) { assert uri.getScheme().equals(CONTENT_SCHEME); try { return context.getContentResolver().openInputStream(uri); } catch (Exception e) { Log.e(TAG, "Unable to open content URL: " + uri); return null; } } /** * Determine the mime type for an Android resource. * @param context The context manager. * @param stream The opened input stream which to examine. * @param url The url from which the stream was opened. * @return The mime type or null if the type is unknown. */ @CalledByNative public static String getMimeType(Context context, InputStream stream, String url) { Uri uri = verifyUrl(url); if (uri == null) { return null; } try { String path = uri.getPath(); // The content URL type can be queried directly. if (uri.getScheme().equals(CONTENT_SCHEME)) { return context.getContentResolver().getType(uri); // Asset files may have a known extension. } else if (uri.getScheme().equals(FILE_SCHEME) && path.startsWith(nativeGetAndroidAssetPath())) { String mimeType = URLConnection.guessContentTypeFromName(path); if (mimeType != null) { return mimeType; } } } catch (Exception ex) { Log.e(TAG, "Unable to get mime type" + url); return null; } // Fall back to sniffing the type from the stream. try { return URLConnection.guessContentTypeFromStream(stream); } catch (IOException e) { return null; } } /** * Make sure the given string URL is correctly formed and parse it into a Uri. * @return a Uri instance, or null if the URL was invalid. */ private static Uri verifyUrl(String url) { if (url == null) { return null; } Uri uri = Uri.parse(url); if (uri == null) { Log.e(TAG, "Malformed URL: " + url); return null; } String path = uri.getPath(); if (path == null || path.length() == 0) { Log.e(TAG, "URL does not have a path: " + url); return null; } return uri; } /** * Set the context to be used for resolving resource queries. * @param context Context to be used, or null for the default application * context. */ public static void setResourceContextForTesting(Context context) { nativeSetResourceContextForTesting(context); } private static native void nativeSetResourceContextForTesting(Context context); private static native String nativeGetAndroidAssetPath(); private static native String nativeGetAndroidResourcePath(); }