[android] Feature: Change path of the resources cache db
This commit is contained in:
committed by
Łukasz Paczos
parent
42144ce1dc
commit
5e58a0d81d
@@ -8,12 +8,11 @@ import android.os.Looper;
|
||||
import android.support.annotation.Keep;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import android.support.annotation.RestrictTo;
|
||||
import com.mapbox.mapboxsdk.LibraryLoader;
|
||||
import com.mapbox.mapboxsdk.MapStrictMode;
|
||||
import com.mapbox.mapboxsdk.Mapbox;
|
||||
import com.mapbox.mapboxsdk.R;
|
||||
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
|
||||
import com.mapbox.mapboxsdk.log.Logger;
|
||||
import com.mapbox.mapboxsdk.maps.TelemetryDefinition;
|
||||
import com.mapbox.mapboxsdk.net.ConnectivityReceiver;
|
||||
import com.mapbox.mapboxsdk.storage.FileSource;
|
||||
@@ -135,24 +134,17 @@ public class OfflineManager {
|
||||
deleteAmbientDatabase(this.context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the current instance of the offline manager.
|
||||
*/
|
||||
@RestrictTo(RestrictTo.Scope.LIBRARY)
|
||||
public static void clear() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
private void deleteAmbientDatabase(final Context context) {
|
||||
// Delete the file in a separate thread to avoid affecting the UI
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
String path = FileSource.getInternalCachePath(context) + File.separator + "mbgl-cache.db";
|
||||
File file = new File(path);
|
||||
if (file.exists()) {
|
||||
file.delete();
|
||||
Logger.d(TAG, String.format("Old ambient cache database deleted to save space: %s", path));
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Logger.e(TAG, "Failed to delete old ambient cache database: ", exception);
|
||||
MapStrictMode.strictModeViolation(exception);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
final String path = FileSource.getInternalCachePath(context) + File.separator + "mbgl-cache.db";
|
||||
FileUtils.deleteFile(path);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
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.os.Handler;
|
||||
import android.os.Looper;
|
||||
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.offline.OfflineManager;
|
||||
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;
|
||||
|
||||
import com.mapbox.mapboxsdk.log.Logger;
|
||||
|
||||
/**
|
||||
* Holds a central reference to the core's DefaultFileSource for as long as
|
||||
* there are active mapviews / offline managers
|
||||
@@ -28,6 +32,8 @@ import com.mapbox.mapboxsdk.log.Logger;
|
||||
public class FileSource {
|
||||
|
||||
private static final String TAG = "Mbgl-FileSource";
|
||||
private static final String MAPBOX_SHARED_PREFERENCES = "MapboxSharedPreferences";
|
||||
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
|
||||
@@ -53,6 +59,29 @@ public class FileSource {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This callback receives an asynchronous response containing the new path of the
|
||||
* resources cache database.
|
||||
*/
|
||||
@Keep
|
||||
public interface SetResourcesCachePathCallback {
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
@@ -79,17 +108,51 @@ public class FileSource {
|
||||
*/
|
||||
@NonNull
|
||||
private static String getCachePath(@NonNull Context context) {
|
||||
SharedPreferences preferences = context.getSharedPreferences(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(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);
|
||||
PackageManager.GET_META_DATA);
|
||||
if (appInfo.metaData != null) {
|
||||
isExternalStorageConfiguration = appInfo.metaData.getBoolean(
|
||||
MapboxConstants.KEY_META_DATA_SET_STORAGE_EXTERNAL,
|
||||
MapboxConstants.DEFAULT_SET_STORAGE_EXTERNAL
|
||||
MapboxConstants.KEY_META_DATA_SET_STORAGE_EXTERNAL,
|
||||
MapboxConstants.DEFAULT_SET_STORAGE_EXTERNAL
|
||||
);
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException exception) {
|
||||
@@ -99,24 +162,7 @@ public class FileSource {
|
||||
Logger.e(TAG, "Failed to read the storage key: ", exception);
|
||||
MapStrictMode.strictModeViolation(exception);
|
||||
}
|
||||
|
||||
String cachePath = null;
|
||||
if (isExternalStorageConfiguration && isExternalStorageReadable()) {
|
||||
try {
|
||||
// Try getting the external storage path
|
||||
cachePath = context.getExternalFilesDir(null).getAbsolutePath();
|
||||
} catch (NullPointerException exception) {
|
||||
Logger.e(TAG, "Failed to obtain the external storage path: ", exception);
|
||||
MapStrictMode.strictModeViolation(exception);
|
||||
}
|
||||
}
|
||||
|
||||
if (cachePath == null) {
|
||||
// Default to internal storage
|
||||
cachePath = context.getFilesDir().getAbsolutePath();
|
||||
}
|
||||
|
||||
return cachePath;
|
||||
return isExternalStorageConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,8 +182,8 @@ public class FileSource {
|
||||
}
|
||||
|
||||
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).");
|
||||
+ " make sure you've requested READ_EXTERNAL_STORAGE or WRITE_EXTERNAL_STORAGE"
|
||||
+ " permissions in your app Manifest (defaulting to internal storage).");
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -166,9 +212,9 @@ public class FileSource {
|
||||
@NonNull
|
||||
@Override
|
||||
protected String[] doInBackground(Context... contexts) {
|
||||
return new String[] {
|
||||
getCachePath(contexts[0]),
|
||||
contexts[0].getCacheDir().getAbsolutePath()
|
||||
return new String[]{
|
||||
getCachePath(contexts[0]),
|
||||
contexts[0].getCacheDir().getAbsolutePath()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -217,6 +263,92 @@ public class FileSource {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the path of the resources cache database.
|
||||
* Note that the external storage setting needs to be activated in the manifest.
|
||||
*
|
||||
* @param context the context of the path
|
||||
* @param path the new database path
|
||||
* @param callback the callback to obtain the result
|
||||
*/
|
||||
public static void setResourcesCachePath(@NonNull Context context,
|
||||
@NonNull final String path,
|
||||
@NonNull final SetResourcesCachePathCallback callback) {
|
||||
|
||||
if (getInstance(context).isActivated()) {
|
||||
final String activatedMessage = "Cannot set path, file source is activated!";
|
||||
Logger.w(TAG, activatedMessage);
|
||||
callback.onError(activatedMessage);
|
||||
} else {
|
||||
if (path.equals(resourcesCachePath)) {
|
||||
// no need to change the path
|
||||
callback.onSuccess(path);
|
||||
} else {
|
||||
final WeakReference<Context> contextWeakReference = new WeakReference<>(context);
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final Context context = contextWeakReference.get();
|
||||
final String message;
|
||||
if (context != null) {
|
||||
if (!isPathWritable(path)) {
|
||||
message = "Path is not writable: " + path;
|
||||
} else {
|
||||
message = null;
|
||||
|
||||
final SharedPreferences.Editor editor =
|
||||
context.getSharedPreferences(MAPBOX_SHARED_PREFERENCES, Context.MODE_PRIVATE).edit();
|
||||
if (!editor.putString(MAPBOX_SHARED_PREFERENCE_RESOURCES_CACHE_PATH, path).commit()) {
|
||||
Logger.w(TAG, "Cannot store cache path in shared preferences.");
|
||||
}
|
||||
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
setResourcesCachePath(context, path);
|
||||
callback.onSuccess(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
message = "Context is null";
|
||||
}
|
||||
|
||||
if (message != null) {
|
||||
Logger.w(TAG, message);
|
||||
new Handler(Looper.getMainLooper()).post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
callback.onError(message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void setResourcesCachePath(@NonNull Context context, @NonNull String path) {
|
||||
resourcesCachePathLoaderLock.lock();
|
||||
resourcesCachePath = path;
|
||||
reinitializeOfflineManager(context);
|
||||
resourcesCachePathLoaderLock.unlock();
|
||||
}
|
||||
|
||||
private static boolean isPathWritable(String path) {
|
||||
if (path == null || path.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
return new File(path).canWrite();
|
||||
}
|
||||
|
||||
private static void reinitializeOfflineManager(@NonNull Context context) {
|
||||
final FileSource fileSource = FileSource.getInstance(context);
|
||||
fileSource.initialize(Mapbox.getAccessToken(), resourcesCachePath, context.getResources().getAssets());
|
||||
OfflineManager.clear();
|
||||
}
|
||||
|
||||
private static void lockPathLoaders() {
|
||||
internalCachePathLoaderLock.lock();
|
||||
resourcesCachePathLoaderLock.lock();
|
||||
|
||||
@@ -3,11 +3,15 @@ package com.mapbox.mapboxsdk.utils;
|
||||
import android.os.AsyncTask;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import com.mapbox.mapboxsdk.log.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
public class FileUtils {
|
||||
|
||||
private static final String TAG = "Mbgl-FileUtils";
|
||||
|
||||
/**
|
||||
* Task checking whether app's process can read a file.
|
||||
*/
|
||||
@@ -121,4 +125,30 @@ public class FileUtils {
|
||||
*/
|
||||
void onError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file asynchronously in a separate thread.
|
||||
*
|
||||
* @param path the path of the file that should be deleted
|
||||
*/
|
||||
public static void deleteFile(@NonNull final String path) {
|
||||
// Delete the file in a separate thread to avoid affecting the UI
|
||||
new Thread(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
File file = new File(path);
|
||||
if (file.exists()) {
|
||||
if (file.delete()) {
|
||||
Logger.d(TAG, "File deleted to save space: " + path);
|
||||
} else {
|
||||
Logger.e(TAG, "Failed to delete file: " + path);
|
||||
}
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
Logger.e(TAG, "Failed to delete file: ", exception);
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +316,17 @@
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activity.FeatureOverviewActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.offline.ChangeResourcesCachePathActivity"
|
||||
android:description="@string/description_change_resources_cache_path"
|
||||
android:label="@string/activity_change_resources_cache_path">
|
||||
<meta-data
|
||||
android:name="@string/category"
|
||||
android:value="@string/category_offline" />
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value=".activity.FeatureOverviewActivity" />
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.imagegenerator.SnapshotActivity"
|
||||
android:description="@string/description_snapshot"
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.mapbox.mapboxsdk.testapp.activity.offline
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import com.mapbox.mapboxsdk.storage.FileSource
|
||||
import com.mapbox.mapboxsdk.testapp.R
|
||||
import kotlinx.android.synthetic.main.activity_change_resources_cache_path.*
|
||||
import java.io.File
|
||||
|
||||
class ChangeResourcesCachePathActivity : AppCompatActivity(),
|
||||
AdapterView.OnItemClickListener,
|
||||
FileSource.SetResourcesCachePathCallback {
|
||||
|
||||
lateinit var adapter: PathAdapter
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_change_resources_cache_path)
|
||||
|
||||
Thread(Runnable {
|
||||
adapter = PathAdapter(this, obtainFilesPaths(this))
|
||||
listView.adapter = adapter
|
||||
listView.emptyView = empty
|
||||
listView.onItemClickListener = this
|
||||
}).start()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
val path: String? = FileSource.getResourcesCachePath(this)
|
||||
Toast.makeText(this, "Current path: $path", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
|
||||
listView.onItemClickListener = null
|
||||
val path: String = adapter.getItem(position) as String
|
||||
FileSource.setResourcesCachePath(this, path, this)
|
||||
}
|
||||
|
||||
override fun onError(message: String?) {
|
||||
listView.onItemClickListener = this
|
||||
Toast.makeText(this, "Error: $message", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onSuccess(path: String?) {
|
||||
listView.onItemClickListener = this
|
||||
Toast.makeText(this, "New path: $path", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
private fun obtainFilesPaths(context: Context): List<String> {
|
||||
val paths = ArrayList<String>()
|
||||
paths.add(context.filesDir.absolutePath)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
paths.addAll(obtainExternalFilesPathsKitKat(context))
|
||||
} else {
|
||||
paths.addAll(obtainExternalFilesPathsLegacy(context))
|
||||
}
|
||||
paths.add("${File.separator}invalid${File.separator}cache${File.separator}path")
|
||||
return paths
|
||||
}
|
||||
|
||||
private fun obtainExternalFilesPathsLegacy(context: Context): List<String> {
|
||||
val postFix =
|
||||
"${File.separator}Android${File.separator}data${File.separator}${context.packageName}${File.separator}files"
|
||||
val paths = ArrayList<String>()
|
||||
val externalStorage = System.getenv("EXTERNAL_STORAGE")
|
||||
val secondaryStorage = System.getenv("SECONDARY_STORAGE")
|
||||
if (externalStorage != null) {
|
||||
paths.add(externalStorage + postFix)
|
||||
}
|
||||
if (secondaryStorage != null) {
|
||||
val secPaths = secondaryStorage.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
for (path in secPaths) {
|
||||
paths.add(path + postFix)
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||
private fun obtainExternalFilesPathsKitKat(context: Context): List<String> {
|
||||
val paths = ArrayList<String>()
|
||||
val extDirs = context.getExternalFilesDirs(null)
|
||||
for (dir in extDirs) {
|
||||
if (dir != null) {
|
||||
paths.add(dir.absolutePath)
|
||||
}
|
||||
}
|
||||
return paths
|
||||
}
|
||||
|
||||
class PathAdapter(private val context: Context, private val paths: List<String>) : BaseAdapter() {
|
||||
|
||||
override fun getItem(position: Int): Any {
|
||||
return paths[position]
|
||||
}
|
||||
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
|
||||
override fun getCount(): Int {
|
||||
return paths.size
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View {
|
||||
val viewHolder: ViewHolder
|
||||
val view: View
|
||||
|
||||
if (convertView == null) {
|
||||
viewHolder = ViewHolder()
|
||||
view = LayoutInflater.from(context).inflate(android.R.layout.simple_list_item_1, parent, false)
|
||||
viewHolder.textView = view.findViewById(android.R.id.text1)
|
||||
view?.tag = viewHolder
|
||||
} else {
|
||||
view = convertView
|
||||
viewHolder = view.tag as ViewHolder
|
||||
}
|
||||
|
||||
viewHolder.textView?.text = paths[position]
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
class ViewHolder {
|
||||
var textView: TextView? = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ListView
|
||||
android:id="@+id/listView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
<TextView
|
||||
android:id="@android:id/empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/no_results"
|
||||
android:textSize="24sp"/>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -20,6 +20,7 @@
|
||||
<string name="description_offline">Offline Map example</string>
|
||||
<string name="description_update_metadata">Update metadata example</string>
|
||||
<string name="description_offline_region_delete">Delete region example</string>
|
||||
<string name="description_change_resources_cache_path">Change resources cache path example</string>
|
||||
<string name="description_animated_symbollayer">Animate the position change of a symbol layer</string>
|
||||
<string name="description_polyline">Add a polyline to a map</string>
|
||||
<string name="description_polygon">Add a polygon to a map</string>
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
<string name="activity_offline">Offline Map</string>
|
||||
<string name="activity_update_metadata">Update metadata Map</string>
|
||||
<string name="activity_offline_region_delete">Delete region</string>
|
||||
<string name="activity_change_resources_cache_path">Change resources cache path</string>
|
||||
<string name="activity_minmax_zoom">Min/Max Zoom</string>
|
||||
<string name="activity_viewpager">ViewPager</string>
|
||||
<string name="activity_runtime_style">Runtime Style</string>
|
||||
|
||||
Reference in New Issue
Block a user