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 com.android.settingslib.collapsingtoolbar.EdgeToEdgeUtils 41 import dagger.hilt.android.AndroidEntryPoint 42 import javax.inject.Inject 43 44 /** Activity in charge of coordinating migration navigation, fragments and dialogs. */ 45 @AndroidEntryPoint(FragmentActivity::class) 46 class MigrationActivity : Hilt_MigrationActivity() { 47 48 companion object { 49 private const val TAG = "MigrationActivity" 50 const val MIGRATION_ACTIVITY_INTENT = "android.health.connect.action.MIGRATION" 51 52 fun maybeRedirectToMigrationActivity( 53 activity: Activity, 54 migrationRestoreState: MigrationRestoreState, 55 ): Boolean { 56 57 val sharedPreference = 58 activity.getSharedPreferences(USER_ACTIVITY_TRACKER, Context.MODE_PRIVATE) 59 60 if ( 61 migrationRestoreState.migrationUiState == MigrationUiState.MODULE_UPGRADE_REQUIRED 62 ) { 63 val moduleUpdateSeen = sharedPreference.getBoolean(MODULE_UPDATE_NEEDED_SEEN, false) 64 65 if (!moduleUpdateSeen) { 66 activity.startActivity(createMigrationActivityIntent(activity)) 67 activity.finish() 68 return true 69 } 70 } else if ( 71 migrationRestoreState.migrationUiState == MigrationUiState.APP_UPGRADE_REQUIRED 72 ) { 73 val appUpdateSeen = sharedPreference.getBoolean(APP_UPDATE_NEEDED_SEEN, false) 74 75 if (!appUpdateSeen) { 76 activity.startActivity(createMigrationActivityIntent(activity)) 77 activity.finish() 78 return true 79 } 80 } else if ( 81 migrationRestoreState.migrationUiState == MigrationUiState.ALLOWED_PAUSED || 82 migrationRestoreState.migrationUiState == MigrationUiState.ALLOWED_NOT_STARTED 83 ) { 84 val allowedPausedSeen = 85 sharedPreference.getBoolean(INTEGRATION_PAUSED_SEEN_KEY, false) 86 87 if (!allowedPausedSeen) { 88 activity.startActivity(createMigrationActivityIntent(activity)) 89 activity.finish() 90 return true 91 } 92 } else if ( 93 migrationRestoreState.migrationUiState == MigrationUiState.IN_PROGRESS || 94 migrationRestoreState.dataRestoreState == DataRestoreUiState.IN_PROGRESS 95 ) { 96 activity.startActivity(createMigrationActivityIntent(activity)) 97 activity.finish() 98 return true 99 } 100 101 return false 102 } 103 104 fun maybeShowMigrationDialog( 105 migrationRestoreState: MigrationRestoreState, 106 activity: FragmentActivity, 107 appName: String, 108 ) { 109 val (migrationUiState, dataRestoreUiState, dataErrorState) = migrationRestoreState 110 111 when { 112 dataRestoreUiState == DataRestoreUiState.IN_PROGRESS -> { 113 showDataRestoreInProgressDialog(activity) { _, _ -> activity.finish() } 114 } 115 migrationUiState == MigrationUiState.IN_PROGRESS -> { 116 val message = 117 activity.getString( 118 R.string.migration_in_progress_permissions_dialog_content, 119 appName, 120 ) 121 showMigrationInProgressDialog(activity, message) { _, _ -> activity.finish() } 122 } 123 migrationUiState in 124 listOf( 125 MigrationUiState.ALLOWED_PAUSED, 126 MigrationUiState.ALLOWED_NOT_STARTED, 127 MigrationUiState.APP_UPGRADE_REQUIRED, 128 MigrationUiState.MODULE_UPGRADE_REQUIRED, 129 ) -> { 130 val message = 131 activity.getString( 132 R.string.migration_pending_permissions_dialog_content, 133 appName, 134 ) 135 showMigrationPendingDialog( 136 activity, 137 message, 138 positiveButtonAction = null, 139 negativeButtonAction = { _, _ -> 140 activity.startActivity(Intent(MIGRATION_ACTIVITY_INTENT)) 141 activity.finish() 142 }, 143 ) 144 } 145 migrationUiState == MigrationUiState.COMPLETE -> { 146 maybeShowWhatsNewDialog(activity) 147 } 148 else -> { 149 // show nothing 150 } 151 } 152 } 153 154 fun showMigrationPendingDialog( 155 context: Context, 156 message: String, 157 positiveButtonAction: DialogInterface.OnClickListener? = null, 158 negativeButtonAction: DialogInterface.OnClickListener? = null, 159 ) { 160 AlertDialogBuilder(context, MigrationElement.MIGRATION_PENDING_DIALOG_CONTAINER) 161 .setTitle(R.string.migration_pending_permissions_dialog_title) 162 .setMessage(message) 163 .setCancelable(false) 164 .setNeutralButton( 165 R.string.migration_pending_permissions_dialog_button_start_integration, 166 MigrationElement.MIGRATION_PENDING_DIALOG_CANCEL_BUTTON, 167 negativeButtonAction, 168 ) 169 .setPositiveButton( 170 R.string.migration_pending_permissions_dialog_button_continue, 171 MigrationElement.MIGRATION_PENDING_DIALOG_CONTINUE_BUTTON, 172 positiveButtonAction, 173 ) 174 .create() 175 .show() 176 } 177 178 fun showMigrationInProgressDialog( 179 context: Context, 180 message: String, 181 negativeButtonAction: DialogInterface.OnClickListener? = null, 182 ) { 183 AlertDialogBuilder(context, MigrationElement.MIGRATION_IN_PROGRESS_DIALOG_CONTAINER) 184 .setTitle(R.string.migration_in_progress_permissions_dialog_title) 185 .setMessage(message) 186 .setCancelable(false) 187 .setNegativeButton( 188 R.string.migration_in_progress_permissions_dialog_button_got_it, 189 MigrationElement.MIGRATION_IN_PROGRESS_DIALOG_BUTTON, 190 negativeButtonAction, 191 ) 192 .create() 193 .show() 194 } 195 196 fun showDataRestoreInProgressDialog( 197 context: Context, 198 negativeButtonAction: DialogInterface.OnClickListener? = null, 199 ) { 200 AlertDialogBuilder(context, DataRestoreElement.RESTORE_IN_PROGRESS_DIALOG_CONTAINER) 201 .setTitle(R.string.data_restore_in_progress_dialog_title) 202 .setMessage(R.string.data_restore_in_progress_content) 203 .setCancelable(false) 204 .setNegativeButton( 205 R.string.data_restore_in_progress_dialog_button, 206 DataRestoreElement.RESTORE_IN_PROGRESS_DIALOG_BUTTON, 207 negativeButtonAction, 208 ) 209 .create() 210 .show() 211 } 212 213 fun maybeShowWhatsNewDialog( 214 context: Context, 215 negativeButtonAction: DialogInterface.OnClickListener? = null, 216 ) { 217 val sharedPreference = 218 context.getSharedPreferences(USER_ACTIVITY_TRACKER, Context.MODE_PRIVATE) 219 val dialogSeen = sharedPreference.getBoolean(WHATS_NEW_DIALOG_SEEN, false) 220 221 if (!dialogSeen) { 222 AlertDialogBuilder(context, MigrationElement.MIGRATION_DONE_DIALOG_CONTAINER) 223 .setTitle(R.string.migration_whats_new_dialog_title) 224 .setMessage(R.string.migration_whats_new_dialog_content) 225 .setCancelable(false) 226 .setNegativeButton( 227 R.string.migration_whats_new_dialog_button, 228 MigrationElement.MIGRATION_DONE_DIALOG_BUTTON, 229 ) { unusedDialogInterface, unusedInt -> 230 sharedPreference.edit().apply { 231 putBoolean(WHATS_NEW_DIALOG_SEEN, true) 232 apply() 233 } 234 negativeButtonAction?.onClick(unusedDialogInterface, unusedInt) 235 } 236 .create() 237 .show() 238 } 239 } 240 241 private fun createMigrationActivityIntent(context: Context): Intent { 242 return Intent(context, MigrationActivity::class.java).apply { 243 addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 244 addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) 245 } 246 } 247 } 248 249 @Inject lateinit var deviceInfoUtils: DeviceInfoUtils 250 251 override fun onCreate(savedInstanceState: Bundle?) { 252 super.onCreate(savedInstanceState) 253 // This flag ensures a non system app cannot show an overlay on Health Connect. b/313425281 254 window.addSystemFlags( 255 WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS 256 ) 257 258 // Handles unsupported devices and user profiles. 259 if (!deviceInfoUtils.isHealthConnectAvailable(this)) { 260 Log.e(TAG, "Health connect is not available for this user or hardware, finishing!") 261 finish() 262 return 263 } 264 265 setContentView(R.layout.activity_migration) 266 EdgeToEdgeUtils.enable(this) 267 } 268 269 override fun onBackPressed() { 270 val navController = findNavController(R.id.nav_host_fragment) 271 if (!navController.popBackStack()) { 272 finish() 273 } 274 } 275 276 override fun onNavigateUp(): Boolean { 277 val navController = findNavController(R.id.nav_host_fragment) 278 if (!navController.popBackStack()) { 279 finish() 280 } 281 return true 282 } 283 } 284