package com.gaiagps.iburn.database; import android.content.Context; import android.os.Environment; import com.gaiagps.iburn.Bytestreams; import com.gaiagps.iburn.Constants; import com.gaiagps.iburn.PrefsHelper; import com.gaiagps.iburn.R; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.util.concurrent.atomic.AtomicBoolean; import rx.Observable; import rx.schedulers.Schedulers; import rx.subjects.PublishSubject; import timber.log.Timber; /** * Manages the MBTiles database, allowing clients to subscribe to a stream of events representing * changes to the database. This allows us to seamlessly update the underlying database while clients are * connected. * <p> * Created by davidbrodsky on 7/1/15. */ public class MapProvider { public static final String MBTILE_DESTINATION = "iburn2016.mbtiles"; /** Key for mbtiles version stored in {@link PrefsHelper} and retrieved by {@link com.gaiagps.iburn.api.IBurnService} * for tile updating. */ private static final String MBTILES_RESOURCE_NAME = "iburn.mbtiles.jar"; private static MapProvider provider; public static MapProvider getInstance(Context context) { if (provider == null) provider = new MapProvider(context); return provider; } private Context context; private PrefsHelper prefs; private File mapDatabaseFile; private AtomicBoolean writingFile = new AtomicBoolean(false); // This subject alerts observers of changes to the database file private final PublishSubject<File> databaseSubject = PublishSubject.create(); public MapProvider(Context context) { this.context = context; this.prefs = new PrefsHelper(context); } public Observable<File> getMapDatabase() { if (mapDatabaseFile != null) return databaseSubject.startWith(mapDatabaseFile); mapDatabaseFile = getMBTilesFile(prefs.getResourceVersion(MBTILES_RESOURCE_NAME)); if (mapDatabaseFile.exists()) return databaseSubject.startWith(mapDatabaseFile); if (!writingFile.getAndSet(true)) { Observable.just(mapDatabaseFile) .subscribeOn(Schedulers.io()) .doOnNext(destFile -> copyResourceToFile(context, R.raw.iburn, destFile)) .subscribe(databaseSubject::onNext); } return databaseSubject; } public void offerMapUpgrade(InputStream newMap, long version) throws IOException { File dest = getMBTilesFile(version); // Bug in version 12 created 'dest' as a directory, causing EISDIR error on attempted write if (dest.exists() && dest.isDirectory()) dest.delete(); File destDirectory = dest.getParentFile(); if (destDirectory.mkdirs() || destDirectory.isDirectory()) { FileOutputStream fos = new FileOutputStream(dest); Bytestreams.copy(newMap, fos); newMap.close(); fos.close(); // Notify observers Timber.d("Notifying observers of new database %s", dest.getAbsolutePath()); databaseSubject.onNext(dest); } else { Timber.e("Tiles directory not available. Could not copy tiles"); } } /** * @return the expected location of the MBTiles database. The file may not yet exist */ private File getMBTilesFile(long version) { return new File(String.format("%s/iburn/tiles/%s.%d", context.getFilesDir().getAbsolutePath(), MBTILE_DESTINATION, version)); } private static boolean copyResourceToFile(Context c, int resourceId, File destination) { try { File parent = destination.getParentFile(); if (parent.mkdirs() || parent.isDirectory()) { Timber.d("Copying MBTiles"); InputStream in = c.getResources().openRawResource(resourceId); Bytestreams.copy(in, new FileOutputStream(destination)); Timber.d("MBTiles copying complete. Deleting old mbtiles"); deleteMbTilesInDirectoryExcept(parent, destination); } } catch (IOException e) { Timber.e(e, "Error copying MBTiles"); } return false; } private static void deleteMbTilesInDirectoryExcept(File directory, File doNotDelete) { FilenameFilter filter = (dir, filename) -> filename.contains(".mbtiles"); File[] files = directory.listFiles(filter); for (File file : files) { if (!file.getAbsolutePath().equals(doNotDelete.getAbsolutePath())) { Timber.d("Will delete %s", file.getAbsolutePath()); file.delete(); } } } }