diff --git a/app/build.gradle b/app/build.gradle index f767cfc823..593efb2304 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,6 +31,7 @@ ext { retrofit2Version = '2.8.1' okhttp3Version = '4.6.0' coroutinesVersion = '1.3.5' + activityVersion = '1.2.0-alpha03' } @@ -264,6 +265,8 @@ dependencies { implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation 'androidx.percentlayout:percentlayout:1.0.0' implementation "androidx.preference:preference-ktx:1.1.1" + implementation "androidx.activity:activity:${activityVersion}" + implementation "androidx.activity:activity-ktx:${activityVersion}" implementation 'com.google.android.material:material:1.1.0' implementation 'com.wdullaer:materialdatetimepicker:4.2.3' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index caf37d462d..2bd4527cf6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -82,6 +82,7 @@ + diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/ActivitiesModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/ActivitiesModule.kt index e7411ad56b..2f6d515b0e 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/ActivitiesModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/ActivitiesModule.kt @@ -6,6 +6,7 @@ import info.nightscout.androidaps.MainActivity import info.nightscout.androidaps.activities.* import info.nightscout.androidaps.historyBrowser.HistoryBrowseActivity import info.nightscout.androidaps.plugins.general.maintenance.activities.LogSettingActivity +import info.nightscout.androidaps.plugins.general.maintenance.activities.PrefImportListActivity import info.nightscout.androidaps.plugins.general.overview.activities.QuickWizardListActivity import info.nightscout.androidaps.plugins.general.smsCommunicator.activities.SmsCommunicatorOtpActivity import info.nightscout.androidaps.plugins.pump.common.dialog.RileyLinkBLEScanActivity @@ -48,4 +49,5 @@ abstract class ActivitiesModule { @ContributesAndroidInjector abstract fun contributesStatsActivity(): StatsActivity @ContributesAndroidInjector abstract fun contributesSurveyActivity(): SurveyActivity @ContributesAndroidInjector abstract fun contributesTDDStatsActivity(): TDDStatsActivity + @ContributesAndroidInjector abstract fun contributesPrefImportListActivity(): PrefImportListActivity } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/PreferencesModule.kt b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/PreferencesModule.kt index fa2df68ef0..39fb959c15 100644 --- a/app/src/main/java/info/nightscout/androidaps/dependencyInjection/PreferencesModule.kt +++ b/app/src/main/java/info/nightscout/androidaps/dependencyInjection/PreferencesModule.kt @@ -3,6 +3,7 @@ package info.nightscout.androidaps.dependencyInjection import dagger.Module import dagger.android.ContributesAndroidInjector import info.nightscout.androidaps.plugins.general.maintenance.ImportExportPrefs +import info.nightscout.androidaps.plugins.general.maintenance.PrefFileListProvider import info.nightscout.androidaps.plugins.general.maintenance.formats.ClassicPrefsFormat import info.nightscout.androidaps.plugins.general.maintenance.formats.EncryptedPrefsFormat import info.nightscout.androidaps.utils.CryptoUtil @@ -15,4 +16,5 @@ abstract class PreferencesModule { @ContributesAndroidInjector abstract fun importExportPrefsInjector(): ImportExportPrefs @ContributesAndroidInjector abstract fun encryptedPrefsFormatInjector(): EncryptedPrefsFormat @ContributesAndroidInjector abstract fun classicPrefsFormatInjector(): ClassicPrefsFormat + @ContributesAndroidInjector abstract fun prefImportListProviderInjector(): PrefFileListProvider } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/constraints/versionChecker/VersionCheckerUtils.kt b/app/src/main/java/info/nightscout/androidaps/plugins/constraints/versionChecker/VersionCheckerUtils.kt index d1ce90fc72..636d623bd0 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/constraints/versionChecker/VersionCheckerUtils.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/constraints/versionChecker/VersionCheckerUtils.kt @@ -116,6 +116,14 @@ class VersionCheckerUtils @Inject constructor( private fun String?.toNumberList() = this?.numericVersionPart().takeIf { !it.isNullOrBlank() }?.split(".")?.map { it.toInt() } + fun versionDigits(versionString: String?): IntArray { + val digits = mutableListOf() + versionString?.numericVersionPart().toNumberList()?.let { + digits.addAll(it.take(4)) + } + return digits.toIntArray() + } + fun findVersion(file: String?): String? { val regex = "(.*)version(.*)\"(((\\d+)\\.)+(\\d+))\"(.*)".toRegex() return file?.lines()?.filter { regex.matches(it) }?.mapNotNull { regex.matchEntire(it)?.groupValues?.getOrNull(3) }?.firstOrNull() diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt index 1d72212fb3..b859a1dee5 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/ImportExportPrefs.kt @@ -6,12 +6,12 @@ import android.bluetooth.BluetoothAdapter import android.content.Context import android.content.Intent import android.content.pm.PackageManager -import android.os.Build -import android.os.Environment import android.provider.Settings +import androidx.activity.invoke import androidx.annotation.StringRes import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity import info.nightscout.androidaps.BuildConfig import info.nightscout.androidaps.R import info.nightscout.androidaps.activities.PreferencesActivity @@ -22,9 +22,9 @@ import info.nightscout.androidaps.plugins.bus.RxBusWrapper import info.nightscout.androidaps.plugins.general.maintenance.formats.* import info.nightscout.androidaps.plugins.general.smsCommunicator.otp.OneTimePassword import info.nightscout.androidaps.utils.DateUtil +import info.nightscout.androidaps.utils.ToastUtils import info.nightscout.androidaps.utils.alertDialogs.OKDialog import info.nightscout.androidaps.utils.alertDialogs.OKDialog.show -import info.nightscout.androidaps.utils.ToastUtils import info.nightscout.androidaps.utils.alertDialogs.PrefImportSummaryDialog import info.nightscout.androidaps.utils.alertDialogs.TwoMessagesAlertDialog import info.nightscout.androidaps.utils.alertDialogs.WarningDialog @@ -32,8 +32,6 @@ import info.nightscout.androidaps.utils.buildHelper.BuildHelper import info.nightscout.androidaps.utils.protection.PasswordCheck import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.sharedPreferences.SP -import org.joda.time.DateTime -import org.joda.time.Days import java.io.File import java.io.FileNotFoundException import java.io.IOException @@ -51,37 +49,25 @@ private val PERMISSIONS_STORAGE = arrayOf( Manifest.permission.WRITE_EXTERNAL_STORAGE ) -private const val IMPORT_AGE_NOT_YET_OLD_DAYS = 60 - @Singleton class ImportExportPrefs @Inject constructor( private var log: AAPSLogger, private val resourceHelper: ResourceHelper, private val sp: SP, private val buildHelper: BuildHelper, - private val otp: OneTimePassword, private val rxBus: RxBusWrapper, private val passwordCheck: PasswordCheck, private val classicPrefsFormat: ClassicPrefsFormat, - private val encryptedPrefsFormat: EncryptedPrefsFormat + private val encryptedPrefsFormat: EncryptedPrefsFormat, + private val prefFileList: PrefFileListProvider ) { val TAG = LTag.CORE - private val path = File(Environment.getExternalStorageDirectory().toString()) - - private val file = File(path, resourceHelper.gs(R.string.app_name) + "Preferences") - private val encFile = File(path, resourceHelper.gs(R.string.app_name) + "Preferences.json") - - fun prefsImportFile(): File { - return if (encFile.exists()) encFile else file - } - fun prefsFileExists(): Boolean { - return encFile.exists() || file.exists() + return prefFileList.listPreferenceFiles().size > 0 } - fun exportSharedPreferences(f: Fragment) { f.activity?.let { exportSharedPreferences(it) } } @@ -135,9 +121,6 @@ class ImportExportPrefs @Inject constructor( return name } - private fun getCurrentDeviceModelString() = - Build.MANUFACTURER + " " + Build.MODEL + " (" + Build.DEVICE + ")" - private fun prefsEncryptionIsDisabled() = buildHelper.isEngineeringMode() && !sp.getBoolean(resourceHelper.gs(R.string.key_maintenance_encrypt_exported_prefs), true) @@ -173,36 +156,41 @@ class ImportExportPrefs @Inject constructor( return true } - private fun askToConfirmExport(activity: Activity, then: ((password: String) -> Unit)) { + private fun askToConfirmExport(activity: Activity, fileToExport: File, then: ((password: String) -> Unit)) { if (!prefsEncryptionIsDisabled() && !assureMasterPasswordSet(activity, R.string.nav_export)) return TwoMessagesAlertDialog.showAlert(activity, resourceHelper.gs(R.string.nav_export), - resourceHelper.gs(R.string.export_to) + " " + encFile + " ?", + resourceHelper.gs(R.string.export_to) + " " + fileToExport + " ?", resourceHelper.gs(R.string.password_preferences_encrypt_prompt), { askForMasterPassIfNeeded(activity, R.string.preferences_export_canceled, then) - }, null, R.drawable.ic_header_export) + }, null, R.drawable.ic_header_export) } - private fun askToConfirmImport(activity: Activity, fileToImport: File, then: ((password: String) -> Unit)) { + private fun askToConfirmImport(activity: Activity, fileToImport: PrefsFile, then: ((password: String) -> Unit)) { - if (encFile.exists()) { + if (fileToImport.handler == PrefsFormatsHandler.ENCRYPTED) { if (!assureMasterPasswordSet(activity, R.string.nav_import)) return TwoMessagesAlertDialog.showAlert(activity, resourceHelper.gs(R.string.nav_import), - resourceHelper.gs(R.string.import_from) + " " + fileToImport + " ?", + resourceHelper.gs(R.string.import_from) + " " + fileToImport.file + " ?", resourceHelper.gs(R.string.password_preferences_decrypt_prompt), { askForMasterPass(activity, R.string.preferences_import_canceled, then) }, null, R.drawable.ic_header_import) } else { OKDialog.showConfirmation(activity, resourceHelper.gs(R.string.nav_import), - resourceHelper.gs(R.string.import_from) + " " + fileToImport + " ?", + resourceHelper.gs(R.string.import_from) + " " + fileToImport.file + " ?", Runnable { then("") }) } } private fun exportSharedPreferences(activity: Activity) { - askToConfirmExport(activity) { password -> + + prefFileList.ensureExportDirExists() + val legacyFile = prefFileList.legacyFile() + val newFile = prefFileList.newExportFile() + + askToConfirmExport(activity, newFile) { password -> try { val entries: MutableMap = mutableMapOf() for ((key, value) in sp.getAll()) { @@ -211,12 +199,14 @@ class ImportExportPrefs @Inject constructor( val prefs = Prefs(entries, prepareMetadata(activity)) - classicPrefsFormat.savePreferences(file, prefs) - encryptedPrefsFormat.savePreferences(encFile, prefs, password) + if (BuildConfig.DEBUG && buildHelper.isEngineeringMode()) { + classicPrefsFormat.savePreferences(legacyFile, prefs) + } + encryptedPrefsFormat.savePreferences(newFile, prefs, password) ToastUtils.okToast(activity, resourceHelper.gs(R.string.exported)) } catch (e: FileNotFoundException) { - ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + encFile) + ToastUtils.errorToast(activity, resourceHelper.gs(R.string.filenotfound) + " " + newFile) log.error(TAG, "Unhandled exception", e) } catch (e: IOException) { ToastUtils.errorToast(activity, e.message) @@ -226,21 +216,38 @@ class ImportExportPrefs @Inject constructor( } fun importSharedPreferences(fragment: Fragment) { - fragment.activity?.let { importSharedPreferences(it) } + fragment.activity?.let { fragmentAct -> + val callForPrefFile = fragmentAct.prepareCall(PrefsFileContract()) { + it?.let { + importSharedPreferences(fragmentAct, it) + } + } + callForPrefFile.invoke() + } } - fun importSharedPreferences(activity: Activity) { + fun importSharedPreferences(activity: FragmentActivity) { + val callForPrefFile = activity.prepareCall(PrefsFileContract()) { + it?.let { + importSharedPreferences(activity, it) + } + } + callForPrefFile.invoke() + } - val importFile = prefsImportFile() + private fun importSharedPreferences(activity: Activity, importFile: PrefsFile) { askToConfirmImport(activity, importFile) { password -> - val format: PrefsFormat = if (encFile.exists()) encryptedPrefsFormat else classicPrefsFormat + val format: PrefsFormat = when (importFile.handler) { + PrefsFormatsHandler.CLASSIC -> classicPrefsFormat + PrefsFormatsHandler.ENCRYPTED -> encryptedPrefsFormat + } try { - val prefs = format.loadPreferences(importFile, password) - prefs.metadata = checkMetadata(prefs.metadata) + val prefs = format.loadPreferences(importFile.file, password) + prefs.metadata = prefFileList.checkMetadata(prefs.metadata) // import is OK when we do not have errors (warnings are allowed) val importOk = checkIfImportIsOk(prefs) @@ -276,45 +283,6 @@ class ImportExportPrefs @Inject constructor( } } - // check metadata for known issues, change their status and add info with explanations - private fun checkMetadata(metadata: Map): Map { - val meta = metadata.toMutableMap() - - meta[PrefsMetadataKey.AAPS_FLAVOUR]?.let { flavour -> - val flavourOfPrefs = flavour.value - if (flavour.value != BuildConfig.FLAVOR) { - flavour.status = PrefsStatus.WARN - flavour.info = resourceHelper.gs(R.string.metadata_warning_different_flavour, flavourOfPrefs, BuildConfig.FLAVOR) - } - } - - meta[PrefsMetadataKey.DEVICE_MODEL]?.let { model -> - if (model.value != getCurrentDeviceModelString()) { - model.status = PrefsStatus.WARN - model.info = resourceHelper.gs(R.string.metadata_warning_different_device) - } - } - - meta[PrefsMetadataKey.CREATED_AT]?.let { createdAt -> - try { - val date1 = DateTime.parse(createdAt.value); - val date2 = DateTime.now() - - val daysOld = Days.daysBetween(date1.toLocalDate(), date2.toLocalDate()).getDays() - - if (daysOld > IMPORT_AGE_NOT_YET_OLD_DAYS) { - createdAt.status = PrefsStatus.WARN - createdAt.info = resourceHelper.gs(R.string.metadata_warning_old_export, daysOld.toString()) - } - } catch (e: Exception) { - createdAt.status = PrefsStatus.WARN - createdAt.info = resourceHelper.gs(R.string.metadata_warning_date_format) - } - } - - return meta - } - private fun checkIfImportIsOk(prefs: Prefs): Boolean { var importOk = true diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/PrefFileListProvider.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/PrefFileListProvider.kt new file mode 100644 index 0000000000..52910dfff1 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/PrefFileListProvider.kt @@ -0,0 +1,219 @@ +package info.nightscout.androidaps.plugins.general.maintenance + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.Environment +import android.os.Parcelable +import androidx.activity.result.contract.ActivityResultContract +import info.nightscout.androidaps.BuildConfig +import info.nightscout.androidaps.R +import info.nightscout.androidaps.logging.AAPSLogger +import info.nightscout.androidaps.plugins.constraints.versionChecker.VersionCheckerUtils +import info.nightscout.androidaps.plugins.general.maintenance.activities.PrefImportListActivity +import info.nightscout.androidaps.plugins.general.maintenance.formats.* +import info.nightscout.androidaps.utils.resources.ResourceHelper +import info.nightscout.androidaps.utils.sharedPreferences.SP +import info.nightscout.androidaps.utils.storage.Storage +import kotlinx.android.parcel.Parcelize +import kotlinx.android.parcel.RawValue +import org.joda.time.DateTime +import org.joda.time.Days +import org.joda.time.Hours +import org.joda.time.LocalDateTime +import org.joda.time.format.DateTimeFormat +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +enum class PrefsImportDir { + ROOT_DIR, + AAPS_DIR +} + +@Parcelize +data class PrefsFile( + val file: File, + val baseDir: File, + val dirKind: PrefsImportDir, + val handler: PrefsFormatsHandler, + + // metadata here is used only for list display + val metadata: @RawValue Map +) : Parcelable + +class PrefsFileContract : ActivityResultContract() { + + companion object { + const val OUTPUT_PARAM = "prefs_file" + } + + override fun parseResult(resultCode: Int, intent: Intent?): PrefsFile? { + return when (resultCode) { + Activity.RESULT_OK -> intent?.getParcelableExtra(OUTPUT_PARAM) + else -> null + } + } + + override fun createIntent(context: Context, input: Void?): Intent { + return Intent(context, PrefImportListActivity::class.java) + } +} + +fun getCurrentDeviceModelString() = + Build.MANUFACTURER + " " + Build.MODEL + " (" + Build.DEVICE + ")" + +@Singleton +class PrefFileListProvider @Inject constructor( + private val resourceHelper: ResourceHelper, + private val classicPrefsFormat: ClassicPrefsFormat, + private val encryptedPrefsFormat: EncryptedPrefsFormat, + private val storage: Storage, + private val versionCheckerUtils: VersionCheckerUtils +) { + + companion object { + private val path = File(Environment.getExternalStorageDirectory().toString()) + private val aapsPath = File(path, "AAPS" + File.separator + "preferences") + private const val IMPORT_AGE_NOT_YET_OLD_DAYS = 60 + } + + /** + * This function tries to list possible preference files from main SDCard root dir and AAPS/preferences dir + * and tries to do quick assessment for preferences format plausibility. + * It does NOT load full metadata or is 100% accurate - it tries to do QUICK detection, based on: + * - file name and extension + * - predicted file contents + */ + fun listPreferenceFiles(loadMetadata: Boolean = false): MutableList { + val prefFiles = mutableListOf() + + // searching rood dir for legacy files + path.walk().maxDepth(1).filter { it.isFile && (it.name.endsWith(".json") || it.name.contains("Preferences")) }.forEach { + val contents = storage.getFileContents(it) + val detectedNew = encryptedPrefsFormat.isPreferencesFile(it, contents) + val detectedOld = !detectedNew && classicPrefsFormat.isPreferencesFile(it, contents) + if (detectedNew || detectedOld) { + val formatHandler = if (detectedNew) PrefsFormatsHandler.ENCRYPTED else PrefsFormatsHandler.CLASSIC + prefFiles.add(PrefsFile(it, path, PrefsImportDir.ROOT_DIR, formatHandler, metadataFor(loadMetadata, formatHandler, contents))) + } + } + + // searching dedicated dir, only for new JSON format + aapsPath.walk().filter { it.isFile && it.name.endsWith(".json") }.forEach { + val contents = storage.getFileContents(it) + if (encryptedPrefsFormat.isPreferencesFile(it, contents)) { + prefFiles.add(PrefsFile(it, aapsPath, PrefsImportDir.AAPS_DIR, PrefsFormatsHandler.ENCRYPTED, metadataFor(loadMetadata, PrefsFormatsHandler.ENCRYPTED, contents))) + } + } + + // we sort only if we have metadata to be used for that + if (loadMetadata) { + prefFiles.sortWith( + compareByDescending { it.handler } + .thenBy { it.metadata[PrefsMetadataKey.AAPS_FLAVOUR]?.status } + .thenByDescending { it.metadata[PrefsMetadataKey.CREATED_AT]?.value } + ) + } + + return prefFiles + } + + private fun metadataFor(loadMetadata: Boolean, formatHandler: PrefsFormatsHandler, contents: String): PrefMetadataMap { + if (!loadMetadata) { + return mapOf() + } + return checkMetadata(when (formatHandler) { + PrefsFormatsHandler.CLASSIC -> classicPrefsFormat.loadMetadata(contents) + PrefsFormatsHandler.ENCRYPTED -> encryptedPrefsFormat.loadMetadata(contents) + }) + } + + fun legacyFile(): File { + return File(path, resourceHelper.gs(R.string.app_name) + "Preferences") + } + + fun ensureExportDirExists() { + if (!aapsPath.exists()) { + aapsPath.mkdirs() + } + } + + fun newExportFile(): File { + val timeLocal = LocalDateTime.now().toString(DateTimeFormat.forPattern("yyyy-MM-dd'_'HHmmss")) + return File(aapsPath, timeLocal + "_" + BuildConfig.FLAVOR + ".json") + } + + // check metadata for known issues, change their status and add info with explanations + fun checkMetadata(metadata: Map): Map { + val meta = metadata.toMutableMap() + + meta[PrefsMetadataKey.AAPS_FLAVOUR]?.let { flavour -> + val flavourOfPrefs = flavour.value + if (flavour.value != BuildConfig.FLAVOR) { + flavour.status = PrefsStatus.WARN + flavour.info = resourceHelper.gs(R.string.metadata_warning_different_flavour, flavourOfPrefs, BuildConfig.FLAVOR) + } + } + + meta[PrefsMetadataKey.DEVICE_MODEL]?.let { model -> + if (model.value != getCurrentDeviceModelString()) { + model.status = PrefsStatus.WARN + model.info = resourceHelper.gs(R.string.metadata_warning_different_device) + } + } + + meta[PrefsMetadataKey.CREATED_AT]?.let { createdAt -> + try { + val date1 = DateTime.parse(createdAt.value); + val date2 = DateTime.now() + + val daysOld = Days.daysBetween(date1.toLocalDate(), date2.toLocalDate()).getDays() + + if (daysOld > IMPORT_AGE_NOT_YET_OLD_DAYS) { + createdAt.status = PrefsStatus.WARN + createdAt.info = resourceHelper.gs(R.string.metadata_warning_old_export, daysOld.toString()) + } + } catch (e: Exception) { + createdAt.status = PrefsStatus.WARN + createdAt.info = resourceHelper.gs(R.string.metadata_warning_date_format) + } + } + + meta[PrefsMetadataKey.AAPS_VERSION]?.let { version -> + val currentAppVer = versionCheckerUtils.versionDigits(BuildConfig.VERSION_NAME) + val metadataVer = versionCheckerUtils.versionDigits(version.value) + + if ((currentAppVer.size >= 2) && (metadataVer.size >= 2) && (Math.abs(currentAppVer[1] - metadataVer[1]) > 1)) { + version.status = PrefsStatus.WARN + version.info = resourceHelper.gs(R.string.metadata_warning_different_version) + } + + if ((currentAppVer.isNotEmpty()) && (metadataVer.isNotEmpty()) && (currentAppVer[0] != metadataVer[0])) { + version.status = PrefsStatus.WARN + version.info = resourceHelper.gs(R.string.metadata_urgent_different_version) + } + } + + return meta + } + + fun formatExportedAgo(utcTime: String): String { + val refTime = DateTime.now() + val itTime = DateTime.parse(utcTime) + val days = Days.daysBetween(itTime, refTime).days + val hours = Hours.hoursBetween(itTime, refTime).hours + + return if (hours == 0) { + resourceHelper.gs(R.string.exported_less_than_hour_ago) + } else if ((hours < 24) && (hours > 0)) { + resourceHelper.gs(R.string.exported_ago, resourceHelper.gq(R.plurals.objective_hours, hours, hours)) + } else if ((days < IMPORT_AGE_NOT_YET_OLD_DAYS) && (days > 0)) { + resourceHelper.gs(R.string.exported_ago, resourceHelper.gq(R.plurals.objective_days, days, days)) + } else { + resourceHelper.gs(R.string.exported_at, utcTime.substring(0, 10)) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/activities/PrefImportListActivity.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/activities/PrefImportListActivity.kt new file mode 100644 index 0000000000..af19414794 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/activities/PrefImportListActivity.kt @@ -0,0 +1,135 @@ +package info.nightscout.androidaps.plugins.general.maintenance.activities + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import dagger.android.support.DaggerAppCompatActivity +import info.nightscout.androidaps.R +import info.nightscout.androidaps.plugins.general.maintenance.PrefFileListProvider +import info.nightscout.androidaps.plugins.general.maintenance.PrefsFile +import info.nightscout.androidaps.plugins.general.maintenance.PrefsFileContract +import info.nightscout.androidaps.plugins.general.maintenance.formats.PrefsFormatsHandler +import info.nightscout.androidaps.plugins.general.maintenance.formats.PrefsMetadataKey +import info.nightscout.androidaps.plugins.general.maintenance.formats.PrefsStatus +import info.nightscout.androidaps.utils.LocaleHelper +import info.nightscout.androidaps.utils.resources.ResourceHelper +import kotlinx.android.synthetic.main.maintenance_importlist_activity.* +import javax.inject.Inject + +class PrefImportListActivity : DaggerAppCompatActivity() { + + @Inject lateinit var resourceHelper: ResourceHelper + @Inject lateinit var prefFileListProvider: PrefFileListProvider + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setTheme(R.style.AppTheme) + setContentView(R.layout.maintenance_importlist_activity) + + title = resourceHelper.gs(R.string.preferences_import_list_title) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setDisplayShowTitleEnabled(true) + + importlist_recyclerview.layoutManager = LinearLayoutManager(this) + importlist_recyclerview.adapter = RecyclerViewAdapter(prefFileListProvider.listPreferenceFiles(loadMetadata = true)) + } + + inner class RecyclerViewAdapter internal constructor(private var prefFileList: List) : RecyclerView.Adapter() { + + inner class PrefFileViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + var fileName: TextView = itemView.findViewById(R.id.filelist_name) + var fileDir: TextView = itemView.findViewById(R.id.filelist_dir) + var metaDateTime: TextView = itemView.findViewById(R.id.meta_date_time) + var metaDeviceName: TextView = itemView.findViewById(R.id.meta_device_name) + var metaAppVersion: TextView = itemView.findViewById(R.id.meta_app_version) + var metaVariantFormat: TextView = itemView.findViewById(R.id.meta_variant_format) + + var metalineName: View = itemView.findViewById(R.id.metaline_name) + var metaDateTimeIcon: View = itemView.findViewById(R.id.meta_date_time_icon) + + init { + itemView.isClickable = true + itemView.setOnClickListener { v: View -> + val prefFile = fileName.tag as PrefsFile + val i = Intent() + + i.putExtra(PrefsFileContract.OUTPUT_PARAM, prefFile) + setResult(Activity.RESULT_OK, i) + finish() + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PrefFileViewHolder { + val v = LayoutInflater.from(parent.context).inflate(R.layout.maintenance_importlist_item, parent, false) + return PrefFileViewHolder(v) + } + + override fun getItemCount(): Int { + return prefFileList.size + } + + override fun onBindViewHolder(holder: PrefFileViewHolder, position: Int) { + val prefFile = prefFileList[position] + holder.fileName.text = prefFile.file.name + holder.fileName.tag = prefFile + + holder.fileDir.text = resourceHelper.gs(R.string.in_directory, prefFile.file.parentFile.absolutePath) + + val visible = if (prefFile.handler == PrefsFormatsHandler.CLASSIC) View.GONE else View.VISIBLE + holder.metalineName.visibility = visible + holder.metaDateTimeIcon.visibility = visible + holder.metaAppVersion.visibility = visible + + if (prefFile.handler == PrefsFormatsHandler.CLASSIC) { + holder.metaVariantFormat.text = resourceHelper.gs(R.string.metadata_format_old) + holder.metaVariantFormat.setTextColor(resourceHelper.gc(R.color.metadataTextWarning)) + holder.metaDateTime.text = " " + } else { + + prefFile.metadata[PrefsMetadataKey.AAPS_FLAVOUR]?.let { + holder.metaVariantFormat.text = it.value + val color = if (it.status == PrefsStatus.OK) R.color.metadataOk else R.color.metadataTextWarning + holder.metaVariantFormat.setTextColor(resourceHelper.gc(color)) + } + + prefFile.metadata[PrefsMetadataKey.CREATED_AT]?.let { + holder.metaDateTime.text = prefFileListProvider.formatExportedAgo(it.value) + } + + prefFile.metadata[PrefsMetadataKey.AAPS_VERSION]?.let { + holder.metaAppVersion.text = it.value + val color = if (it.status == PrefsStatus.OK) R.color.metadataOk else R.color.metadataTextWarning + holder.metaAppVersion.setTextColor(resourceHelper.gc(color)) + } + + prefFile.metadata[PrefsMetadataKey.DEVICE_NAME]?.let { + holder.metaDeviceName.text = it.value + } + + } + + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + finish() + return true + } + return false + } + + public override fun attachBaseContext(newBase: Context) { + super.attachBaseContext(LocaleHelper.wrap(newBase)) + } +} \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt index 4689f549a6..40b1ceada4 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/ClassicPrefsFormat.kt @@ -1,5 +1,6 @@ package info.nightscout.androidaps.plugins.general.maintenance.formats +import info.nightscout.androidaps.Constants import info.nightscout.androidaps.R import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.storage.Storage @@ -19,6 +20,11 @@ class ClassicPrefsFormat @Inject constructor( val FORMAT_KEY = "aaps_old" } + override fun isPreferencesFile(file: File, preloadedContents: String?): Boolean { + val contents = preloadedContents ?: storage.getFileContents(file) + return contents.contains("units::" + Constants.MGDL) || contents.contains("units::" + Constants.MMOL) + } + override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) { try { val contents = prefs.values.entries.joinToString("\n") { entry -> @@ -35,7 +41,6 @@ class ClassicPrefsFormat @Inject constructor( override fun loadPreferences(file: File, masterPassword: String?): Prefs { var lineParts: Array val entries: MutableMap = mutableMapOf() - val metadata: MutableMap = mutableMapOf() try { val rawLines = storage.getFileContents(file).split("\n") @@ -46,9 +51,7 @@ class ClassicPrefsFormat @Inject constructor( } } - metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN, resourceHelper.gs(R.string.metadata_warning_outdated_format)) - - return Prefs(entries, metadata) + return Prefs(entries, loadMetadata()) } catch (e: FileNotFoundException) { throw PrefFileNotFoundError(file.absolutePath) @@ -57,4 +60,10 @@ class ClassicPrefsFormat @Inject constructor( } } + override fun loadMetadata(contents: String?): PrefMetadataMap { + val metadata: MutableMap = mutableMapOf() + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(FORMAT_KEY, PrefsStatus.WARN, resourceHelper.gs(R.string.metadata_warning_outdated_format)) + return metadata + } + } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt index 463b5a31cb..ec88c07bec 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/EncryptedPrefsFormat.kt @@ -27,6 +27,16 @@ class EncryptedPrefsFormat @Inject constructor( val FORMAT_KEY_NOENC = "aaps_structured" private val KEY_CONSCIENCE = "if you remove/change this, please make sure you know the consequences!" + private val FORMAT_TEST_REGEX = Regex("(\\\"format\\\"\\s*\\:\\s*\\\"aaps_[^\"]*\\\")") + } + + override fun isPreferencesFile(file: File, preloadedContents: String?): Boolean { + return if (file.absolutePath.endsWith(".json")) { + val contents = preloadedContents ?: storage.getFileContents(file) + FORMAT_TEST_REGEX.containsMatchIn(contents) + } else { + false + } } override fun savePreferences(file: File, prefs: Prefs, masterPassword: String?) { @@ -97,7 +107,6 @@ class EncryptedPrefsFormat @Inject constructor( override fun loadPreferences(file: File, masterPassword: String?): Prefs { val entries: MutableMap = mutableMapOf() - val metadata: MutableMap = mutableMapOf() val issues = LinkedList() try { @@ -105,25 +114,11 @@ class EncryptedPrefsFormat @Inject constructor( val fileContents = jsonBody.replace(Regex("(?is)(\\\"file_hash\\\"\\s*\\:\\s*\\\")([^\"]*)(\\\")"), "$1--to-be-calculated--$3") val calculatedFileHash = cryptoUtil.hmac256(fileContents, KEY_CONSCIENCE) val container = JSONObject(jsonBody) + val metadata: MutableMap = loadMetadata(container) - if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content") && container.has("metadata")) { + if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content")) { val fileFormat = container.getString(PrefsMetadataKey.FILE_FORMAT.key) - - if ((fileFormat != FORMAT_KEY_ENC) && (fileFormat != FORMAT_KEY_NOENC)) { - throw PrefFormatError("Unsupported file format: " + fileFormat) - } - - val meta = container.getJSONObject("metadata") val security = container.getJSONObject("security") - - metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(fileFormat, PrefsStatus.OK) - for (key in meta.keys()) { - val metaKey = PrefsMetadataKey.fromKey(key) - if (metaKey != null) { - metadata[metaKey] = PrefMetadata(meta.getString(key), PrefsStatus.OK) - } - } - val encrypted = fileFormat == FORMAT_KEY_ENC var secure: PrefsStatus = PrefsStatus.OK var decryptedOk = false @@ -208,8 +203,6 @@ class EncryptedPrefsFormat @Inject constructor( } metadata[PrefsMetadataKey.ENCRYPTION] = PrefMetadata(encryptionDescStr, secure, issuesStr) - } else { - metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(resourceHelper.gs(R.string.prefdecrypt_wrong_json), PrefsStatus.ERROR) } return Prefs(entries, metadata) @@ -223,4 +216,35 @@ class EncryptedPrefsFormat @Inject constructor( } } + override fun loadMetadata(contents: String?): PrefMetadataMap { + contents?.let { + val container = JSONObject(contents) + return loadMetadata(container) + } + return mutableMapOf() + } + + private fun loadMetadata(container: JSONObject): MutableMap { + val metadata: MutableMap = mutableMapOf() + if (container.has(PrefsMetadataKey.FILE_FORMAT.key) && container.has("security") && container.has("content") && container.has("metadata")) { + val fileFormat = container.getString(PrefsMetadataKey.FILE_FORMAT.key) + if ((fileFormat != FORMAT_KEY_ENC) && (fileFormat != FORMAT_KEY_NOENC)) { + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(resourceHelper.gs(R.string.metadata_format_other), PrefsStatus.ERROR) + } else { + val meta = container.getJSONObject("metadata") + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(fileFormat, PrefsStatus.OK) + for (key in meta.keys()) { + val metaKey = PrefsMetadataKey.fromKey(key) + if (metaKey != null) { + metadata[metaKey] = PrefMetadata(meta.getString(key), PrefsStatus.OK) + } + } + } + } else { + metadata[PrefsMetadataKey.FILE_FORMAT] = PrefMetadata(resourceHelper.gs(R.string.prefdecrypt_wrong_json), PrefsStatus.ERROR) + } + + return metadata; + } + } \ No newline at end of file diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt index a47add0dd7..d0d3935023 100644 --- a/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt +++ b/app/src/main/java/info/nightscout/androidaps/plugins/general/maintenance/formats/PrefsFormat.kt @@ -1,12 +1,14 @@ package info.nightscout.androidaps.plugins.general.maintenance.formats import android.content.Context +import android.os.Parcelable import androidx.annotation.DrawableRes import androidx.annotation.StringRes import info.nightscout.androidaps.R +import kotlinx.android.parcel.Parcelize import java.io.File -enum class PrefsMetadataKey(val key: String, @DrawableRes val icon:Int, @StringRes val label:Int) { +enum class PrefsMetadataKey(val key: String, @DrawableRes val icon: Int, @StringRes val label: Int) { FILE_FORMAT("format", R.drawable.ic_meta_format, R.string.metadata_label_format), CREATED_AT("created_at", R.drawable.ic_meta_date, R.string.metadata_label_created_at), @@ -33,16 +35,15 @@ enum class PrefsMetadataKey(val key: String, @DrawableRes val icon:Int, @StringR } } - } - fun formatForDisplay(context: Context, value:String): String { + fun formatForDisplay(context: Context, value: String): String { return when (this) { FILE_FORMAT -> when (value) { - ClassicPrefsFormat.FORMAT_KEY -> context.getString(R.string.metadata_format_old) - EncryptedPrefsFormat.FORMAT_KEY_ENC -> context.getString(R.string.metadata_format_new) + ClassicPrefsFormat.FORMAT_KEY -> context.getString(R.string.metadata_format_old) + EncryptedPrefsFormat.FORMAT_KEY_ENC -> context.getString(R.string.metadata_format_new) EncryptedPrefsFormat.FORMAT_KEY_NOENC -> context.getString(R.string.metadata_format_debug) - else -> context.getString(R.string.metadata_format_other) + else -> context.getString(R.string.metadata_format_other) } CREATED_AT -> value.replace("T", " ").replace("Z", " (UTC)") else -> value @@ -51,16 +52,21 @@ enum class PrefsMetadataKey(val key: String, @DrawableRes val icon:Int, @StringR } -data class PrefMetadata(var value : String, var status : PrefsStatus, var info : String? = null) +@Parcelize +data class PrefMetadata(var value: String, var status: PrefsStatus, var info: String? = null) : Parcelable -data class Prefs(val values : Map, var metadata : Map) +typealias PrefMetadataMap = Map + +data class Prefs(val values: Map, var metadata: PrefMetadataMap) interface PrefsFormat { fun savePreferences(file: File, prefs: Prefs, masterPassword: String? = null) - fun loadPreferences(file: File, masterPassword: String? = null) : Prefs + fun loadPreferences(file: File, masterPassword: String? = null): Prefs + fun loadMetadata(contents: String? = null): PrefMetadataMap + fun isPreferencesFile(file: File, preloadedContents: String? = null): Boolean } -enum class PrefsStatus(@DrawableRes val icon:Int) { +enum class PrefsStatus(@DrawableRes val icon: Int) { OK(R.drawable.ic_meta_ok), WARN(R.drawable.ic_meta_warning), ERROR(R.drawable.ic_meta_error), @@ -68,6 +74,11 @@ enum class PrefsStatus(@DrawableRes val icon:Int) { DISABLED(R.drawable.ic_meta_error) } +enum class PrefsFormatsHandler { + CLASSIC, + ENCRYPTED +} + class PrefFileNotFoundError(message: String) : Exception(message) class PrefIOError(message: String) : Exception(message) class PrefFormatError(message: String) : Exception(message) diff --git a/app/src/main/res/layout/maintenance_importlist_activity.xml b/app/src/main/res/layout/maintenance_importlist_activity.xml new file mode 100644 index 0000000000..0bdd43b927 --- /dev/null +++ b/app/src/main/res/layout/maintenance_importlist_activity.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/maintenance_importlist_item.xml b/app/src/main/res/layout/maintenance_importlist_item.xml new file mode 100644 index 0000000000..8c2cfc0557 --- /dev/null +++ b/app/src/main/res/layout/maintenance_importlist_item.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index c1873d786f..de49a0aee5 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -90,7 +90,11 @@ #FF8C00 #03A9F4 + #77dd77 #FF8C00 #FF5555 + #FFFFFF + #BBBBBB + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1bae0aafa4..c07ae20309 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -256,6 +256,8 @@ Export canceled! Preferences were NOT exported! Import canceled! Preferences were NOT imported! + Select file to import + Please check preferences before importing: Preferences cannot be imported! Preferences should not be imported! @@ -269,6 +271,8 @@ You are using the outdated legacy format from old versions of AAPS, which is not secure! Only use it as a last resort, if you do not have an export in current, JSON format. Imported preferences are already %1$s days old! Maybe you have more up-to-date preferences or you choose the wrong file? Remember to export preferences regularly! Invalid date-time format! + Preferences from different minor version of application. It is OK if you are importing after upgrade, but check after import if preferences are still correct! + Preferences from different major version of application. Major versions differ significantly and may have incompatible preferences! Make sure after import that preferences are still correct! File format Created at @@ -296,6 +300,12 @@ Missing encryption configuration, settings format is invalid! Unsupported or not specified encryption algorithm! + exported today + exported %1$s ago + exported at %1$s + exported less than hour ago + in directory: %1$s + DanaR Connecting Connected @@ -1548,10 +1558,6 @@ STOP Selected RileyLink Scan - Bluetooth Low Energy not supported. - Bluetooth not enabled. - Location Is Not Enabled - For Bluetooth discovery to work on newer devices, location must be enabled. AAPS does not track your location and it can be disabled after pairing is successful. Enable No Scanning diff --git a/app/src/test/java/info/nightscout/androidaps/plugins/constraints/versionChecker/VersionCheckerUtilsKtTest.kt b/app/src/test/java/info/nightscout/androidaps/plugins/constraints/versionChecker/VersionCheckerUtilsKtTest.kt index 17a9c171d0..db693198bb 100644 --- a/app/src/test/java/info/nightscout/androidaps/plugins/constraints/versionChecker/VersionCheckerUtilsKtTest.kt +++ b/app/src/test/java/info/nightscout/androidaps/plugins/constraints/versionChecker/VersionCheckerUtilsKtTest.kt @@ -6,6 +6,7 @@ import info.nightscout.androidaps.logging.AAPSLogger import info.nightscout.androidaps.plugins.bus.RxBusWrapper import info.nightscout.androidaps.utils.resources.ResourceHelper import info.nightscout.androidaps.utils.sharedPreferences.SP +import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test @@ -25,6 +26,41 @@ class VersionCheckerUtilsKtTest : TestBase() { versionCheckerUtils = VersionCheckerUtils(aapsLogger, sp, resourceHelper, rxBus, context) } + @Test + fun `should handle invalid version`() { + assertArrayEquals(intArrayOf(), versionCheckerUtils.versionDigits("definitely not version string")) + } + + @Test + fun `should handle empty version`() { + assertArrayEquals(intArrayOf(), versionCheckerUtils.versionDigits("")) + } + + @Test + fun `should parse 2 digit version`() { + assertArrayEquals(intArrayOf(0, 999), versionCheckerUtils.versionDigits("0.999-beta")) + } + + @Test + fun `should parse 3 digit version`() { + assertArrayEquals(intArrayOf(6, 83, 93), versionCheckerUtils.versionDigits("6.83.93")) + } + + @Test + fun `should parse 4 digit version`() { + assertArrayEquals(intArrayOf(42, 7, 13, 101), versionCheckerUtils.versionDigits("42.7.13.101")) + } + + @Test + fun `should parse 4 digit version with extra`() { + assertArrayEquals(intArrayOf(1, 2, 3, 4), versionCheckerUtils.versionDigits("1.2.3.4-RC5")) + } + + @Test + fun `should parse version but only 4 digits are taken`() { + assertArrayEquals(intArrayOf(67, 8, 31, 5), versionCheckerUtils.versionDigits("67.8.31.5.153.4.2")) + } + /* @Test fun `should keep 2 digit version`() { diff --git a/core/build.gradle b/core/build.gradle index 0a6122fa64..55829c53d1 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -42,7 +42,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0' + implementation "androidx.preference:preference-ktx:1.1.1" implementation "com.google.dagger:dagger-android:$dagger_version" implementation "com.google.dagger:dagger-android-support:$dagger_version" diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml index 46d6d492d4..0ff2fcd7b8 100644 --- a/core/src/main/AndroidManifest.xml +++ b/core/src/main/AndroidManifest.xml @@ -1,2 +1,5 @@ + package="info.nightscout.androidaps.core" > + + + diff --git a/core/src/main/java/info/nightscout/androidaps/plugins/pump/common/ble/BlePreCheck.kt b/core/src/main/java/info/nightscout/androidaps/plugins/pump/common/ble/BlePreCheck.kt new file mode 100644 index 0000000000..497ec61fbc --- /dev/null +++ b/core/src/main/java/info/nightscout/androidaps/plugins/pump/common/ble/BlePreCheck.kt @@ -0,0 +1,87 @@ +package info.nightscout.androidaps.plugins.pump.common.ble + +import android.Manifest +import android.bluetooth.BluetoothAdapter +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.location.LocationManager +import android.provider.Settings +import androidx.appcompat.app.AppCompatActivity +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import info.nightscout.androidaps.core.R +import info.nightscout.androidaps.utils.alertDialogs.OKDialog +import info.nightscout.androidaps.utils.resources.ResourceHelper +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class BlePreCheck @Inject constructor( + val resourceHelper: ResourceHelper +) { + + companion object { + private const val PERMISSION_REQUEST_COARSE_LOCATION = 30241 // arbitrary. + } + + fun prerequisitesCheck(activity: AppCompatActivity): Boolean { + if (!activity.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { + OKDialog.show(activity, resourceHelper.gs(R.string.message), resourceHelper.gs(R.string.rileylink_scanner_ble_not_supported)) + return false + } else { + // Use this check to determine whether BLE is supported on the device. Then + // you can selectively disable BLE-related features. + if (ContextCompat.checkSelfPermission(activity, Manifest.permission.ACCESS_COARSE_LOCATION) != PackageManager.PERMISSION_GRANTED) { + // your code that requires permission + ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.ACCESS_COARSE_LOCATION), PERMISSION_REQUEST_COARSE_LOCATION) + } + + val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + // Ensures Bluetooth is available on the device and it is enabled. If not, + // displays a dialog requesting user permission to enable Bluetooth. + if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled) { + OKDialog.show(activity, resourceHelper.gs(R.string.message), resourceHelper.gs(R.string.rileylink_scanner_ble_not_enabled)) + return false + } else { + // Will request that GPS be enabled for devices running Marshmallow or newer. + if (!isLocationEnabled(activity)) { + requestLocation(activity) + return false + } + } + } + return true + } + + /** + * Determine if GPS is currently enabled. + * + * + * On Android 6 (Marshmallow), location needs to be enabled for Bluetooth discovery to work. + * + * @param context The current app context. + * @return true if location is enabled, false otherwise. + */ + private fun isLocationEnabled(context: Context): Boolean { + val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager + return locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER) || + locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER) + } + + /** + * Prompt the user to enable GPS location if it isn't already on. + * + * @param activity The currently visible activity. + */ + private fun requestLocation(activity: AppCompatActivity) { + if (isLocationEnabled(activity)) { + return + } + + // Shamelessly borrowed from http://stackoverflow.com/a/10311877/868533 + OKDialog.showConfirmation(activity, resourceHelper.gs(R.string.location_not_found_title), resourceHelper.gs(R.string.location_not_found_message), Runnable { + activity.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) + }) + } +} \ No newline at end of file diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index b89cf096bd..a0d615ea71 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -7,4 +7,9 @@ Cancel DISMISS + Bluetooth Low Energy not supported. + Bluetooth not enabled. + Location Is Not Enabled + For Bluetooth discovery to work on newer devices, location must be enabled. AAPS does not track your location and it can be disabled after pairing is successful. + \ No newline at end of file