1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.healthconnect.controller.migration 17 18 import android.app.Activity 19 import android.content.Context 20 import android.content.DialogInterface 21 import android.content.Intent 22 import android.os.Bundle 23 import android.util.Log 24 import android.view.WindowManager 25 import androidx.fragment.app.FragmentActivity 26 import androidx.navigation.findNavController 27 import com.android.healthconnect.controller.R 28 import com.android.healthconnect.controller.migration.api.MigrationRestoreState 29 import com.android.healthconnect.controller.migration.api.MigrationRestoreState.DataRestoreUiState 30 import com.android.healthconnect.controller.migration.api.MigrationRestoreState.MigrationUiState 31 import com.android.healthconnect.controller.shared.Constants.APP_UPDATE_NEEDED_SEEN 32 import com.android.healthconnect.controller.shared.Constants.INTEGRATION_PAUSED_SEEN_KEY 33 import com.android.healthconnect.controller.shared.Constants.MODULE_UPDATE_NEEDED_SEEN 34 import com.android.healthconnect.controller.shared.Constants.USER_ACTIVITY_TRACKER 35 import com.android.healthconnect.controller.shared.Constants.WHATS_NEW_DIALOG_SEEN 36 import com.android.healthconnect.controller.shared.dialog.AlertDialogBuilder 37 import com.android.healthconnect.controller.utils.DeviceInfoUtils 38 import com.android.healthconnect.controller.utils.logging.DataRestoreElement 39 import com.android.healthconnect.controller.utils.logging.MigrationElement 40 import dagger.hilt.android.AndroidEntryPoint 41 import javax.inject.Inject 42 43 /** Activity in charge of coordinating migration navigation, fragments and dialogs. */ 44 @AndroidEntryPoint(FragmentActivity::class) 45 class MigrationActivity : Hilt_MigrationActivity() { 46 47 companion object { 48 private const val TAG = "MigrationActivity" 49 const val MIGRATION_ACTIVITY_INTENT = "android.health.connect.action.MIGRATION" 50 51 fun maybeRedirectToMigrationActivity( 52 activity: Activity, 53 migrationRestoreState: MigrationRestoreState 54 ): Boolean { 55 56 val sharedPreference = 57 activity.getSharedPreferences(USER_ACTIVITY_TRACKER, Context.MODE_PRIVATE) 58 59 if (migrationRestoreState.migrationUiState == 60 MigrationUiState.MODULE_UPGRADE_REQUIRED) { 61 val moduleUpdateSeen = sharedPreference.getBoolean(MODULE_UPDATE_NEEDED_SEEN, false) 62 63 if (!moduleUpdateSeen) { 64 activity.startActivity(createMigrationActivityIntent(activity)) 65 activity.finish() 66 return true 67 } 68 } else if (migrationRestoreState.migrationUiState == 69 MigrationUiState.APP_UPGRADE_REQUIRED) { 70 val appUpdateSeen = sharedPreference.getBoolean(APP_UPDATE_NEEDED_SEEN, false) 71 72 if (!appUpdateSeen) { 73 activity.startActivity(createMigrationActivityIntent(activity)) 74 activity.finish() 75 return true 76 } 77 } else if (migrationRestoreState.migrationUiState == MigrationUiState.ALLOWED_PAUSED || 78 migrationRestoreState.migrationUiState == MigrationUiState.ALLOWED_NOT_STARTED) { 79 val allowedPausedSeen = 80 sharedPreference.getBoolean(INTEGRATION_PAUSED_SEEN_KEY, false) 81 82 if (!allowedPausedSeen) { 83 activity.startActivity(createMigrationActivityIntent(activity)) 84 activity.finish() 85 return true 86 } 87 } else if (migrationRestoreState.migrationUiState == MigrationUiState.IN_PROGRESS || 88 migrationRestoreState.dataRestoreState == DataRestoreUiState.IN_PROGRESS) { 89 activity.startActivity(createMigrationActivityIntent(activity)) 90 activity.finish() 91 return true 92 } 93 94 return false 95 } 96 97 fun maybeShowMigrationDialog( 98 migrationRestoreState: MigrationRestoreState, 99 activity: FragmentActivity, 100 appName: String 101 ) { 102 val (migrationUiState, dataRestoreUiState, dataErrorState) = migrationRestoreState 103 104 when { 105 dataRestoreUiState == DataRestoreUiState.IN_PROGRESS -> { 106 showDataRestoreInProgressDialog(activity) { _, _ -> activity.finish() } 107 } 108 migrationUiState == MigrationUiState.IN_PROGRESS -> { 109 val message = 110 activity.getString( 111 R.string.migration_in_progress_permissions_dialog_content, appName) 112 showMigrationInProgressDialog(activity, message) { _, _ -> activity.finish() } 113 } 114 migrationUiState in 115 listOf( 116 MigrationUiState.ALLOWED_PAUSED, 117 MigrationUiState.ALLOWED_NOT_STARTED, 118 MigrationUiState.APP_UPGRADE_REQUIRED, 119 MigrationUiState.MODULE_UPGRADE_REQUIRED) -> { 120 val message = 121 activity.getString( 122 R.string.migration_pending_permissions_dialog_content, appName) 123 showMigrationPendingDialog( 124 activity, 125 message, 126 positiveButtonAction = null, 127 negativeButtonAction = { _, _ -> 128 activity.startActivity(Intent(MIGRATION_ACTIVITY_INTENT)) 129 activity.finish() 130 }) 131 } 132 migrationUiState == MigrationUiState.COMPLETE -> { 133 maybeShowWhatsNewDialog(activity) 134 } 135 else -> { 136 // show nothing 137 } 138 } 139 } 140 141 fun showMigrationPendingDialog( 142 context: Context, 143 message: String, 144 positiveButtonAction: DialogInterface.OnClickListener? = null, 145 negativeButtonAction: DialogInterface.OnClickListener? = null 146 ) { 147 AlertDialogBuilder(context, MigrationElement.MIGRATION_PENDING_DIALOG_CONTAINER) 148 .setTitle(R.string.migration_pending_permissions_dialog_title) 149 .setMessage(message) 150 .setCancelable(false) 151 .setNeutralButton( 152 R.string.migration_pending_permissions_dialog_button_start_integration, 153 MigrationElement.MIGRATION_PENDING_DIALOG_CANCEL_BUTTON, 154 negativeButtonAction) 155 .setPositiveButton( 156 R.string.migration_pending_permissions_dialog_button_continue, 157 MigrationElement.MIGRATION_PENDING_DIALOG_CONTINUE_BUTTON, 158 positiveButtonAction) 159 .create() 160 .show() 161 } 162 163 fun showMigrationInProgressDialog( 164 context: Context, 165 message: String, 166 negativeButtonAction: DialogInterface.OnClickListener? = null 167 ) { 168 AlertDialogBuilder(context, MigrationElement.MIGRATION_IN_PROGRESS_DIALOG_CONTAINER) 169 .setTitle(R.string.migration_in_progress_permissions_dialog_title) 170 .setMessage(message) 171 .setCancelable(false) 172 .setNegativeButton( 173 R.string.migration_in_progress_permissions_dialog_button_got_it, 174 MigrationElement.MIGRATION_IN_PROGRESS_DIALOG_BUTTON, 175 negativeButtonAction) 176 .create() 177 .show() 178 } 179 180 fun showDataRestoreInProgressDialog( 181 context: Context, 182 negativeButtonAction: DialogInterface.OnClickListener? = null 183 ) { 184 AlertDialogBuilder(context, DataRestoreElement.RESTORE_IN_PROGRESS_DIALOG_CONTAINER) 185 .setTitle(R.string.data_restore_in_progress_dialog_title) 186 .setMessage(R.string.data_restore_in_progress_content) 187 .setCancelable(false) 188 .setNegativeButton( 189 R.string.data_restore_in_progress_dialog_button, 190 DataRestoreElement.RESTORE_IN_PROGRESS_DIALOG_BUTTON, 191 negativeButtonAction) 192 .create() 193 .show() 194 } 195 196 fun maybeShowWhatsNewDialog( 197 context: Context, 198 negativeButtonAction: DialogInterface.OnClickListener? = null 199 ) { 200 val sharedPreference = 201 context.getSharedPreferences(USER_ACTIVITY_TRACKER, Context.MODE_PRIVATE) 202 val dialogSeen = sharedPreference.getBoolean(WHATS_NEW_DIALOG_SEEN, false) 203 204 if (!dialogSeen) { 205 AlertDialogBuilder(context, MigrationElement.MIGRATION_DONE_DIALOG_CONTAINER) 206 .setTitle(R.string.migration_whats_new_dialog_title) 207 .setMessage(R.string.migration_whats_new_dialog_content) 208 .setCancelable(false) 209 .setNegativeButton( 210 R.string.migration_whats_new_dialog_button, 211 MigrationElement.MIGRATION_DONE_DIALOG_BUTTON) { 212 unusedDialogInterface, 213 unusedInt -> 214 sharedPreference.edit().apply { 215 putBoolean(WHATS_NEW_DIALOG_SEEN, true) 216 apply() 217 } 218 negativeButtonAction?.onClick(unusedDialogInterface, unusedInt) 219 } 220 .create() 221 .show() 222 } 223 } 224 225 private fun createMigrationActivityIntent(context: Context): Intent { 226 return Intent(context, MigrationActivity::class.java).apply { 227 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 228 addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) 229 } 230 } 231 } 232 233 @Inject lateinit var deviceInfoUtils: DeviceInfoUtils 234 235 override fun onCreate(savedInstanceState: Bundle?) { 236 super.onCreate(savedInstanceState) 237 // This flag ensures a non system app cannot show an overlay on Health Connect. b/313425281 238 window.addSystemFlags( 239 WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS) 240 241 // Handles unsupported devices and user profiles. 242 if (!deviceInfoUtils.isHealthConnectAvailable(this)) { 243 Log.e(TAG, "Health connect is not available for this user or hardware, finishing!") 244 finish() 245 return 246 } 247 248 setContentView(R.layout.activity_migration) 249 } 250 251 override fun onBackPressed() { 252 val navController = findNavController(R.id.nav_host_fragment) 253 if (!navController.popBackStack()) { 254 finish() 255 } 256 } 257 258 override fun onNavigateUp(): Boolean { 259 val navController = findNavController(R.id.nav_host_fragment) 260 if (!navController.popBackStack()) { 261 finish() 262 } 263 return true 264 } 265 } 266