[android] Feature: Change path of the resources cache db

This commit is contained in:
Arne Kaiser
2019-01-10 13:02:12 +01:00
committed by Łukasz Paczos
parent 42144ce1dc
commit 5e58a0d81d
8 changed files with 377 additions and 48 deletions

View File

@@ -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);
}
/**

View File

@@ -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();

View File

@@ -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();
}
}

View File

@@ -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"

View File

@@ -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
}
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>