AssetHelper.java
/*
* Copyright 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.webkit.internal;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Build;
import android.util.TypedValue;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLConnection;
import java.util.zip.GZIPInputStream;
/**
* A Utility class for opening resources, assets and files for
* {@link androidx.webkit.WebViewAssetLoader}.
* Forked from the chromuim project org.chromium.android_webview.AndroidProtocolHandler
*/
public class AssetHelper {
private static final String TAG = "AssetHelper";
/**
* Default value to be used as MIME type if guessing MIME type failed.
*/
public static final String DEFAULT_MIME_TYPE = "text/plain";
@NonNull private Context mContext;
public AssetHelper(@NonNull Context context) {
this.mContext = context;
}
@NonNull
private static InputStream handleSvgzStream(@NonNull String path,
@NonNull InputStream stream) throws IOException {
return path.endsWith(".svgz") ? new GZIPInputStream(stream) : stream;
}
@NonNull
private static String removeLeadingSlash(@NonNull String path) {
if (path.length() > 1 && path.charAt(0) == '/') {
path = path.substring(1);
}
return path;
}
private int getFieldId(@NonNull String resourceType, @NonNull String resourceName) {
String packageName = mContext.getPackageName();
int id = mContext.getResources().getIdentifier(resourceName, resourceType, packageName);
return id;
}
private int getValueType(int fieldId) {
TypedValue value = new TypedValue();
mContext.getResources().getValue(fieldId, value, true);
return value.type;
}
/**
* Open an InputStream for an Android resource.
*
* @param path Path of the form "resource_type/resource_name.ext".
* @return An {@link InputStream} to the Android resource.
*/
@NonNull
public InputStream openResource(@NonNull String path)
throws Resources.NotFoundException, IOException {
path = removeLeadingSlash(path);
// The path must be of the form "resource_type/resource_name.ext".
String[] pathSegments = path.split("/", -1);
if (pathSegments.length != 2) {
throw new IllegalArgumentException("Incorrect resource path: " + path);
}
String resourceType = pathSegments[0];
String resourceName = pathSegments[1];
// Drop the file extension.
int dotIndex = resourceName.lastIndexOf('.');
if (dotIndex != -1) {
resourceName = resourceName.substring(0, dotIndex);
}
int fieldId = getFieldId(resourceType, resourceName);
int valueType = getValueType(fieldId);
if (valueType != TypedValue.TYPE_STRING) {
throw new IOException(
String.format("Expected %s resource to be of TYPE_STRING but was %d",
path, valueType));
}
return handleSvgzStream(path, mContext.getResources().openRawResource(fieldId));
}
/**
* Open an InputStream for an Android asset.
*
* @param path Path to the asset file to load.
* @return An {@link InputStream} to the Android asset.
*/
@NonNull
public InputStream openAsset(@NonNull String path) throws IOException {
path = removeLeadingSlash(path);
AssetManager assets = mContext.getAssets();
return handleSvgzStream(path, assets.open(path, AssetManager.ACCESS_STREAMING));
}
/**
* Open an {@code InputStream} for a file in application data directories.
*
* @param file The file to be opened.
* @return An {@code InputStream} for the requested file.
*/
@NonNull
public static InputStream openFile(@NonNull File file) throws FileNotFoundException,
IOException {
FileInputStream fis = new FileInputStream(file);
return handleSvgzStream(file.getPath(), fis);
}
/**
* Resolves the given relative child string path against the given parent directory.
*
* It resolves the given child path and creates a {@link File} object using the canonical path
* of that file if its canonical path starts with the canonical path of the parent directory.
*
* @param parent {@link File} for the parent directory.
* @param child Relative path for the child file.
* @return {@link File} for the given child path or {@code null} if the given path doesn't
* resolve to be a child of the given parent.
*/
@Nullable
public static File getCanonicalFileIfChild(@NonNull File parent, @NonNull String child)
throws IOException {
String parentCanonicalPath = getCanonicalDirPath(parent);
String childCanonicalPath = new File(parent, child).getCanonicalPath();
if (childCanonicalPath.startsWith(parentCanonicalPath)) {
return new File(childCanonicalPath);
}
return null;
}
/**
* Returns the canonical path for the given directory with a {@code "/"} at the end if doesn't
* have one.
*
* Having a slash {@code "/"} at the end of a directory path is important when checking if a
* directory is a parent of another child directory or a file.
* E.g: the directory {@code "/some/path/to"} is not a parent of "/some/path/to_file". However,
* it will pass the {@code parentPath.startsWith(childPath)} check.
*/
@NonNull
public static String getCanonicalDirPath(@NonNull File file) throws IOException {
String canonicalPath = file.getCanonicalPath();
if (!canonicalPath.endsWith("/")) canonicalPath += "/";
return canonicalPath;
}
/**
* Get the data dir for an application.
*
* @param context the {@link Context} used to get the data dir.
* @return data dir {@link File} for that app.
*/
@NonNull
public static File getDataDir(@NonNull Context context) {
// Context#getDataDir is only available in APIs >= 24.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
return context.getDataDir();
} else {
// For APIs < 24 cache dir is created under the data dir.
return context.getCacheDir().getParentFile();
}
}
/**
* Use {@link URLConnection#guessContentTypeFromName} to guess MIME type or return the
* {@link DEFAULT_MIME_TYPE} if it can't guess.
*
* @param filePath path of the file to guess its MIME type.
* @return MIME type guessed from file extension or {@link DEFAULT_MIME_TYPE}.
*/
@NonNull
public static String guessMimeType(@NonNull String filePath) {
String mimeType = URLConnection.guessContentTypeFromName(filePath);
return mimeType == null ? DEFAULT_MIME_TYPE : mimeType;
}
}