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 5 package org.chromium.android_webview; 6 7 import android.content.Context; 8 import android.content.res.AssetManager; 9 import android.net.Uri; 10 import android.util.Log; 11 import android.util.TypedValue; 12 13 import org.chromium.base.CalledByNative; 14 import org.chromium.base.JNINamespace; 15 16 import java.io.IOException; 17 import java.io.InputStream; 18 import java.net.URLConnection; 19 import 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") 26 public 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 open(Context context, String url)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 getFieldId(Context context, String assetType, String assetName)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 getValueType(Context context, int fieldId)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 openResource(Context context, Uri uri)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 openAsset(Context context, Uri uri)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 openContent(Context context, Uri uri)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 getMimeType(Context context, InputStream stream, String url)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 */ verifyUrl(String url)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 */ setResourceContextForTesting(Context context)214 public static void setResourceContextForTesting(Context context) { 215 nativeSetResourceContextForTesting(context); 216 } 217 nativeSetResourceContextForTesting(Context context)218 private static native void nativeSetResourceContextForTesting(Context context); nativeGetAndroidAssetPath()219 private static native String nativeGetAndroidAssetPath(); nativeGetAndroidResourcePath()220 private static native String nativeGetAndroidResourcePath(); 221 } 222