/* * Copyright (C) 2012-2016 The Android Money Manager Ex Project Team * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.money.manager.ex.sync; import android.app.IntentService; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Message; import android.os.Messenger; import android.text.TextUtils; import com.cloudrail.si.types.CloudMetaData; import com.money.manager.ex.MoneyManagerApplication; import com.money.manager.ex.R; import com.money.manager.ex.core.RequestCodes; import com.money.manager.ex.home.DatabaseMetadata; import com.money.manager.ex.home.MainActivity; import com.money.manager.ex.home.RecentDatabasesProvider; import com.money.manager.ex.sync.events.SyncStartingEvent; import com.money.manager.ex.sync.events.SyncStoppingEvent; import com.money.manager.ex.utils.MmxFileUtils; import com.money.manager.ex.utils.NetworkUtils; import org.greenrobot.eventbus.EventBus; import java.io.File; import java.io.IOException; import javax.inject.Inject; import rx.SingleSubscriber; import rx.subscriptions.CompositeSubscription; import timber.log.Timber; /** * The background service that synchronizes the database file. * It is being invoked by the timer. * It displays the sync notification and invokes the cloud api. */ public class SyncService extends IntentService { public static final String INTENT_EXTRA_MESSENGER = "com.money.manager.ex.sync.MESSENGER"; public SyncService() { super("com.money.manager.ex.sync.SyncService"); } @Inject RecentDatabasesProvider recentDatabasesProvider; private CompositeSubscription compositeSubscription; private Messenger mOutMessenger; private NotificationManager mNotificationManager; @Override public void onCreate() { super.onCreate(); compositeSubscription = new CompositeSubscription(); mNotificationManager = (NotificationManager) getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); MoneyManagerApplication.getApp().iocComponent.inject(this); } @Override protected void onHandleIntent(Intent intent) { String action = intent != null ? intent.getAction() : "null"; Timber.d("Running sync service: %s", action); sendStartEvent(); // Check if there is a messenger. Used to send the messages back. if (intent.getExtras().containsKey(SyncService.INTENT_EXTRA_MESSENGER)) { mOutMessenger = intent.getParcelableExtra(SyncService.INTENT_EXTRA_MESSENGER); } // check if the device is online. NetworkUtils network = new NetworkUtils(getApplicationContext()); if (!network.isOnline()) { Timber.i("Can't sync. Device not online."); sendMessage(SyncServiceMessage.NOT_ON_WIFI); sendStopEvent(); return; } SyncManager sync = new SyncManager(getApplicationContext()); String localFilename = intent.getStringExtra(SyncConstants.INTENT_EXTRA_LOCAL_FILE); String remoteFilename = intent.getStringExtra(SyncConstants.INTENT_EXTRA_REMOTE_FILE); // check if file is correct if (TextUtils.isEmpty(localFilename) || TextUtils.isEmpty(remoteFilename)) { sendStopEvent(); return; } CloudMetaData remoteFile = sync.loadMetadata(remoteFilename); if (remoteFile == null) { sendMessage(SyncServiceMessage.ERROR); sendStopEvent(); return; } File localFile = new File(localFilename); // todo: modify this part after db initial upload has been implemented. // if (remoteFile == null) { // // file not found on remote server. // if (intent.getAction().equals(SyncConstants.INTENT_ACTION_UPLOAD)) { // // Create a new entry in the root? // Log.w(LOGCAT, "remoteFile is null. SyncService forcing creation of the new remote file."); // remoteFile = new CloudMetaData(); // remoteFile.setPath(remoteFilename); // } else { // Timber.e("remoteFile is null. SyncService.onHandleIntent premature exit."); // sendMessage(SyncServiceMessage.ERROR); // return; // } // } // check if name is same if (!localFile.getName().toLowerCase().equals(remoteFile.getName().toLowerCase())) { Timber.w("Local filename different from the remote!"); sendMessage(SyncServiceMessage.ERROR); sendStopEvent(); return; } // Execute action. switch (action) { case SyncConstants.INTENT_ACTION_DOWNLOAD: triggerDownload(localFile, remoteFile); break; case SyncConstants.INTENT_ACTION_UPLOAD: triggerUpload(localFile, remoteFile); break; case SyncConstants.INTENT_ACTION_SYNC: default: triggerSync(localFile, remoteFile); break; } } @Override public void onDestroy() { if (!compositeSubscription.isUnsubscribed()) { compositeSubscription.unsubscribe(); } super.onDestroy(); } // private private void triggerDownload(final File localFile, final CloudMetaData remoteFile) { final SyncManager sync = new SyncManager(getApplicationContext()); Notification notification = new SyncNotificationFactory(getApplicationContext()) .getNotificationForDownload(); final NotificationManager notificationManager = (NotificationManager) getApplicationContext() .getSystemService(Context.NOTIFICATION_SERVICE); final File tempFile = new File(localFile.toString() + "-download"); Timber.d("Download file. Local file: %s, remote file: %s", localFile.getPath(), remoteFile.getPath()); if (notification != null && notificationManager != null) { notificationManager.notify(SyncConstants.NOTIFICATION_SYNC_IN_PROGRESS, notification); } sendMessage(SyncServiceMessage.STARTING_DOWNLOAD); compositeSubscription.add( sync.downloadSingle(remoteFile, tempFile) // do not run on another thread as then the service will be destroyed. // .subscribeOn(Schedulers.io()) .subscribe(new SingleSubscriber<Void>() { @Override public void onSuccess(Void value) { //onDownloadHandler.onPostExecute(true); afterDownload(notificationManager, tempFile, localFile, remoteFile, sync); sendMessage(SyncServiceMessage.DOWNLOAD_COMPLETE); sendStopEvent(); } @Override public void onError(Throwable error) { Timber.e(error, "async download"); sendMessage(SyncServiceMessage.ERROR); sendStopEvent(); } }) ); } private void afterDownload(NotificationManager notificationManager, File tempFile, File localFile, CloudMetaData remoteFile, SyncManager sync) { if (notificationManager == null) return; notificationManager.cancel(SyncConstants.NOTIFICATION_SYNC_IN_PROGRESS); // copy file try { MmxFileUtils.copy(tempFile, localFile); tempFile.delete(); } catch (IOException e) { Timber.e(e, "copying downloaded database file"); return; } sync.saveRemoteLastModifiedDate(localFile.getAbsolutePath(), remoteFile); // Create the notification for opening of the downloaded file. // The Intent is passed to the notification and called if clicked on. Intent intent = new SyncCommon().getIntentForOpenDatabase(getApplicationContext(), localFile); PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), RequestCodes.SELECT_FILE, intent, 0); Notification completeNotification = new SyncNotificationFactory(getApplicationContext()) .getNotificationDownloadComplete(pendingIntent); // notify notificationManager.notify(SyncConstants.NOTIFICATION_SYNC_OPEN_FILE, completeNotification); } private void triggerUpload(final File localFile, CloudMetaData remoteFile) { Timber.d("Uploading db. Local file: %s, remote file: %s", localFile.getPath(), remoteFile.getPath()); showNotificationUploading(); sendMessage(SyncServiceMessage.STARTING_UPLOAD); // upload SyncManager sync = new SyncManager(getApplicationContext()); boolean result = sync.upload(localFile.getPath(), remoteFile.getPath()); // notification, upload complete showNotificationUploadComplete(result, localFile); sendMessage(SyncServiceMessage.UPLOAD_COMPLETE); sendStopEvent(); } private void showNotificationUploadComplete(boolean result, File localFile) { if (mNotificationManager == null) return; mNotificationManager.cancel(SyncConstants.NOTIFICATION_SYNC_IN_PROGRESS); if (!result) return; // create notification for open file Intent intent = new Intent(getApplicationContext(), MainActivity.class); intent.setData(Uri.fromFile(localFile)); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), RequestCodes.SELECT_FILE, intent, 0); // notification Notification notification = new SyncNotificationFactory(getApplicationContext()) .getNotificationUploadComplete(pendingIntent); // notify mNotificationManager.notify(SyncConstants.NOTIFICATION_SYNC_OPEN_FILE, notification); } private void triggerSync(File localFile, CloudMetaData remoteFile) { SyncManager sync = new SyncManager(getApplicationContext()); // are there local changes? boolean isLocalModified = false; DatabaseMetadata currentDb = this.recentDatabasesProvider .get(localFile.getAbsolutePath()); // todo remove the null-check below after the default record is established. if (currentDb != null) { isLocalModified = currentDb.isLocalFileChanged; } Timber.d("local file has changes: %b", isLocalModified); // are there remote changes? boolean isRemoteModified = false; try { isRemoteModified = sync.isRemoteFileModified(remoteFile); } catch (RuntimeException e) { Timber.e(e, "No remote change data found in metadata! Please re-synchronize manually."); // notify the user! sendMessage(SyncServiceMessage.ERROR); return; } Timber.d("Remote file has changes: %b", isRemoteModified); // possible outcomes: if (!isLocalModified && !isRemoteModified) { sendMessage(SyncServiceMessage.FILE_NOT_CHANGED); sendStopEvent(); return; } if (isLocalModified && isRemoteModified) { // if both changed, there is a conflict! Timber.w(getString(R.string.both_files_modified)); sendMessage(SyncServiceMessage.CONFLICT); sendStopEvent(); showNotificationForConflict(); return; } if (isRemoteModified) { Timber.d("Remote file %s changed. Triggering download.", remoteFile.getPath()); // download file triggerDownload(localFile, remoteFile); return; } if (isLocalModified) { Timber.d("Local file %s has changed. Triggering upload.", localFile.getPath()); // upload file triggerUpload(localFile, remoteFile); return; } } private boolean sendMessage(SyncServiceMessage message) { if (mOutMessenger == null) return true; Message msg = new Message(); msg.what = message.code; try { mOutMessenger.send(msg); } catch (Exception e) { Timber.e(e, "sending message from the sync service"); return false; } return true; } private void sendStartEvent() { // send notification via event bus if (EventBus.getDefault().hasSubscriberForEvent(SyncStartingEvent.class)) { EventBus.getDefault().post(new SyncStartingEvent()); } } private void sendStopEvent() { if (EventBus.getDefault().hasSubscriberForEvent(SyncStoppingEvent.class)) { EventBus.getDefault().post(new SyncStoppingEvent()); } } private void showNotificationUploading() { Notification notification = new SyncNotificationFactory(getApplicationContext()) .getNotificationUploading(); // send notification, upload starting if (notification == null || mNotificationManager == null) return; mNotificationManager.notify(SyncConstants.NOTIFICATION_SYNC_IN_PROGRESS, notification); } private void showNotificationForConflict() { Notification notification = new SyncNotificationFactory(getApplicationContext()) .getNotificationForConflict(); mNotificationManager.notify(SyncConstants.NOTIFICATION_SYNC_ERROR, notification); } }