1 /* 2 * Copyright (C) 2023 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.data; 18 19 import android.app.PendingIntent; 20 import android.content.ComponentName; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.content.res.Resources; 24 import android.safetycenter.SafetyCenterEntry; 25 import android.safetycenter.SafetySourceData; 26 import android.safetycenter.SafetySourceIssue; 27 import android.safetycenter.SafetySourceStatus; 28 import android.util.Log; 29 30 import com.android.modules.utils.build.SdkLevel; 31 import com.android.safetycenter.PendingIntentFactory; 32 import com.android.safetycenter.SafetyCenterFlags; 33 34 import java.util.List; 35 36 /** 37 * A class to work around an issue with the {@code AndroidLockScreen} safety source, by potentially 38 * overriding its {@link SafetySourceData}. 39 */ 40 final class AndroidLockScreenFix { 41 42 private static final String TAG = "AndroidLockScreenFix"; 43 44 private static final String ANDROID_LOCK_SCREEN_SOURCE_ID = "AndroidLockScreen"; 45 private static final int SUSPECT_REQ_CODE = 0; 46 // Arbitrary values to construct PendingIntents that are guaranteed not to be equal due to 47 // these request codes not being equal. The values match the ones in Settings QPR, in case we 48 // ever end up using these request codes in QPR. 49 private static final int ANDROID_LOCK_SCREEN_ENTRY_REQ_CODE = 1; 50 private static final int ANDROID_LOCK_SCREEN_ICON_ACTION_REQ_CODE = 2; 51 AndroidLockScreenFix()52 private AndroidLockScreenFix() {} 53 shouldApplyFix(String sourceId)54 static boolean shouldApplyFix(String sourceId) { 55 if (SdkLevel.isAtLeastU()) { 56 // No need to override on U+ as the issue has been fixed in a T QPR release. 57 // As such, U+ fields for the SafetySourceData are not taken into account in the methods 58 // below. 59 return false; 60 } 61 if (!ANDROID_LOCK_SCREEN_SOURCE_ID.equals(sourceId)) { 62 return false; 63 } 64 return SafetyCenterFlags.getReplaceLockScreenIconAction(); 65 } 66 67 /** 68 * Overrides the {@link SafetySourceData} of the {@code AndroidLockScreen} source by replacing 69 * its {@link PendingIntent}s. 70 * 71 * <p>This is done because of a bug in the Settings app where the {@link PendingIntent}s created 72 * end up referencing either the {@link SafetyCenterEntry#getPendingIntent()} or the {@link 73 * SafetyCenterEntry.IconAction#getPendingIntent()}. The reason for this is that {@link 74 * PendingIntent} instances are cached and keyed by an object which does not take into account 75 * the underlying {@link Intent} extras; and these two {@link Intent}s only differ by the extras 76 * that they set. 77 * 78 * <p>We fix this issue by recreating the desired {@link PendingIntent}s manually here, using 79 * different request codes for the different {@link PendingIntent}s to ensure new instances are 80 * created (the key does take into account the request code). 81 */ applyFix(Context context, SafetySourceData data)82 static SafetySourceData applyFix(Context context, SafetySourceData data) { 83 SafetySourceData.Builder overriddenData = 84 SafetySourceDataOverrides.copyDataToBuilderWithoutIssues(data); 85 86 SafetySourceStatus originalStatus = data.getStatus(); 87 if (originalStatus != null) { 88 overriddenData.setStatus(overrideTiramisuSafetySourceStatus(context, originalStatus)); 89 } 90 91 List<SafetySourceIssue> issues = data.getIssues(); 92 for (int i = 0; i < issues.size(); i++) { 93 overriddenData.addIssue(overrideTiramisuIssue(context, issues.get(i))); 94 } 95 96 return overriddenData.build(); 97 } 98 overrideTiramisuSafetySourceStatus( Context context, SafetySourceStatus status)99 private static SafetySourceStatus overrideTiramisuSafetySourceStatus( 100 Context context, SafetySourceStatus status) { 101 SafetySourceStatus.Builder overriddenStatus = 102 SafetySourceDataOverrides.copyStatusToBuilder(status); 103 104 PendingIntent originalPendingIntent = status.getPendingIntent(); 105 if (originalPendingIntent != null) { 106 overriddenStatus.setPendingIntent( 107 overridePendingIntent( 108 context, originalPendingIntent, /* isIconAction= */ false)); 109 } 110 111 SafetySourceStatus.IconAction iconAction = status.getIconAction(); 112 if (iconAction != null) { 113 overriddenStatus.setIconAction( 114 overrideTiramisuIconAction(context, status.getIconAction())); 115 } 116 117 return overriddenStatus.build(); 118 } 119 overrideTiramisuIconAction( Context context, SafetySourceStatus.IconAction iconAction)120 private static SafetySourceStatus.IconAction overrideTiramisuIconAction( 121 Context context, SafetySourceStatus.IconAction iconAction) { 122 return new SafetySourceStatus.IconAction( 123 iconAction.getIconType(), 124 overridePendingIntent( 125 context, iconAction.getPendingIntent(), /* isIconAction= */ true)); 126 } 127 overrideTiramisuIssue( Context context, SafetySourceIssue issue)128 private static SafetySourceIssue overrideTiramisuIssue( 129 Context context, SafetySourceIssue issue) { 130 SafetySourceIssue.Builder overriddenIssue = 131 SafetySourceDataOverrides.copyIssueToBuilderWithoutActions(issue); 132 133 List<SafetySourceIssue.Action> actions = issue.getActions(); 134 for (int i = 0; i < actions.size(); i++) { 135 SafetySourceIssue.Action action = actions.get(i); 136 overriddenIssue.addAction(overrideTiramisuIssueAction(context, action)); 137 } 138 139 return overriddenIssue.build(); 140 } 141 overrideTiramisuIssueAction( Context context, SafetySourceIssue.Action action)142 private static SafetySourceIssue.Action overrideTiramisuIssueAction( 143 Context context, SafetySourceIssue.Action action) { 144 PendingIntent pendingIntent = 145 overridePendingIntent( 146 context, action.getPendingIntent(), /* isIconAction= */ false); 147 return SafetySourceDataOverrides.overrideActionPendingIntent(action, pendingIntent); 148 } 149 overridePendingIntent( Context context, PendingIntent pendingIntent, boolean isIconAction)150 private static PendingIntent overridePendingIntent( 151 Context context, PendingIntent pendingIntent, boolean isIconAction) { 152 String settingsPackageName = pendingIntent.getCreatorPackage(); 153 int userId = pendingIntent.getCreatorUserHandle().getIdentifier(); 154 Context settingsPackageContext = 155 PendingIntentFactory.createPackageContextAsUser( 156 context, settingsPackageName, userId); 157 if (settingsPackageContext == null) { 158 return pendingIntent; 159 } 160 if (hasFixedSettingsIssue(settingsPackageContext)) { 161 return pendingIntent; 162 } 163 PendingIntent suspectPendingIntent = 164 PendingIntentFactory.getNullableActivityPendingIntent( 165 settingsPackageContext, 166 SUSPECT_REQ_CODE, 167 newBaseLockScreenIntent(settingsPackageName), 168 PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_NO_CREATE); 169 if (suspectPendingIntent == null) { 170 // Nothing was cached. 171 return pendingIntent; 172 } 173 if (!suspectPendingIntent.equals(pendingIntent)) { 174 // The pending intent is not hitting this caching issue, so we should skip the override. 175 return pendingIntent; 176 } 177 // We’re most likely hitting the caching issue described in this method’s documentation, so 178 // we should ensure we create brand new pending intents where applicable by using different 179 // request codes. We only perform this override for the applicable pending intents. 180 // This is important because there are scenarios where the Settings app provides different 181 // pending intents (e.g. in the work profile), and in this case we shouldn't override them. 182 if (isIconAction) { 183 Log.i( 184 TAG, 185 "Replacing " + ANDROID_LOCK_SCREEN_SOURCE_ID + " icon action pending intent"); 186 return PendingIntentFactory.getActivityPendingIntent( 187 settingsPackageContext, 188 ANDROID_LOCK_SCREEN_ICON_ACTION_REQ_CODE, 189 newLockScreenIconActionIntent(settingsPackageName), 190 PendingIntent.FLAG_IMMUTABLE); 191 } 192 Log.i(TAG, "Replacing " + ANDROID_LOCK_SCREEN_SOURCE_ID + " entry or issue pending intent"); 193 return PendingIntentFactory.getActivityPendingIntent( 194 settingsPackageContext, 195 ANDROID_LOCK_SCREEN_ENTRY_REQ_CODE, 196 newLockScreenIntent(settingsPackageName), 197 PendingIntent.FLAG_IMMUTABLE); 198 } 199 hasFixedSettingsIssue(Context settingsPackageContext)200 private static boolean hasFixedSettingsIssue(Context settingsPackageContext) { 201 Resources settingsResources = settingsPackageContext.getResources(); 202 int hasSettingsFixedIssueResourceId = 203 settingsResources.getIdentifier( 204 "config_isSafetyCenterLockScreenPendingIntentFixed", 205 "bool", 206 settingsPackageContext.getPackageName()); 207 if (hasSettingsFixedIssueResourceId != Resources.ID_NULL) { 208 return settingsResources.getBoolean(hasSettingsFixedIssueResourceId); 209 } 210 return false; 211 } 212 newBaseLockScreenIntent(String settingsPackageName)213 private static Intent newBaseLockScreenIntent(String settingsPackageName) { 214 return new Intent(Intent.ACTION_MAIN) 215 .setComponent( 216 new ComponentName( 217 settingsPackageName, settingsPackageName + ".SubSettings")) 218 .putExtra(":settings:source_metrics", 1917); 219 } 220 newLockScreenIntent(String settingsPackageName)221 private static Intent newLockScreenIntent(String settingsPackageName) { 222 String targetFragment = 223 settingsPackageName + ".password.ChooseLockGeneric$ChooseLockGenericFragment"; 224 return newBaseLockScreenIntent(settingsPackageName) 225 .putExtra(":settings:show_fragment", targetFragment) 226 .putExtra("page_transition_type", 1); 227 } 228 newLockScreenIconActionIntent(String settingsPackageName)229 private static Intent newLockScreenIconActionIntent(String settingsPackageName) { 230 String targetFragment = settingsPackageName + ".security.screenlock.ScreenLockSettings"; 231 return newBaseLockScreenIntent(settingsPackageName) 232 .putExtra(":settings:show_fragment", targetFragment) 233 .putExtra("page_transition_type", 0); 234 } 235 } 236