/*
* Copyright 2013 i'm SpA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package it.imwatch.nfclottery;
import android.content.Context;
import android.util.Log;
import com.dropbox.sync.android.*;
import it.imwatch.nfclottery.data.provider.NFCMLProvider;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import static it.imwatch.nfclottery.Const.DEBUG;
/**
* An helper class to perform Dropbox syncing operations.
*/
public class DropboxHelper {
private static final String TAG = DropboxHelper.class.getSimpleName();
// TODO: define the ApiKeys.java file with DROPBOX_APP_KEY and DROPBOX_APP_SECRET to use Dropbox
private static final String DB_SYNC_FILE = "db_file.db";
private static final String LOCAL_DB_FILE = "../databases/" + NFCMLProvider.DATABASE_NAME;
/**
* Pulls the database from Dropbox (restores a backup).
* The main operations are performed on a background thread.
*
* @param context The current context
* @param listener The Dropbox operations listener for this operation
*/
public static void pullDB(final Context context, final DropboxOperationsListener listener) {
if (DEBUG) Log.d(TAG, "Beginning a DB pull operation");
if (context == null) {
Log.e(TAG, "pullDB called with a null context");
return;
}
// First of all, check the Dropbox link status
final DbxAccountManager dbxAcctMgr = getAccountManager(context);
if (dbxAcctMgr == null || !dbxAcctMgr.hasLinkedAccount()) {
Log.e(TAG, "Dropbox user not authenticated. Can't pull!");
return;
}
// Do the actual pulling in a worker thread to avoid blocking the UI
Thread t = new Thread(new PullRunnable(listener, dbxAcctMgr, context));
t.setName("DropboxPuller");
t.setPriority(Thread.MIN_PRIORITY);
t.start();
}
/**
* Pushes the local database to Dropbox (backs it up).
* The operations aren't performed on a background thread, unlike
* what {@link #pullDB(android.content.Context,
* it.imwatch.nfclottery.DropboxHelper.DropboxOperationsListener)}.
*
* @param context The current context
*/
public static void pushDB(Context context) {
if (DEBUG) Log.d(TAG, "Beginning a DB pull operation");
if (context == null) {
Log.e(TAG, "pushDB called with a null context");
return;
}
// First of all, check the Dropbox link status
final DbxAccountManager dbxAcctMgr = getAccountManager(context);
if (dbxAcctMgr == null || !dbxAcctMgr.hasLinkedAccount()) {
Log.e(TAG, "Dropbox user not authenticated. Can't push!");
return;
}
// Retrieve the local DB file
File localDBFile = getLocalDbFile(context);
if (localDBFile == null) {
Log.e(TAG, "Cannot retrieve local DB file. Can't push.");
return;
}
try {
if (DEBUG) Log.d(TAG, "Starting DB push...");
uploadDbToDropbox(dbxAcctMgr, localDBFile);
}
catch (DbxException e) {
Log.w(TAG, "Dropbox exception while pushing database", e);
}
catch (IOException e) {
Log.w(TAG, "I/O exception while pushing database", e);
}
}
/**
* Uploads the specified database file to Dropbox.
*
* @param dbxAcctMgr The Dropbox Account Manager used to access the Dropbox Filesystem
* @param localDBFile The local database file to upload
*
* @throws IOException Thrown when an I/O error occours in the local cache or the given file
*/
private static void uploadDbToDropbox(DbxAccountManager dbxAcctMgr, File localDBFile) throws IOException {
DbxFileSystem dbxFs = DbxFileSystem.forAccount(dbxAcctMgr.getLinkedAccount());
DbxPath remotePath = new DbxPath(DB_SYNC_FILE);
DbxFile remoteFile;
if (dbxFs.exists(remotePath)) {
remoteFile = dbxFs.open(remotePath);
}
else {
remoteFile = dbxFs.create(remotePath);
}
// Write to the Dropbox remote file
remoteFile.writeFromExistingFile(localDBFile, false);
remoteFile.close();
Log.i(TAG, "Database pushed to Dropbox");
}
/**
* Retrieves the local DB File, if it exists.
*
* @param context The current context
*
* @return Returns the local DB File instance, or null if
* it doesn't exist or it can't be found
*/
private static File getLocalDbFile(Context context) {
final File filesDir = context.getFilesDir();
if (filesDir == null) {
Log.w(TAG, "Can't retrieve the app's files directory");
return null;
}
File localDBFile = new File(filesDir.getPath(), LOCAL_DB_FILE);
if (!localDBFile.exists()) {
Log.w(TAG, "Local DB not yet created.");
return null;
}
return localDBFile;
}
/**
* Replaces the local database file with the specified file from Dropbox.
*
* @param context The current context
* @param remoteFile The Dropbox copy of the database file. Will be closed when the
* replace operation is completed!
* @param listener The Dropbox operations listener for this operation
*
* @return Returns true if the operation has been successful, false otherwise
*/
private static boolean replaceDbFile(Context context, DbxFile remoteFile, DropboxOperationsListener listener) {
if (DEBUG) Log.d(TAG, "Replacing the DB file with Dropbox file: " + remoteFile.getPath());
if (context == null) {
Log.e(TAG, "replaceDbFile called with a null context");
return false;
}
if (listener != null) {
listener.onStartRestoreOperation();
}
boolean replaceSuccess = false;
FileInputStream inputStream = null;
FileOutputStream outputStream = null;
try {
final File filesDir = context.getFilesDir();
if (filesDir == null) {
Log.e(TAG, "Can't retrieve the app's files directory");
return false;
}
String db_path = context.getFilesDir().getPath();
if (!db_path.endsWith("/")) db_path += "/";
File localFile = new File(db_path + LOCAL_DB_FILE);
inputStream = remoteFile.getReadStream();
outputStream = new FileOutputStream(localFile);
int read;
byte[] bytes = new byte[1024];
while ((read = inputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, read);
}
replaceSuccess = true;
}
catch (IOException e) {
Log.e(TAG, "Unable to replace DB file", e);
}
finally {
remoteFile.close();
try {
inputStream.close();
}
catch (Throwable ignored) { }
try {
outputStream.close();
}
catch (Throwable ignored) { }
}
return replaceSuccess;
}
/**
* Gets the instance of the Dropbox Account Manager associated with this
* application's context.
* <p/>
* <b>Note:</b> always check if there is a linked account by calling the
* {@link com.dropbox.sync.android.DbxAccountManager#hasLinkedAccount()}
* method, before actually using Dropbox APIs.</code>
*
* @param context The current context
*
* @return Returns the correct Dropbox Account Manager instance
*/
public static DbxAccountManager getAccountManager(Context context) {
return DbxAccountManager.getInstance(context.getApplicationContext(),
ApiKeys.DROPBOX_APP_KEY,
ApiKeys.DROPBOX_APP_SECRET);
}
/**
* Simple listener for Dropbox operations callbacks.
*/
public interface DropboxOperationsListener {
/**
* Called when a restore operation from Dropbox begins.
*/
public void onStartRestoreOperation();
/**
* Called when a restore operation from Dropbox is completed.
*
* @param success True if the operation was successfully completed,
* false otherwise.
*/
public void onRemoteDBRestored(boolean success);
}
/**
* A Runnable that pulls the database from Dropbox.
*/
private static class PullRunnable implements Runnable {
private DropboxOperationsListener mListener;
private DbxAccountManager mAccountMgr;
private Context mContext;
public PullRunnable(DropboxOperationsListener listener, DbxAccountManager accountMgr,
Context context) {
mListener = listener;
mAccountMgr = accountMgr;
mContext = context;
}
@Override
public void run() {
Log.d(TAG, "Starting DB pull thread" + (mListener != null ? " with a listener" : ""));
try {
DbxFileSystem dbxFs = DbxFileSystem.forAccount(mAccountMgr.getLinkedAccount());
DbxPath remotePath = new DbxPath(DB_SYNC_FILE);
if (dbxFs.exists(remotePath)) {
updateDropboxRemoteFile(dbxFs, remotePath);
}
else {
Log.i(TAG, "DB file not yet uploaded to Dropbox. Nothing to pull.");
if (mListener != null) mListener.onRemoteDBRestored(false);
}
}
catch (DbxException e) {
Log.e(TAG, "Error while pulling the DB from Dropbox", e);
if (mListener != null) mListener.onRemoteDBRestored(false);
}
}
/**
* Updates the local (cached) version of a remote file in Dropbox.
*
* @param dbxFs The Dropbox filesystem to use
* @param remotePath The path of the remote file
*
* @throws DbxException Thrown in case of problems during the operation
*/
private void updateDropboxRemoteFile(DbxFileSystem dbxFs, DbxPath remotePath) throws DbxException {
final DbxFile remoteFile = dbxFs.open(remotePath);
if (!remoteFile.getSyncStatus().isLatest) {
if (DEBUG) Log.d(TAG, "Cached DB file not at the lastest version. Updating it..");
remoteFile.getNewerStatus();
remoteFile.addListener(new DbxFile.Listener() {
@Override
public void onFileChange(DbxFile dbxFile) {
try {
if (DEBUG) Log.d(TAG, "File change event received on DbxFile.Listener()");
if (dbxFile.getSyncStatus().isLatest) {
if (DEBUG) Log.d(TAG, "Cached DB at the lastest version");
updateDBFile(dbxFile);
dbxFile.removeListener(this);
}
else {
Log.e(TAG, "Cached DB file hasn't been updated to the latest version");
if (mListener != null) mListener.onRemoteDBRestored(false);
}
}
catch (DbxException e) {
Log.e(TAG, "Error in the Dropbox pull listener", e);
if (mListener != null) mListener.onRemoteDBRestored(false);
}
}
});
}
else {
// No need to update the file cache, it's already the latest version
updateDBFile(remoteFile);
}
}
/**
* Updates the app's DB using a remote file in Dropbox.
*
* @param dbxFile The Dropbox file with the database
*
* @throws DbxException Thrown in case of problems during the operation
*/
private void updateDBFile(DbxFile dbxFile) throws DbxException {
dbxFile.update();
final boolean success = replaceDbFile(mContext, dbxFile, mListener);
if (mListener != null) mListener.onRemoteDBRestored(success);
}
}
}