package com.mapbox.mapboxsdk.storage; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.os.AsyncTask; import android.os.Environment; import android.support.annotation.Keep; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.UiThread; import com.mapbox.mapboxsdk.MapStrictMode; import com.mapbox.mapboxsdk.Mapbox; import com.mapbox.mapboxsdk.constants.MapboxConstants; import com.mapbox.mapboxsdk.log.Logger; import com.mapbox.mapboxsdk.utils.FileUtils; import com.mapbox.mapboxsdk.utils.ThreadUtils; import java.io.File; import java.lang.ref.WeakReference; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Holds a central reference to the core's DefaultFileSource for as long as * there are active mapviews / offline managers */ public class FileSource { private static final String TAG = "Mbgl-FileSource"; private static final String MAPBOX_SHARED_PREFERENCE_RESOURCES_CACHE_PATH = "fileSourceResourcesCachePath"; private static final Lock resourcesCachePathLoaderLock = new ReentrantLock(); private static final Lock internalCachePathLoaderLock = new ReentrantLock(); @Nullable private static String resourcesCachePath; private static String internalCachePath; /** * This callback allows implementors to transform URLs before they are requested * from the internet. This can be used add or remove custom parameters, or reroute * certain requests to other servers or endpoints. */ @Keep public interface ResourceTransformCallback { /** * Called whenever a URL needs to be transformed. * * @param kind the kind of URL to be transformed. * @param url the URL to be transformed * @return a URL that will now be downloaded. */ String onURL(@Resource.Kind int kind, String url); } /** * This callback receives an asynchronous response containing the new path of the * resources cache database. */ @Keep public interface ResourcesCachePathChangeCallback { /** * Receives the new database path * * @param path the path of the current resources cache database */ void onSuccess(String path); /** * Receives an error message if setting the path was not successful * * @param message the error message */ void onError(String message); } // File source instance is kept alive after initialization private static FileSource INSTANCE; /** * Get the single instance of FileSource. * * @param context the context to derive the cache path from * @return the single instance of FileSource */ @UiThread public static synchronized FileSource getInstance(@NonNull Context context) { if (INSTANCE == null) { INSTANCE = new FileSource(getResourcesCachePath(context), context.getResources().getAssets()); } return INSTANCE; } /** * Get files directory path for a context. * * @param context the context to derive the files directory path from * @return the files directory path */ @NonNull private static String getCachePath(@NonNull Context context) { SharedPreferences preferences = context.getSharedPreferences( MapboxConstants.MAPBOX_SHARED_PREFERENCES, Context.MODE_PRIVATE); String cachePath = preferences.getString(MAPBOX_SHARED_PREFERENCE_RESOURCES_CACHE_PATH, null); if (!isPathWritable(cachePath)) { // Use default path cachePath = getDefaultCachePath(context); // Reset stored cache path SharedPreferences.Editor editor = context.getSharedPreferences(MapboxConstants.MAPBOX_SHARED_PREFERENCES, Context.MODE_PRIVATE).edit(); editor.remove(MAPBOX_SHARED_PREFERENCE_RESOURCES_CACHE_PATH).apply(); } return cachePath; } /** * Get the default resources cache path depending on the external storage configuration * * @param context the context to derive the files directory path from * @return the default directory path */ @NonNull private static String getDefaultCachePath(@NonNull Context context) { if (isExternalStorageConfiguration(context) && isExternalStorageReadable()) { File externalFilesDir = context.getExternalFilesDir(null); if (externalFilesDir != null) { return externalFilesDir.getAbsolutePath(); } } return context.getFilesDir().getAbsolutePath(); } private static boolean isExternalStorageConfiguration(@NonNull Context context) { // Default value boolean isExternalStorageConfiguration = MapboxConstants.DEFAULT_SET_STORAGE_EXTERNAL; try { // Try getting a custom value from the app Manifest ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo(context.getPackageName(), PackageManager.GET_META_DATA); if (appInfo.metaData != null) { isExternalStorageConfiguration = appInfo.metaData.getBoolean( MapboxConstants.KEY_META_DATA_SET_STORAGE_EXTERNAL, MapboxConstants.DEFAULT_SET_STORAGE_EXTERNAL ); } } catch (PackageManager.NameNotFoundException exception) { Logger.e(TAG, "Failed to read the package metadata: ", exception); MapStrictMode.strictModeViolation(exception); } catch (Exception exception) { Logger.e(TAG, "Failed to read the storage key: ", exception); MapStrictMode.strictModeViolation(exception); } return isExternalStorageConfiguration; } /** * Checks if external storage is available to at least read. In order for this to work, make * sure you include <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> * (or WRITE_EXTERNAL_STORAGE) for API level < 18 in your app Manifest. *
* Code from https://developer.android.com/guide/topics/data/data-storage.html#filesExternal *
* * @return true if external storage is readable */ public static boolean isExternalStorageReadable() { String state = Environment.getExternalStorageState(); if (Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) { return true; } Logger.w(TAG, "External storage was requested but it isn't readable. For API level < 18" + " make sure you've requested READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE" + " permissions in your app Manifest (defaulting to internal storage)."); return false; } /** * Initializes file directories paths. * * @param context the context to derive paths from */ @UiThread public static void initializeFileDirsPaths(Context context) { ThreadUtils.checkThread(TAG); lockPathLoaders(); if (resourcesCachePath == null || internalCachePath == null) { new FileDirsPathsTask().execute(context); } } private static class FileDirsPathsTask extends AsyncTask* The callback will be executed on the main thread once for every requested URL. *
* * @param callback the callback to be invoked or null to reset */ @Keep public native void setResourceTransform(final ResourceTransformCallback callback); @Keep private native void setResourceCachePath(String path); @Keep private native void initialize(String accessToken, String cachePath, AssetManager assetManager); @Override @Keep protected native void finalize() throws Throwable; }