• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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