blob: c428c6b523c0c1f58ab81a2a0a88d5b0fde608a4 [file] [log] [blame]
/**
* Copyright (C) 2022 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.permissions.connectedapps
import android.content.Intent
import android.content.Intent.EXTRA_PACKAGE_NAME
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.core.os.bundleOf
import androidx.fragment.app.commitNow
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import androidx.preference.Preference
import androidx.preference.PreferenceGroup
import com.android.healthconnect.controller.R
import com.android.healthconnect.controller.deletion.DeletionConstants
import com.android.healthconnect.controller.deletion.DeletionConstants.DELETION_TYPE
import com.android.healthconnect.controller.deletion.DeletionConstants.FRAGMENT_TAG_DELETION
import com.android.healthconnect.controller.deletion.DeletionFragment
import com.android.healthconnect.controller.deletion.DeletionType
import com.android.healthconnect.controller.permissions.connectedapps.ConnectedAppsViewModel.DisconnectAllState
import com.android.healthconnect.controller.permissions.shared.Constants.EXTRA_APP_NAME
import com.android.healthconnect.controller.permissions.shared.HelpAndFeedbackFragment.Companion.APP_INTEGRATION_REQUEST_BUCKET_ID
import com.android.healthconnect.controller.permissions.shared.HelpAndFeedbackFragment.Companion.FEEDBACK_INTENT_RESULT_CODE
import com.android.healthconnect.controller.shared.app.ConnectedAppMetadata
import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.ALLOWED
import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.DENIED
import com.android.healthconnect.controller.shared.app.ConnectedAppStatus.INACTIVE
import com.android.healthconnect.controller.shared.dialog.AlertDialogBuilder
import com.android.healthconnect.controller.shared.inactiveapp.InactiveAppPreference
import com.android.healthconnect.controller.shared.preference.BannerPreference
import com.android.healthconnect.controller.shared.preference.HealthPreference
import com.android.healthconnect.controller.shared.preference.HealthPreferenceFragment
import com.android.healthconnect.controller.utils.AttributeResolver
import com.android.healthconnect.controller.utils.DeviceInfoUtils
import com.android.healthconnect.controller.utils.dismissLoadingDialog
import com.android.healthconnect.controller.utils.logging.AppPermissionsElement
import com.android.healthconnect.controller.utils.logging.DisconnectAllAppsDialogElement
import com.android.healthconnect.controller.utils.logging.HealthConnectLogger
import com.android.healthconnect.controller.utils.logging.PageName
import com.android.healthconnect.controller.utils.setupMenu
import com.android.healthconnect.controller.utils.setupSharedMenu
import com.android.healthconnect.controller.utils.showLoadingDialog
import com.android.settingslib.widget.AppPreference
import com.android.settingslib.widget.TopIntroPreference
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject
/** Fragment for connected apps screen. */
@AndroidEntryPoint(HealthPreferenceFragment::class)
class ConnectedAppsFragment : Hilt_ConnectedAppsFragment() {
companion object {
private const val TOP_INTRO = "connected_apps_top_intro"
const val ALLOWED_APPS_CATEGORY = "allowed_apps"
private const val NOT_ALLOWED_APPS = "not_allowed_apps"
private const val INACTIVE_APPS = "inactive_apps"
private const val THINGS_TO_TRY = "things_to_try_app_permissions_screen"
private const val SETTINGS_AND_HELP = "settings_and_help"
}
init {
this.setPageName(PageName.APP_PERMISSIONS_PAGE)
}
@Inject lateinit var logger: HealthConnectLogger
@Inject lateinit var deviceInfoUtils: DeviceInfoUtils
private val viewModel: ConnectedAppsViewModel by viewModels()
private lateinit var searchMenuItem: MenuItem
private val mTopIntro: TopIntroPreference by lazy {
preferenceScreen.findPreference(TOP_INTRO)!!
}
private val mAllowedAppsCategory: PreferenceGroup by lazy {
preferenceScreen.findPreference(ALLOWED_APPS_CATEGORY)!!
}
private val mNotAllowedAppsCategory: PreferenceGroup by lazy {
preferenceScreen.findPreference(NOT_ALLOWED_APPS)!!
}
private val mInactiveAppsCategory: PreferenceGroup by lazy {
preferenceScreen.findPreference(INACTIVE_APPS)!!
}
private val mThingsToTryCategory: PreferenceGroup by lazy {
preferenceScreen.findPreference(THINGS_TO_TRY)!!
}
private val mSettingsAndHelpCategory: PreferenceGroup by lazy {
preferenceScreen.findPreference(SETTINGS_AND_HELP)!!
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
super.onCreatePreferences(savedInstanceState, rootKey)
setPreferencesFromResource(R.xml.connected_apps_screen, rootKey)
if (childFragmentManager.findFragmentByTag(FRAGMENT_TAG_DELETION) == null) {
childFragmentManager.commitNow { add(DeletionFragment(), FRAGMENT_TAG_DELETION) }
}
}
private fun openRemoveAllAppsAccessDialog(apps: List<ConnectedAppMetadata>) {
AlertDialogBuilder(this)
.setLogName(DisconnectAllAppsDialogElement.DISCONNECT_ALL_APPS_DIALOG_CONTAINER)
.setIcon(R.attr.disconnectAllIcon)
.setTitle(R.string.permissions_disconnect_all_dialog_title)
.setMessage(R.string.permissions_disconnect_all_dialog_message)
.setNegativeButton(
android.R.string.cancel,
DisconnectAllAppsDialogElement.DISCONNECT_ALL_APPS_DIALOG_CANCEL_BUTTON)
.setPositiveButton(
R.string.permissions_disconnect_all_dialog_disconnect,
DisconnectAllAppsDialogElement.DISCONNECT_ALL_APPS_DIALOG_REMOVE_ALL_BUTTON) { _, _
->
if (!viewModel.disconnectAllApps(apps)) {
Toast.makeText(requireContext(), R.string.default_error, Toast.LENGTH_SHORT)
.show()
}
}
.create()
.show()
}
override fun onResume() {
super.onResume()
viewModel.loadConnectedApps()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
observeConnectedApps()
observeRevokeAllAppsPermissions()
}
private fun observeRevokeAllAppsPermissions() {
viewModel.disconnectAllState.observe(viewLifecycleOwner) { state ->
when (state) {
is DisconnectAllState.Loading -> {
showLoadingDialog()
}
else -> {
dismissLoadingDialog()
}
}
}
}
private fun observeConnectedApps() {
viewModel.connectedApps.observe(viewLifecycleOwner) { connectedApps ->
clearAllCategories()
if (connectedApps.isEmpty()) {
setupSharedMenu(viewLifecycleOwner, logger)
setUpEmptyState()
} else {
setupMenu(R.menu.connected_apps, viewLifecycleOwner, logger) { menuItem ->
when (menuItem.itemId) {
R.id.menu_search -> {
searchMenuItem = menuItem
logger.logInteraction(AppPermissionsElement.SEARCH_BUTTON)
findNavController().navigate(R.id.action_connectedApps_to_searchApps)
true
}
else -> false
}
}
logger.logImpression(AppPermissionsElement.SEARCH_BUTTON)
mTopIntro?.title = getString(R.string.connected_apps_text)
mThingsToTryCategory?.isVisible = false
setAppAndSettingsCategoriesVisibility(true)
val connectedAppsGroup = connectedApps.groupBy { it.status }
val allowedApps = connectedAppsGroup[ALLOWED].orEmpty()
val notAllowedApps = connectedAppsGroup[DENIED].orEmpty()
val activeApps: MutableList<ConnectedAppMetadata> = allowedApps.toMutableList()
activeApps.addAll(notAllowedApps)
mSettingsAndHelpCategory?.addPreference(
getRemoveAccessForAllAppsPreference().apply {
isEnabled = allowedApps.isNotEmpty()
setOnPreferenceClickListener {
openRemoveAllAppsAccessDialog(activeApps)
true
}
})
if (deviceInfoUtils.isPlayStoreAvailable(requireContext()) ||
deviceInfoUtils.isSendFeedbackAvailable(requireContext())) {
mSettingsAndHelpCategory?.addPreference(getHelpAndFeedbackPreference())
}
updateAllowedApps(allowedApps)
updateDeniedApps(notAllowedApps)
updateInactiveApps(connectedAppsGroup[INACTIVE].orEmpty())
}
}
}
private fun updateInactiveApps(appsList: List<ConnectedAppMetadata>) {
if (appsList.isEmpty()) {
preferenceScreen.removePreference(mInactiveAppsCategory)
} else {
appsList.forEach { app ->
val inactiveAppPreference =
InactiveAppPreference(requireContext()).also {
it.title = app.appMetadata.appName
it.icon = app.appMetadata.icon
it.logName = AppPermissionsElement.INACTIVE_APP_DELETE_BUTTON
it.setOnDeleteButtonClickListener {
val appDeletionType =
DeletionType.DeletionTypeAppData(
app.appMetadata.packageName, app.appMetadata.appName)
childFragmentManager.setFragmentResult(
DeletionConstants.START_INACTIVE_APP_DELETION_EVENT,
bundleOf(DELETION_TYPE to appDeletionType))
}
}
mInactiveAppsCategory?.addPreference(inactiveAppPreference)
}
}
}
private fun updateAllowedApps(appsList: List<ConnectedAppMetadata>) {
if (appsList.isEmpty()) {
mAllowedAppsCategory?.addPreference(getNoAppsPreference(R.string.no_apps_allowed))
} else {
appsList.forEach { app ->
mAllowedAppsCategory?.addPreference(
getAppPreference(app) { navigateToAppInfoScreen(app) })
}
}
}
private fun updateDeniedApps(appsList: List<ConnectedAppMetadata>) {
if (appsList.isEmpty()) {
mNotAllowedAppsCategory?.addPreference(getNoAppsPreference(R.string.no_apps_denied))
} else {
appsList.forEach { app ->
mNotAllowedAppsCategory?.addPreference(
getAppPreference(app) { navigateToAppInfoScreen(app) })
}
}
}
private fun navigateToAppInfoScreen(app: ConnectedAppMetadata) {
findNavController()
.navigate(
R.id.action_connectedApps_to_connectedApp,
bundleOf(
EXTRA_PACKAGE_NAME to app.appMetadata.packageName,
EXTRA_APP_NAME to app.appMetadata.appName))
}
private fun getNoAppsPreference(@StringRes res: Int): Preference {
return Preference(requireContext()).also {
it.setTitle(res)
it.isSelectable = false
}
}
private fun getAppPreference(
app: ConnectedAppMetadata,
onClick: (() -> Unit)? = null
): AppPreference {
return HealthAppPreference(requireContext(), app.appMetadata).also {
if (app.status == ALLOWED) {
it.logName = AppPermissionsElement.CONNECTED_APP_BUTTON
} else if (app.status == DENIED) {
it.logName = AppPermissionsElement.NOT_CONNECTED_APP_BUTTON
}
it.setOnPreferenceClickListener {
onClick?.invoke()
true
}
}
}
private fun getRemoveAccessForAllAppsPreference(): HealthPreference {
return HealthPreference(requireContext()).also {
it.title = resources.getString(R.string.disconnect_all_apps)
it.icon =
AttributeResolver.getDrawable(requireContext(), R.attr.removeAccessForAllAppsIcon)
it.logName = AppPermissionsElement.REMOVE_ALL_APPS_PERMISSIONS_BUTTON
}
}
private fun getHelpAndFeedbackPreference(): HealthPreference {
return HealthPreference(requireContext()).also {
it.title = resources.getString(R.string.help_and_feedback)
it.icon = AttributeResolver.getDrawable(requireContext(), R.attr.helpAndFeedbackIcon)
it.logName = AppPermissionsElement.HELP_AND_FEEDBACK_BUTTON
it.setOnPreferenceClickListener {
findNavController().navigate(R.id.action_connectedApps_to_helpAndFeedback)
true
}
}
}
private fun getCheckForUpdatesPreference(): HealthPreference {
return HealthPreference(requireContext()).also {
it.title = resources.getString(R.string.check_for_updates)
it.icon = AttributeResolver.getDrawable(requireContext(), R.attr.checkForUpdatesIcon)
it.summary = resources.getString(R.string.check_for_updates_description)
it.logName = AppPermissionsElement.CHECK_FOR_UPDATES_BUTTON
it.setOnPreferenceClickListener {
findNavController().navigate(R.id.action_connected_apps_to_updated_apps)
true
}
}
}
private fun getSeeAllCompatibleAppsPreference(): HealthPreference {
return HealthPreference(requireContext()).also {
it.title = resources.getString(R.string.see_all_compatible_apps)
it.icon =
AttributeResolver.getDrawable(requireContext(), R.attr.seeAllCompatibleAppsIcon)
it.summary = resources.getString(R.string.see_all_compatible_apps_description)
it.logName = AppPermissionsElement.SEE_ALL_COMPATIBLE_APPS_BUTTON
it.setOnPreferenceClickListener {
findNavController().navigate(R.id.action_connected_apps_to_play_store)
true
}
}
}
private fun getSendFeedbackPreference(): Preference {
return Preference(requireContext()).also {
it.title = resources.getString(R.string.send_feedback)
it.icon = AttributeResolver.getDrawable(requireContext(), R.attr.sendFeedbackIcon)
it.summary = resources.getString(R.string.send_feedback_description)
it.setOnPreferenceClickListener {
val intent = Intent(Intent.ACTION_BUG_REPORT)
intent.putExtra("category_tag", APP_INTEGRATION_REQUEST_BUCKET_ID)
activity?.startActivityForResult(intent, FEEDBACK_INTENT_RESULT_CODE)
true
}
}
}
// TODO (b/275602235) Use this banner to indicate one or more apps need updating to work with
// Android U
private fun getAppUpdateNeededBanner(): BannerPreference {
return BannerPreference(requireContext()).also { banner ->
banner.setButton(resources.getString(R.string.app_update_needed_banner_button))
banner.title = resources.getString(R.string.app_update_needed_banner_title)
banner.summary =
resources.getString(R.string.app_update_needed_banner_description_multiple)
banner.setIcon(R.drawable.ic_apps_outage)
banner.setButtonOnClickListener {
// TODO (b/275602235) navigate to play store
}
banner.setIsDismissable(true)
banner.setDismissAction { preferenceScreen.removePreference(banner) }
}
}
private fun setUpEmptyState() {
mTopIntro?.title = getString(R.string.connected_apps_empty_list_section_title)
if (deviceInfoUtils.isPlayStoreAvailable(requireContext()) ||
deviceInfoUtils.isSendFeedbackAvailable(requireContext())) {
mThingsToTryCategory?.isVisible = true
}
if (deviceInfoUtils.isPlayStoreAvailable(requireContext())) {
mThingsToTryCategory?.addPreference(getCheckForUpdatesPreference())
mThingsToTryCategory?.addPreference(getSeeAllCompatibleAppsPreference())
}
if (deviceInfoUtils.isSendFeedbackAvailable(requireContext())) {
mThingsToTryCategory?.addPreference(getSendFeedbackPreference())
}
setAppAndSettingsCategoriesVisibility(false)
}
private fun setAppAndSettingsCategoriesVisibility(isVisible: Boolean) {
mInactiveAppsCategory?.isVisible = isVisible
mAllowedAppsCategory?.isVisible = isVisible
mNotAllowedAppsCategory?.isVisible = isVisible
mSettingsAndHelpCategory?.isVisible = isVisible
}
private fun clearAllCategories() {
mThingsToTryCategory?.removeAll()
mAllowedAppsCategory?.removeAll()
mNotAllowedAppsCategory?.removeAll()
mInactiveAppsCategory?.removeAll()
mSettingsAndHelpCategory?.removeAll()
}
}