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