/* * Copyright (C) 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.healthconnect.controller.route import android.app.Activity import android.content.Intent import android.health.connect.HealthConnectManager.EXTRA_EXERCISE_ROUTE import android.health.connect.HealthConnectManager.EXTRA_SESSION_ID import android.health.connect.datatypes.ExerciseRoute import android.os.Bundle import android.util.Log import android.view.WindowManager import android.widget.Button import android.widget.LinearLayout import android.widget.TextView import androidx.activity.viewModels import androidx.annotation.VisibleForTesting import androidx.appcompat.app.AlertDialog import androidx.fragment.app.FragmentActivity import com.android.healthconnect.controller.R import com.android.healthconnect.controller.dataentries.formatters.ExerciseSessionFormatter import com.android.healthconnect.controller.migration.MigrationActivity.Companion.maybeShowWhatsNewDialog import com.android.healthconnect.controller.migration.MigrationActivity.Companion.showDataRestoreInProgressDialog import com.android.healthconnect.controller.migration.MigrationActivity.Companion.showMigrationInProgressDialog import com.android.healthconnect.controller.migration.MigrationActivity.Companion.showMigrationPendingDialog import com.android.healthconnect.controller.migration.MigrationViewModel import com.android.healthconnect.controller.migration.api.MigrationRestoreState import com.android.healthconnect.controller.migration.api.MigrationRestoreState.DataRestoreUiState import com.android.healthconnect.controller.migration.api.MigrationRestoreState.MigrationUiState import com.android.healthconnect.controller.route.ExerciseRouteViewModel.SessionWithAttribution import com.android.healthconnect.controller.shared.app.AppInfoReader import com.android.healthconnect.controller.shared.dialog.AlertDialogBuilder import com.android.healthconnect.controller.shared.map.MapView import com.android.healthconnect.controller.utils.LocalDateTimeFormatter import com.android.healthconnect.controller.utils.boldAppName import com.android.healthconnect.controller.utils.logging.HealthConnectLogger import com.android.healthconnect.controller.utils.logging.RouteRequestElement import com.android.settingslib.widget.SettingsThemeHelper import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.runBlocking /** Request route activity for Health Connect. */ @AndroidEntryPoint(FragmentActivity::class) class RouteRequestActivity : Hilt_RouteRequestActivity() { companion object { private const val TAG = "RouteRequestActivity" } @Inject lateinit var appInfoReader: AppInfoReader @VisibleForTesting var dialog: AlertDialog? = null @VisibleForTesting lateinit var infoDialog: AlertDialog @Inject lateinit var healthConnectLogger: HealthConnectLogger private val viewModel: ExerciseRouteViewModel by viewModels() private val migrationViewModel: MigrationViewModel by viewModels() private val sessionIdExtra: String? get() = intent.getStringExtra(EXTRA_SESSION_ID) private var requester: String? = null private var migrationRestoreState = MigrationUiState.UNKNOWN private var sessionWithAttribution: SessionWithAttribution? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This flag ensures a non system app cannot show an overlay on Health Connect. b/313425281 window.addSystemFlags( WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS ) if (sessionIdExtra == null || callingPackage == null) { Log.e(TAG, "Invalid Intent Extras, finishing.") finishCancelled() return } val callingPackageName = callingPackage!! if (!viewModel.isReadRoutesPermissionDeclared(callingPackageName)) { Log.e(TAG, "Read permission not declared") finishCancelled() return } viewModel.getExerciseWithRoute(sessionIdExtra!!) runBlocking { requester = appInfoReader.getAppMetadata(callingPackageName).appName } viewModel.exerciseSession.observe(this) { session -> this.sessionWithAttribution = session setupRequestDialog(session, callingPackageName) } migrationViewModel.migrationState.observe(this) { migrationState -> when (migrationState) { is MigrationViewModel.MigrationFragmentState.WithData -> { maybeShowMigrationDialog(migrationState.migrationRestoreState) this.migrationRestoreState = migrationState.migrationRestoreState.migrationUiState } else -> { // do nothing } } } } private fun setupRequestDialog(data: SessionWithAttribution?, callingPackage: String) { if ( data == null || data.session.route == null || data.session.route?.routeLocations.isNullOrEmpty() ) { Log.e(TAG, "No route or empty route, finishing.") finishCancelled() return } val session = data.session val route = session.route!! if ( session.metadata.dataOrigin.packageName == callingPackage && viewModel.isRouteReadOrWritePermissionGranted(callingPackage) ) { finishWithResult(route) return } if (viewModel.isSessionInaccessible(callingPackage, session)) { Log.i(TAG, "Requested exercise session is inaccessible.") finishCancelled() return } if (viewModel.isReadRoutesPermissionGranted(callingPackage)) { finishWithResult(route) return } if (viewModel.isReadRoutesPermissionUserFixed(callingPackage)) { finishCancelled() return } val sessionDetails = applicationContext.getString( R.string.date_owner_format, LocalDateTimeFormatter(applicationContext).formatLongDate(session.startTime), data.appInfo.appName, ) val sessionTitle = if (session.title.isNullOrBlank()) ExerciseSessionFormatter.Companion.getExerciseType( applicationContext, session.exerciseType, ) else session.title val view = layoutInflater.inflate( if (SettingsThemeHelper.isExpressiveTheme(applicationContext)) R.layout.route_request_dialog_expressive else R.layout.route_request_dialog_legacy, null, ) val text = applicationContext.getString(R.string.request_route_header_title, requester) val title = boldAppName(requester, text) view.findViewById(R.id.map_view).setRoute(session.route!!) view.findViewById(R.id.session_title).text = sessionTitle view.findViewById(R.id.date_app).text = sessionDetails view.findViewById(R.id.more_info).setOnClickListener { healthConnectLogger.logInteraction( RouteRequestElement.EXERCISE_ROUTE_DIALOG_INFORMATION_BUTTON ) dialog?.hide() setupInfoDialog() infoDialog.show() } view.findViewById