1 /* 2 * Copyright (C) 2022 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 17 package com.android.permissioncontroller.safetycenter.ui 18 19 import android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE 20 import android.os.Bundle 21 import android.safetycenter.SafetyCenterEntryGroup 22 import android.util.Log 23 import androidx.annotation.RequiresApi 24 import androidx.preference.PreferenceGroup 25 import com.android.permissioncontroller.Constants.EXTRA_SESSION_ID 26 import com.android.permissioncontroller.R 27 import com.android.permissioncontroller.safetycenter.ui.SafetyBrandChipPreference.Companion.closeSubpage 28 import com.android.permissioncontroller.safetycenter.ui.model.SafetyCenterUiData 29 import com.android.safetycenter.resources.SafetyCenterResourcesApk 30 import com.android.settingslib.widget.FooterPreference 31 import com.android.settingslib.widget.SettingsThemeHelper 32 33 /** A fragment that represents a generic subpage in Safety Center. */ 34 @RequiresApi(UPSIDE_DOWN_CAKE) 35 class SafetyCenterSubpageFragment : SafetyCenterFragment() { 36 37 private lateinit var sourceGroupId: String 38 private lateinit var subpageBrandChip: SafetyBrandChipPreference 39 private lateinit var subpageIllustration: SafetyIllustrationPreference 40 private lateinit var subpageIssueGroup: PreferenceGroup 41 private lateinit var subpageEntryGroup: PreferenceGroup 42 private lateinit var subpageFooter: FooterPreference 43 onCreatePreferencesnull44 override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { 45 super.onCreatePreferences(savedInstanceState, rootKey) 46 setPreferencesFromResource(R.xml.safety_center_subpage, rootKey) 47 sourceGroupId = requireArguments().getString(SOURCE_GROUP_ID_KEY)!! 48 49 subpageBrandChip = preferenceScreen.findPreference(BRAND_CHIP_KEY)!! 50 subpageIllustration = preferenceScreen.findPreference(ILLUSTRATION_KEY)!! 51 subpageIssueGroup = preferenceScreen.findPreference(ISSUE_GROUP_KEY)!! 52 subpageEntryGroup = preferenceScreen.findPreference(ENTRY_GROUP_KEY)!! 53 subpageFooter = preferenceScreen.findPreference(FOOTER_KEY)!! 54 55 subpageBrandChip.setupListener(requireActivity(), safetyCenterSessionId) 56 setupIllustration() 57 setupFooter() 58 maybeRemoveSpacer() 59 60 prerenderCurrentSafetyCenterData() 61 } 62 configureInteractionLoggernull63 override fun configureInteractionLogger() { 64 val logger = safetyCenterViewModel.interactionLogger 65 logger.sessionId = safetyCenterSessionId 66 logger.navigationSource = NavigationSource.fromIntent(requireActivity().getIntent()) 67 logger.viewType = ViewType.SUBPAGE 68 logger.groupId = sourceGroupId 69 } 70 onResumenull71 override fun onResume() { 72 super.onResume() 73 safetyCenterViewModel.pageOpen(sourceGroupId) 74 } 75 renderSafetyCenterDatanull76 override fun renderSafetyCenterData(uiData: SafetyCenterUiData?) { 77 Log.v(TAG, "renderSafetyCenterEntryGroup called with $uiData") 78 val entryGroup = uiData?.getMatchingGroup(sourceGroupId) 79 if (entryGroup == null) { 80 Log.w(TAG, "$sourceGroupId doesn't match any of the existing SafetySourcesGroup IDs") 81 closeSubpage(requireActivity(), requireContext(), safetyCenterSessionId) 82 return 83 } 84 85 requireActivity().title = entryGroup.title 86 updateSafetyCenterIssues(uiData) 87 updateSafetyCenterEntries(entryGroup) 88 } 89 setupIllustrationnull90 private fun setupIllustration() { 91 val resName = "illustration_${SnakeCaseConverter.fromCamelCase(sourceGroupId)}" 92 val context = requireContext() 93 val drawable = SafetyCenterResourcesApk(context).getDrawableByName(resName, context.theme) 94 if (drawable == null) { 95 Log.w(TAG, "$sourceGroupId doesn't have any matching illustration") 96 subpageIllustration.isVisible = false 97 } 98 99 subpageIllustration.illustrationDrawable = drawable 100 } 101 setupFooternull102 private fun setupFooter() { 103 val resName = "${SnakeCaseConverter.fromCamelCase(sourceGroupId)}_footer" 104 val footerText = SafetyCenterResourcesApk(requireContext()).getStringByName(resName) 105 if (footerText.isEmpty()) { 106 Log.w(TAG, "$sourceGroupId doesn't have any matching footer") 107 subpageFooter.isVisible = false 108 } 109 // footer is ordered last by default 110 // in order to keep a spacer after the footer, footer needs to be the second from last 111 subpageFooter.order = Int.MAX_VALUE - 2 112 subpageFooter.summary = footerText 113 } 114 maybeRemoveSpacernull115 private fun maybeRemoveSpacer() { 116 if (SettingsThemeHelper.isExpressiveTheme(requireContext())) { 117 val spacerPreference = preferenceScreen.findPreference<SpacerPreference>(SPACER_KEY)!! 118 preferenceScreen.removePreference(spacerPreference) 119 } 120 } 121 updateSafetyCenterIssuesnull122 private fun updateSafetyCenterIssues(uiData: SafetyCenterUiData?) { 123 subpageIssueGroup.removeAll() 124 val subpageIssues = uiData?.getMatchingIssues(sourceGroupId) 125 val subpageDismissedIssues = uiData?.getMatchingDismissedIssues(sourceGroupId) 126 127 subpageIllustration.isVisible = 128 subpageIssues.isNullOrEmpty() && subpageIllustration.illustrationDrawable != null 129 130 if (subpageIssues.isNullOrEmpty() && subpageDismissedIssues.isNullOrEmpty()) { 131 Log.w(TAG, "$sourceGroupId doesn't have any matching SafetyCenterIssues") 132 return 133 } 134 135 collapsableIssuesCardHelper.addIssues( 136 requireContext(), 137 safetyCenterViewModel, 138 getChildFragmentManager(), 139 subpageIssueGroup, 140 subpageIssues, 141 subpageDismissedIssues, 142 uiData.resolvedIssues, 143 requireActivity().taskId, 144 ) 145 } 146 updateSafetyCenterEntriesnull147 private fun updateSafetyCenterEntries(entryGroup: SafetyCenterEntryGroup) { 148 Log.v(TAG, "updateSafetyCenterEntries called with $entryGroup") 149 subpageEntryGroup.removeAll() 150 for (entry in entryGroup.entries) { 151 subpageEntryGroup.addPreference( 152 SafetySubpageEntryPreference( 153 requireContext(), 154 PendingIntentSender.getTaskIdForEntry( 155 entry.id, 156 sameTaskSourceIds, 157 requireActivity(), 158 ), 159 entry, 160 safetyCenterViewModel, 161 ) 162 ) 163 } 164 } 165 166 companion object { 167 private val TAG = SafetyCenterSubpageFragment::class.java.simpleName 168 private const val BRAND_CHIP_KEY = "subpage_brand_chip" 169 private const val ILLUSTRATION_KEY = "subpage_illustration" 170 private const val ISSUE_GROUP_KEY = "subpage_issue_group" 171 private const val ENTRY_GROUP_KEY = "subpage_entry_group" 172 private const val FOOTER_KEY = "subpage_footer" 173 private const val SPACER_KEY = "subpage_spacer" 174 private const val SOURCE_GROUP_ID_KEY = "source_group_id" 175 176 /** Creates an instance of SafetyCenterSubpageFragment with the arguments set */ 177 @JvmStatic newInstancenull178 fun newInstance(sessionId: Long, groupId: String): SafetyCenterSubpageFragment { 179 val args = Bundle() 180 args.putLong(EXTRA_SESSION_ID, sessionId) 181 args.putString(SOURCE_GROUP_ID_KEY, groupId) 182 183 val subpageFragment = SafetyCenterSubpageFragment() 184 subpageFragment.setArguments(args) 185 return subpageFragment 186 } 187 } 188 } 189