Skip to content
This repository has been archived by the owner on Aug 8, 2023. It is now read-only.

Expose offline database merge API #12860

Merged
merged 5 commits into from
Sep 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,27 @@

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.R;
import com.mapbox.mapboxsdk.geometry.LatLngBounds;
import com.mapbox.mapboxsdk.log.Logger;
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.
Expand Down Expand Up @@ -92,6 +100,27 @@ public interface CreateOfflineRegionCallback {
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
*/
Expand Down Expand Up @@ -183,6 +212,155 @@ public void run() {
});
}

/**
* Merge offline regions from a secondary database into the main offline database.
* <p>
* When the merge is completed, or fails, the {@link MergeOfflineRegionsCallback} will be invoked on the main thread.
* <p>
* 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.
LukasPaczos marked this conversation as resolved.
Show resolved Hide resolved
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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) {
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<Object, Void, Object> {
private final WeakReference<OfflineManager> offlineManagerWeakReference;
private final WeakReference<MergeOfflineRegionsCallback> callbackWeakReference;

CopyTempDatabaseFileTask(OfflineManager offlineManager, MergeOfflineRegionsCallback callback) {
this.offlineManagerWeakReference = new WeakReference<>(offlineManager);
this.callbackWeakReference = new WeakReference<>(callback);
}

@Override
protected Object doInBackground(Object... objects) {
File src = (File) objects[0];
File dst = (File) objects[1];

try {
copyTempDatabaseFile(src, dst);
return dst;
} catch (IOException ex) {
return ex.getMessage();
}
}

@Override
protected void onPostExecute(Object object) {
MergeOfflineRegionsCallback callback = callbackWeakReference.get();
if (callback != null) {
OfflineManager offlineManager = offlineManagerWeakReference.get();
if (object instanceof File && offlineManager != null) {
// successfully copied the file, perform merge
File dst = (File) object;
offlineManager.mergeOfflineDatabaseFiles(dst, callback, true);
} else if (object instanceof String) {
// error occurred
callback.onError((String) object);
}
}
}
}

private static void copyTempDatabaseFile(File sourceFile, File destFile) throws IOException {
if (!destFile.exists() && !destFile.createNewFile()) {
throw new IOException("Unable to copy database file for merge.");
}

FileChannel source = null;
FileChannel destination = null;

try {
source = new FileInputStream(sourceFile).getChannel();
destination = new FileOutputStream(destFile).getChannel();
destination.transferFrom(source, 0, source.size());
} catch (IOException ex) {
throw new IOException(String.format("Unable to copy database file for merge. %s", ex.getMessage()));
} finally {
if (source != null) {
source.close();
}
if (destination != null) {
destination.close();
}
}
}

private void mergeOfflineDatabaseFiles(@NonNull File file, @NonNull final MergeOfflineRegionsCallback callback,
boolean isTemporaryFile) {
fileSource.activate();
mergeOfflineRegions(fileSource, file.getAbsolutePath(), new MergeOfflineRegionsCallback() {
@Override
public void onMerge(OfflineRegion[] offlineRegions) {
getHandler().post(new Runnable() {
@Override
public void run() {
fileSource.deactivate();
if (isTemporaryFile) {
file.delete();
}
callback.onMerge(offlineRegions);
}
});
}

@Override
public void onError(String error) {
getHandler().post(new Runnable() {
@Override
public void run() {
fileSource.deactivate();
if (isTemporaryFile) {
file.delete();
}
callback.onError(error);
}
});
}
});
}

/**
* Create an offline region in the database.
* <p>
Expand Down Expand Up @@ -272,4 +450,6 @@ private boolean isValidOfflineRegionDefinition(OfflineRegionDefinition definitio
private native void createOfflineRegion(FileSource fileSource, OfflineRegionDefinition definition,
byte[] metadata, CreateOfflineRegionCallback callback);

@Keep
private native void mergeOfflineRegions(FileSource fileSource, String path, MergeOfflineRegionsCallback callback);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.mapbox.mapboxsdk.utils;

import android.os.AsyncTask;

import java.io.File;
import java.lang.ref.WeakReference;

public class FileUtils {

/**
* Task checking whether app's process can read a file.
*/
public static class CheckFileReadPermissionTask extends AsyncTask<File, Void, Boolean> {
private final WeakReference<OnCheckFileReadPermissionListener> listenerWeakReference;

public CheckFileReadPermissionTask(OnCheckFileReadPermissionListener listener) {
this.listenerWeakReference = new WeakReference<>(listener);
}

@Override
protected Boolean doInBackground(File... files) {
try {
return files[0].canRead();
} catch (Exception ex) {
return false;
}
}

@Override
protected void onCancelled() {
OnCheckFileReadPermissionListener listener = listenerWeakReference.get();
if (listener != null) {
listener.onError();
}
}

@Override
protected void onPostExecute(Boolean result) {
OnCheckFileReadPermissionListener listener = listenerWeakReference.get();
if (listener != null) {
if (result) {
listener.onReadPermissionGranted();
} else {
listener.onError();
}
}
}
}

/**
* Interface definition for a callback invoked when checking file's read permissions.
*/
public interface OnCheckFileReadPermissionListener {

/**
* Invoked when app's process has a permission to read a file.
*/
void onReadPermissionGranted();

/**
* Invoked when app's process doesn't have a permission to read a file or an error occurs.
*/
void onError();
}

/**
* Task checking whether app's process can write to a file.
*/
public static class CheckFileWritePermissionTask extends AsyncTask<File, Void, Boolean> {
private final WeakReference<OnCheckFileWritePermissionListener> listenerWeakReference;

public CheckFileWritePermissionTask(OnCheckFileWritePermissionListener listener) {
this.listenerWeakReference = new WeakReference<>(listener);
}

@Override
protected Boolean doInBackground(File... files) {
try {
return files[0].canWrite();
} catch (Exception ex) {
return false;
}
}

@Override
protected void onCancelled() {
OnCheckFileWritePermissionListener listener = listenerWeakReference.get();
if (listener != null) {
listener.onError();
}
}

@Override
protected void onPostExecute(Boolean result) {
OnCheckFileWritePermissionListener listener = listenerWeakReference.get();
if (listener != null) {
if (result) {
listener.onWritePermissionGranted();
} else {
listener.onError();
}
}
}
}

/**
* Interface definition for a callback invoked when checking file's write permissions.
*/
public interface OnCheckFileWritePermissionListener {

/**
* Invoked when app's process has a permission to write to a file.
*/
void onWritePermissionGranted();

/**
* Invoked when app's process doesn't have a permission to write to a file or an error occurs.
*/
void onError();
}
}
Loading