├── .github └── workflows │ └── release_tag.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── balti │ │ └── migrate │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── balti │ │ │ └── migrate │ │ │ ├── BaseApplication.kt │ │ │ ├── app │ │ │ ├── MainActivity.kt │ │ │ ├── MainActivityViewModel.kt │ │ │ ├── di │ │ │ │ └── AppDiModule.kt │ │ │ └── ui │ │ │ │ ├── components │ │ │ │ └── MainScreenNavContainer.kt │ │ │ │ ├── navigation │ │ │ │ ├── AppNavBarItem.kt │ │ │ │ ├── Graph.kt │ │ │ │ ├── HomeGraphViewModel.kt │ │ │ │ └── RestoreRouteChoicesViewModel.kt │ │ │ │ ├── screens │ │ │ │ ├── appSettings │ │ │ │ │ ├── AppSettings.kt │ │ │ │ │ ├── AppSettingsAction.kt │ │ │ │ │ ├── AppSettingsState.kt │ │ │ │ │ └── AppSettingsViewModel.kt │ │ │ │ ├── home │ │ │ │ │ ├── AboutDialog.kt │ │ │ │ │ ├── ScreenHome.kt │ │ │ │ │ ├── ScreenHomeAction.kt │ │ │ │ │ ├── ScreenHomeState.kt │ │ │ │ │ └── ScreenHomeViewModel.kt │ │ │ │ └── setupPermission │ │ │ │ │ ├── SetupPermissionScreen.kt │ │ │ │ │ ├── SetupPermissionScreenAction.kt │ │ │ │ │ ├── SetupPermissionScreenState.kt │ │ │ │ │ └── SetupPermissionScreenViewModel.kt │ │ │ │ └── theme │ │ │ │ ├── Color.kt │ │ │ │ ├── Theme.kt │ │ │ │ └── Type.kt │ │ │ ├── backup │ │ │ ├── data │ │ │ │ ├── service │ │ │ │ │ └── BackupService.kt │ │ │ │ └── sources │ │ │ │ │ ├── BackupNotificationHandlerImpl.kt │ │ │ │ │ ├── callLog │ │ │ │ │ ├── CallLogDBWriter.kt │ │ │ │ │ └── CallLogSource.kt │ │ │ │ │ ├── contacts │ │ │ │ │ ├── ContactsDBWriter.kt │ │ │ │ │ └── ContactsSource.kt │ │ │ │ │ └── sms │ │ │ │ │ ├── SmsDBWriter.kt │ │ │ │ │ └── SmsSource.kt │ │ │ ├── di │ │ │ │ └── BackupDiModule.kt │ │ │ └── ui │ │ │ │ └── screens │ │ │ │ ├── backupName │ │ │ │ ├── BackupName.kt │ │ │ │ ├── BackupNameAction.kt │ │ │ │ ├── BackupNameState.kt │ │ │ │ └── BackupNameViewModel.kt │ │ │ │ ├── listScreen │ │ │ │ ├── callLogBackupSelection │ │ │ │ │ ├── CallLogBackupSelection.kt │ │ │ │ │ ├── CallLogBackupSelectionAction.kt │ │ │ │ │ ├── CallLogBackupSelectionState.kt │ │ │ │ │ └── CallLogBackupSelectionViewModel.kt │ │ │ │ ├── contactBackupSelection │ │ │ │ │ ├── ContactBackupSelection.kt │ │ │ │ │ ├── ContactBackupSelectionAction.kt │ │ │ │ │ ├── ContactBackupSelectionState.kt │ │ │ │ │ ├── ContactBackupSelectionViewModel.kt │ │ │ │ │ ├── ContactComponents.kt │ │ │ │ │ ├── LocalAndSyncedContacts.kt │ │ │ │ │ ├── OnlyLocalContacts.kt │ │ │ │ │ ├── OnlySyncedContacts.kt │ │ │ │ │ └── SyncedWarningDialog.kt │ │ │ │ └── smsBackupSelection │ │ │ │ │ ├── SmsBackupSelection.kt │ │ │ │ │ ├── SmsBackupSelectionAction.kt │ │ │ │ │ ├── SmsBackupSelectionState.kt │ │ │ │ │ └── SmsBackupSelectionViewModel.kt │ │ │ │ └── progressScreen │ │ │ │ ├── BackupProgressScreen.kt │ │ │ │ ├── BackupProgressScreenAction.kt │ │ │ │ ├── BackupProgressScreenState.kt │ │ │ │ └── BackupProgressScreenViewModel.kt │ │ │ ├── common │ │ │ ├── data │ │ │ │ ├── model │ │ │ │ │ ├── CallLogData.kt │ │ │ │ │ ├── ContactData.kt │ │ │ │ │ ├── JavaFile.kt │ │ │ │ │ ├── MediaStoreDownloadFile.kt │ │ │ │ │ ├── NotificationInfo.kt │ │ │ │ │ ├── SafFile.kt │ │ │ │ │ └── SmsData.kt │ │ │ │ ├── repository │ │ │ │ │ └── ProgressLogRepositoryImpl.kt │ │ │ │ └── sources │ │ │ │ │ ├── ContextSourceImpl.kt │ │ │ │ │ ├── PreferencesImpl.kt │ │ │ │ │ └── fileSystem │ │ │ │ │ ├── ExportDirectoryBrowserMediaStore.kt │ │ │ │ │ ├── ExportDirectoryBrowserSafFile.kt │ │ │ │ │ ├── FileSystemSourceImpl.kt │ │ │ │ │ ├── TextWriterImpl.kt │ │ │ │ │ ├── TransferUtils.kt │ │ │ │ │ ├── TransferUtilsJavaFile.kt │ │ │ │ │ ├── TransferUtilsMediaStoreDownload.kt │ │ │ │ │ └── TransferUtilsSafFile.kt │ │ │ ├── di │ │ │ │ └── CommonDiModule.kt │ │ │ ├── ui │ │ │ │ ├── components │ │ │ │ │ ├── CountBar.kt │ │ │ │ │ ├── EmptyImageVector.kt │ │ │ │ │ ├── KeepScreenOn.kt │ │ │ │ │ ├── LoadingDialog.kt │ │ │ │ │ ├── LoadingProgressBar.kt │ │ │ │ │ ├── LocationSelector.kt │ │ │ │ │ ├── NextFab.kt │ │ │ │ │ ├── RenderCallLogItem.kt │ │ │ │ │ ├── RenderContactItem.kt │ │ │ │ │ └── RenderSmsItem.kt │ │ │ │ ├── listScreen │ │ │ │ │ ├── ListLoadingLayout.kt │ │ │ │ │ ├── ListNoDataLayout.kt │ │ │ │ │ ├── ListPermissionRequestLayout.kt │ │ │ │ │ └── ListScreenShell.kt │ │ │ │ └── progressScreen │ │ │ │ │ ├── ErrorLayoutToggle.kt │ │ │ │ │ ├── ProgressLogLayout.kt │ │ │ │ │ └── ProgressScreenBottomBar.kt │ │ │ └── utils │ │ │ │ ├── DBUtils.kt │ │ │ │ ├── DateUtils.kt │ │ │ │ ├── DeepLinkUtils.kt │ │ │ │ ├── ListItemUtils.kt │ │ │ │ ├── NotificationUtils.kt │ │ │ │ ├── PermissionUtils.kt │ │ │ │ └── ServiceUtils.kt │ │ │ └── restore │ │ │ ├── data │ │ │ ├── service │ │ │ │ └── RestoreService.kt │ │ │ └── sources │ │ │ │ ├── RestoreNotificationHandlerImpl.kt │ │ │ │ ├── callLog │ │ │ │ ├── CallLogDBReader.kt │ │ │ │ └── CallLogRestore.kt │ │ │ │ ├── contacts │ │ │ │ └── ContactsDBReader.kt │ │ │ │ └── sms │ │ │ │ ├── SmsDBReader.kt │ │ │ │ ├── SmsRestore.kt │ │ │ │ └── dummies │ │ │ │ ├── DummyComposeSmsActivity.kt │ │ │ │ ├── DummyMmsBroadcastReceiver.kt │ │ │ │ ├── DummySmsBroadcastReceiver.kt │ │ │ │ └── DummySmsSendService.kt │ │ │ ├── di │ │ │ └── RestoreDiModule.kt │ │ │ └── ui │ │ │ └── screens │ │ │ ├── browseRestoreDirectory │ │ │ ├── BrowseRestoreDirectory.kt │ │ │ ├── BrowseRestoreDirectoryActions.kt │ │ │ ├── BrowseRestoreDirectoryState.kt │ │ │ └── BrowseRestoreDirectoryViewModel.kt │ │ │ ├── listScreen │ │ │ ├── callLogRestoreSelection │ │ │ │ ├── CallLogRestoreSelection.kt │ │ │ │ ├── CallLogRestoreSelectionAction.kt │ │ │ │ ├── CallLogRestoreSelectionState.kt │ │ │ │ └── CallLogRestoreSelectionViewModel.kt │ │ │ ├── contactRestoreSelection │ │ │ │ ├── ContactRestoreSelection.kt │ │ │ │ ├── ContactRestoreSelectionAction.kt │ │ │ │ ├── ContactRestoreSelectionState.kt │ │ │ │ └── ContactRestoreSelectionViewModel.kt │ │ │ └── smsRestoreSelection │ │ │ │ ├── SmsRestoreSelection.kt │ │ │ │ ├── SmsRestoreSelectionAction.kt │ │ │ │ ├── SmsRestoreSelectionState.kt │ │ │ │ └── SmsRestoreSelectionViewModel.kt │ │ │ ├── progressScreen │ │ │ ├── RestoreProgressScreen.kt │ │ │ ├── RestoreProgressScreenAction.kt │ │ │ ├── RestoreProgressScreenState.kt │ │ │ ├── RestoreProgressScreenViewModel.kt │ │ │ └── SmsAppChangeDialog.kt │ │ │ └── restoreSummary │ │ │ ├── RestoreSummary.kt │ │ │ ├── RestoreSummaryAction.kt │ │ │ ├── RestoreSummaryItemState.kt │ │ │ ├── RestoreSummaryState.kt │ │ │ ├── RestoreSummaryViewModel.kt │ │ │ └── components │ │ │ ├── DelegateRestore.kt │ │ │ ├── SimpleYesNoDialog.kt │ │ │ ├── SpecialPermissions.kt │ │ │ ├── StandardRestore.kt │ │ │ └── SummaryItem.kt │ └── res │ │ ├── drawable │ │ ├── app_icon_round.png │ │ ├── baseline_voicemail_24.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── notification_icon_00.xml │ │ ├── notification_icon_05.xml │ │ ├── notification_icon_10.xml │ │ ├── notification_icon_15.xml │ │ ├── notification_icon_20.xml │ │ ├── notification_icon_25.xml │ │ ├── notification_icon_30.xml │ │ ├── notification_icon_35.xml │ │ ├── notification_icon_40.xml │ │ ├── notification_icon_45.xml │ │ ├── notification_icon_50.xml │ │ ├── notification_icon_55.xml │ │ ├── notification_icon_60.xml │ │ ├── notification_icon_65.xml │ │ ├── notification_icon_70.xml │ │ ├── notification_icon_75.xml │ │ ├── notification_icon_80.xml │ │ ├── notification_icon_85.xml │ │ ├── notification_icon_90.xml │ │ ├── notification_icon_95.xml │ │ ├── notification_rotating_icon.xml │ │ ├── outline_close_24.xml │ │ └── outline_warning_24.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── values-night │ │ └── themes.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── file_paths.xml │ └── test │ └── java │ └── balti │ └── migrate │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── display_assets ├── Screenshot_01.png ├── Screenshot_02.png ├── Screenshot_03.png ├── Screenshot_04.png └── feature_graphic.png ├── domain ├── .gitignore ├── build.gradle.kts └── src │ └── main │ └── java │ └── baltiapps │ └── migrate │ └── domain │ ├── Constants.kt │ ├── backup │ ├── model │ │ └── BackupLocation.kt │ ├── repository │ │ └── BackupDataRepository.kt │ ├── sources │ │ └── DataSource.kt │ └── usecase │ │ ├── BackupCallLogUseCase.kt │ │ ├── BackupContactsUseCase.kt │ │ ├── BackupSmsUseCase.kt │ │ ├── ReadCallLogForBackupUseCase.kt │ │ ├── ReadContactsForBackupUseCase.kt │ │ └── ReadSmsForBackupUseCase.kt │ ├── common │ ├── Extensions.kt │ ├── model │ │ ├── CallLogListItem.kt │ │ ├── ContactListItem.kt │ │ ├── DataItem.kt │ │ ├── GenericFile.kt │ │ ├── ItemCreationDate.kt │ │ ├── ListItem.kt │ │ ├── Progress.kt │ │ └── SmsListItem.kt │ ├── repository │ │ ├── DataRepository.kt │ │ └── ProgressLogRepository.kt │ ├── sources │ │ ├── ContextSource.kt │ │ ├── NotificationHandler.kt │ │ ├── Preferences.kt │ │ └── fileSystem │ │ │ ├── ExportDirectoryBrowser.kt │ │ │ ├── FileSystemSource.kt │ │ │ └── FileSystemUtils.kt │ ├── usecase │ │ ├── StageSelectedCallLogs.kt │ │ ├── StageSelectedContacts.kt │ │ └── StageSelectedSms.kt │ └── utils │ │ └── BackupFilesUtils.kt │ ├── exceptions │ ├── ContentReadException.kt │ ├── PermissionException.kt │ ├── UnknownDataTypeException.kt │ └── UnknownFileTypeException.kt │ └── restore │ ├── repository │ └── RestoreDataRepository.kt │ ├── sources │ └── DataRestore.kt │ └── usecase │ ├── ExportContactsForRestoreUseCase.kt │ ├── ReadCallLogForRestoreUseCase.kt │ ├── ReadContactsForRestoreUseCase.kt │ ├── ReadFilesFromBackupUseCase.kt │ ├── ReadSmsForRestoreUseCase.kt │ ├── RestoreCallLogUseCase.kt │ └── RestoreSmsUseCase.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── privacy_policy.md ├── settings.gradle.kts └── stuff_to_do.txt /.github/workflows/release_tag.yml: -------------------------------------------------------------------------------- 1 | name: Build APK on Tag 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | build: 10 | name: Build and release 11 | runs-on: ubuntu-latest 12 | 13 | env: 14 | KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} 15 | KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} 16 | KEY_ALIAS: ${{ secrets.KEY_ALIAS }} 17 | KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} 18 | 19 | steps: 20 | - name: Checkout source code 21 | uses: actions/checkout@v4 22 | 23 | - name: Set up JDK 24 | uses: actions/setup-java@v4 25 | with: 26 | distribution: 'temurin' 27 | java-version: '21' 28 | 29 | - name: Set up Gradle 30 | uses: gradle/actions/setup-gradle@v4 31 | 32 | - name: Grant execute permission to gradlew 33 | run: chmod +x ./gradlew 34 | 35 | - name: Build APK 36 | run: ./gradlew assembleRelease 37 | 38 | - name: Build AAB 39 | run: ./gradlew bundleRelease 40 | 41 | - name: Create GitHub Release 42 | uses: softprops/action-gh-release@v2 43 | with: 44 | tag_name: ${{ github.ref_name }} 45 | name: ${{ github.ref_name }} 46 | files: | 47 | app/build/outputs/apk/release/*.apk 48 | app/build/outputs/bundle/release/*.aab 49 | env: 50 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 51 | 52 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # License for "Migrate - Data Backup]" 2 | 3 | Copyright © BaltiApps 4 | 5 | ## 1. Definitions 6 | - **"Software"** refers to **Migrate - Data Backup** and any associated files distributed under this license. 7 | - **"You"** (or "Your") refers to any individual or entity using, modifying, or distributing the Software. 8 | 9 | ## 2. Grant of License 10 | Subject to the Conditions set forth below, BaltiApps hereby grants You a worldwide, royalty-free, non-exclusive license to: 11 | 12 | - Use, modify, and fork the Software for **personal, non-commercial purposes**; 13 | - Contribute modifications back to the original repository; 14 | - Distribute modified or unmodified copies of the Software, provided attribution is maintained; 15 | - Integrate the Software, with or without modifications, into custom ROMs or OEM ROMs. 16 | 17 | ## 3. Conditions 18 | Your rights under this License are subject to the following conditions: 19 | 20 | 1. **Attribution:** You must maintain clear and visible attribution to "BaltiApps" in any distributed version of the Software or its modifications. 21 | 22 | 2. **Non-Commercial Use Only:** 23 | - You may not sell, license, or otherwise monetize the Software or any derivative works without **prior written permission** from BaltiApps. 24 | - Permission may be granted or denied at BaltiApps' sole discretion. 25 | 26 | 3. **Commercial ROMs and OEM Use:** 27 | - If the Software is integrated into a commercial custom ROM or OEM ROM, **prior written permission** must be obtained from BaltiApps. 28 | - Permission may be granted or denied at BaltiApps' sole discretion. 29 | 30 | ## 4. Disclaimer 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. 32 | 33 | IN NO EVENT SHALL BALTIAPPS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Migrate - Data Backup 2 | 3 | ![Feature graphic](display_assets/feature_graphic.png) 4 | 5 | ## Branch version 6 | Version: 6.0.1 (Artemis) 7 | 8 | ## Screenshots 9 | 10 | 11 | 12 | 13 | ## Features 14 | 15 | - Backup and restore 16 | - Call log history 17 | - SMS 18 | - Contacts 19 | 20 | ## Planned features 21 | 22 | - Backup calender entries 23 | - Possible support for password protection 24 | - Root based features - using magisk or ADB root on custom ROMs 25 | - App backup 26 | - Some system features backup 27 | 28 | ## Compilation guide 29 | 1. Clone the three repositories. 30 | ``` 31 | git clone https://github.com/BaltiApps/Migrate-OSS.git 32 | ``` 33 | 2. Open `Migrate-OSS` project in Android Studio. Then compile and run (`Shift+F10` in most cases). 34 | 35 | ### Links 36 | 37 | [Privacy Policy](privacy_policy.md) 38 | 39 | [Telegram group link](https://t.me/migrateApp) 40 | [XDA thread Link](https://forum.xda-developers.com/t/app-root-5-0-1st-nov-2020-migrate-custom-rom-migration-tool.3862763/) 41 | [Official Google Play Store download](https://play.google.com/store/apps/details?id=balti.migrate) 42 | [Older versions on AndroidFileHost](https://www.androidfilehost.com/?w=files&flid=285270) 43 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/balti/migrate/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("balti.migrate", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate 2 | 3 | import android.app.Application 4 | import balti.migrate.app.di.appDiModule 5 | import balti.migrate.backup.di.backupDiModule 6 | import balti.migrate.common.di.commonDiModule 7 | import balti.migrate.restore.di.restoreDiModule 8 | import org.koin.android.ext.koin.androidContext 9 | import org.koin.core.context.startKoin 10 | import timber.log.Timber 11 | 12 | class BaseApplication: Application() { 13 | 14 | override fun onCreate() { 15 | super.onCreate() 16 | if (BuildConfig.DEBUG) { 17 | Timber.plant(Timber.DebugTree()) 18 | } 19 | startKoin { 20 | androidContext(this@BaseApplication) 21 | modules(commonDiModule, backupDiModule, restoreDiModule, appDiModule) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.setContent 7 | import androidx.activity.enableEdgeToEdge 8 | import androidx.compose.foundation.isSystemInDarkTheme 9 | import balti.migrate.app.ui.navigation.Graph 10 | import balti.migrate.app.ui.theme.MigrateTheme 11 | import balti.migrate.backup.data.service.BackupService 12 | import balti.migrate.restore.data.service.RestoreService 13 | import baltiapps.migrate.domain.ACTION_CANCEL_BACKUP 14 | import baltiapps.migrate.domain.ACTION_CANCEL_RESTORE 15 | import baltiapps.migrate.domain.ACTION_START_BACKUP 16 | import baltiapps.migrate.domain.ACTION_START_RESTORE 17 | import baltiapps.migrate.domain.EXTRA_BACKUP_NAME 18 | import baltiapps.migrate.domain.EXTRA_BACKUP_URI_STRING 19 | import baltiapps.migrate.domain.backup.model.BackupLocation 20 | import baltiapps.migrate.domain.common.sources.Preferences.DarkMode 21 | import org.koin.androidx.viewmodel.ext.android.viewModel 22 | 23 | class MainActivity : ComponentActivity() { 24 | 25 | private val viewModel: MainActivityViewModel by viewModel() 26 | 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | enableEdgeToEdge() 30 | setContent { 31 | MigrateTheme( 32 | darkTheme = when(viewModel.darkMode.value) { 33 | DarkMode.LIGHT -> false 34 | DarkMode.DARK -> true 35 | DarkMode.SYSTEM -> isSystemInDarkTheme() 36 | }, 37 | dynamicColor = viewModel.shouldFollowSystemColors.value, 38 | ) { 39 | Graph( 40 | startBackupService = ::startBackupService, 41 | cancelBackup = ::cancelBackup, 42 | startRestoreService = ::startRestoreService, 43 | cancelRestore = ::cancelRestore, 44 | shouldShowPermissionScreen = viewModel::shouldShowPermissionScreen, 45 | updateUiState = viewModel::updateUiState, 46 | ) 47 | } 48 | } 49 | } 50 | 51 | private fun startBackupService(backupLocation: BackupLocation) { 52 | Intent(this, BackupService::class.java).apply { 53 | action = ACTION_START_BACKUP 54 | putExtra(EXTRA_BACKUP_URI_STRING, backupLocation.backupUriString) 55 | putExtra(EXTRA_BACKUP_NAME, backupLocation.backupName) 56 | }.run { 57 | startForegroundService(this) 58 | } 59 | } 60 | 61 | private fun cancelBackup() { 62 | Intent(this, BackupService::class.java).apply { 63 | action = ACTION_CANCEL_BACKUP 64 | }.run { 65 | startService(this) 66 | } 67 | } 68 | 69 | private fun startRestoreService() { 70 | Intent(this, RestoreService::class.java).apply { 71 | action = ACTION_START_RESTORE 72 | }.run { 73 | startForegroundService(this) 74 | } 75 | } 76 | 77 | private fun cancelRestore() { 78 | Intent(this, RestoreService::class.java).apply { 79 | action = ACTION_CANCEL_RESTORE 80 | }.run { 81 | startService(this) 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/MainActivityViewModel.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.ViewModel 5 | import baltiapps.migrate.domain.common.sources.Preferences 6 | import baltiapps.migrate.domain.common.sources.Preferences.DarkMode 7 | 8 | class MainActivityViewModel( 9 | private val preferences: Preferences, 10 | ): ViewModel() { 11 | 12 | val darkMode = mutableStateOf(preferences.getDarkMode()) 13 | val shouldFollowSystemColors = mutableStateOf(preferences.shouldFollowSystemColors()) 14 | 15 | fun updateUiState(darkMode: DarkMode, followSystemColors: Boolean) { 16 | this.darkMode.value = darkMode 17 | shouldFollowSystemColors.value = followSystemColors 18 | } 19 | 20 | fun shouldShowPermissionScreen(): Boolean { 21 | return preferences.shouldShowPermissionScreen() 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/di/AppDiModule.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.di 2 | 3 | import balti.migrate.app.MainActivityViewModel 4 | import balti.migrate.app.ui.navigation.HomeGraphViewModel 5 | import balti.migrate.app.ui.navigation.RestoreRouteChoicesViewModel 6 | import balti.migrate.app.ui.screens.appSettings.AppSettingsViewModel 7 | import balti.migrate.app.ui.screens.home.ScreenHomeViewModel 8 | import balti.migrate.app.ui.screens.setupPermission.SetupPermissionScreenViewModel 9 | import org.koin.core.module.dsl.viewModelOf 10 | import org.koin.dsl.module 11 | 12 | val appDiModule = module { 13 | 14 | /* ViewModel */ 15 | 16 | viewModelOf(::MainActivityViewModel) 17 | 18 | viewModelOf(::HomeGraphViewModel) 19 | viewModelOf(::RestoreRouteChoicesViewModel) 20 | viewModelOf(::SetupPermissionScreenViewModel) 21 | viewModelOf(::ScreenHomeViewModel) 22 | viewModelOf(::AppSettingsViewModel) 23 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/components/MainScreenNavContainer.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.material3.NavigationBar 8 | import androidx.compose.material3.NavigationBarItem 9 | import androidx.compose.material3.Scaffold 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import balti.migrate.app.ui.navigation.AppNavBarItem 14 | 15 | @Composable 16 | fun MainScreenNavContainer( 17 | appNavBarItems: List, 18 | currentNavRoute: AppNavBarItem, 19 | onBottomNavItemSelected: (AppNavBarItem) -> Unit, 20 | content: @Composable () -> Unit, 21 | ) { 22 | Scaffold( 23 | modifier = Modifier.fillMaxSize(), 24 | bottomBar = { 25 | NavigationBar { 26 | appNavBarItems.forEach { item -> 27 | NavigationBarItem( 28 | selected = currentNavRoute == item, 29 | onClick = { 30 | if (item != currentNavRoute) { 31 | onBottomNavItemSelected(item) 32 | } 33 | }, 34 | icon = { 35 | Icon( 36 | imageVector = if (currentNavRoute == item) { 37 | item.filledIconVector 38 | } else item.unfilledIconVector, 39 | contentDescription = item.label, 40 | ) 41 | }, 42 | label = { 43 | Text(text = item.label) 44 | } 45 | ) 46 | } 47 | } 48 | } 49 | ) { padding -> 50 | Box( 51 | modifier = Modifier.fillMaxSize().padding(padding) 52 | ) { 53 | content() 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/navigation/AppNavBarItem.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.navigation 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | 5 | data class AppNavBarItem( 6 | val route: Any, 7 | val label: String, 8 | val filledIconVector: ImageVector, 9 | val unfilledIconVector: ImageVector, 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/navigation/HomeGraphViewModel.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.navigation 2 | 3 | import androidx.lifecycle.ViewModel 4 | import baltiapps.migrate.domain.backup.repository.BackupDataRepository 5 | 6 | class HomeGraphViewModel( 7 | private val backupRepository: BackupDataRepository, 8 | ) : ViewModel() { 9 | fun resetBackupRepository() { 10 | backupRepository.resetRepository() 11 | } 12 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/navigation/RestoreRouteChoicesViewModel.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.navigation 2 | 3 | import androidx.lifecycle.ViewModel 4 | import baltiapps.migrate.domain.restore.repository.RestoreDataRepository 5 | 6 | class RestoreRouteChoicesViewModel( 7 | private val dataRepository: RestoreDataRepository 8 | ) : ViewModel() { 9 | 10 | private val routeMap = mapOf( 11 | RouteCallLogRestoreSelection to { dataRepository.getCallLogBackupFile() != null }, 12 | RouteSmsRestoreSelection to { dataRepository.getSmsBackupFile() != null }, 13 | RouteContactRestoreSelection to { dataRepository.getContactBackupFile() != null }, 14 | RouteRestoreSummary to { true }, 15 | ) 16 | 17 | private var pointerIndex = -1 18 | 19 | fun findNextRoute(): Any { 20 | while (pointerIndex < routeMap.size) { 21 | pointerIndex++ 22 | val route = routeMap.keys.elementAtOrNull(pointerIndex) 23 | if (route != null && routeMap[route]?.invoke() == true) { 24 | return route 25 | } 26 | } 27 | return routeMap.keys.last() 28 | } 29 | 30 | fun onBackFromRoute() { 31 | pointerIndex-- 32 | } 33 | 34 | fun resetRestorePointer() { 35 | pointerIndex = -1 36 | } 37 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/screens/appSettings/AppSettingsAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.screens.appSettings 2 | 3 | import baltiapps.migrate.domain.common.sources.Preferences 4 | 5 | sealed class AppSettingsAction { 6 | data class ChangeDarkMode( 7 | val darkMode: Preferences.DarkMode, 8 | val updateUiState: (darkMode: Preferences.DarkMode, followSystemColors: Boolean) -> Unit, 9 | ) : AppSettingsAction() 10 | data class ChangeShouldFollowSystemColors( 11 | val shouldFollow: Boolean, 12 | val updateUiState: (darkMode: Preferences.DarkMode, followSystemColors: Boolean) -> Unit, 13 | ) : AppSettingsAction() 14 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/screens/appSettings/AppSettingsState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.screens.appSettings 2 | 3 | import baltiapps.migrate.domain.common.sources.Preferences 4 | 5 | data class AppSettingsState( 6 | val darkMode: Preferences.DarkMode, 7 | val shouldFollowSystemColors: Boolean, 8 | ) 9 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/screens/appSettings/AppSettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.screens.appSettings 2 | 3 | import androidx.lifecycle.ViewModel 4 | import baltiapps.migrate.domain.common.sources.Preferences 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.asStateFlow 7 | import kotlinx.coroutines.flow.update 8 | 9 | class AppSettingsViewModel( 10 | private val preferences: Preferences, 11 | ): ViewModel() { 12 | 13 | private val _state = MutableStateFlow( 14 | AppSettingsState( 15 | darkMode = preferences.getDarkMode(), 16 | shouldFollowSystemColors = preferences.shouldFollowSystemColors(), 17 | ) 18 | ) 19 | val state = _state.asStateFlow() 20 | 21 | fun onAction(action: AppSettingsAction) { 22 | when (action) { 23 | is AppSettingsAction.ChangeDarkMode -> { 24 | preferences.setDarkMode(action.darkMode) 25 | action.updateUiState( 26 | action.darkMode, 27 | _state.value.shouldFollowSystemColors, 28 | ) 29 | _state.update { 30 | it.copy(darkMode = action.darkMode) 31 | } 32 | } 33 | is AppSettingsAction.ChangeShouldFollowSystemColors -> { 34 | preferences.setFollowSystemColors(action.shouldFollow) 35 | action.updateUiState( 36 | _state.value.darkMode, 37 | action.shouldFollow, 38 | ) 39 | _state.update { 40 | it.copy(shouldFollowSystemColors = action.shouldFollow) 41 | } 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/screens/home/ScreenHomeAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.screens.home 2 | 3 | sealed class ScreenHomeAction { 4 | data object OnAboutButtonClicked: ScreenHomeAction() 5 | data object OnAboutDialogDismissed: ScreenHomeAction() 6 | data object OnAppBackupUnavailableDialogDismissed: ScreenHomeAction() 7 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/screens/home/ScreenHomeState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.screens.home 2 | 3 | data class ScreenHomeState( 4 | val shouldShowAboutDialog: Boolean = false, 5 | val shouldShowAppBackupUnavailableDialog: Boolean = false, 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/screens/home/ScreenHomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.screens.home 2 | 3 | import androidx.lifecycle.ViewModel 4 | import baltiapps.migrate.domain.common.sources.Preferences 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.asStateFlow 7 | import kotlinx.coroutines.flow.update 8 | 9 | class ScreenHomeViewModel( 10 | private val preferences: Preferences, 11 | ): ViewModel() { 12 | 13 | private val _state = MutableStateFlow( 14 | ScreenHomeState( 15 | shouldShowAppBackupUnavailableDialog = preferences.shouldShowAppBackupUnavailable(), 16 | ) 17 | ) 18 | val state = _state.asStateFlow() 19 | 20 | fun performAction(action: ScreenHomeAction) { 21 | when (action) { 22 | is ScreenHomeAction.OnAboutButtonClicked -> { 23 | _state.update { it.copy(shouldShowAboutDialog = true) } 24 | } 25 | is ScreenHomeAction.OnAboutDialogDismissed -> { 26 | _state.update { it.copy(shouldShowAboutDialog = false) } 27 | } 28 | is ScreenHomeAction.OnAppBackupUnavailableDialogDismissed -> { 29 | preferences.setShouldShowAppBackupUnavailable(false) 30 | _state.update { it.copy(shouldShowAppBackupUnavailableDialog = false) } 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/screens/setupPermission/SetupPermissionScreenAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.screens.setupPermission 2 | 3 | sealed class SetupPermissionScreenAction { 4 | data class OnCallLogPermissionsResult(val isGranted: Boolean): SetupPermissionScreenAction() 5 | data class OnSmsPermissionResult(val isGranted: Boolean): SetupPermissionScreenAction() 6 | data class OnContactsPermissionResult(val isGranted: Boolean): SetupPermissionScreenAction() 7 | data class OnNotificationPermissionResult(val isGranted: Boolean): SetupPermissionScreenAction() 8 | data object OnAllPermissionsGranted: SetupPermissionScreenAction() 9 | data class OnAllPermissionsResult(val isGranted: Boolean): SetupPermissionScreenAction() 10 | data class OnSkipClicked(val dontShowAgain: Boolean): SetupPermissionScreenAction() 11 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/screens/setupPermission/SetupPermissionScreenState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.screens.setupPermission 2 | 3 | data class SetupPermissionScreenState( 4 | val isCallLogPermissionsGranted: Boolean = false, 5 | val isSmsReadPermissionGranted: Boolean = false, 6 | val isContactsReadPermissionGranted: Boolean = false, 7 | val isNotificationPermissionGranted: Boolean = false, 8 | ) { 9 | val isAllPermissionsGranted: Boolean = isCallLogPermissionsGranted && 10 | isSmsReadPermissionGranted && 11 | isContactsReadPermissionGranted && 12 | isNotificationPermissionGranted 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/screens/setupPermission/SetupPermissionScreenViewModel.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.screens.setupPermission 2 | 3 | import androidx.lifecycle.ViewModel 4 | import balti.migrate.common.utils.PermissionUtils 5 | import baltiapps.migrate.domain.common.sources.ContextSource 6 | import baltiapps.migrate.domain.common.sources.Preferences 7 | import kotlinx.coroutines.flow.MutableStateFlow 8 | import kotlinx.coroutines.flow.asStateFlow 9 | import kotlinx.coroutines.flow.update 10 | 11 | class SetupPermissionScreenViewModel( 12 | private val contextSource: ContextSource, 13 | private val preferences: Preferences, 14 | ) : ViewModel() { 15 | 16 | private val _state = MutableStateFlow(SetupPermissionScreenState()) 17 | val state = _state.asStateFlow() 18 | 19 | private fun updateState(isGranted: Boolean? = null) { 20 | _state.update { 21 | it.copy( 22 | isCallLogPermissionsGranted = isGranted ?: contextSource.checkPermissions( 23 | PermissionUtils.callLogPermissions 24 | ), 25 | isSmsReadPermissionGranted = isGranted ?: contextSource.checkPermission( 26 | PermissionUtils.smsReadPermission 27 | ), 28 | isContactsReadPermissionGranted = isGranted ?: contextSource.checkPermission( 29 | PermissionUtils.contactsReadPermission 30 | ), 31 | isNotificationPermissionGranted = isGranted 32 | ?: PermissionUtils.notificationsPermission?.run { 33 | contextSource.checkPermission(this) 34 | } ?: true, 35 | ) 36 | } 37 | } 38 | 39 | init { 40 | updateState() 41 | } 42 | 43 | fun performAction(action: SetupPermissionScreenAction) { 44 | when (action) { 45 | is SetupPermissionScreenAction.OnCallLogPermissionsResult -> { 46 | _state.update { it.copy(isCallLogPermissionsGranted = action.isGranted) } 47 | } 48 | is SetupPermissionScreenAction.OnSmsPermissionResult -> { 49 | _state.update { it.copy(isSmsReadPermissionGranted = action.isGranted) } 50 | } 51 | is SetupPermissionScreenAction.OnContactsPermissionResult -> { 52 | _state.update { it.copy(isContactsReadPermissionGranted = action.isGranted) } 53 | } 54 | is SetupPermissionScreenAction.OnNotificationPermissionResult -> { 55 | _state.update { it.copy(isNotificationPermissionGranted = action.isGranted) } 56 | } 57 | is SetupPermissionScreenAction.OnAllPermissionsResult -> { 58 | if (action.isGranted) { 59 | updateState(isGranted = true) 60 | } else updateState() 61 | } 62 | is SetupPermissionScreenAction.OnAllPermissionsGranted -> { 63 | preferences.setShouldShowPermissionScreen(false) 64 | } 65 | is SetupPermissionScreenAction.OnSkipClicked -> { 66 | if (action.dontShowAgain) { 67 | preferences.setShouldShowPermissionScreen(false) 68 | } 69 | } 70 | } 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/app/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.app.ui.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | val AppTypography = Typography() 10 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/data/sources/contacts/ContactsDBWriter.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.data.sources.contacts 2 | 3 | import android.content.ContentValues 4 | import android.database.sqlite.SQLiteDatabase 5 | import balti.migrate.common.data.model.ContactData 6 | import balti.migrate.common.utils.DBUtils 7 | import baltiapps.migrate.domain.ContactsDBConstants.Companion.CONTACTS_TABLE_NAME 8 | import baltiapps.migrate.domain.ContactsDBConstants.Companion.DISPLAY_NAME 9 | import baltiapps.migrate.domain.ContactsDBConstants.Companion.VCF_CONTENT 10 | import baltiapps.migrate.domain.REDACTED 11 | import baltiapps.migrate.domain.common.getPercentage 12 | import baltiapps.migrate.domain.common.model.GenericFile 13 | import baltiapps.migrate.domain.common.model.Progress 14 | import baltiapps.migrate.domain.common.runCatchingWithProgress 15 | import baltiapps.migrate.domain.common.sources.fileSystem.DBWriter 16 | import kotlinx.coroutines.Dispatchers 17 | import kotlinx.coroutines.flow.Flow 18 | import kotlinx.coroutines.flow.flow 19 | import kotlinx.coroutines.flow.flowOn 20 | import java.io.File 21 | 22 | class ContactsDBWriter( 23 | private val dbUtils: DBUtils, 24 | ): DBWriter { 25 | 26 | private lateinit var sqLiteDatabase: SQLiteDatabase 27 | 28 | override fun setup(file: GenericFile) { 29 | val dbFile = File(file.path).apply { 30 | if (exists()) delete() 31 | } 32 | 33 | dbUtils.getDataBase(dbFile).apply { 34 | val sqlDropTable = "DROP TABLE IF EXISTS $CONTACTS_TABLE_NAME" 35 | val sqlCreateTable = "CREATE TABLE $CONTACTS_TABLE_NAME ( " + 36 | "id INTEGER PRIMARY KEY" + 37 | ", $DISPLAY_NAME TEXT" + 38 | ", $VCF_CONTENT TEXT" + 39 | ")" 40 | sqLiteDatabase = this 41 | execSQL(sqlDropTable) 42 | execSQL(sqlCreateTable) 43 | } 44 | } 45 | 46 | override fun writeRows(dataItems: List): Flow { 47 | return flow { 48 | dataItems.forEachIndexed { index, item -> 49 | val progress = Progress( 50 | progressType = Progress.ProgressType.CONTACTS_BACKUP, 51 | percentage = getPercentage(index + 1, dataItems.size), 52 | logs = "CONTACT (${index + 1}/${dataItems.size}) ${item.logInfo}", 53 | logsForStorage = "CONTACT (${index + 1}/${dataItems.size}) $REDACTED", 54 | ) 55 | runCatchingWithProgress(progress) { 56 | writeRow(item) 57 | }.run { emit(this) } 58 | } 59 | }.flowOn(Dispatchers.IO) 60 | } 61 | 62 | private fun writeRow(dataItem: ContactData) { 63 | val contentValues = ContentValues() 64 | Pair(contentValues, dataItem).let { (c, d) -> 65 | c.put("id", d._id) 66 | c.put(DISPLAY_NAME, d.displayName) 67 | c.put(VCF_CONTENT, d.vcfContent) 68 | } 69 | sqLiteDatabase.insert(CONTACTS_TABLE_NAME, null, contentValues) 70 | } 71 | 72 | override fun close() { 73 | if (::sqLiteDatabase.isInitialized) { 74 | sqLiteDatabase.close() 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/backupName/BackupNameAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.backupName 2 | 3 | sealed class BackupNameAction { 4 | data class NameChanged(val name: String): BackupNameAction() 5 | data class OnSafLocationSelected( 6 | val uriString: String?, 7 | ): BackupNameAction() 8 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/backupName/BackupNameState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.backupName 2 | 3 | data class BackupNameState( 4 | val backupName: String, 5 | val isSaf: Boolean?, 6 | val safUriString: String?, 7 | val locationString: String, 8 | val isSafUriAccessible: Boolean, 9 | ) 10 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/backupName/BackupNameViewModel.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.backupName 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import androidx.core.net.toUri 6 | import androidx.lifecycle.ViewModel 7 | import androidx.lifecycle.viewModelScope 8 | import balti.migrate.common.data.model.MediaStoreDownloadFile 9 | import balti.migrate.common.data.model.SafFile 10 | import balti.migrate.common.data.sources.fileSystem.TransferUtils 11 | import balti.migrate.common.utils.getDefaultBackupName 12 | import baltiapps.migrate.domain.common.sources.Preferences 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.asStateFlow 15 | import kotlinx.coroutines.flow.update 16 | import kotlinx.coroutines.launch 17 | 18 | class BackupNameViewModel( 19 | private val applicationContext: Context, 20 | private val preferences: Preferences, 21 | ) : ViewModel() { 22 | 23 | private val safLocationString: String 24 | get() = preferences.getCustomLocationParameter() 25 | private val safLocationUri: Uri 26 | get() = safLocationString.toUri() 27 | private val isSafLocationAccessible: Boolean 28 | get() = TransferUtils.hasPermission(applicationContext, safLocationUri) 29 | 30 | private fun getLocationLabel(uriString: String): String { 31 | if (uriString.isBlank()) return MediaStoreDownloadFile.EXPORT_PATH_PREFIX 32 | 33 | return TransferUtils.getUriFilePath(safLocationUri).ifBlank { 34 | TransferUtils.getUriFileName(applicationContext, safLocationUri) 35 | } 36 | } 37 | 38 | private val _state = MutableStateFlow( 39 | BackupNameState( 40 | backupName = getDefaultBackupName(), 41 | isSaf = null, 42 | safUriString = null, 43 | locationString = "", 44 | isSafUriAccessible = false, 45 | ) 46 | ) 47 | val state = _state.asStateFlow() 48 | 49 | private fun updateState() { 50 | viewModelScope.launch { 51 | _state.update { 52 | it.copy( 53 | isSaf = safLocationString.isNotBlank(), 54 | safUriString = safLocationString.takeIf { 55 | it.isNotBlank() && isSafLocationAccessible 56 | }, 57 | locationString = getLocationLabel(safLocationString), 58 | isSafUriAccessible = isSafLocationAccessible, 59 | ) 60 | } 61 | } 62 | } 63 | 64 | init { 65 | updateState() 66 | } 67 | 68 | fun onAction(action: BackupNameAction) { 69 | when (action) { 70 | is BackupNameAction.NameChanged -> { 71 | _state.update { 72 | it.copy(backupName = action.name) 73 | } 74 | } 75 | is BackupNameAction.OnSafLocationSelected -> { 76 | if (action.uriString == null) { 77 | return 78 | } else { 79 | preferences.setCustomLocationParameter(action.uriString) 80 | updateState() 81 | } 82 | } 83 | } 84 | } 85 | 86 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/listScreen/callLogBackupSelection/CallLogBackupSelectionAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.listScreen.callLogBackupSelection 2 | 3 | import baltiapps.migrate.domain.common.model.CallLogListItem 4 | 5 | sealed class CallLogBackupSelectionAction { 6 | data class OnPermissionResult(val isGranted: Boolean): CallLogBackupSelectionAction() 7 | data class ToggleCallLogItem(val item: CallLogListItem): CallLogBackupSelectionAction() 8 | data class ToggleAllCallLog(val isChecked: Boolean): CallLogBackupSelectionAction() 9 | data class StageCallLogs(val onStagingDone: () -> Unit): CallLogBackupSelectionAction() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/listScreen/callLogBackupSelection/CallLogBackupSelectionState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.listScreen.callLogBackupSelection 2 | 3 | import baltiapps.migrate.domain.common.model.CallLogListItem 4 | import baltiapps.migrate.domain.common.model.Progress 5 | 6 | data class CallLogBackupSelectionState( 7 | val progress: Progress = Progress.Empty, 8 | val callLogList: List = emptyList(), 9 | val isStaging: Boolean = false, 10 | val hasPermission: Boolean = true, 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/listScreen/contactBackupSelection/ContactBackupSelectionAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.listScreen.contactBackupSelection 2 | 3 | import baltiapps.migrate.domain.common.model.ContactListItem 4 | 5 | sealed class ContactBackupSelectionAction { 6 | data class OnPermissionResult(val isGranted: Boolean): ContactBackupSelectionAction() 7 | data class ToggleContactItem(val item: ContactListItem): ContactBackupSelectionAction() 8 | data class ToggleAllContacts(val isChecked: Boolean): ContactBackupSelectionAction() 9 | data class StageContacts(val onStagingDone: () -> Unit): ContactBackupSelectionAction() 10 | data class ToggleSyncedContactsVisibility(val isVisible: Boolean): ContactBackupSelectionAction() 11 | data class ToggleLocalContactsVisibility(val isVisible: Boolean): ContactBackupSelectionAction() 12 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/listScreen/contactBackupSelection/ContactBackupSelectionState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.listScreen.contactBackupSelection 2 | 3 | import baltiapps.migrate.domain.common.model.ContactListItem 4 | import baltiapps.migrate.domain.common.model.Progress 5 | 6 | data class ContactBackupSelectionState( 7 | val progress: Progress = Progress.Empty, 8 | val contactList: List = emptyList(), 9 | val isStaging: Boolean = false, 10 | val hasPermission: Boolean = true, 11 | val syncedContactsExpanded: Boolean = false, 12 | val localContactsExpanded: Boolean = true, 13 | ) { 14 | val syncedContacts = contactList.filter { !it.isLocalContact } 15 | val localContacts = contactList.filter { it.isLocalContact } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/listScreen/contactBackupSelection/ContactComponents.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.listScreen.contactBackupSelection 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.ColumnScope 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.lazy.LazyListScope 12 | import androidx.compose.foundation.lazy.items 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.filled.KeyboardArrowDown 15 | import androidx.compose.material.icons.filled.KeyboardArrowUp 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.graphics.ColorFilter 21 | import androidx.compose.ui.res.stringResource 22 | import androidx.compose.ui.unit.dp 23 | import balti.migrate.R 24 | import balti.migrate.common.ui.components.CountBar 25 | import balti.migrate.common.ui.components.RenderContactItem 26 | import baltiapps.migrate.domain.common.model.ContactListItem 27 | 28 | @Composable 29 | fun ContactHeader( 30 | header: @Composable ColumnScope.() -> Unit, 31 | isExpanded: Boolean, 32 | onClick: () -> Unit, 33 | modifier: Modifier = Modifier, 34 | ) { 35 | Row( 36 | modifier = modifier 37 | .fillMaxWidth() 38 | .clickable { onClick() } 39 | .background(MaterialTheme.colorScheme.surfaceContainer) 40 | .padding(horizontal = 16.dp, vertical = 8.dp), 41 | verticalAlignment = Alignment.CenterVertically, 42 | ) { 43 | Column( 44 | modifier = Modifier.weight(1f), 45 | content = header 46 | ) 47 | Image( 48 | imageVector = if (isExpanded) { 49 | Icons.Default.KeyboardArrowUp 50 | } else Icons.Default.KeyboardArrowDown, 51 | contentDescription = if (isExpanded) { 52 | stringResource(R.string.collapse_list) 53 | } else stringResource(R.string.expand_list), 54 | colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface) 55 | ) 56 | } 57 | } 58 | 59 | fun LazyListScope.showContactList( 60 | contactList: List, 61 | isStaging: Boolean, 62 | onItemToggled: (item: ContactListItem) -> Unit, 63 | modifier: Modifier = Modifier, 64 | shouldShowCountBar: Boolean = true, 65 | ) { 66 | if (shouldShowCountBar) { 67 | stickyHeader { 68 | val selectedItems = contactList.filter { it.isChecked } 69 | CountBar( 70 | totalCount = contactList.size, 71 | selectedCount = selectedItems.size, 72 | ) 73 | } 74 | } 75 | items( 76 | items = contactList, 77 | key = { it._id } 78 | ) { item -> 79 | RenderContactItem( 80 | item = item, 81 | enabled = !isStaging, 82 | onItemToggled = onItemToggled, 83 | ) 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/listScreen/contactBackupSelection/OnlyLocalContacts.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.listScreen.contactBackupSelection 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import baltiapps.migrate.domain.common.model.ContactListItem 8 | 9 | @Composable 10 | fun OnlyLocalContacts( 11 | localContactList: List, 12 | isStaging: Boolean, 13 | onItemToggled: (item: ContactListItem) -> Unit, 14 | modifier: Modifier = Modifier 15 | ) { 16 | LazyColumn( 17 | modifier = modifier.fillMaxSize() 18 | ) { 19 | showContactList( 20 | contactList = localContactList, 21 | isStaging = isStaging, 22 | onItemToggled = onItemToggled, 23 | ) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/listScreen/contactBackupSelection/OnlySyncedContacts.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.listScreen.contactBackupSelection 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.OutlinedButton 13 | import androidx.compose.material3.Text 14 | import androidx.compose.material3.TextButton 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.res.stringResource 19 | import androidx.compose.ui.text.style.TextAlign 20 | import androidx.compose.ui.unit.dp 21 | import balti.migrate.R 22 | import baltiapps.migrate.domain.common.model.ContactListItem 23 | 24 | @Composable 25 | fun OnlySyncedContacts( 26 | syncedContactList: List, 27 | isStaging: Boolean, 28 | shouldShowContacts: Boolean, 29 | onShowContactsConfirmation: () -> Unit, 30 | onItemToggled: (item: ContactListItem) -> Unit, 31 | showSyncedContactsWhyNotRecommended: () -> Unit, 32 | modifier: Modifier = Modifier 33 | ) { 34 | if (shouldShowContacts) { 35 | LazyColumn( 36 | modifier = modifier 37 | .padding(8.dp) 38 | .fillMaxSize() 39 | ) { 40 | showContactList( 41 | contactList = syncedContactList, 42 | isStaging = isStaging, 43 | onItemToggled = onItemToggled, 44 | ) 45 | } 46 | } else { 47 | Column( 48 | modifier = modifier.fillMaxSize(), 49 | horizontalAlignment = Alignment.CenterHorizontally, 50 | verticalArrangement = Arrangement.Center, 51 | ) { 52 | Text( 53 | text = stringResource(R.string.all_contacts_are_synced_contacts), 54 | style = MaterialTheme.typography.bodyLarge, 55 | textAlign = TextAlign.Center 56 | ) 57 | Spacer(Modifier.size(12.dp)) 58 | Text( 59 | text = stringResource(R.string.synced_contacts_backup_is_not_recommended_expanded), 60 | style = MaterialTheme.typography.bodyMedium, 61 | textAlign = TextAlign.Center 62 | ) 63 | Spacer(Modifier.size(12.dp)) 64 | TextButton( 65 | onClick = { showSyncedContactsWhyNotRecommended() } 66 | ) { 67 | Text(stringResource(R.string.why)) 68 | } 69 | OutlinedButton( 70 | onClick = onShowContactsConfirmation, 71 | border = BorderStroke(1.dp, MaterialTheme.colorScheme.primary) 72 | ) { 73 | Text(stringResource(R.string.show_synced_contacts_anyway)) 74 | } 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/listScreen/contactBackupSelection/SyncedWarningDialog.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.listScreen.contactBackupSelection 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.Text 5 | import androidx.compose.material3.TextButton 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.LaunchedEffect 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.setValue 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import balti.migrate.R 14 | 15 | 16 | private var shouldShowSyncedWarningDialog by mutableStateOf(false) 17 | 18 | @Composable 19 | fun SyncedWarningDialog() { 20 | if (shouldShowSyncedWarningDialog) { 21 | AlertDialog( 22 | onDismissRequest = { 23 | shouldShowSyncedWarningDialog = false 24 | }, 25 | confirmButton = { 26 | TextButton( 27 | onClick = { 28 | shouldShowSyncedWarningDialog = false 29 | } 30 | ) { 31 | Text(stringResource(R.string.dismiss)) 32 | } 33 | }, 34 | title = { 35 | Text(stringResource(R.string.synced_contacts_backup_is_not_recommended)) 36 | }, 37 | text = { 38 | Text(stringResource(R.string.synced_contacts_not_recommended_justification)) 39 | }, 40 | ) 41 | } 42 | } 43 | 44 | fun showSyncedContactsWarningDialog() { 45 | shouldShowSyncedWarningDialog = true 46 | } 47 | 48 | @Preview 49 | @Composable 50 | private fun DialogPreview() { 51 | LaunchedEffect(Unit) { 52 | showSyncedContactsWarningDialog() 53 | } 54 | SyncedWarningDialog() 55 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/listScreen/smsBackupSelection/SmsBackupSelectionAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.listScreen.smsBackupSelection 2 | 3 | import baltiapps.migrate.domain.common.model.SmsListItem 4 | 5 | sealed class SmsBackupSelectionAction { 6 | data class OnPermissionResult(val isGranted: Boolean): SmsBackupSelectionAction() 7 | data class ToggleSmsItem(val item: SmsListItem): SmsBackupSelectionAction() 8 | data class ToggleAllSms(val isChecked: Boolean): SmsBackupSelectionAction() 9 | data class StageSms(val onStagingDone: () -> Unit): SmsBackupSelectionAction() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/listScreen/smsBackupSelection/SmsBackupSelectionState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.listScreen.smsBackupSelection 2 | 3 | import baltiapps.migrate.domain.common.model.Progress 4 | import baltiapps.migrate.domain.common.model.SmsListItem 5 | 6 | data class SmsBackupSelectionState( 7 | val progress: Progress = Progress.Empty, 8 | val smsList: List = emptyList(), 9 | val isStaging: Boolean = false, 10 | val hasPermission: Boolean = true, 11 | ) 12 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/progressScreen/BackupProgressScreenAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.progressScreen 2 | 3 | sealed class BackupProgressScreenAction { 4 | data class ToggleErrorOnly(val enabled: Boolean) : BackupProgressScreenAction() 5 | data object CancelBackup : BackupProgressScreenAction() 6 | data object PauseProgressLogs : BackupProgressScreenAction() 7 | data object ResumeProgressLogs : BackupProgressScreenAction() 8 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/progressScreen/BackupProgressScreenState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.progressScreen 2 | 3 | import baltiapps.migrate.domain.common.model.Progress 4 | 5 | data class BackupProgressScreenState( 6 | val progressList: List, 7 | val errorList: List, 8 | val headingText: String, 9 | val errorOnly: Boolean, 10 | val isCancelling: Boolean, 11 | val isBackupFinished: Boolean, 12 | ) { 13 | val isLoading: Boolean = progressList.isEmpty() && errorList.isEmpty() 14 | companion object { 15 | val Empty = BackupProgressScreenState( 16 | progressList = emptyList(), 17 | errorList = emptyList(), 18 | headingText = "", 19 | errorOnly = false, 20 | isCancelling = false, 21 | isBackupFinished = false, 22 | ) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/backup/ui/screens/progressScreen/BackupProgressScreenViewModel.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.backup.ui.screens.progressScreen 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import balti.migrate.backup.data.service.BackupService 6 | import baltiapps.migrate.domain.common.repository.ProgressLogRepository 7 | import baltiapps.migrate.domain.common.sources.ContextSource 8 | import baltiapps.migrate.domain.common.sources.Preferences 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.asStateFlow 12 | import kotlinx.coroutines.flow.update 13 | import kotlinx.coroutines.launch 14 | 15 | class BackupProgressScreenViewModel( 16 | private val contextSource: ContextSource, 17 | private val preferences: Preferences, 18 | private val progressLogRepository: ProgressLogRepository, 19 | ) : ViewModel() { 20 | 21 | private val _state = MutableStateFlow(BackupProgressScreenState.Empty) 22 | val state = _state.asStateFlow() 23 | 24 | private var isLogsPaused: Boolean = false 25 | 26 | private fun startObserving() { 27 | viewModelScope.launch { 28 | progressLogRepository.setProgressObserver { p, e -> 29 | if (p.isEmpty()) return@setProgressObserver 30 | val latestProgress = p.last() 31 | val isFinished = latestProgress.isBackupFinished() 32 | val cancelling = if (isFinished) false else _state.value.isCancelling 33 | if (isLogsPaused && !latestProgress.isBackupFinished()) return@setProgressObserver 34 | val headingText = contextSource.getProgressTitle(latestProgress.progressType) 35 | _state.update { 36 | it.copy( 37 | progressList = p, 38 | errorList = e, 39 | headingText = headingText, 40 | isBackupFinished = latestProgress.isBackupFinished(), 41 | isCancelling = cancelling 42 | ) 43 | } 44 | } 45 | } 46 | } 47 | 48 | init { 49 | startObserving() 50 | if (!BackupService.isRunning) { 51 | viewModelScope.launch(Dispatchers.IO) { 52 | progressLogRepository.populateWithValues( 53 | progressList = preferences.getLastSavedBackupProgressList(), 54 | errorList = preferences.getLastSavedBackupErrorList(), 55 | ) 56 | } 57 | } 58 | } 59 | 60 | fun performAction(action: BackupProgressScreenAction) { 61 | when (action) { 62 | is BackupProgressScreenAction.ToggleErrorOnly -> { 63 | _state.update { it.copy(errorOnly = action.enabled) } 64 | } 65 | is BackupProgressScreenAction.CancelBackup -> { 66 | _state.update { 67 | it.copy(isCancelling = true) 68 | } 69 | } 70 | is BackupProgressScreenAction.PauseProgressLogs -> { isLogsPaused = true } 71 | is BackupProgressScreenAction.ResumeProgressLogs -> { 72 | isLogsPaused = false 73 | progressLogRepository.dispatchLatestObservedProgress() 74 | } 75 | } 76 | } 77 | 78 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/model/CallLogData.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.model 2 | 3 | import balti.migrate.common.utils.convertToDisplayDate 4 | import baltiapps.migrate.domain.common.model.CallLogListItem 5 | import baltiapps.migrate.domain.common.model.DataItem 6 | import baltiapps.migrate.domain.common.model.ItemCreationDate 7 | 8 | data class CallLogData( 9 | override val _id: String, 10 | override val logInfo: String, 11 | 12 | val callsCachedName: String = "", 13 | val callsCountryIso: String = "", 14 | val callsDataUsage: Long, 15 | val callsFeatures: Int, 16 | val callsGeocodedLocation: String = "", 17 | val callsIsRead: Boolean, 18 | val callsNumber: String = "", 19 | val callsNumberPresentation: Int, 20 | val callsPhoneAccountComponentName: String = "", 21 | val callsPhoneAccountId: String = "", 22 | val callsTranscription: String = "", 23 | val callsType: Int, 24 | val callsVoicemailUri: String = "", 25 | val callsDate: Long, 26 | val callsDuration: Long, 27 | val callsNew: Boolean, 28 | ) : DataItem { 29 | override fun toListItem(): CallLogListItem { 30 | return CallLogListItem( 31 | _id = _id, 32 | callStatus = callsType, 33 | displayName = callsCachedName, 34 | displayNumber = callsNumber, 35 | creationDate = ItemCreationDate( 36 | dateInLong = callsDate, 37 | displayDate = convertToDisplayDate(callsDate) 38 | ), 39 | isChecked = true, 40 | ) 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/model/ContactData.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.model 2 | 3 | import baltiapps.migrate.domain.common.model.ContactListItem 4 | import baltiapps.migrate.domain.common.model.DataItem 5 | 6 | data class ContactData( 7 | override val _id: String, 8 | val displayName: String, 9 | val vcfContent: String, 10 | val isLocalContact : Boolean, 11 | override val logInfo: String, 12 | ) : DataItem { 13 | override fun toListItem(): ContactListItem { 14 | return ContactListItem( 15 | _id = _id, 16 | displayName = displayName, 17 | isLocalContact = isLocalContact, 18 | isChecked = true, 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/model/JavaFile.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.model 2 | 3 | import baltiapps.migrate.domain.common.model.GenericFile 4 | import java.io.File 5 | 6 | class JavaFile(val file: File) : GenericFile { 7 | constructor(path: String): this(File(path)) 8 | constructor(parentFile: JavaFile, child: String) : this(File(parentFile.file, child)) 9 | 10 | override val path: String = file.absolutePath 11 | override val name: String = file.name 12 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/model/MediaStoreDownloadFile.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.model 2 | 3 | import baltiapps.migrate.domain.common.model.GenericFile 4 | 5 | class MediaStoreDownloadFile( 6 | override val path: String, 7 | override val isValidBackupDirectory: Boolean = false 8 | ) : GenericFile { 9 | 10 | companion object { 11 | const val EXPORT_PATH_PREFIX = "Download/Migrate" 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/model/NotificationInfo.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.model 2 | 3 | data class NotificationInfo( 4 | val notificationId: Int, 5 | val notificationChannelId: String, 6 | val icon: Int, 7 | val title: String, 8 | val text: String? = null, 9 | val progress: Int = -1, 10 | val maxProgress: Int = -1, 11 | val isIndeterminate: Boolean = false, 12 | val shouldShowProgress: Boolean = false, 13 | ) 14 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/model/SafFile.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.model 2 | 3 | import android.net.Uri 4 | import baltiapps.migrate.domain.common.model.GenericFile 5 | 6 | data class SafFile( 7 | val uriToLocation: Uri, 8 | override val name: String, 9 | override val path: String = uriToLocation.path ?: "", 10 | override val isValidBackupDirectory: Boolean = false, 11 | val parent: SafFile? = null, 12 | ) : GenericFile { 13 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/model/SmsData.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.model 2 | 3 | import balti.migrate.common.utils.convertToDisplayDate 4 | import baltiapps.migrate.domain.common.model.DataItem 5 | import baltiapps.migrate.domain.common.model.ItemCreationDate 6 | import baltiapps.migrate.domain.common.model.SmsListItem 7 | 8 | data class SmsData( 9 | override val _id: String, 10 | override val logInfo: String, 11 | 12 | val smsAddress: String, 13 | val smsBody: String, 14 | val smsDate: Long, 15 | val smsDateSent: Long, 16 | val smsType: Int, 17 | val smsPerson: String, 18 | val smsProtocol: Int, 19 | val smsSeen: Boolean, 20 | val smsServiceCenter: String, 21 | val smsStatus: Int, 22 | val smsSubject: String, 23 | val smsThreadID: Int, 24 | val smsErrorCode: Int, 25 | val smsRead: Boolean, 26 | val smsLocked: Boolean, 27 | val smsReplyPathPresent: Boolean, 28 | ): DataItem { 29 | override fun toListItem(): SmsListItem { 30 | return SmsListItem( 31 | _id = _id, 32 | smsAddress = smsAddress, 33 | smsBody = smsBody, 34 | smsType = smsType, 35 | creationDate = ItemCreationDate( 36 | dateInLong = smsDate, 37 | displayDate = convertToDisplayDate(smsDate) 38 | ), 39 | isChecked = true, 40 | ) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/repository/ProgressLogRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.repository 2 | 3 | import android.content.Context 4 | import balti.migrate.R 5 | import baltiapps.migrate.domain.BREAK_LINE 6 | import baltiapps.migrate.domain.common.model.Progress 7 | import baltiapps.migrate.domain.common.repository.ProgressLogRepository 8 | 9 | open class ProgressLogRepositoryImpl ( 10 | applicationContext: Context, 11 | ): ProgressLogRepository() { 12 | override val truncatedIndicator: Progress = Progress.Empty.copy( 13 | logs = "${applicationContext.getString(R.string.older_logs_truncated)}\n${BREAK_LINE}\n" 14 | ) 15 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/sources/ContextSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.sources 2 | 3 | import android.app.role.RoleManager 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.os.Environment 7 | import androidx.core.content.ContextCompat 8 | import balti.migrate.R 9 | import baltiapps.migrate.domain.PermissionConstants 10 | import baltiapps.migrate.domain.common.model.Progress 11 | import baltiapps.migrate.domain.common.sources.ContextSource 12 | 13 | class ContextSourceImpl(private val context: Context): ContextSource { 14 | override fun getProgressTitle(progressType: Progress.ProgressType): String { 15 | return when (progressType) { 16 | Progress.ProgressType.CONTACTS_BACKUP -> context.getString(R.string.label_contacts_backup) 17 | Progress.ProgressType.CALL_LOG_BACKUP -> context.getString(R.string.label_call_log_backup) 18 | Progress.ProgressType.SMS_BACKUP -> context.getString(R.string.label_sms_backup) 19 | Progress.ProgressType.BACKUP_FINISHED -> context.getString(R.string.backup_finished) 20 | Progress.ProgressType.BACKUP_FINISHED_WITH_ERRORS -> context.getString(R.string.backup_finished_with_errors) 21 | Progress.ProgressType.EXPORTING_BACKUP -> context.getString(R.string.exporting_backup) 22 | Progress.ProgressType.BACKUP_CANCELLED -> context.getString(R.string.backup_cancelled) 23 | Progress.ProgressType.CALL_LOG_RESTORE -> context.getString(R.string.label_call_log_restore) 24 | Progress.ProgressType.SMS_RESTORE -> context.getString(R.string.label_sms_restore) 25 | Progress.ProgressType.RESTORE_FINISHED -> context.getString(R.string.restore_finished) 26 | Progress.ProgressType.RESTORE_FINISHED_WITH_ERRORS -> context.getString(R.string.restore_finished_with_errors) 27 | Progress.ProgressType.RESTORE_CANCELLED -> context.getString(R.string.restore_cancelled) 28 | else -> context.getString(R.string.loading) 29 | } 30 | } 31 | 32 | override fun checkPermission(permission: String): Boolean { 33 | return when (permission) { 34 | PermissionConstants.DEFAULT_SMS_APP -> checkDefaultSmsApp() 35 | else -> ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED 36 | } 37 | } 38 | 39 | override fun checkPermissions(permissions: List): Boolean { 40 | return permissions.map { ContextCompat.checkSelfPermission(context, it) } 41 | .all { it == PackageManager.PERMISSION_GRANTED } 42 | } 43 | 44 | private fun checkDefaultSmsApp(): Boolean { 45 | val roleManager = context.getSystemService(RoleManager::class.java) 46 | return roleManager.isRoleHeld(RoleManager.ROLE_SMS) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/sources/fileSystem/ExportDirectoryBrowserSafFile.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.sources.fileSystem 2 | 3 | import android.content.Context 4 | import androidx.documentfile.provider.DocumentFile 5 | import balti.migrate.common.data.model.SafFile 6 | import baltiapps.migrate.domain.common.sources.fileSystem.ExportDirectoryBrowser 7 | import baltiapps.migrate.domain.common.utils.BackupFilesUtils 8 | 9 | class ExportDirectoryBrowserSafFile( 10 | private val applicationContext: Context, 11 | ): ExportDirectoryBrowser { 12 | override suspend fun getDirectories(root: SafFile): List { 13 | val documentFile = 14 | DocumentFile.fromTreeUri(applicationContext, root.uriToLocation) 15 | ?: return emptyList() 16 | 17 | return documentFile.listFiles() 18 | .filter { it.isDirectory } 19 | .map { child -> 20 | val filesInChildren = child.listFiles() 21 | val isValidBackupDirectory = filesInChildren.any { 22 | BackupFilesUtils.shouldImportFile(it.name ?: "") 23 | } 24 | SafFile( 25 | uriToLocation = child.uri, 26 | name = child.name ?: "", 27 | isValidBackupDirectory = isValidBackupDirectory, 28 | parent = root, 29 | ) 30 | } 31 | } 32 | 33 | override suspend fun getFilesUnder(directory: SafFile): List { 34 | val documentFile = 35 | DocumentFile.fromTreeUri(applicationContext, directory.uriToLocation) 36 | ?: return emptyList() 37 | 38 | return documentFile.listFiles().map { 39 | SafFile( 40 | uriToLocation = it.uri, 41 | name = it.name ?: "", 42 | ) 43 | } 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/sources/fileSystem/TextWriterImpl.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.sources.fileSystem 2 | 3 | import baltiapps.migrate.domain.common.sources.fileSystem.TextWriter 4 | import java.io.File 5 | import java.io.FileWriter 6 | 7 | class TextWriterImpl: TextWriter { 8 | private lateinit var file: File 9 | private lateinit var fileWriter: FileWriter 10 | 11 | override fun setup(fileLocation: String, append: Boolean) { 12 | File(fileLocation).run { 13 | if (!append && exists()) { 14 | delete() 15 | } 16 | file = this 17 | fileWriter = FileWriter(file, true) 18 | } 19 | } 20 | 21 | override fun write(data: String) { 22 | fileWriter.write(data) 23 | } 24 | 25 | override fun writeLine(data: String) { 26 | write("${data}\n") 27 | } 28 | 29 | override fun close() { 30 | if (::file.isInitialized) { 31 | fileWriter.close() 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/data/sources/fileSystem/TransferUtilsJavaFile.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.data.sources.fileSystem 2 | 3 | import balti.migrate.common.data.model.JavaFile 4 | import timber.log.Timber 5 | import java.io.File 6 | 7 | class TransferUtilsJavaFile { 8 | fun transferJavaFileToJavaFile( 9 | source: JavaFile, 10 | destinationDirectory: JavaFile, 11 | deleteSource: Boolean, 12 | relativeFilePathFilter: (String) -> Boolean = { true }, 13 | ): Boolean { 14 | Timber.i("TJJ - Transfer JavaFile -> JavaFile") 15 | Timber.i("TJJ - source path - ${source.path}") 16 | Timber.i("TJJ - dest. path - ${destinationDirectory.path}") 17 | 18 | source.file.walkTopDown().forEach { file -> 19 | Timber.i("TJJ - file to copy - ${file.absolutePath}") 20 | 21 | val relDirPath = TransferUtils.relativeDirectoryPath( 22 | source = source, 23 | currentFile = file, 24 | ) 25 | 26 | val relativeFilePath = "$relDirPath/${file.name}" 27 | 28 | Timber.i("TJJ - relative dir path - $relDirPath") 29 | Timber.i("TJJ - relative file path - $relativeFilePath") 30 | 31 | try { 32 | Timber.i("TJJ - attempt transfer") 33 | if (relativeFilePathFilter(relativeFilePath)) { 34 | val targetDir = File(destinationDirectory.file, relDirPath) 35 | targetDir.mkdirs() 36 | 37 | Timber.i("TJJ - create relative directory - ${targetDir.absolutePath}") 38 | 39 | val targetFile = File(targetDir, file.name) 40 | 41 | Timber.i("TJJ - copy to ${targetFile.absolutePath}") 42 | file.copyTo(targetFile, overwrite = true) 43 | Timber.i("TJJ - copy to ${targetFile.absolutePath} success") 44 | 45 | if (deleteSource) { 46 | file.delete().apply { 47 | Timber.i("TJJ - deleted file ${file.absolutePath} - success - $this") 48 | } 49 | } 50 | } else { 51 | Timber.i("TJJ - not copying file, relative path \"$relativeFilePath\" did not qualify") 52 | } 53 | } catch (e: Exception) { 54 | e.printStackTrace() 55 | Timber.e("TJJ - exception - ${e.message}") 56 | return false 57 | } 58 | } 59 | 60 | Timber.i("TJJ - finished all transfers") 61 | return true 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/di/CommonDiModule.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.di 2 | 3 | import balti.migrate.common.data.sources.ContextSourceImpl 4 | import balti.migrate.common.data.sources.PreferencesImpl 5 | import balti.migrate.common.data.sources.fileSystem.FileSystemSourceImpl 6 | import balti.migrate.common.data.sources.fileSystem.ExportDirectoryBrowserMediaStore 7 | import balti.migrate.common.data.sources.fileSystem.ExportDirectoryBrowserSafFile 8 | import balti.migrate.common.data.sources.fileSystem.TextWriterImpl 9 | import balti.migrate.common.utils.DBUtils 10 | import balti.migrate.common.utils.ListItemUtils 11 | import baltiapps.migrate.domain.common.sources.ContextSource 12 | import baltiapps.migrate.domain.common.sources.Preferences 13 | import baltiapps.migrate.domain.common.sources.fileSystem.ExportDirectoryBrowser 14 | import baltiapps.migrate.domain.common.sources.fileSystem.FileSystemSource 15 | import baltiapps.migrate.domain.common.sources.fileSystem.TextWriter 16 | import baltiapps.migrate.domain.common.usecase.StageSelectedCallLogs 17 | import baltiapps.migrate.domain.common.usecase.StageSelectedContacts 18 | import baltiapps.migrate.domain.common.usecase.StageSelectedSms 19 | import org.koin.core.module.dsl.singleOf 20 | import org.koin.core.qualifier.named 21 | import org.koin.dsl.bind 22 | import org.koin.dsl.module 23 | 24 | enum class Names { 25 | TEXT_WRITER, 26 | MEDIA_STORE_EXPORT_DIRECTORY_BROWSER, 27 | SAF_EXPORT_DIRECTORY_BROWSER, 28 | } 29 | 30 | val commonDiModule = module { 31 | 32 | /* Utils */ 33 | 34 | singleOf(::DBUtils) 35 | singleOf(::ListItemUtils) 36 | 37 | /* Sources */ 38 | 39 | single>(named(Names.TEXT_WRITER)) { 40 | TextWriterImpl() 41 | } 42 | 43 | singleOf(::FileSystemSourceImpl) bind FileSystemSource::class 44 | 45 | singleOf(::ContextSourceImpl) bind ContextSource::class 46 | 47 | singleOf(::PreferencesImpl) bind Preferences::class 48 | 49 | single>(named(Names.MEDIA_STORE_EXPORT_DIRECTORY_BROWSER)) { 50 | ExportDirectoryBrowserMediaStore( 51 | applicationContext = get(), 52 | dbUtils = get(), 53 | ) 54 | } 55 | 56 | single>(named(Names.SAF_EXPORT_DIRECTORY_BROWSER)) { 57 | ExportDirectoryBrowserSafFile( 58 | applicationContext = get(), 59 | ) 60 | } 61 | 62 | /* Use cases */ 63 | 64 | singleOf(::StageSelectedContacts) 65 | singleOf(::StageSelectedCallLogs) 66 | singleOf(::StageSelectedSms) 67 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/ui/components/CountBar.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.ui.components 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.widthIn 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.tooling.preview.Preview 16 | import androidx.compose.ui.unit.dp 17 | import balti.migrate.R 18 | 19 | @Composable 20 | fun CountBar( 21 | totalCount: Int, 22 | selectedCount: Int, 23 | modifier: Modifier = Modifier, 24 | ) { 25 | Row( 26 | modifier = modifier 27 | .fillMaxWidth() 28 | .background(MaterialTheme.colorScheme.surfaceContainer) 29 | .padding(horizontal = 16.dp, vertical = 8.dp), 30 | verticalAlignment = Alignment.CenterVertically, 31 | ) { 32 | Text( 33 | text = stringResource(R.string.selected_items), 34 | style = MaterialTheme.typography.labelLarge, 35 | ) 36 | Spacer( 37 | modifier = Modifier 38 | .widthIn(min = 8.dp) 39 | .weight(1F) 40 | ) 41 | Text( 42 | text = "$selectedCount/$totalCount", 43 | style = MaterialTheme.typography.labelLarge, 44 | ) 45 | } 46 | } 47 | 48 | @Preview 49 | @Composable 50 | private fun CountBarPreview() { 51 | CountBar( 52 | totalCount = 10, 53 | selectedCount = 5, 54 | ) 55 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/ui/components/EmptyImageVector.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.ui.components 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | import androidx.compose.ui.unit.dp 5 | 6 | val EmptyImageVector = ImageVector.Builder( 7 | name = "EmptyImageVector", 8 | defaultWidth = 0.dp, 9 | defaultHeight = 0.dp, 10 | viewportWidth = 0f, 11 | viewportHeight = 0f, 12 | ).build() -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/ui/components/KeepScreenOn.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.ui.components 2 | 3 | import android.view.Window 4 | import android.view.WindowManager 5 | import androidx.activity.compose.LocalActivity 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.DisposableEffect 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.lifecycle.LifecycleEventObserver 10 | import androidx.lifecycle.LifecycleOwner 11 | import androidx.lifecycle.compose.LocalLifecycleOwner 12 | 13 | /** 14 | * Composable function that keeps the screen on while the composable is in the foreground. 15 | * 16 | * This uses the FLAG_KEEP_SCREEN_ON window flag for a more efficient and system-friendly approach. 17 | * It also handles adding and removing the flag according to the lifecycle events of the composable. 18 | * 19 | * @param shouldKeepScreenOn Flag to dynamically control whether to keep the screen on. 20 | * Defaults to true. 21 | * @param lifecycleOwner The LifecycleOwner for this composable. Defaults to LocalLifecycleOwner.current. 22 | */ 23 | @Composable 24 | fun KeepScreenOn( 25 | shouldKeepScreenOn: Boolean = true, 26 | lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, 27 | ) { 28 | val activity = LocalActivity.current 29 | val window: Window? = activity?.window 30 | DisposableEffect(lifecycleOwner, shouldKeepScreenOn) { 31 | val observer = LifecycleEventObserver { _, event -> 32 | when (event) { 33 | Lifecycle.Event.ON_RESUME -> { 34 | if (shouldKeepScreenOn) { 35 | window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 36 | } 37 | } 38 | 39 | Lifecycle.Event.ON_PAUSE -> { 40 | if (shouldKeepScreenOn) { 41 | window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 42 | } 43 | } 44 | 45 | else -> {} 46 | } 47 | } 48 | 49 | lifecycleOwner.lifecycle.addObserver(observer) 50 | 51 | onDispose { 52 | window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) 53 | lifecycleOwner.lifecycle.removeObserver(observer) 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/ui/components/LoadingDialog.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.ui.components 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.res.stringResource 14 | import androidx.compose.ui.tooling.preview.Preview 15 | import androidx.compose.ui.unit.dp 16 | import androidx.compose.ui.window.Dialog 17 | import androidx.compose.ui.window.DialogProperties 18 | import balti.migrate.R 19 | import baltiapps.migrate.domain.common.model.Progress 20 | 21 | @Composable 22 | fun LoadingDialog( 23 | text: String, 24 | progress: Progress?, 25 | modifier: Modifier = Modifier, 26 | ) { 27 | Dialog( 28 | onDismissRequest = {}, 29 | properties = DialogProperties( 30 | dismissOnClickOutside = false, 31 | dismissOnBackPress = false, 32 | ) 33 | ) { 34 | Surface( 35 | modifier = modifier 36 | .fillMaxWidth(), 37 | shape = RoundedCornerShape(16.dp), 38 | color = MaterialTheme.colorScheme.background, 39 | ) { 40 | Row( 41 | modifier = Modifier 42 | .fillMaxWidth() 43 | .padding(horizontal = 32.dp, vertical = 16.dp), 44 | verticalAlignment = Alignment.CenterVertically, 45 | ) { 46 | Text( 47 | text = text, 48 | modifier = Modifier.weight(1f), 49 | ) 50 | LoadingProgressBar( 51 | progress = progress 52 | ) 53 | } 54 | } 55 | } 56 | } 57 | 58 | @Composable 59 | @Preview 60 | private fun DialogPreview1() { 61 | LoadingDialog( 62 | text = stringResource(R.string.loading), 63 | progress = null, 64 | ) 65 | } 66 | 67 | @Composable 68 | @Preview 69 | private fun DialogPreview2() { 70 | LoadingDialog( 71 | text = stringResource(R.string.exporting_percentage, 40), 72 | progress = Progress.Empty.copy(percentage = 0.4), 73 | ) 74 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/ui/components/LoadingProgressBar.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.ui.components 2 | 3 | import androidx.compose.material3.CircularProgressIndicator 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import baltiapps.migrate.domain.common.model.Progress 7 | 8 | @Composable 9 | fun LoadingProgressBar( 10 | progress: Progress?, 11 | modifier: Modifier = Modifier, 12 | ) { 13 | if (progress == null) { 14 | CircularProgressIndicator( 15 | modifier = modifier, 16 | ) 17 | return 18 | } 19 | CircularProgressIndicator( 20 | modifier = modifier, 21 | progress = { 22 | progress.percentage.toFloat() 23 | }, 24 | ) 25 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/ui/components/RenderContactItem.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.material3.Checkbox 6 | import androidx.compose.material3.ListItem 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.draw.alpha 11 | import androidx.compose.ui.res.stringResource 12 | import androidx.compose.ui.tooling.preview.Preview 13 | import balti.migrate.R 14 | import baltiapps.migrate.domain.common.model.ContactListItem 15 | 16 | @Composable 17 | fun RenderContactItem( 18 | item: ContactListItem, 19 | enabled: Boolean = true, 20 | onItemToggled: (ContactListItem) -> Unit, 21 | ) { 22 | val alphaModifier = Modifier.alpha( 23 | if (enabled) 1f else 0.38f 24 | ) 25 | ListItem( 26 | modifier = Modifier.clickable(enabled = enabled) { 27 | onItemToggled(item) 28 | }, 29 | headlineContent = { 30 | Text( 31 | text = item.displayName.ifBlank { stringResource(R.string.no_name_contact) }, 32 | modifier = alphaModifier, 33 | ) 34 | }, 35 | trailingContent = { 36 | Checkbox( 37 | checked = item.isChecked, 38 | onCheckedChange = null, 39 | enabled = enabled, 40 | ) 41 | } 42 | ) 43 | } 44 | 45 | @Preview 46 | @Composable 47 | private fun ContactDisplayItemPreview() { 48 | val item1 = ContactListItem( 49 | _id = "", 50 | displayName = "John Price", 51 | isLocalContact = false, 52 | isChecked = true, 53 | ) 54 | val item2 = ContactListItem( 55 | _id = "", 56 | displayName = "Soap McTavish", 57 | isLocalContact = false, 58 | isChecked = true, 59 | ) 60 | val item3 = ContactListItem( 61 | _id = "", 62 | displayName = "Kyle Garrick", 63 | isLocalContact = false, 64 | isChecked = false, 65 | ) 66 | val item4 = ContactListItem( 67 | _id = "", 68 | displayName = "", 69 | isLocalContact = true, 70 | isChecked = false 71 | ) 72 | Column { 73 | RenderContactItem(item1) {} 74 | RenderContactItem(item2, false) {} 75 | RenderContactItem(item3) {} 76 | RenderContactItem(item4) {} 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/ui/listScreen/ListLoadingLayout.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.ui.listScreen 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import balti.migrate.common.ui.components.LoadingProgressBar 9 | import baltiapps.migrate.domain.common.model.Progress 10 | 11 | @Composable 12 | fun ListLoadingLayout( 13 | progress: Progress, 14 | modifier: Modifier = Modifier, 15 | ) { 16 | Box( 17 | modifier = modifier.fillMaxSize(), 18 | ) { 19 | LoadingProgressBar( 20 | modifier = Modifier.align(Alignment.Center), 21 | progress = progress, 22 | ) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/ui/listScreen/ListNoDataLayout.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.ui.listScreen 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.outlined.Info 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.alpha 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.tooling.preview.Preview 19 | import androidx.compose.ui.unit.dp 20 | import balti.migrate.R 21 | 22 | @Composable 23 | fun ListNoDataLayout( 24 | modifier: Modifier = Modifier, 25 | title: String? = null, 26 | ) { 27 | Column( 28 | modifier = modifier.padding(8.dp).fillMaxSize(), 29 | horizontalAlignment = Alignment.CenterHorizontally, 30 | verticalArrangement = Arrangement.spacedBy( 31 | space = 4.dp, 32 | alignment = Alignment.CenterVertically 33 | ), 34 | ) { 35 | val dim = 0.75f 36 | Image( 37 | imageVector = Icons.Outlined.Info, 38 | contentDescription = null, 39 | modifier = Modifier.size(36.dp).alpha(dim), 40 | ) 41 | Text( 42 | text = title ?: stringResource(R.string.no_data_title_default), 43 | style = MaterialTheme.typography.titleMedium, 44 | modifier = Modifier.alpha(dim), 45 | ) 46 | Text( 47 | text = stringResource(R.string.no_data_desc), 48 | style = MaterialTheme.typography.bodyMedium, 49 | modifier = Modifier.alpha(dim), 50 | ) 51 | } 52 | } 53 | 54 | @Preview(showBackground = true) 55 | @Composable 56 | private fun ListNoDataPreview() { 57 | ListNoDataLayout() 58 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/ui/listScreen/ListPermissionRequestLayout.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.ui.listScreen 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.material3.Button 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TextButton 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.res.stringResource 15 | import androidx.compose.ui.text.style.TextAlign 16 | import androidx.compose.ui.tooling.preview.Preview 17 | import androidx.compose.ui.unit.dp 18 | import androidx.compose.ui.unit.sp 19 | import balti.migrate.R 20 | 21 | @Composable 22 | fun ListPermissionRequestLayout( 23 | description: String, 24 | onRequestPermission: () -> Unit, 25 | onSkip: (() -> Unit)?, 26 | modifier: Modifier = Modifier, 27 | ) { 28 | Column( 29 | modifier = modifier.fillMaxSize(), 30 | verticalArrangement = Arrangement.Center, 31 | horizontalAlignment = Alignment.CenterHorizontally, 32 | ) { 33 | Text( 34 | text = description, 35 | textAlign = TextAlign.Center, 36 | lineHeight = 20.sp 37 | ) 38 | Spacer(modifier = Modifier.size(16.dp)) 39 | Button( 40 | onClick = onRequestPermission 41 | ) { 42 | Text(text = stringResource(R.string.request_permission)) 43 | } 44 | if (onSkip == null) return@Column 45 | TextButton( 46 | onClick = onSkip 47 | ) { 48 | Text(stringResource(R.string.skip)) 49 | } 50 | } 51 | } 52 | 53 | @Preview 54 | @Composable 55 | private fun RequestPreview() { 56 | ListPermissionRequestLayout( 57 | description = stringResource(R.string.contacts_backup_permission_description), 58 | onRequestPermission = {}, 59 | onSkip = {} 60 | ) 61 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/ui/progressScreen/ErrorLayoutToggle.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.ui.progressScreen 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.material3.Switch 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.res.stringResource 11 | import balti.migrate.R 12 | 13 | @Composable 14 | fun ErrorLayoutToggle( 15 | isErrorOnly: Boolean, 16 | onToggleErrorOnly: (Boolean) -> Unit, 17 | modifier: Modifier = Modifier 18 | ) { 19 | Row( 20 | modifier = modifier, 21 | horizontalArrangement = Arrangement.SpaceBetween, 22 | verticalAlignment = Alignment.CenterVertically, 23 | ) { 24 | Text(stringResource(R.string.show_only_errors)) 25 | Switch( 26 | checked = isErrorOnly, 27 | onCheckedChange = onToggleErrorOnly 28 | ) 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/ui/progressScreen/ProgressScreenBottomBar.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.ui.progressScreen 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.KeyboardArrowDown 5 | import androidx.compose.material.icons.outlined.KeyboardArrowUp 6 | import androidx.compose.material3.BottomAppBar 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.IconButton 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.rememberCoroutineScope 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.res.stringResource 13 | import balti.migrate.R 14 | 15 | @Composable 16 | fun ProgressScreenBottomBar( 17 | onScrollToTop: () -> Unit, 18 | onScrollToBottom: () -> Unit, 19 | modifier: Modifier = Modifier, 20 | fabContent: @Composable () -> Unit, 21 | ) { 22 | BottomAppBar( 23 | modifier = modifier, 24 | actions = { 25 | IconButton( 26 | onClick = onScrollToTop, 27 | ) { 28 | Icon( 29 | imageVector = Icons.Outlined.KeyboardArrowUp, 30 | contentDescription = stringResource( 31 | R.string.scroll_up 32 | ) 33 | ) 34 | } 35 | IconButton( 36 | onClick = onScrollToBottom, 37 | ) { 38 | Icon( 39 | imageVector = Icons.Outlined.KeyboardArrowDown, 40 | contentDescription = stringResource( 41 | R.string.scroll_down 42 | ) 43 | ) 44 | } 45 | }, 46 | floatingActionButton = fabContent 47 | ) 48 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/utils/DBUtils.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.utils 2 | 3 | import android.content.ContentValues 4 | import android.content.Context 5 | import android.database.Cursor 6 | import android.database.sqlite.SQLiteDatabase 7 | import android.net.Uri 8 | import baltiapps.migrate.domain.exceptions.ContentReadException 9 | import baltiapps.migrate.domain.exceptions.UnknownDataTypeException 10 | import java.io.File 11 | 12 | class DBUtils { 13 | inline fun getCursorData( 14 | cursor: Cursor, 15 | columnName: String, 16 | ): T { 17 | val columnIndex = cursor.getColumnIndex(columnName) 18 | if (columnIndex < 0) 19 | throw ContentReadException("Column $columnName index less than 0 - $columnIndex") 20 | return when(T::class) { 21 | String::class -> (cursor.getString(columnIndex) ?: "") as T 22 | Int::class -> cursor.getInt(columnIndex) as T 23 | Long::class -> cursor.getLong(columnIndex) as T 24 | Boolean::class -> (cursor.getInt(columnIndex) > 0) as T 25 | else -> throw UnknownDataTypeException( 26 | type = T::class, 27 | message = "Cursor read - Unknown data type - ${T::class}" 28 | ) 29 | } 30 | } 31 | 32 | inline fun putContentData( 33 | contentValues: ContentValues, 34 | columnName: String, 35 | data: T, 36 | ) { 37 | when(T::class) { 38 | String::class -> contentValues.put(columnName, data as String) 39 | Int::class -> contentValues.put(columnName, data as Int) 40 | Long::class -> contentValues.put(columnName, data as Long) 41 | Boolean::class -> contentValues.put(columnName, if (data as Boolean) 1 else 0) 42 | else -> throw UnknownDataTypeException( 43 | type = T::class, 44 | message = "Enter content values - Unknown data type - ${T::class}" 45 | ) 46 | } 47 | } 48 | 49 | fun getDataBase(dbFile: File): SQLiteDatabase { 50 | val location = dbFile.canonicalPath 51 | if (!dbFile.exists()) { 52 | dbFile.createNewFile() 53 | } 54 | return SQLiteDatabase.openDatabase( 55 | location, 56 | null, 57 | SQLiteDatabase.NO_LOCALIZED_COLLATORS or SQLiteDatabase.OPEN_READWRITE 58 | ) 59 | } 60 | 61 | fun getCursor( 62 | context: Context, 63 | uri: Uri, 64 | ): Cursor { 65 | return context.contentResolver.query( 66 | uri, 67 | null, 68 | null, 69 | null, 70 | null, 71 | ) ?: throw ContentReadException("Read cursor is null for Uri - $uri") 72 | } 73 | 74 | fun getCursor( 75 | db: SQLiteDatabase, 76 | tableName: String, 77 | ): Cursor { 78 | return db.query( 79 | tableName, 80 | null, 81 | null, 82 | null, 83 | null, 84 | null, 85 | null, 86 | ) 87 | } 88 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/utils/DateUtils.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.utils 2 | 3 | import java.text.SimpleDateFormat 4 | import java.util.Date 5 | import java.util.Locale 6 | 7 | private val sdf by lazy { 8 | SimpleDateFormat("MMM dd, yyyy - hh:mm a", Locale.getDefault()) 9 | } 10 | 11 | fun convertToDisplayDate(date: Long): String { 12 | return sdf.format(Date(date)) 13 | } 14 | 15 | fun getDefaultBackupName(): String { 16 | val format = "dd-MMM-yyyy_hh-mm-ss-a" 17 | val sdf = SimpleDateFormat(format, Locale.getDefault()) 18 | return sdf.format(Date().time) 19 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/utils/DeepLinkUtils.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.utils 2 | 3 | import android.app.PendingIntent 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.net.Uri 7 | import androidx.core.net.toUri 8 | import balti.migrate.BuildConfig 9 | import balti.migrate.app.MainActivity 10 | 11 | class DeepLinkUtils { 12 | 13 | enum class MigrateUri(val uriString: String) { 14 | UriProgressBackup("${BuildConfig.SCHEMA}://${BuildConfig.HOST_PROGRESS_BACKUP}"), 15 | UriProgressRestore("${BuildConfig.SCHEMA}://${BuildConfig.HOST_PROGRESS_RESTORE}"), 16 | ; 17 | 18 | val uri: Uri get() = uriString.toUri() 19 | } 20 | 21 | fun getPendingIntent(uri: MigrateUri, context: Context): PendingIntent { 22 | val intent = Intent( 23 | Intent.ACTION_VIEW, 24 | uri.uri, 25 | context, 26 | MainActivity::class.java 27 | ) 28 | return PendingIntent.getActivity( 29 | context, 30 | uri.ordinal + 1, 31 | intent, 32 | PendingIntent.FLAG_IMMUTABLE 33 | ) 34 | } 35 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/utils/ListItemUtils.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.utils 2 | 3 | import baltiapps.migrate.domain.common.model.CallLogListItem 4 | import baltiapps.migrate.domain.common.model.ContactListItem 5 | import baltiapps.migrate.domain.common.model.ListItem 6 | import baltiapps.migrate.domain.common.model.SmsListItem 7 | 8 | class ListItemUtils { 9 | 10 | @Suppress("UNCHECKED_CAST") 11 | fun toggleAllItems( 12 | list: List, 13 | isChecked: Boolean, 14 | filter: (T) -> Boolean = { true }, 15 | ): List { 16 | return list.mapNotNull { 17 | if (!filter(it)) it 18 | else when (it) { 19 | is ContactListItem -> it.copy(isChecked = isChecked) 20 | is CallLogListItem -> it.copy(isChecked = isChecked) 21 | is SmsListItem -> it.copy(isChecked = isChecked) 22 | else -> null 23 | } 24 | } as List 25 | } 26 | 27 | @Suppress("UNCHECKED_CAST") 28 | fun toggleSingleItem(list: List, item: T): List { 29 | val newItem = when(item) { 30 | is ContactListItem -> item.copy(isChecked = !item.isChecked) 31 | is CallLogListItem -> item.copy(isChecked = !item.isChecked) 32 | is SmsListItem -> item.copy(isChecked = !item.isChecked) 33 | else -> null 34 | } ?: return emptyList() 35 | return replaceListItem( 36 | list = list, 37 | item = item, 38 | newItem = newItem as T, 39 | ) 40 | } 41 | 42 | private fun replaceListItem( 43 | list: List, 44 | item: T, 45 | newItem: T, 46 | ): List { 47 | return list.toMutableList().apply { 48 | val index = indexOf(item) 49 | if (index != -1) { 50 | this[index] = newItem 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/utils/NotificationUtils.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.utils 2 | 3 | import android.app.NotificationChannel 4 | import android.app.NotificationManager 5 | import android.content.Context 6 | import androidx.core.app.NotificationCompat 7 | import balti.migrate.common.data.model.NotificationInfo 8 | import balti.migrate.R 9 | import android.app.PendingIntent 10 | import android.content.Intent 11 | 12 | fun NotificationManager.makeNotificationChannel( 13 | channelId: String, 14 | channelDesc: String, 15 | importance: Int, 16 | ) { 17 | createNotificationChannel( 18 | NotificationChannel(channelId, channelDesc, importance) 19 | ) 20 | } 21 | 22 | fun NotificationInfo.convertToNotificationBuilder( 23 | context: Context, 24 | ): NotificationCompat.Builder { 25 | return NotificationCompat.Builder(context, notificationChannelId).apply { 26 | setSmallIcon(icon) 27 | setContentTitle(title) 28 | setContentText(text) 29 | if (shouldShowProgress) { 30 | setProgress(maxProgress, progress, isIndeterminate) 31 | } 32 | } 33 | } 34 | 35 | fun NotificationInfo.getCancelAction( 36 | context: Context, 37 | intent: () -> Intent 38 | ): NotificationCompat.Action { 39 | return NotificationCompat.Action( 40 | R.drawable.outline_close_24, 41 | context.getString(R.string.cancel), 42 | PendingIntent.getService( 43 | context, 44 | notificationId, 45 | intent(), 46 | PendingIntent.FLAG_IMMUTABLE, 47 | ), 48 | ) 49 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/common/utils/ServiceUtils.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.common.utils 2 | 3 | import baltiapps.migrate.domain.BREAK_LINE 4 | import baltiapps.migrate.domain.common.model.Progress 5 | import baltiapps.migrate.domain.common.repository.ProgressLogRepository 6 | import baltiapps.migrate.domain.common.sources.ContextSource 7 | import baltiapps.migrate.domain.common.sources.fileSystem.TextWriter 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlin.coroutines.cancellation.CancellationException 10 | 11 | class ServiceUtils( 12 | private val contextSource: ContextSource, 13 | private val progressLogRepository: ProgressLogRepository, 14 | private val logWriter: TextWriter, 15 | private val errorWriter: TextWriter, 16 | ) { 17 | suspend fun collectLogs( 18 | progress: Progress, 19 | ) { 20 | if (progress.isFailure || progress.isFinished()) { 21 | progressLogRepository.pushError(progress) 22 | errorWriter.writeLine(progress.logsForStorage) 23 | } 24 | progressLogRepository.pushProgress(progress) 25 | logWriter.writeLine(progress.logsForStorage) 26 | } 27 | 28 | suspend fun emitHeadingLog( 29 | progressType: Progress.ProgressType, 30 | ) { 31 | val headingTitle = contextSource.getProgressTitle(progressType) 32 | collectLogs( 33 | progress = Progress( 34 | progressType = progressType, 35 | percentage = 1.0, 36 | logs = "\n${headingTitle}\n${BREAK_LINE}\n", 37 | isLogHeading = true, 38 | ), 39 | ) 40 | } 41 | 42 | suspend fun runStage( 43 | shouldRun: () -> Boolean, 44 | stageBody: () -> Flow, 45 | progressType: Progress.ProgressType, 46 | errorMessage: (Exception) -> String, 47 | ) { 48 | try { 49 | if (shouldRun()) { 50 | emitHeadingLog(progressType) 51 | stageBody().collect { 52 | collectLogs(it) 53 | } 54 | } 55 | } catch (e: Exception) { 56 | if (e is CancellationException) throw e 57 | e.printStackTrace() 58 | Progress( 59 | progressType = progressType, 60 | percentage = 1.0, 61 | logs = errorMessage(e), 62 | isFailure = true, 63 | ).run { 64 | collectLogs(this) 65 | } 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/data/sources/contacts/ContactsDBReader.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.data.sources.contacts 2 | 3 | import android.database.Cursor 4 | import android.database.sqlite.SQLiteDatabase 5 | import balti.migrate.common.data.model.ContactData 6 | import balti.migrate.common.utils.DBUtils 7 | import baltiapps.migrate.domain.ContactsDBConstants 8 | import baltiapps.migrate.domain.common.getPercentage 9 | import baltiapps.migrate.domain.common.model.GenericFile 10 | import baltiapps.migrate.domain.common.model.Progress 11 | import baltiapps.migrate.domain.common.sources.fileSystem.DBReader 12 | import baltiapps.migrate.domain.exceptions.ContentReadException 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.flow.Flow 15 | import kotlinx.coroutines.flow.flow 16 | import kotlinx.coroutines.flow.flowOn 17 | import java.io.File 18 | 19 | class ContactsDBReader( 20 | private val dbUtils: DBUtils, 21 | ): DBReader { 22 | 23 | private lateinit var sqLiteDatabase: SQLiteDatabase 24 | 25 | override fun setup(file: GenericFile) { 26 | val dbFile = File(file.path).apply { 27 | if (!canRead()) throw ContentReadException("Cannot read contact DB file - ${file.path}") 28 | } 29 | 30 | sqLiteDatabase = dbUtils.getDataBase(dbFile) 31 | } 32 | 33 | override fun readRows(onFinished: (List) -> Unit): Flow { 34 | val dataList = mutableListOf() 35 | 36 | return flow { 37 | val cursor = dbUtils.getCursor(sqLiteDatabase, ContactsDBConstants.CONTACTS_TABLE_NAME) 38 | 39 | val totalCount = cursor.count 40 | if (totalCount == 0) return@flow 41 | cursor.moveToFirst() 42 | 43 | for (i in 0 until totalCount) { 44 | getSingleContact(cursor).run { 45 | dataList.add(this) 46 | emit( 47 | Progress( 48 | progressType = Progress.ProgressType.CONTACTS_BACKUP_READ, 49 | percentage = getPercentage(i+1, totalCount), 50 | logs = this.logInfo 51 | ) 52 | ) 53 | cursor.moveToNext() 54 | } 55 | } 56 | cursor.close() 57 | onFinished(dataList) 58 | }.flowOn(Dispatchers.IO) 59 | } 60 | 61 | private fun getSingleContact( 62 | cursor: Cursor, 63 | ): ContactData { 64 | 65 | dbUtils.run { 66 | val displayName = getCursorData(cursor, ContactsDBConstants.DISPLAY_NAME) 67 | 68 | return ContactData( 69 | _id = getCursorData(cursor, "id"), 70 | displayName = displayName, 71 | vcfContent = getCursorData(cursor, ContactsDBConstants.VCF_CONTENT), 72 | isLocalContact = true, 73 | logInfo = displayName, 74 | ) 75 | } 76 | } 77 | 78 | override fun close() { 79 | if (::sqLiteDatabase.isInitialized) { 80 | sqLiteDatabase.close() 81 | } 82 | } 83 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/data/sources/sms/dummies/DummyComposeSmsActivity.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.data.sources.sms.dummies 2 | 3 | import android.app.Activity 4 | import android.os.Bundle 5 | import android.widget.Toast 6 | import balti.migrate.R 7 | 8 | class DummyComposeSmsActivity : Activity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | Toast.makeText(this, R.string.please_change_sms_app, Toast.LENGTH_LONG).show() 12 | finish() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/data/sources/sms/dummies/DummyMmsBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.data.sources.sms.dummies 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.provider.Telephony 7 | import android.widget.Toast 8 | import balti.migrate.R 9 | 10 | class DummyMmsBroadcastReceiver: BroadcastReceiver() { 11 | override fun onReceive(context: Context?, intent: Intent?) { 12 | if (intent?.action != Telephony.Sms.Intents.WAP_PUSH_DELIVER_ACTION) return 13 | Toast.makeText(context, R.string.new_sms_received, Toast.LENGTH_LONG).show() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/data/sources/sms/dummies/DummySmsBroadcastReceiver.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.data.sources.sms.dummies 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.provider.Telephony 7 | import android.widget.Toast 8 | import balti.migrate.R 9 | 10 | class DummySmsBroadcastReceiver: BroadcastReceiver() { 11 | override fun onReceive(context: Context?, intent: Intent?) { 12 | if (intent?.action != Telephony.Sms.Intents.SMS_DELIVER_ACTION) return 13 | Toast.makeText(context, R.string.new_sms_received, Toast.LENGTH_LONG).show() 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/data/sources/sms/dummies/DummySmsSendService.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.data.sources.sms.dummies 2 | 3 | import android.app.Service 4 | import android.content.Intent 5 | import android.os.IBinder 6 | 7 | class DummySmsSendService : Service() { 8 | override fun onBind(intent: Intent): IBinder? { 9 | return null 10 | } 11 | 12 | override fun onCreate() { 13 | super.onCreate() 14 | stopSelf() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/browseRestoreDirectory/BrowseRestoreDirectoryActions.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.browseRestoreDirectory 2 | 3 | import baltiapps.migrate.domain.common.model.GenericFile 4 | 5 | sealed class BrowseRestoreDirectoryActions { 6 | data object OnReloadExportDirectory: BrowseRestoreDirectoryActions() 7 | data class OnExportDirectoryOpen(val directory: GenericFile): BrowseRestoreDirectoryActions() 8 | data object OnExportDirectoryUp: BrowseRestoreDirectoryActions() 9 | data class OnSafLocationSelected( 10 | val uriString: String?, 11 | ): BrowseRestoreDirectoryActions() 12 | data class OnExportDirectorySelected( 13 | val directory: GenericFile, 14 | val onImportFinished: () -> Unit, 15 | ): BrowseRestoreDirectoryActions() 16 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/browseRestoreDirectory/BrowseRestoreDirectoryState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.browseRestoreDirectory 2 | 3 | import baltiapps.migrate.domain.common.model.GenericFile 4 | 5 | data class BrowseRestoreDirectoryState( 6 | val isLoading: Boolean, 7 | val exportDirectoriesToShow: List, 8 | val currentExportDirectory: GenericFile, 9 | val isImporting: Boolean, 10 | val isBackAllowed: Boolean, 11 | val isSaf: Boolean?, 12 | val locationString: String, 13 | val isSafUriAccessible: Boolean, 14 | ) { 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/listScreen/callLogRestoreSelection/CallLogRestoreSelectionAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.listScreen.callLogRestoreSelection 2 | 3 | import baltiapps.migrate.domain.common.model.CallLogListItem 4 | 5 | sealed class CallLogRestoreSelectionAction { 6 | data class OnPermissionResult(val isGranted: Boolean): CallLogRestoreSelectionAction() 7 | data class ToggleCallLogItem(val item: CallLogListItem): CallLogRestoreSelectionAction() 8 | data class ToggleAllCallLog(val isChecked: Boolean): CallLogRestoreSelectionAction() 9 | data class StageCallLogs(val onStagingDone: () -> Unit): CallLogRestoreSelectionAction() 10 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/listScreen/callLogRestoreSelection/CallLogRestoreSelectionState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.listScreen.callLogRestoreSelection 2 | 3 | import baltiapps.migrate.domain.common.model.CallLogListItem 4 | import baltiapps.migrate.domain.common.model.Progress 5 | 6 | data class CallLogRestoreSelectionState( 7 | val progress: Progress = Progress.Empty, 8 | val callLogList: List = emptyList(), 9 | val isStaging: Boolean = false, 10 | val hasPermission: Boolean = true, 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/listScreen/contactRestoreSelection/ContactRestoreSelectionAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.listScreen.contactRestoreSelection 2 | 3 | import baltiapps.migrate.domain.common.model.ContactListItem 4 | 5 | sealed class ContactRestoreSelectionAction { 6 | data class ToggleContactItem(val item: ContactListItem): ContactRestoreSelectionAction() 7 | data class ToggleAllContacts(val isChecked: Boolean): ContactRestoreSelectionAction() 8 | data class StageContacts(val onStagingDone: () -> Unit): ContactRestoreSelectionAction() 9 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/listScreen/contactRestoreSelection/ContactRestoreSelectionState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.listScreen.contactRestoreSelection 2 | 3 | import baltiapps.migrate.domain.common.model.ContactListItem 4 | import baltiapps.migrate.domain.common.model.Progress 5 | 6 | data class ContactRestoreSelectionState( 7 | val progress: Progress = Progress.Empty, 8 | val contactListItems: List = emptyList(), 9 | val isStaging: Boolean = false, 10 | ) -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/listScreen/contactRestoreSelection/ContactRestoreSelectionViewModel.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.listScreen.contactRestoreSelection 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import balti.migrate.common.utils.ListItemUtils 6 | import baltiapps.migrate.domain.common.usecase.StageSelectedContacts 7 | import baltiapps.migrate.domain.restore.repository.RestoreDataRepository 8 | import baltiapps.migrate.domain.restore.usecase.ReadContactsForRestoreUseCase 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.asStateFlow 11 | import kotlinx.coroutines.flow.onCompletion 12 | import kotlinx.coroutines.flow.update 13 | import kotlinx.coroutines.launch 14 | 15 | class ContactRestoreSelectionViewModel( 16 | private val listItemUtils: ListItemUtils, 17 | private val dataRepository: RestoreDataRepository, 18 | private val readContactsForRestoreUseCase: ReadContactsForRestoreUseCase, 19 | private val stageSelectedCallLogs: StageSelectedContacts, 20 | ) : ViewModel() { 21 | 22 | private val _state = MutableStateFlow(ContactRestoreSelectionState()) 23 | val state = _state.asStateFlow() 24 | 25 | init { 26 | viewModelScope.launch { 27 | readContactsForRestoreUseCase.invoke().onCompletion { 28 | _state.update { 29 | it.copy( 30 | progress = it.progress.copy(percentage = 1.0), 31 | contactListItems = dataRepository.contactsListItems 32 | ) 33 | } 34 | }.collect { 35 | _state.update { state -> 36 | state.copy(progress = it) 37 | } 38 | } 39 | } 40 | } 41 | 42 | fun onAction(action: ContactRestoreSelectionAction) = viewModelScope.launch { 43 | when(action) { 44 | is ContactRestoreSelectionAction.ToggleContactItem -> { 45 | val result = listItemUtils.toggleSingleItem(_state.value.contactListItems, action.item) 46 | _state.update { it.copy(contactListItems = result) } 47 | } 48 | is ContactRestoreSelectionAction.ToggleAllContacts -> { 49 | val result = listItemUtils.toggleAllItems(_state.value.contactListItems, action.isChecked) 50 | _state.update { it.copy(contactListItems = result) } 51 | } 52 | is ContactRestoreSelectionAction.StageContacts -> { 53 | _state.update { it.copy(isStaging = true) } 54 | stageSelectedCallLogs.invoke( 55 | allListItems = _state.value.contactListItems, 56 | dataRepository = dataRepository, 57 | ) 58 | _state.update { it.copy(isStaging = false) } 59 | action.onStagingDone() 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/listScreen/smsRestoreSelection/SmsRestoreSelectionAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.listScreen.smsRestoreSelection 2 | 3 | import baltiapps.migrate.domain.common.model.SmsListItem 4 | 5 | sealed class SmsRestoreSelectionAction { 6 | data class ToggleSmsItem(val item: SmsListItem) : SmsRestoreSelectionAction() 7 | data class ToggleAllSms(val isChecked: Boolean) : SmsRestoreSelectionAction() 8 | data class StageSms(val onStagingDone: () -> Unit) : SmsRestoreSelectionAction() 9 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/listScreen/smsRestoreSelection/SmsRestoreSelectionState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.listScreen.smsRestoreSelection 2 | 3 | import baltiapps.migrate.domain.common.model.Progress 4 | import baltiapps.migrate.domain.common.model.SmsListItem 5 | 6 | data class SmsRestoreSelectionState( 7 | val progress: Progress = Progress.Empty, 8 | val smsList: List = emptyList(), 9 | val isStaging: Boolean = false, 10 | ) 11 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/listScreen/smsRestoreSelection/SmsRestoreSelectionViewModel.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.listScreen.smsRestoreSelection 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import balti.migrate.common.utils.ListItemUtils 6 | import baltiapps.migrate.domain.common.usecase.StageSelectedSms 7 | import baltiapps.migrate.domain.restore.repository.RestoreDataRepository 8 | import baltiapps.migrate.domain.restore.usecase.ReadSmsForRestoreUseCase 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.asStateFlow 11 | import kotlinx.coroutines.flow.onCompletion 12 | import kotlinx.coroutines.flow.update 13 | import kotlinx.coroutines.launch 14 | 15 | class SmsRestoreSelectionViewModel( 16 | private val listItemUtils: ListItemUtils, 17 | private val dataRepository: RestoreDataRepository, 18 | private val readSmsForRestoreUseCase: ReadSmsForRestoreUseCase, 19 | private val stageSelectedSms: StageSelectedSms, 20 | ): ViewModel() { 21 | 22 | private val _state = MutableStateFlow(SmsRestoreSelectionState()) 23 | val state = _state.asStateFlow() 24 | 25 | init { 26 | viewModelScope.launch { 27 | readSmsForRestoreUseCase.invoke().onCompletion { 28 | _state.update { 29 | it.copy( 30 | progress = it.progress.copy(percentage = 1.0), 31 | smsList = dataRepository.smsListItems 32 | ) 33 | } 34 | }.collect { 35 | _state.update { state -> 36 | state.copy( 37 | progress = it 38 | ) 39 | } 40 | } 41 | } 42 | } 43 | 44 | fun onAction(action: SmsRestoreSelectionAction) = viewModelScope.launch { 45 | when(action) { 46 | is SmsRestoreSelectionAction.ToggleSmsItem -> { 47 | val result = listItemUtils.toggleSingleItem(_state.value.smsList, action.item) 48 | _state.update { it.copy(smsList = result) } 49 | } 50 | is SmsRestoreSelectionAction.ToggleAllSms -> { 51 | val result = listItemUtils.toggleAllItems(_state.value.smsList, action.isChecked) 52 | _state.update { it.copy(smsList = result) } 53 | } 54 | is SmsRestoreSelectionAction.StageSms -> { 55 | _state.update { it.copy(isStaging = true) } 56 | stageSelectedSms.invoke( 57 | allListItems = _state.value.smsList, 58 | dataRepository = dataRepository, 59 | ) 60 | _state.update { it.copy(isStaging = false) } 61 | action.onStagingDone() 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/progressScreen/RestoreProgressScreenAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.progressScreen 2 | 3 | sealed class RestoreProgressScreenAction { 4 | data class ToggleErrorOnly(val enabled: Boolean) : RestoreProgressScreenAction() 5 | data object CancelRestore : RestoreProgressScreenAction() 6 | data object PauseProgressLogs : RestoreProgressScreenAction() 7 | data object ResumeProgressLogs : RestoreProgressScreenAction() 8 | data object OnChangeSmsApp: RestoreProgressScreenAction() 9 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/progressScreen/RestoreProgressScreenState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.progressScreen 2 | 3 | import baltiapps.migrate.domain.common.model.Progress 4 | 5 | data class RestoreProgressScreenState( 6 | val progressList: List, 7 | val errorList: List, 8 | val headingText: String, 9 | val errorOnly: Boolean, 10 | val isCancelling: Boolean, 11 | val isRestoreFinished: Boolean, 12 | val shouldChangeSmsApp: Boolean, 13 | ) { 14 | val isLoading: Boolean get() = progressList.isEmpty() && errorList.isEmpty() 15 | companion object { 16 | val Empty = RestoreProgressScreenState( 17 | progressList = emptyList(), 18 | errorList = emptyList(), 19 | headingText = "", 20 | errorOnly = false, 21 | isCancelling = false, 22 | isRestoreFinished = false, 23 | shouldChangeSmsApp = false, 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/progressScreen/SmsAppChangeDialog.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.progressScreen 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.Text 5 | import androidx.compose.material3.TextButton 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.res.stringResource 9 | import androidx.compose.ui.text.SpanStyle 10 | import androidx.compose.ui.text.buildAnnotatedString 11 | import androidx.compose.ui.text.font.FontStyle 12 | import androidx.compose.ui.text.font.FontWeight 13 | import androidx.compose.ui.text.withStyle 14 | import androidx.compose.ui.tooling.preview.Preview 15 | import androidx.compose.ui.window.DialogProperties 16 | import balti.migrate.R 17 | 18 | @Composable 19 | fun SmsAppChangeDialog( 20 | shouldShow: Boolean, 21 | onAgree: () -> Unit, 22 | modifier: Modifier = Modifier, 23 | ) { 24 | if (shouldShow) { 25 | AlertDialog( 26 | modifier = modifier, 27 | onDismissRequest = {}, 28 | title = { 29 | Text(stringResource(R.string.sms_app_change_dialog_title)) 30 | }, 31 | text = { 32 | val annotatedText = buildAnnotatedString { 33 | append(stringResource(R.string.sms_app_change_dialog_message)) 34 | append("\n\n") 35 | withStyle( 36 | style = SpanStyle(fontWeight = FontWeight.Bold) 37 | ) { 38 | append(stringResource(R.string.this_is_mandatory)) 39 | } 40 | append("\n\n") 41 | withStyle( 42 | style = SpanStyle(fontStyle = FontStyle.Italic) 43 | ) { 44 | append(stringResource(R.string.force_close_message)) 45 | } 46 | } 47 | Text(annotatedText) 48 | }, 49 | confirmButton = { 50 | TextButton(onClick = onAgree) { 51 | Text(stringResource(R.string.proceed)) 52 | } 53 | }, 54 | dismissButton = null, 55 | properties = DialogProperties( 56 | dismissOnBackPress = false, 57 | dismissOnClickOutside = false, 58 | ) 59 | ) 60 | } 61 | } 62 | 63 | @Preview 64 | @Composable 65 | private fun DialogPreview() { 66 | SmsAppChangeDialog( 67 | shouldShow = true, 68 | onAgree = {}, 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/restoreSummary/RestoreSummaryAction.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.restoreSummary 2 | 3 | sealed class RestoreSummaryAction { 4 | data class StartRestore(val runService: () -> Unit) : RestoreSummaryAction() 5 | data object OnUserProceedContactImport : RestoreSummaryAction() 6 | data object SkipContacts : RestoreSummaryAction() 7 | data object OnContactImported : RestoreSummaryAction() 8 | data object OnUserProceedSetDefaultSmsApp : RestoreSummaryAction() 9 | data object SkipSms : RestoreSummaryAction() 10 | data object OnDefaultSmsAppSet : RestoreSummaryAction() 11 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/restoreSummary/RestoreSummaryItemState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.restoreSummary 2 | 3 | enum class RestoreSummaryItemState { 4 | REQUEST_USER_INPUT, 5 | ON_USER_INPUT_POSITIVE, 6 | ON_USER_INPUT_NEGATIVE, 7 | WAITING, 8 | PROCESSING, 9 | DONE, 10 | CANCELLED, 11 | ERROR, 12 | UNKNOWN, 13 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/restoreSummary/RestoreSummaryState.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.restoreSummary 2 | 3 | import baltiapps.migrate.domain.common.model.Progress 4 | 5 | data class RestoreSummaryState( 6 | val isInitialized: Boolean = false, 7 | val countdown: Int = 5, 8 | val countContacts: Int = 0, 9 | val countCallLogs: Int = 0, 10 | val countSms: Int = 0, 11 | val contactsExportProgress: Progress = Progress.Empty, 12 | val contactSummaryState: RestoreSummaryItemState, 13 | val smsSummaryState: RestoreSummaryItemState, 14 | ) -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/restoreSummary/components/DelegateRestore.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.restoreSummary.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.outlined.Contacts 11 | import androidx.compose.material3.HorizontalDivider 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import balti.migrate.R 19 | import balti.migrate.restore.ui.screens.restoreSummary.RestoreSummaryState 20 | 21 | 22 | @Composable 23 | fun DelegatedRestore( 24 | state: RestoreSummaryState, 25 | modifier: Modifier = Modifier, 26 | ) { 27 | if (state.countContacts <= 0) return 28 | Column( 29 | modifier = modifier.fillMaxWidth(), 30 | ) { 31 | Column( 32 | modifier = modifier 33 | .fillMaxWidth() 34 | .padding(horizontal = 16.dp), 35 | verticalArrangement = Arrangement.spacedBy(2.dp), 36 | ) { 37 | Text( 38 | text = stringResource(R.string.delegated_restore), 39 | style = MaterialTheme.typography.labelMedium, 40 | color = MaterialTheme.colorScheme.onSurfaceVariant, 41 | ) 42 | HorizontalDivider() 43 | } 44 | Spacer(Modifier.size(8.dp)) 45 | SummaryItem( 46 | headlineStringRes = R.string.contacts_to_restore, 47 | count = state.countContacts, 48 | icon = Icons.Outlined.Contacts, 49 | state = state.contactSummaryState, 50 | ) 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/restoreSummary/components/SimpleYesNoDialog.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.restoreSummary.components 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.Icon 5 | import androidx.compose.material3.Text 6 | import androidx.compose.material3.TextButton 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.vector.ImageVector 10 | import androidx.compose.ui.res.stringResource 11 | import androidx.compose.ui.window.DialogProperties 12 | import balti.migrate.R 13 | 14 | @Composable 15 | fun SimpleYesNoDialog( 16 | dialogText: String, 17 | onProceed: () -> Unit, 18 | onSkip: () -> Unit, 19 | modifier: Modifier = Modifier, 20 | icon: ImageVector? = null, 21 | titleText: String? = null, 22 | ) { 23 | AlertDialog( 24 | onDismissRequest = {}, 25 | modifier = modifier, 26 | text = { 27 | Text(dialogText) 28 | }, 29 | title = titleText?.run { 30 | { Text(titleText) } 31 | }, 32 | confirmButton = { 33 | TextButton(onClick = onProceed) { 34 | Text(stringResource(R.string.proceed)) 35 | } 36 | }, 37 | dismissButton = { 38 | TextButton(onClick = onSkip) { 39 | Text(stringResource(R.string.skip)) 40 | } 41 | }, 42 | icon = if (icon != null) { 43 | { 44 | Icon( 45 | imageVector = icon, 46 | contentDescription = null 47 | ) 48 | } 49 | } else null, 50 | properties = DialogProperties( 51 | dismissOnClickOutside = false, 52 | dismissOnBackPress = false, 53 | ) 54 | ) 55 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/restoreSummary/components/SpecialPermissions.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.restoreSummary.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.outlined.Sms 11 | import androidx.compose.material3.HorizontalDivider 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.res.stringResource 17 | import androidx.compose.ui.unit.dp 18 | import balti.migrate.R 19 | import balti.migrate.restore.ui.screens.restoreSummary.RestoreSummaryState 20 | 21 | @Composable 22 | fun SpecialPermissions( 23 | state: RestoreSummaryState, 24 | modifier: Modifier = Modifier, 25 | ) { 26 | if (state.countSms <= 0) return 27 | Column( 28 | modifier = modifier.fillMaxWidth(), 29 | ) { 30 | Column( 31 | modifier = modifier 32 | .fillMaxWidth() 33 | .padding(horizontal = 16.dp), 34 | verticalArrangement = Arrangement.spacedBy(2.dp), 35 | ) { 36 | Text( 37 | text = stringResource(R.string.special_permissions_required), 38 | style = MaterialTheme.typography.labelMedium, 39 | color = MaterialTheme.colorScheme.onSurfaceVariant, 40 | ) 41 | HorizontalDivider() 42 | } 43 | Spacer(Modifier.size(8.dp)) 44 | SummaryItem( 45 | headlineStringRes = R.string.set_as_default_sms_app, 46 | count = null, 47 | icon = Icons.Outlined.Sms, 48 | state = state.smsSummaryState, 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/restoreSummary/components/StandardRestore.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.restoreSummary.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.size 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.outlined.Call 11 | import androidx.compose.material.icons.outlined.Sms 12 | import androidx.compose.material3.HorizontalDivider 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.Text 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.res.stringResource 18 | import androidx.compose.ui.unit.dp 19 | import balti.migrate.R 20 | import balti.migrate.restore.ui.screens.restoreSummary.RestoreSummaryItemState 21 | import balti.migrate.restore.ui.screens.restoreSummary.RestoreSummaryState 22 | 23 | @Composable 24 | fun StandardRestore( 25 | state: RestoreSummaryState, 26 | modifier: Modifier = Modifier, 27 | ) { 28 | if ((state.countCallLogs + state.countSms) <= 0) return 29 | Column( 30 | modifier = modifier.fillMaxWidth(), 31 | ) { 32 | Column( 33 | modifier = modifier 34 | .fillMaxWidth() 35 | .padding(horizontal = 16.dp), 36 | verticalArrangement = Arrangement.spacedBy(2.dp), 37 | ) { 38 | Text( 39 | text = stringResource(R.string.migrate_restore), 40 | style = MaterialTheme.typography.labelMedium, 41 | color = MaterialTheme.colorScheme.onSurfaceVariant, 42 | ) 43 | HorizontalDivider() 44 | } 45 | Spacer(Modifier.size(8.dp)) 46 | if (state.countCallLogs > 0) { 47 | SummaryItem( 48 | headlineStringRes = R.string.call_logs_to_restore, 49 | count = state.countCallLogs, 50 | icon = Icons.Outlined.Call, 51 | state = RestoreSummaryItemState.UNKNOWN, 52 | ) 53 | } 54 | if (state.countSms > 0) { 55 | SummaryItem( 56 | headlineStringRes = R.string.sms_to_restore, 57 | count = state.countSms, 58 | icon = Icons.Outlined.Sms, 59 | state = RestoreSummaryItemState.UNKNOWN, 60 | ) 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/balti/migrate/restore/ui/screens/restoreSummary/components/SummaryItem.kt: -------------------------------------------------------------------------------- 1 | package balti.migrate.restore.ui.screens.restoreSummary.components 2 | 3 | import androidx.annotation.StringRes 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.outlined.Close 6 | import androidx.compose.material.icons.outlined.Done 7 | import androidx.compose.material.icons.outlined.HourglassEmpty 8 | import androidx.compose.material.icons.outlined.SaveAlt 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.ListItem 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.Text 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.vector.ImageVector 16 | import androidx.compose.ui.res.stringResource 17 | import balti.migrate.common.ui.components.EmptyImageVector 18 | import balti.migrate.restore.ui.screens.restoreSummary.RestoreSummaryItemState 19 | 20 | @Composable 21 | fun SummaryItem( 22 | @StringRes headlineStringRes: Int, 23 | count: Int?, 24 | icon: ImageVector, 25 | state: RestoreSummaryItemState, 26 | modifier: Modifier = Modifier, 27 | ) { 28 | ListItem( 29 | modifier = modifier, 30 | headlineContent = { 31 | Text( 32 | text = count?.let { stringResource(headlineStringRes, it) } ?: stringResource(headlineStringRes), 33 | style = MaterialTheme.typography.titleMedium, 34 | ) 35 | }, 36 | leadingContent = { 37 | Icon( 38 | imageVector = icon, 39 | contentDescription = null, 40 | ) 41 | }, 42 | trailingContent = { 43 | Icon( 44 | imageVector = when (state) { 45 | RestoreSummaryItemState.WAITING -> Icons.Outlined.HourglassEmpty 46 | RestoreSummaryItemState.REQUEST_USER_INPUT -> Icons.Outlined.HourglassEmpty 47 | RestoreSummaryItemState.PROCESSING -> Icons.Outlined.SaveAlt 48 | RestoreSummaryItemState.DONE -> Icons.Outlined.Done 49 | RestoreSummaryItemState.CANCELLED -> Icons.Outlined.Close 50 | RestoreSummaryItemState.ON_USER_INPUT_NEGATIVE -> Icons.Outlined.Close 51 | else -> EmptyImageVector 52 | }, 53 | contentDescription = null, 54 | ) 55 | } 56 | ) 57 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/app_icon_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaltiApps/Migrate-OSS/561ddf5458cf98cb144f5fa9da7a508fd91329f0/app/src/main/res/drawable/app_icon_round.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_voicemail_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_00.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_05.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_10.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_15.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_20.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_25.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_30.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_35.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_40.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_45.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_50.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_55.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_60.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_65.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_70.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_75.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_80.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_85.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_90.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_icon_95.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/notification_rotating_icon.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_close_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/outline_warning_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |