summaryrefslogtreecommitdiff
path: root/platform/android
diff options
context:
space:
mode:
Diffstat (limited to 'platform/android')
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java30
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java190
-rw-r--r--platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/FileUtils.java30
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml11
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/ChangeResourcesCachePathActivity.kt141
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_change_resources_cache_path.xml21
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml1
-rw-r--r--platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml1
8 files changed, 377 insertions, 48 deletions
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java
index 6731efd4b8..0d85be18a5 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/offline/OfflineManager.java
@@ -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);
}
/**
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java
index d3dba6f90c..8df527657c 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/storage/FileSource.java
@@ -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();
diff --git a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/FileUtils.java b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/FileUtils.java
index 52009d20ef..500e784602 100644
--- a/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/FileUtils.java
+++ b/platform/android/MapboxGLAndroidSDK/src/main/java/com/mapbox/mapboxsdk/utils/FileUtils.java
@@ -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();
+ }
}
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
index c8986d6775..28e284abb3 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/AndroidManifest.xml
@@ -317,6 +317,17 @@
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"
android:label="@string/activity_snapshot">
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/ChangeResourcesCachePathActivity.kt b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/ChangeResourcesCachePathActivity.kt
new file mode 100644
index 0000000000..2c429e829d
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/java/com/mapbox/mapboxsdk/testapp/activity/offline/ChangeResourcesCachePathActivity.kt
@@ -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
+ }
+ }
+} \ No newline at end of file
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_change_resources_cache_path.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_change_resources_cache_path.xml
new file mode 100644
index 0000000000..1eb999caf5
--- /dev/null
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/layout/activity_change_resources_cache_path.xml
@@ -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>
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
index 40698eae78..21ebeaabd5 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/descriptions.xml
@@ -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>
diff --git a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
index 290a157dd1..26f56f29b1 100644
--- a/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
+++ b/platform/android/MapboxGLAndroidSDKTestApp/src/main/res/values/titles.xml
@@ -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>