From cb3b1e47a14de03562b3a29f8a14ea1e041befed Mon Sep 17 00:00:00 2001 From: Moez Bhatti Date: Thu, 23 Aug 2018 17:47:17 -0400 Subject: [PATCH] Implement logic to export messages to json --- build.gradle | 1 + data/build.gradle | 1 + .../QKSMS/repository/BackupRepositoryImpl.kt | 96 +++++++++++++++---- .../com/moez/QKSMS/util/QkFileObserver.kt | 2 +- .../QKSMS/model/{Backup.kt => BackupFile.kt} | 4 +- .../moez/QKSMS/repository/BackupRepository.kt | 4 +- presentation/build.gradle | 1 + .../QKSMS/feature/backup/BackupAdapter.kt | 8 +- .../QKSMS/feature/backup/BackupController.kt | 4 +- .../QKSMS/feature/backup/BackupPresenter.kt | 5 + .../moez/QKSMS/feature/backup/BackupState.kt | 4 +- .../moez/QKSMS/feature/backup/BackupView.kt | 5 +- .../com/moez/QKSMS/injection/AppModule.kt | 7 ++ 13 files changed, 106 insertions(+), 36 deletions(-) rename domain/src/main/java/com/moez/QKSMS/model/{Backup.kt => BackupFile.kt} (93%) diff --git a/build.gradle b/build.gradle index 2e6a8313..15103d91 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,7 @@ buildscript { ext.ktx_version = "1.0.0-alpha1" ext.lifecycle_version = '2.0.0-alpha1' ext.mockito_version = '2.18.3' + ext.moshi_version = '1.6.0' ext.realm_version = '4.3.1' ext.realm_adapters_version = '2.1.0' ext.rxandroid_version = '2.0.1' diff --git a/data/build.gradle b/data/build.gradle index 137ea738..dfc56ede 100644 --- a/data/build.gradle +++ b/data/build.gradle @@ -74,6 +74,7 @@ dependencies { implementation "androidx.core:core-ktx:$ktx_version" implementation "com.f2prateek.rx.preferences2:rx-preferences:$rx_preferences_version" implementation "com.jakewharton.timber:timber:$timber_version" + implementation "com.squareup.moshi:moshi:$moshi_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "me.leolin:ShortcutBadger:1.1.21" implementation project(":android-smsmms") diff --git a/data/src/main/java/com/moez/QKSMS/repository/BackupRepositoryImpl.kt b/data/src/main/java/com/moez/QKSMS/repository/BackupRepositoryImpl.kt index cf52e8d5..85da6f80 100644 --- a/data/src/main/java/com/moez/QKSMS/repository/BackupRepositoryImpl.kt +++ b/data/src/main/java/com/moez/QKSMS/repository/BackupRepositoryImpl.kt @@ -21,36 +21,51 @@ package com.moez.QKSMS.repository import android.animation.ObjectAnimator import android.os.Environment import androidx.core.animation.addListener -import com.moez.QKSMS.model.Backup +import com.moez.QKSMS.model.BackupFile +import com.moez.QKSMS.model.Message import com.moez.QKSMS.util.QkFileObserver +import com.squareup.moshi.Moshi import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers +import io.reactivex.schedulers.Schedulers import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.Subject +import io.realm.Realm import java.io.File +import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.* import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton + @Singleton -class BackupRepositoryImpl @Inject constructor() : BackupRepository { +class BackupRepositoryImpl @Inject constructor( + private val moshi: Moshi +) : BackupRepository { companion object { private val BACKUP_DIRECTORY = Environment.getExternalStorageDirectory().toString() + "/QKSMS/Backups" } + data class Backup( + val messages: List + ) + data class BackupMessage( val protocol: Int, val address: String, val date: Long, - val type: Int, + val type: String, val body: String, - val service_center: String?, + val serviceCenter: String?, val read: Boolean, val status: Int, val locked: Boolean, - val date_sent: Long) + val dateSent: Long) + // Subjects to emit our progress events to private val backupProgress: Subject = BehaviorSubject.createDefault(BackupRepository.Progress.Idle()) private val restoreProgress: Subject = BehaviorSubject.createDefault(BackupRepository.Progress.Idle()) @@ -58,34 +73,75 @@ class BackupRepositoryImpl @Inject constructor() : BackupRepository { // If a backup or restore is already running, don't do anything if (isBackupOrRestoreRunning()) return - backupProgress.onNext(BackupRepository.Progress.Running(0)) + // Map all the messages into our object we'll use for the Json mapping + val backupMessages = Realm.getDefaultInstance().use { realm -> + // Get the messages from realm + val messages = realm.where(Message::class.java).findAll().createSnapshot() + val messageCount = messages.size - Thread.sleep(1000) + // Map the messages to the new format + val startTime = System.currentTimeMillis() + messages.mapIndexed { index, message -> + val progress = index.toDouble() / messageCount * 100 + val elapsed = System.currentTimeMillis() - startTime + val remaining = when { + index > 100 -> TimeUnit.MILLISECONDS.toSeconds((elapsed.toDouble() / index * (messageCount - index)).toLong()) + else -> 0 + } - val animator = ObjectAnimator.ofInt(0, 100) - animator.duration = 3000 - animator.addUpdateListener { - val remaining = TimeUnit.MILLISECONDS.toSeconds(it.duration - it.currentPlayTime) + 1 - backupProgress.onNext(BackupRepository.Progress.Running(it.animatedValue as Int, "$remaining seconds remaining")) + // Update the progress + backupProgress.onNext(BackupRepository.Progress.Running(progress.toInt(), "$remaining seconds remaining")) + messageToBackupMessage(message) + } } - animator.addListener(onEnd = { - backupProgress.onNext(BackupRepository.Progress.Idle()) - }) - AndroidSchedulers.mainThread().scheduleDirect { animator.start() } + + // Update the status, and set the progress to be indeterminate since we can no longer calculate progress + backupProgress.onNext(BackupRepository.Progress.Running(0, "Saving...")) + + // Convert the data to json + val adapter = moshi.adapter(Backup::class.java).indent("\t") + val json = adapter.toJson(Backup(backupMessages)).toByteArray() + + try { + // Create the directory and file + val dir = File(BACKUP_DIRECTORY).apply { mkdirs() } + val file = File(dir, "backup-${SimpleDateFormat("yyyyMMddHHmmss", Locale.getDefault()).format(System.currentTimeMillis())}.json") + + // Write the log to the file + FileOutputStream(file, true).use { fileOutputStream -> fileOutputStream.write(json) } + } catch (e: Exception) { + } + + backupProgress.onNext(BackupRepository.Progress.Idle()) } + private fun messageToBackupMessage(message: Message): BackupMessage = BackupMessage( + protocol = 0, + address = message.address, + date = message.date, + type = message.type, + body = message.body, + serviceCenter = null, + read = message.read, + status = message.deliveryStatus, + locked = message.locked, + dateSent = message.dateSent) + override fun getBackupProgress(): Observable = backupProgress - override fun getBackups(): Observable> = QkFileObserver(BACKUP_DIRECTORY) - .observable - .map { path -> File(path).listFiles() } + override fun getBackups(): Observable> = QkFileObserver(BACKUP_DIRECTORY).observable + .map { File(BACKUP_DIRECTORY).listFiles() ?: arrayOf() } + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.computation()) .map { files -> files.map { file -> val date = file.lastModified() + val messages = 0 val size = file.length() - Backup(date, listOf(), size) + BackupFile(date, messages, size) } } + .map { files -> files.sortedByDescending { file -> file.date } } override fun performRestore() { // If a backup or restore is already running, don't do anything diff --git a/data/src/main/java/com/moez/QKSMS/util/QkFileObserver.kt b/data/src/main/java/com/moez/QKSMS/util/QkFileObserver.kt index 304e215d..a0e03e10 100644 --- a/data/src/main/java/com/moez/QKSMS/util/QkFileObserver.kt +++ b/data/src/main/java/com/moez/QKSMS/util/QkFileObserver.kt @@ -22,7 +22,7 @@ import android.os.FileObserver import io.reactivex.Observable import io.reactivex.subjects.BehaviorSubject -class QkFileObserver(path: String) : FileObserver(path) { +class QkFileObserver(private val path: String) : FileObserver(path) { private val subject = BehaviorSubject.createDefault(path).toSerialized() diff --git a/domain/src/main/java/com/moez/QKSMS/model/Backup.kt b/domain/src/main/java/com/moez/QKSMS/model/BackupFile.kt similarity index 93% rename from domain/src/main/java/com/moez/QKSMS/model/Backup.kt rename to domain/src/main/java/com/moez/QKSMS/model/BackupFile.kt index 8c75bafb..9193b41b 100644 --- a/domain/src/main/java/com/moez/QKSMS/model/Backup.kt +++ b/domain/src/main/java/com/moez/QKSMS/model/BackupFile.kt @@ -18,8 +18,8 @@ */ package com.moez.QKSMS.model -data class Backup( +data class BackupFile( val date: Long, - val messages: List, + val messages: Int, val size: Long ) \ No newline at end of file diff --git a/domain/src/main/java/com/moez/QKSMS/repository/BackupRepository.kt b/domain/src/main/java/com/moez/QKSMS/repository/BackupRepository.kt index ab345ca9..d7b5704b 100644 --- a/domain/src/main/java/com/moez/QKSMS/repository/BackupRepository.kt +++ b/domain/src/main/java/com/moez/QKSMS/repository/BackupRepository.kt @@ -18,7 +18,7 @@ */ package com.moez.QKSMS.repository -import com.moez.QKSMS.model.Backup +import com.moez.QKSMS.model.BackupFile import io.reactivex.Observable interface BackupRepository { @@ -35,7 +35,7 @@ interface BackupRepository { /** * Returns a list of all local backups */ - fun getBackups(): Observable> + fun getBackups(): Observable> fun performRestore() diff --git a/presentation/build.gradle b/presentation/build.gradle index e5a4b54c..3af3d254 100644 --- a/presentation/build.gradle +++ b/presentation/build.gradle @@ -180,6 +180,7 @@ dependencies { implementation "com.f2prateek.rx.preferences2:rx-preferences:$rx_preferences_version" implementation "com.google.android:flexbox:0.3.1" implementation "com.jakewharton.timber:timber:$timber_version" + implementation "com.squareup.moshi:moshi:$moshi_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation project(":android-smsmms") implementation project(":common") diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupAdapter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupAdapter.kt index 0274485c..fc4e0ea2 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupAdapter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupAdapter.kt @@ -26,7 +26,7 @@ import com.moez.QKSMS.R import com.moez.QKSMS.common.base.FlowableAdapter import com.moez.QKSMS.common.base.QkViewHolder import com.moez.QKSMS.common.util.DateFormatter -import com.moez.QKSMS.model.Backup +import com.moez.QKSMS.model.BackupFile import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import kotlinx.android.synthetic.main.backup_list_item.view.* @@ -35,9 +35,9 @@ import javax.inject.Inject class BackupAdapter @Inject constructor( private val context: Context, private val dateFormatter: DateFormatter -) : FlowableAdapter() { +) : FlowableAdapter() { - val backupSelected: Subject = PublishSubject.create() + val backupSelected: Subject = PublishSubject.create() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): QkViewHolder { val layoutInflater = LayoutInflater.from(parent.context) @@ -52,7 +52,7 @@ class BackupAdapter @Inject constructor( val backup = getItem(position) val view = holder.itemView - val count = backup.messages.size + val count = backup.messages view.title.text = dateFormatter.getDetailedTimestamp(backup.date) view.messages.text = context.resources.getQuantityString(R.plurals.backup_message_count, count, count) diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupController.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupController.kt index f0b860c5..c850433f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupController.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupController.kt @@ -31,7 +31,7 @@ import com.moez.QKSMS.common.util.extensions.setBackgroundTint import com.moez.QKSMS.common.util.extensions.setTint import com.moez.QKSMS.common.widget.PreferenceView import com.moez.QKSMS.injection.appComponent -import com.moez.QKSMS.model.Backup +import com.moez.QKSMS.model.BackupFile import com.moez.QKSMS.repository.BackupRepository import io.reactivex.Observable import kotlinx.android.synthetic.main.backup_controller.* @@ -135,7 +135,7 @@ class BackupController : QkController( override fun restoreClicks(): Observable<*> = restore.clicks() - override fun restoreFileSelected(): Observable = adapter.backupSelected + override fun restoreFileSelected(): Observable = adapter.backupSelected .doOnNext { dialog.dismiss() } override fun fabClicks(): Observable<*> = fab.clicks() diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt index 05e7690c..17272c3f 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupPresenter.kt @@ -27,6 +27,7 @@ import com.moez.QKSMS.repository.BackupRepository import com.uber.autodispose.kotlin.autoDisposable import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.withLatestFrom +import java.util.concurrent.TimeUnit import javax.inject.Inject class BackupPresenter @Inject constructor( @@ -39,9 +40,13 @@ class BackupPresenter @Inject constructor( init { disposables += backupRepo.getBackupProgress() + .sample(16, TimeUnit.MILLISECONDS) + .distinctUntilChanged() .subscribe { progress -> newState { copy(backupProgress = progress) } } disposables += backupRepo.getRestoreProgress() + .sample(16, TimeUnit.MILLISECONDS) + .distinctUntilChanged() .subscribe { progress -> newState { copy(restoreProgress = progress) } } disposables += backupRepo.getBackups() diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupState.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupState.kt index 461feeaa..cc0b7e02 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupState.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupState.kt @@ -18,13 +18,13 @@ */ package com.moez.QKSMS.feature.backup -import com.moez.QKSMS.model.Backup +import com.moez.QKSMS.model.BackupFile import com.moez.QKSMS.repository.BackupRepository data class BackupState( val backupProgress: BackupRepository.Progress = BackupRepository.Progress.Idle(), val restoreProgress: BackupRepository.Progress = BackupRepository.Progress.Idle(), val lastBackup: Long? = null, - val backups: List = listOf(), + val backups: List = listOf(), val upgraded: Boolean = false ) \ No newline at end of file diff --git a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupView.kt b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupView.kt index 44fca2a6..2efc41e8 100644 --- a/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupView.kt +++ b/presentation/src/main/java/com/moez/QKSMS/feature/backup/BackupView.kt @@ -19,14 +19,13 @@ package com.moez.QKSMS.feature.backup import com.moez.QKSMS.common.base.QkViewContract -import com.moez.QKSMS.model.Backup +import com.moez.QKSMS.model.BackupFile import io.reactivex.Observable -import java.io.File interface BackupView : QkViewContract { fun restoreClicks(): Observable<*> - fun restoreFileSelected(): Observable + fun restoreFileSelected(): Observable fun fabClicks(): Observable<*> fun selectFile() diff --git a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt index 29072c9e..1b595b7c 100644 --- a/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt +++ b/presentation/src/main/java/com/moez/QKSMS/injection/AppModule.kt @@ -67,6 +67,7 @@ import com.moez.QKSMS.repository.ScheduledMessageRepository import com.moez.QKSMS.repository.ScheduledMessageRepositoryImpl import com.moez.QKSMS.repository.SyncRepository import com.moez.QKSMS.repository.SyncRepositoryImpl +import com.squareup.moshi.Moshi import dagger.Module import dagger.Provides import javax.inject.Singleton @@ -90,6 +91,12 @@ class AppModule(private var application: Application) { return RxSharedPreferences.create(preferences) } + @Provides + @Singleton + fun provideMoshi(): Moshi { + return Moshi.Builder().build() + } + @Provides fun provideViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory = factory