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