package com.mapbox.mapboxsdk.offline; import android.annotation.SuppressLint; import android.content.Context; import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.support.annotation.Keep; import android.support.annotation.NonNull; 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; import com.mapbox.mapboxsdk.utils.FileUtils; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.lang.ref.WeakReference; import java.nio.channels.FileChannel; /** * The offline manager is the main entry point for offline-related functionality. * It'll help you list and create offline regions. */ public class OfflineManager { private static final String TAG = "Mbgl - OfflineManager"; // // Static methods // static { LibraryLoader.load(); } // Native peer pointer @Keep private long nativePtr; // Reference to the file source to keep it alive for the // lifetime of this object private final FileSource fileSource; // Makes sure callbacks come back to the main thread private Handler handler; // This object is implemented as a singleton @SuppressLint("StaticFieldLeak") private static OfflineManager instance; // The application context private Context context; /** * This callback receives an asynchronous response containing a list of all * OfflineRegion in the database or an error message otherwise. */ @Keep public interface ListOfflineRegionsCallback { /** * Receives the list of offline regions. * * @param offlineRegions the offline region array */ void onList(OfflineRegion[] offlineRegions); /** * Receives the error message. * * @param error the error message */ void onError(String error); } /** * This callback receives an asynchronous response containing the newly created * OfflineRegion in the database or an error message otherwise. */ @Keep public interface CreateOfflineRegionCallback { /** * Receives the newly created offline region. * * @param offlineRegion the offline region to create */ void onCreate(OfflineRegion offlineRegion); /** * Receives the error message. * * @param error the error message to be shown */ void onError(String error); } /** * This callback receives an asynchronous response containing a list of all * OfflineRegion added to the database during the merge. */ @Keep public interface MergeOfflineRegionsCallback { /** * Receives the list of merged offline regions. * * @param offlineRegions the offline region array */ void onMerge(OfflineRegion[] offlineRegions); /** * Receives the error message. * * @param error the error message */ void onError(String error); } /* * Constructor */ private OfflineManager(Context context) { this.context = context.getApplicationContext(); this.fileSource = FileSource.getInstance(this.context); initialize(fileSource); // Delete any existing previous ambient cache database deleteAmbientDatabase(this.context); } 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(); } /** * Get the single instance of offline manager. * * @param context the context used to host the offline manager * @return the single instance of offline manager */ public static synchronized OfflineManager getInstance(@NonNull Context context) { if (instance == null) { instance = new OfflineManager(context); } return instance; } private Handler getHandler() { if (handler == null) { handler = new Handler(Looper.getMainLooper()); } return handler; } /** * Retrieve all regions in the offline database. *
* The query will be executed asynchronously and the results passed to the given * callback on the main thread. *
* * @param callback the callback to be invoked */ public void listOfflineRegions(@NonNull final ListOfflineRegionsCallback callback) { fileSource.activate(); listOfflineRegions(fileSource, new ListOfflineRegionsCallback() { @Override public void onList(final OfflineRegion[] offlineRegions) { getHandler().post(new Runnable() { @Override public void run() { fileSource.deactivate(); callback.onList(offlineRegions); } }); } @Override public void onError(final String error) { getHandler().post(new Runnable() { @Override public void run() { fileSource.deactivate(); callback.onError(error); } }); } }); } /** * Merge offline regions from a secondary database into the main offline database. ** When the merge is completed, or fails, the {@link MergeOfflineRegionsCallback} will be invoked on the main thread. *
* The secondary database may need to be upgraded to the latest schema. * This is done in-place and requires write-access to the provided path. * If the app's process doesn't have write-access to the provided path, * the file will be copied to the temporary, internal directory for the duration of the merge. *
* Only resources and tiles that belong to a region will be copied over. Identical * regions will be flattened into a single new region in the main database. *
* The operation will be aborted and {@link MergeOfflineRegionsCallback#onError(String)} with an appropriate message * will be invoked if the merge would result in the offline tile count limit being exceeded. *
* Merged regions may not be in a completed status if the secondary database * does not contain all the tiles or resources required by the region definition. * * @param path secondary database writable path * @param callback completion/error callback */ public void mergeOfflineRegions(@NonNull String path, @NonNull final MergeOfflineRegionsCallback callback) { final File src = new File(path); new FileUtils.CheckFileReadPermissionTask(new FileUtils.OnCheckFileReadPermissionListener() { @Override public void onReadPermissionGranted() { new FileUtils.CheckFileWritePermissionTask(new FileUtils.OnCheckFileWritePermissionListener() { @Override public void onWritePermissionGranted() { // path writable, merge and update schema in place if necessary mergeOfflineDatabaseFiles(src, callback, false); } @Override public void onError() { // path not writable, copy the the file to temp directory, then merge and update schema on a copy if // necessary File dst = new File(FileSource.getInternalCachePath(context), src.getName()); new CopyTempDatabaseFileTask(OfflineManager.this, callback).execute(src, dst); } }).execute(src); } @Override public void onError() { // path not readable, abort callback.onError("Secondary database needs to be located in a readable path."); } }).execute(src); } private static final class CopyTempDatabaseFileTask extends AsyncTask