/* * 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.View.GONE import android.view.View.VISIBLE 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.FeatureUtils 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 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 @Inject lateinit var featureUtils: FeatureUtils @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 public 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(R.layout.route_request_dialog, 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