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.safetycenter; 18 19 import static android.os.Build.VERSION_CODES.TIRAMISU; 20 21 import static java.util.Collections.unmodifiableList; 22 import static java.util.Objects.requireNonNull; 23 24 import android.annotation.Nullable; 25 import android.content.res.Resources; 26 import android.safetycenter.config.SafetyCenterConfig; 27 import android.safetycenter.config.SafetySource; 28 import android.safetycenter.config.SafetySourcesGroup; 29 import android.util.ArrayMap; 30 import android.util.Log; 31 32 import androidx.annotation.RequiresApi; 33 34 import com.android.safetycenter.config.ParseException; 35 import com.android.safetycenter.config.SafetyCenterConfigParser; 36 import com.android.safetycenter.resources.SafetyCenterResourcesContext; 37 38 import java.io.InputStream; 39 import java.io.PrintWriter; 40 import java.util.ArrayList; 41 import java.util.List; 42 import java.util.Objects; 43 44 import javax.annotation.concurrent.NotThreadSafe; 45 46 /** 47 * A class that reads the {@link SafetyCenterConfig} and allows overriding it for tests. 48 * 49 * <p>This class isn't thread safe. Thread safety must be handled by the caller. 50 * 51 * @hide 52 */ 53 @RequiresApi(TIRAMISU) 54 @NotThreadSafe 55 public final class SafetyCenterConfigReader { 56 57 private static final String TAG = "SafetyCenterConfigReade"; 58 59 private final SafetyCenterResourcesContext mSafetyCenterResourcesContext; 60 61 @Nullable private SafetyCenterConfigInternal mConfigInternalFromXml; 62 63 @Nullable private SafetyCenterConfigInternal mConfigInternalOverrideForTests; 64 65 /** Creates a {@link SafetyCenterConfigReader} from a {@link SafetyCenterResourcesContext}. */ SafetyCenterConfigReader(SafetyCenterResourcesContext safetyCenterResourcesContext)66 SafetyCenterConfigReader(SafetyCenterResourcesContext safetyCenterResourcesContext) { 67 mSafetyCenterResourcesContext = safetyCenterResourcesContext; 68 } 69 70 /** 71 * Loads the {@link SafetyCenterConfig} from the XML file defined in {@code 72 * safety_center_config.xml}; and returns whether this was successful. 73 * 74 * <p>This method must be called prior to any other call to this class. This call must also be 75 * successful; interacting with this class requires checking that the boolean value returned by 76 * this method was {@code true}. 77 */ loadConfig()78 boolean loadConfig() { 79 SafetyCenterConfig safetyCenterConfig = readSafetyCenterConfig(); 80 if (safetyCenterConfig == null) { 81 return false; 82 } 83 mConfigInternalFromXml = SafetyCenterConfigInternal.from(safetyCenterConfig); 84 return true; 85 } 86 87 /** 88 * Sets an override {@link SafetyCenterConfig} for tests. 89 * 90 * <p>When set, information provided by this class will be based on the overridden {@link 91 * SafetyCenterConfig}. 92 */ setConfigOverrideForTests(SafetyCenterConfig safetyCenterConfig)93 void setConfigOverrideForTests(SafetyCenterConfig safetyCenterConfig) { 94 mConfigInternalOverrideForTests = SafetyCenterConfigInternal.from(safetyCenterConfig); 95 } 96 97 /** 98 * Clears the {@link SafetyCenterConfig} override set by {@link 99 * #setConfigOverrideForTests(SafetyCenterConfig)}, if any. 100 */ clearConfigOverrideForTests()101 void clearConfigOverrideForTests() { 102 mConfigInternalOverrideForTests = null; 103 } 104 105 /** Returns the currently active {@link SafetyCenterConfig}. */ getSafetyCenterConfig()106 SafetyCenterConfig getSafetyCenterConfig() { 107 return getCurrentConfigInternal().getSafetyCenterConfig(); 108 } 109 110 /** Returns the groups of {@link SafetySource}, in the order expected by the UI. */ getSafetySourcesGroups()111 public List<SafetySourcesGroup> getSafetySourcesGroups() { 112 return getCurrentConfigInternal().getSafetyCenterConfig().getSafetySourcesGroups(); 113 } 114 115 /** 116 * Returns the groups of {@link SafetySource}, filtering out any sources where {@link 117 * SafetySources#isLoggable(SafetySource)} is false (and any resultingly empty groups). 118 */ getLoggableSafetySourcesGroups()119 public List<SafetySourcesGroup> getLoggableSafetySourcesGroups() { 120 return getCurrentConfigInternal().getLoggableSourcesGroups(); 121 } 122 123 /** 124 * Returns the {@link ExternalSafetySource} associated with the {@code safetySourceId}, if any. 125 * 126 * <p>The returned {@link SafetySource} can either be associated with the XML or overridden 127 * {@link SafetyCenterConfig}; {@link #isExternalSafetySourceActive(String, String)} can be used 128 * to check if it is associated with the current {@link SafetyCenterConfig}. This is to continue 129 * allowing sources from the XML config to interact with SafetCenter during tests (but their 130 * calls will be no-oped). 131 * 132 * <p>The {@code callingPackageName} can help break the tie when the source is available in both 133 * the overridden config and the "real" config. Otherwise, the test config is preferred. This is 134 * to support overriding "real" sources in tests while ensuring package checks continue to pass 135 * for "real" sources that interact with our APIs. 136 */ 137 @Nullable getExternalSafetySource( String safetySourceId, String callingPackageName)138 public ExternalSafetySource getExternalSafetySource( 139 String safetySourceId, String callingPackageName) { 140 SafetyCenterConfigInternal currentConfig = getCurrentConfigInternal(); 141 SafetyCenterConfigInternal xmlConfig = requireNonNull(mConfigInternalFromXml); 142 if (currentConfig == xmlConfig) { 143 // No override, access source directly. 144 return currentConfig.getExternalSafetySources().get(safetySourceId); 145 } 146 147 ExternalSafetySource externalSafetySourceInTestConfig = 148 currentConfig.getExternalSafetySources().get(safetySourceId); 149 ExternalSafetySource externalSafetySourceInRealConfig = 150 xmlConfig.getExternalSafetySources().get(safetySourceId); 151 152 if (externalSafetySourceInTestConfig != null 153 && Objects.equals( 154 externalSafetySourceInTestConfig.getSafetySource().getPackageName(), 155 callingPackageName)) { 156 return externalSafetySourceInTestConfig; 157 } 158 159 if (externalSafetySourceInRealConfig != null 160 && Objects.equals( 161 externalSafetySourceInRealConfig.getSafetySource().getPackageName(), 162 callingPackageName)) { 163 return externalSafetySourceInRealConfig; 164 } 165 166 if (externalSafetySourceInTestConfig != null) { 167 return externalSafetySourceInTestConfig; 168 } 169 170 return externalSafetySourceInRealConfig; 171 } 172 173 /** 174 * Returns whether the {@code safetySourceId} is associated with an {@link ExternalSafetySource} 175 * that is currently active. 176 * 177 * <p>The source may either be "active" or "inactive". An active source is a source that is 178 * currently expected to interact with our API and may affect Safety Center status. An inactive 179 * source is expected to interact with Safety Center, but is currently being silenced / no-ops 180 * while an override for tests is in place. 181 * 182 * <p>The {@code callingPackageName} is used to differentiate a real source being overridden. It 183 * could be that a test is overriding a real source and as such the real source should not be 184 * able to provide data while its override is in place. 185 */ isExternalSafetySourceActive(String safetySourceId, String callingPackageName)186 public boolean isExternalSafetySourceActive(String safetySourceId, String callingPackageName) { 187 ExternalSafetySource externalSafetySourceInCurrentConfig = 188 getCurrentConfigInternal().getExternalSafetySources().get(safetySourceId); 189 if (externalSafetySourceInCurrentConfig == null) { 190 return false; 191 } 192 return Objects.equals( 193 externalSafetySourceInCurrentConfig.getSafetySource().getPackageName(), 194 callingPackageName); 195 } 196 197 /** 198 * Returns whether the {@code safetySourceId} is associated with an {@link ExternalSafetySource} 199 * that is in the real config XML file (i.e. not being overridden). 200 */ isExternalSafetySourceFromRealConfig(String safetySourceId)201 public boolean isExternalSafetySourceFromRealConfig(String safetySourceId) { 202 return requireNonNull(mConfigInternalFromXml) 203 .getExternalSafetySources() 204 .containsKey(safetySourceId); 205 } 206 207 /** 208 * Returns the {@link Broadcast} defined in the {@link SafetyCenterConfig}, with all the sources 209 * that they should handle and the profile on which they should be dispatched. 210 */ getBroadcasts()211 List<Broadcast> getBroadcasts() { 212 return getCurrentConfigInternal().getBroadcasts(); 213 } 214 getCurrentConfigInternal()215 private SafetyCenterConfigInternal getCurrentConfigInternal() { 216 // We require the XML config must be loaded successfully for SafetyCenterManager APIs to 217 // function, regardless of whether the config is subsequently overridden. 218 requireNonNull(mConfigInternalFromXml); 219 220 if (mConfigInternalOverrideForTests == null) { 221 return mConfigInternalFromXml; 222 } 223 224 return mConfigInternalOverrideForTests; 225 } 226 227 @Nullable readSafetyCenterConfig()228 private SafetyCenterConfig readSafetyCenterConfig() { 229 InputStream in = mSafetyCenterResourcesContext.getSafetyCenterConfig(); 230 if (in == null) { 231 Log.e(TAG, "Cannot get safety center config file, safety center will be disabled."); 232 return null; 233 } 234 235 Resources resources = mSafetyCenterResourcesContext.getResources(); 236 if (resources == null) { 237 Log.e(TAG, "Cannot get safety center resources, safety center will be disabled."); 238 return null; 239 } 240 241 try { 242 SafetyCenterConfig safetyCenterConfig = 243 SafetyCenterConfigParser.parseXmlResource(in, resources); 244 Log.i(TAG, "SafetyCenterConfig read successfully"); 245 return safetyCenterConfig; 246 } catch (ParseException e) { 247 Log.e(TAG, "Cannot read SafetyCenterConfig, safety center will be disabled.", e); 248 return null; 249 } 250 } 251 252 /** Dumps state for debugging purposes. */ dump(PrintWriter fout)253 void dump(PrintWriter fout) { 254 fout.println("XML CONFIG"); 255 fout.println("\t" + mConfigInternalFromXml); 256 fout.println(); 257 fout.println("OVERRIDE CONFIG"); 258 fout.println("\t" + mConfigInternalOverrideForTests); 259 fout.println(); 260 } 261 262 /** A wrapper class around the parsed XML config. */ 263 private static final class SafetyCenterConfigInternal { 264 265 private final SafetyCenterConfig mConfig; 266 private final ArrayMap<String, ExternalSafetySource> mExternalSafetySources; 267 private final List<SafetySourcesGroup> mLoggableSourcesGroups; 268 private final List<Broadcast> mBroadcasts; 269 SafetyCenterConfigInternal( SafetyCenterConfig safetyCenterConfig, ArrayMap<String, ExternalSafetySource> externalSafetySources, List<SafetySourcesGroup> loggableSourcesGroups, List<Broadcast> broadcasts)270 private SafetyCenterConfigInternal( 271 SafetyCenterConfig safetyCenterConfig, 272 ArrayMap<String, ExternalSafetySource> externalSafetySources, 273 List<SafetySourcesGroup> loggableSourcesGroups, 274 List<Broadcast> broadcasts) { 275 mConfig = safetyCenterConfig; 276 mExternalSafetySources = externalSafetySources; 277 mLoggableSourcesGroups = loggableSourcesGroups; 278 mBroadcasts = broadcasts; 279 } 280 getSafetyCenterConfig()281 private SafetyCenterConfig getSafetyCenterConfig() { 282 return mConfig; 283 } 284 getExternalSafetySources()285 private ArrayMap<String, ExternalSafetySource> getExternalSafetySources() { 286 return mExternalSafetySources; 287 } 288 getLoggableSourcesGroups()289 private List<SafetySourcesGroup> getLoggableSourcesGroups() { 290 return mLoggableSourcesGroups; 291 } 292 getBroadcasts()293 private List<Broadcast> getBroadcasts() { 294 return mBroadcasts; 295 } 296 297 @Override equals(Object o)298 public boolean equals(Object o) { 299 if (this == o) return true; 300 if (!(o instanceof SafetyCenterConfigInternal)) return false; 301 SafetyCenterConfigInternal configInternal = (SafetyCenterConfigInternal) o; 302 return mConfig.equals(configInternal.mConfig); 303 } 304 305 @Override hashCode()306 public int hashCode() { 307 return Objects.hash(mConfig); 308 } 309 310 @Override toString()311 public String toString() { 312 return "SafetyCenterConfigInternal{" 313 + "mConfig=" 314 + mConfig 315 + ", mExternalSafetySources=" 316 + mExternalSafetySources 317 + ", mLoggableSourcesGroups=" 318 + mLoggableSourcesGroups 319 + ", mBroadcasts=" 320 + mBroadcasts 321 + '}'; 322 } 323 from(SafetyCenterConfig safetyCenterConfig)324 private static SafetyCenterConfigInternal from(SafetyCenterConfig safetyCenterConfig) { 325 return new SafetyCenterConfigInternal( 326 safetyCenterConfig, 327 extractExternalSafetySources(safetyCenterConfig), 328 extractLoggableSafetySourcesGroups(safetyCenterConfig), 329 unmodifiableList(extractBroadcasts(safetyCenterConfig))); 330 } 331 extractExternalSafetySources( SafetyCenterConfig safetyCenterConfig)332 private static ArrayMap<String, ExternalSafetySource> extractExternalSafetySources( 333 SafetyCenterConfig safetyCenterConfig) { 334 ArrayMap<String, ExternalSafetySource> externalSafetySources = new ArrayMap<>(); 335 List<SafetySourcesGroup> safetySourcesGroups = 336 safetyCenterConfig.getSafetySourcesGroups(); 337 for (int i = 0; i < safetySourcesGroups.size(); i++) { 338 SafetySourcesGroup safetySourcesGroup = safetySourcesGroups.get(i); 339 340 List<SafetySource> safetySources = safetySourcesGroup.getSafetySources(); 341 for (int j = 0; j < safetySources.size(); j++) { 342 SafetySource safetySource = safetySources.get(j); 343 344 if (!SafetySources.isExternal(safetySource)) { 345 continue; 346 } 347 348 boolean hasEntryInStatelessGroup = 349 safetySource.getType() == SafetySource.SAFETY_SOURCE_TYPE_DYNAMIC 350 && safetySourcesGroup.getType() 351 == SafetySourcesGroup 352 .SAFETY_SOURCES_GROUP_TYPE_STATELESS; 353 354 externalSafetySources.put( 355 safetySource.getId(), 356 new ExternalSafetySource(safetySource, hasEntryInStatelessGroup)); 357 } 358 } 359 360 return externalSafetySources; 361 } 362 extractLoggableSafetySourcesGroups( SafetyCenterConfig safetyCenterConfig)363 private static List<SafetySourcesGroup> extractLoggableSafetySourcesGroups( 364 SafetyCenterConfig safetyCenterConfig) { 365 List<SafetySourcesGroup> originalGroups = safetyCenterConfig.getSafetySourcesGroups(); 366 List<SafetySourcesGroup> filteredGroups = new ArrayList<>(originalGroups.size()); 367 368 for (int i = 0; i < originalGroups.size(); i++) { 369 SafetySourcesGroup originalGroup = originalGroups.get(i); 370 371 SafetySourcesGroup.Builder filteredGroupBuilder = 372 SafetySourcesGroups.copyToBuilderWithoutSources(originalGroup); 373 List<SafetySource> originalSources = originalGroup.getSafetySources(); 374 for (int j = 0; j < originalSources.size(); j++) { 375 SafetySource source = originalSources.get(j); 376 377 if (SafetySources.isLoggable(source)) { 378 filteredGroupBuilder.addSafetySource(source); 379 } 380 } 381 382 SafetySourcesGroup filteredGroup = filteredGroupBuilder.build(); 383 if (!filteredGroup.getSafetySources().isEmpty()) { 384 filteredGroups.add(filteredGroup); 385 } 386 } 387 388 return filteredGroups; 389 } 390 extractBroadcasts(SafetyCenterConfig safetyCenterConfig)391 private static List<Broadcast> extractBroadcasts(SafetyCenterConfig safetyCenterConfig) { 392 ArrayMap<String, Broadcast> packageNameToBroadcast = new ArrayMap<>(); 393 List<Broadcast> broadcasts = new ArrayList<>(); 394 List<SafetySourcesGroup> safetySourcesGroups = 395 safetyCenterConfig.getSafetySourcesGroups(); 396 for (int i = 0; i < safetySourcesGroups.size(); i++) { 397 SafetySourcesGroup safetySourcesGroup = safetySourcesGroups.get(i); 398 399 List<SafetySource> safetySources = safetySourcesGroup.getSafetySources(); 400 for (int j = 0; j < safetySources.size(); j++) { 401 SafetySource safetySource = safetySources.get(j); 402 403 if (!SafetySources.isExternal(safetySource)) { 404 continue; 405 } 406 407 Broadcast broadcast = packageNameToBroadcast.get(safetySource.getPackageName()); 408 if (broadcast == null) { 409 broadcast = new Broadcast(safetySource.getPackageName()); 410 packageNameToBroadcast.put(safetySource.getPackageName(), broadcast); 411 broadcasts.add(broadcast); 412 } 413 broadcast.mSourceIdsForProfileParent.add(safetySource.getId()); 414 if (safetySource.isRefreshOnPageOpenAllowed()) { 415 broadcast.mSourceIdsForProfileParentOnPageOpen.add(safetySource.getId()); 416 } 417 boolean needsManagedProfilesBroadcast = 418 SafetySources.supportsManagedProfiles(safetySource); 419 if (needsManagedProfilesBroadcast) { 420 broadcast.mSourceIdsForManagedProfiles.add(safetySource.getId()); 421 if (safetySource.isRefreshOnPageOpenAllowed()) { 422 broadcast.mSourceIdsForManagedProfilesOnPageOpen.add( 423 safetySource.getId()); 424 } 425 } 426 } 427 } 428 429 return broadcasts; 430 } 431 } 432 433 /** 434 * A wrapper class around a {@link SafetySource} that is providing data externally. 435 * 436 * @hide 437 */ 438 public static final class ExternalSafetySource { 439 private final SafetySource mSafetySource; 440 private final boolean mHasEntryInStatelessGroup; 441 ExternalSafetySource(SafetySource safetySource, boolean hasEntryInStatelessGroup)442 private ExternalSafetySource(SafetySource safetySource, boolean hasEntryInStatelessGroup) { 443 mSafetySource = safetySource; 444 mHasEntryInStatelessGroup = hasEntryInStatelessGroup; 445 } 446 447 /** Returns the external {@link SafetySource}. */ getSafetySource()448 public SafetySource getSafetySource() { 449 return mSafetySource; 450 } 451 452 /** 453 * Returns whether the external {@link SafetySource} has an entry in a stateless {@link 454 * SafetySourcesGroup}. 455 */ hasEntryInStatelessGroup()456 public boolean hasEntryInStatelessGroup() { 457 return mHasEntryInStatelessGroup; 458 } 459 460 @Override equals(Object o)461 public boolean equals(Object o) { 462 if (this == o) return true; 463 if (!(o instanceof ExternalSafetySource)) return false; 464 ExternalSafetySource that = (ExternalSafetySource) o; 465 return mHasEntryInStatelessGroup == that.mHasEntryInStatelessGroup 466 && mSafetySource.equals(that.mSafetySource); 467 } 468 469 @Override hashCode()470 public int hashCode() { 471 return Objects.hash(mSafetySource, mHasEntryInStatelessGroup); 472 } 473 474 @Override toString()475 public String toString() { 476 return "ExternalSafetySource{" 477 + "mSafetySource=" 478 + mSafetySource 479 + ", mHasEntryInStatelessGroup=" 480 + mHasEntryInStatelessGroup 481 + '}'; 482 } 483 } 484 485 /** A class that represents a broadcast to be sent to safety sources. */ 486 static final class Broadcast { 487 488 private final String mPackageName; 489 490 private final List<String> mSourceIdsForProfileParent = new ArrayList<>(); 491 private final List<String> mSourceIdsForProfileParentOnPageOpen = new ArrayList<>(); 492 private final List<String> mSourceIdsForManagedProfiles = new ArrayList<>(); 493 private final List<String> mSourceIdsForManagedProfilesOnPageOpen = new ArrayList<>(); 494 Broadcast(String packageName)495 private Broadcast(String packageName) { 496 mPackageName = packageName; 497 } 498 499 /** Returns the package name to dispatch the broadcast to. */ getPackageName()500 String getPackageName() { 501 return mPackageName; 502 } 503 504 /** 505 * Returns the safety source ids associated with this broadcast in the profile owner. 506 * 507 * <p>If this list is empty, there are no sources to dispatch to in the profile owner. 508 */ getSourceIdsForProfileParent()509 List<String> getSourceIdsForProfileParent() { 510 return unmodifiableList(mSourceIdsForProfileParent); 511 } 512 513 /** 514 * Returns the safety source ids associated with this broadcast in the profile owner that 515 * have refreshOnPageOpenAllowed set to true in the XML config. 516 * 517 * <p>If this list is empty, there are no sources to dispatch to in the profile owner. 518 */ getSourceIdsForProfileParentOnPageOpen()519 List<String> getSourceIdsForProfileParentOnPageOpen() { 520 return unmodifiableList(mSourceIdsForProfileParentOnPageOpen); 521 } 522 523 /** 524 * Returns the safety source ids associated with this broadcast in the managed profile(s). 525 * 526 * <p>If this list is empty, there are no sources to dispatch to in the managed profile(s). 527 */ getSourceIdsForManagedProfiles()528 List<String> getSourceIdsForManagedProfiles() { 529 return unmodifiableList(mSourceIdsForManagedProfiles); 530 } 531 532 /** 533 * Returns the safety source ids associated with this broadcast in the managed profile(s) 534 * that have refreshOnPageOpenAllowed set to true in the XML config. 535 * 536 * <p>If this list is empty, there are no sources to dispatch to in the managed profile(s). 537 */ getSourceIdsForManagedProfilesOnPageOpen()538 List<String> getSourceIdsForManagedProfilesOnPageOpen() { 539 return unmodifiableList(mSourceIdsForManagedProfilesOnPageOpen); 540 } 541 542 @Override equals(Object o)543 public boolean equals(Object o) { 544 if (this == o) return true; 545 if (!(o instanceof Broadcast)) return false; 546 Broadcast that = (Broadcast) o; 547 return mPackageName.equals(that.mPackageName) 548 && mSourceIdsForProfileParent.equals(that.mSourceIdsForProfileParent) 549 && mSourceIdsForProfileParentOnPageOpen.equals( 550 that.mSourceIdsForProfileParentOnPageOpen) 551 && mSourceIdsForManagedProfiles.equals(that.mSourceIdsForManagedProfiles) 552 && mSourceIdsForManagedProfilesOnPageOpen.equals( 553 that.mSourceIdsForManagedProfilesOnPageOpen); 554 } 555 556 @Override hashCode()557 public int hashCode() { 558 return Objects.hash( 559 mPackageName, 560 mSourceIdsForProfileParent, 561 mSourceIdsForProfileParentOnPageOpen, 562 mSourceIdsForManagedProfiles, 563 mSourceIdsForManagedProfilesOnPageOpen); 564 } 565 566 @Override toString()567 public String toString() { 568 return "Broadcast{" 569 + "mPackageName='" 570 + mPackageName 571 + "', mSourceIdsForProfileParent=" 572 + mSourceIdsForProfileParent 573 + ", mSourceIdsForProfileParentOnPageOpen=" 574 + mSourceIdsForProfileParentOnPageOpen 575 + ", mSourceIdsForManagedProfiles=" 576 + mSourceIdsForManagedProfiles 577 + ", mSourceIdsForManagedProfilesOnPageOpen=" 578 + mSourceIdsForManagedProfilesOnPageOpen 579 + '}'; 580 } 581 } 582 } 583