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 package com.android.permissioncontroller.safetycenter.ui; 17 18 import static android.content.Intent.ACTION_SAFETY_CENTER; 19 import static android.content.Intent.FLAG_ACTIVITY_FORWARD_RESULT; 20 import static android.os.Build.VERSION_CODES.TIRAMISU; 21 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; 22 import static android.safetycenter.SafetyCenterManager.EXTRA_SAFETY_SOURCES_GROUP_ID; 23 24 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION; 25 import static com.android.permissioncontroller.PermissionControllerStatsLog.PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_CLICKED; 26 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.EXTRA_SETTINGS_FRAGMENT_ARGS_KEY; 27 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PERSONAL_PROFILE_SUFFIX; 28 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PRIVACY_SOURCES_GROUP_ID; 29 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.PRIVATE_PROFILE_SUFFIX; 30 import static com.android.permissioncontroller.safetycenter.SafetyCenterConstants.WORK_PROFILE_SUFFIX; 31 32 import android.app.ActionBar; 33 import android.content.Intent; 34 import android.content.res.Configuration; 35 import android.os.Bundle; 36 import android.provider.Settings; 37 import android.safetycenter.SafetyCenterManager; 38 import android.safetycenter.config.SafetyCenterConfig; 39 import android.safetycenter.config.SafetySource; 40 import android.safetycenter.config.SafetySourcesGroup; 41 import android.text.TextUtils; 42 import android.util.Log; 43 import android.view.View; 44 45 import androidx.annotation.Nullable; 46 import androidx.annotation.RequiresApi; 47 import androidx.core.graphics.Insets; 48 import androidx.core.view.ViewCompat; 49 import androidx.core.view.WindowInsetsCompat; 50 import androidx.fragment.app.Fragment; 51 import androidx.lifecycle.Lifecycle; 52 import androidx.lifecycle.LifecycleEventObserver; 53 import androidx.lifecycle.LifecycleOwner; 54 import androidx.preference.PreferenceFragmentCompat; 55 56 import com.android.permissioncontroller.Constants; 57 import com.android.permissioncontroller.PermissionControllerStatsLog; 58 import com.android.permissioncontroller.R; 59 import com.android.permissioncontroller.permission.utils.Utils; 60 import com.android.permissioncontroller.safetycenter.ui.model.PrivacyControlsViewModel.Pref; 61 import com.android.settingslib.activityembedding.ActivityEmbeddingUtils; 62 import com.android.settingslib.collapsingtoolbar.CollapsingToolbarBaseActivity; 63 64 import java.util.List; 65 import java.util.Objects; 66 67 /** Entry-point activity for SafetyCenter. */ 68 @RequiresApi(TIRAMISU) 69 public final class SafetyCenterActivity extends CollapsingToolbarBaseActivity { 70 71 private static final String TAG = SafetyCenterActivity.class.getSimpleName(); 72 private static final String PRIVACY_CONTROLS_ACTION = "android.settings.PRIVACY_CONTROLS"; 73 private static final String MENU_KEY_SAFETY_CENTER = "top_level_safety_center"; 74 private static final String EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI = 75 "android.provider.extra.SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI"; 76 private static final String EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY = 77 "android.provider.extra.SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY"; 78 private static final String EXTRA_PREVENT_TRAMPOLINE_TO_SETTINGS = 79 "com.android.permissioncontroller.safetycenter.extra.PREVENT_TRAMPOLINE_TO_SETTINGS"; 80 81 private SafetyCenterManager mSafetyCenterManager; 82 83 @Override onCreate(Bundle savedInstanceState)84 public void onCreate(Bundle savedInstanceState) { 85 super.onCreate(savedInstanceState); 86 mSafetyCenterManager = getSystemService(SafetyCenterManager.class); 87 88 if (maybeRedirectIfDisabled()) { 89 return; 90 } 91 92 if (maybeRedirectIntoTwoPaneSettings()) { 93 return; 94 } 95 96 Fragment frag; 97 final boolean maybeOpenSubpage = 98 SafetyCenterUiFlags.getShowSubpages() 99 && getIntent().getAction().equals(ACTION_SAFETY_CENTER); 100 if (maybeOpenSubpage && getIntent().hasExtra(EXTRA_SAFETY_SOURCES_GROUP_ID)) { 101 String groupId = getIntent().getStringExtra(EXTRA_SAFETY_SOURCES_GROUP_ID); 102 frag = openRelevantSubpage(groupId); 103 } else if (maybeOpenSubpage && getIntent().hasExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY)) { 104 String preferenceKey = getIntent().getStringExtra(EXTRA_SETTINGS_FRAGMENT_ARGS_KEY); 105 String groupId = getParentGroupId(preferenceKey); 106 frag = openRelevantSubpage(groupId); 107 } else if (getIntent().getAction().equals(PRIVACY_CONTROLS_ACTION)) { 108 setTitle(R.string.privacy_controls_title); 109 frag = PrivacyControlsFragment.newInstance(); 110 } else { 111 frag = openHomepage(); 112 } 113 114 if (savedInstanceState == null) { 115 getSupportFragmentManager() 116 .beginTransaction() 117 .add(com.android.settingslib.collapsingtoolbar.R.id.content_frame, frag) 118 .commitNow(); 119 } 120 121 configureHomeButton(); 122 123 frag.getLifecycle() 124 .addObserver( 125 new LifecycleEventObserver() { 126 @Override 127 public void onStateChanged( 128 LifecycleOwner unused, Lifecycle.Event event) { 129 if (event != Lifecycle.Event.ON_START) { 130 return; 131 } 132 View listView = getListView(frag); 133 if (listView == null) { 134 return; 135 } 136 int paddingBottom = listView.getPaddingBottom(); 137 ViewCompat.setOnApplyWindowInsetsListener( 138 listView, 139 (v, windowInsets) -> { 140 Insets insets = 141 windowInsets.getInsets( 142 WindowInsetsCompat.Type.systemBars()); 143 v.setPadding( 144 v.getPaddingLeft(), 145 v.getPaddingTop(), 146 v.getPaddingRight(), 147 paddingBottom + insets.bottom); 148 return WindowInsetsCompat.CONSUMED; 149 }); 150 } 151 }); 152 } 153 154 @Override onStart()155 protected void onStart() { 156 super.onStart(); 157 maybeRedirectIfDisabled(); 158 } 159 160 @Override onConfigurationChanged(Configuration newConfig)161 public void onConfigurationChanged(Configuration newConfig) { 162 // We don't set configChanges, but small screen size changes may still be delivered here. 163 super.onConfigurationChanged(newConfig); 164 configureHomeButton(); 165 } 166 167 /** Decide whether a home/back button should be shown or not. */ configureHomeButton()168 private void configureHomeButton() { 169 ActionBar actionBar = getActionBar(); 170 Fragment frag = 171 getSupportFragmentManager() 172 .findFragmentById( 173 com.android.settingslib.collapsingtoolbar.R.id.content_frame); 174 if (actionBar == null || frag == null) { 175 return; 176 } 177 178 // Only the homepage can be considered a "second layer" page as it's the only one that 179 // can be reached from the Settings menu. The other pages are only reachable using 180 // a direct intent (e.g. notification, "first layer") and/or by navigating within Safety 181 // Center ("third layer"). 182 // Note that the homepage can also be a "first layer" page, but that would only happen 183 // if the activity is not embedded. 184 boolean isSecondLayerPage = frag instanceof SafetyCenterScrollWrapperFragment; 185 if (ActivityEmbeddingUtils.shouldHideNavigateUpButton(this, isSecondLayerPage)) { 186 actionBar.setDisplayHomeAsUpEnabled(false); 187 actionBar.setHomeButtonEnabled(false); 188 } 189 } 190 maybeRedirectIfDisabled()191 private boolean maybeRedirectIfDisabled() { 192 if (mSafetyCenterManager == null || !mSafetyCenterManager.isSafetyCenterEnabled()) { 193 Log.w(TAG, "Safety Center disabled, redirecting to settings page"); 194 startActivity( 195 new Intent(getActionToRedirectWhenDisabled()) 196 .addFlags(FLAG_ACTIVITY_FORWARD_RESULT)); 197 finish(); 198 return true; 199 } 200 return false; 201 } 202 getActionToRedirectWhenDisabled()203 private String getActionToRedirectWhenDisabled() { 204 boolean isPrivacyControls = 205 TextUtils.equals(getIntent().getAction(), PRIVACY_CONTROLS_ACTION); 206 if (isPrivacyControls) { 207 return Settings.ACTION_PRIVACY_SETTINGS; 208 } 209 return Settings.ACTION_SETTINGS; 210 } 211 maybeRedirectIntoTwoPaneSettings()212 private boolean maybeRedirectIntoTwoPaneSettings() { 213 return shouldUseTwoPaneSettings() && tryRedirectTwoPaneSettings(); 214 } 215 shouldUseTwoPaneSettings()216 private boolean shouldUseTwoPaneSettings() { 217 if (!ActivityEmbeddingUtils.isEmbeddingActivityEnabled(this)) { 218 return false; 219 } 220 221 Bundle extras = getIntent().getExtras(); 222 if (extras != null && extras.getBoolean(EXTRA_PREVENT_TRAMPOLINE_TO_SETTINGS, false)) { 223 return false; 224 } 225 226 return isTaskRoot() && !ActivityEmbeddingUtils.isActivityEmbedded(this); 227 } 228 229 /** Return {@code true} if the redirection was attempted. */ tryRedirectTwoPaneSettings()230 private boolean tryRedirectTwoPaneSettings() { 231 Intent twoPaneIntent = getTwoPaneIntent(); 232 if (twoPaneIntent == null) { 233 return false; 234 } 235 236 Log.i(TAG, "Safety Center restarting in Settings two-pane layout"); 237 startActivity(twoPaneIntent); 238 finishAndRemoveTask(); 239 return true; 240 } 241 242 @Nullable getTwoPaneIntent()243 private Intent getTwoPaneIntent() { 244 Intent twoPaneIntent = ActivityEmbeddingUtils.buildEmbeddingActivityBaseIntent(this); 245 if (twoPaneIntent == null) { 246 return null; 247 } 248 249 twoPaneIntent.putExtras(getIntent()); 250 twoPaneIntent.putExtra( 251 EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_INTENT_URI, 252 getIntent().toUri(Intent.URI_INTENT_SCHEME)); 253 twoPaneIntent.putExtra( 254 EXTRA_SETTINGS_EMBEDDED_DEEP_LINK_HIGHLIGHT_MENU_KEY, MENU_KEY_SAFETY_CENTER); 255 return twoPaneIntent; 256 } 257 logPrivacySourceMetric()258 private void logPrivacySourceMetric() { 259 Intent intent = getIntent(); 260 if (intent != null && intent.hasExtra(Constants.EXTRA_PRIVACY_SOURCE)) { 261 int privacySource = intent.getIntExtra(Constants.EXTRA_PRIVACY_SOURCE, -1); 262 int uid = intent.getIntExtra(Intent.EXTRA_UID, -1); 263 long sessionId = 264 intent.getLongExtra(Constants.EXTRA_SESSION_ID, Constants.INVALID_SESSION_ID); 265 Log.i( 266 TAG, 267 "privacy source notification metric, source " 268 + privacySource 269 + " uid " 270 + uid 271 + " sessionId " 272 + sessionId); 273 PermissionControllerStatsLog.write( 274 PRIVACY_SIGNAL_NOTIFICATION_INTERACTION, 275 privacySource, 276 uid, 277 PRIVACY_SIGNAL_NOTIFICATION_INTERACTION__ACTION__NOTIFICATION_CLICKED, 278 sessionId); 279 } 280 } 281 openHomepage()282 private Fragment openHomepage() { 283 logPrivacySourceMetric(); 284 setTitle(getString(R.string.safety_center_dashboard_page_title)); 285 return new SafetyCenterScrollWrapperFragment(); 286 } 287 288 @RequiresApi(UPSIDE_DOWN_CAKE) openRelevantSubpage(String groupId)289 private Fragment openRelevantSubpage(String groupId) { 290 if (groupId.isEmpty()) { 291 return openHomepage(); 292 } 293 294 long sessionId = Utils.getOrGenerateSessionId(getIntent()); 295 if (Objects.equals(groupId, PRIVACY_SOURCES_GROUP_ID)) { 296 logPrivacySourceMetric(); 297 return PrivacySubpageFragment.newInstance(sessionId); 298 } 299 300 return SafetyCenterSubpageFragment.newInstance(sessionId, groupId); 301 } 302 303 @RequiresApi(UPSIDE_DOWN_CAKE) getParentGroupId(String preferenceKey)304 private String getParentGroupId(String preferenceKey) { 305 if (Pref.findByKey(preferenceKey) != null) { 306 return PRIVACY_SOURCES_GROUP_ID; 307 } 308 309 SafetyCenterConfig safetyCenterConfig = mSafetyCenterManager.getSafetyCenterConfig(); 310 String[] splitKey; 311 if (preferenceKey.endsWith(PERSONAL_PROFILE_SUFFIX)) { 312 splitKey = preferenceKey.split("_" + PERSONAL_PROFILE_SUFFIX); 313 } else if (preferenceKey.endsWith(WORK_PROFILE_SUFFIX)) { 314 splitKey = preferenceKey.split("_" + WORK_PROFILE_SUFFIX); 315 } else if (preferenceKey.endsWith(PRIVATE_PROFILE_SUFFIX)) { 316 splitKey = preferenceKey.split("_" + PRIVATE_PROFILE_SUFFIX); 317 } else { 318 return ""; 319 } 320 321 if (safetyCenterConfig == null || splitKey.length == 0) { 322 return ""; 323 } 324 325 List<SafetySourcesGroup> groups = safetyCenterConfig.getSafetySourcesGroups(); 326 for (SafetySourcesGroup group : groups) { 327 if (group.getType() != SafetySourcesGroup.SAFETY_SOURCES_GROUP_TYPE_STATEFUL) { 328 // Hidden and static groups are not opened in a subpage. 329 continue; 330 } 331 for (SafetySource source : group.getSafetySources()) { 332 if (Objects.equals(source.getId(), splitKey[0])) { 333 return group.getId(); 334 } 335 } 336 } 337 return ""; 338 } 339 340 @Nullable getListView(Fragment fragment)341 private View getListView(Fragment fragment) { 342 if (fragment instanceof PreferenceFragmentCompat) { 343 return ((PreferenceFragmentCompat) fragment).getListView(); 344 } 345 if (fragment instanceof SafetyCenterScrollWrapperFragment) { 346 Fragment dashboardFragment = 347 fragment.getChildFragmentManager().findFragmentById(R.id.fragment_container); 348 if (dashboardFragment instanceof PreferenceFragmentCompat) { 349 return ((PreferenceFragmentCompat) dashboardFragment).getListView(); 350 } 351 } 352 return null; 353 } 354 } 355