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 java.util.Objects.requireNonNull;
20 
21 import android.annotation.UserIdInt;
22 import android.app.PendingIntent;
23 import android.content.Context;
24 import android.content.Intent;
25 import android.content.pm.ActivityInfo;
26 import android.content.pm.PackageManager;
27 import android.content.pm.PackageManager.ResolveInfoFlags;
28 import android.content.pm.ResolveInfo;
29 import android.os.Binder;
30 import android.os.UserHandle;
31 import android.util.Log;
32 
33 import androidx.annotation.Nullable;
34 
35 import com.android.safetycenter.resources.SafetyCenterResourcesApk;
36 
37 import java.util.Arrays;
38 
39 /**
40  * Helps build or retrieve {@link PendingIntent} instances.
41  *
42  * @hide
43  */
44 public final class PendingIntentFactory {
45 
46     private static final String TAG = "PendingIntentFactory";
47 
48     private static final int DEFAULT_REQUEST_CODE = 0;
49 
50     private static final String IS_SETTINGS_HOMEPAGE = "is_from_settings_homepage";
51 
52     private final Context mContext;
53     private final SafetyCenterResourcesApk mSafetyCenterResourcesApk;
54 
PendingIntentFactory(Context context, SafetyCenterResourcesApk safetyCenterResourcesApk)55     PendingIntentFactory(Context context, SafetyCenterResourcesApk safetyCenterResourcesApk) {
56         mContext = context;
57         mSafetyCenterResourcesApk = safetyCenterResourcesApk;
58     }
59 
60     /**
61      * Creates or retrieves a {@link PendingIntent} that will start a new {@code Activity} matching
62      * the given {@code intentAction}.
63      *
64      * <p>If the given {@code intentAction} resolves, the {@link PendingIntent} will use an implicit
65      * {@link Intent}. Otherwise, the {@link PendingIntent} will explicitly target the {@code
66      * packageName} if it resolves.
67      *
68      * <p>The {@code PendingIntent} is associated with a specific source given by {@code sourceId}.
69      *
70      * <p>Returns {@code null} if the required {@link PendingIntent} cannot be created or if there
71      * is no valid target for the given {@code intentAction}.
72      */
73     @Nullable
getPendingIntent( String sourceId, @Nullable String intentAction, String packageName, @UserIdInt int userId, boolean isQuietModeEnabled)74     public PendingIntent getPendingIntent(
75             String sourceId,
76             @Nullable String intentAction,
77             String packageName,
78             @UserIdInt int userId,
79             boolean isQuietModeEnabled) {
80         if (intentAction == null) {
81             return null;
82         }
83         Context packageContext = createPackageContextAsUser(mContext, packageName, userId);
84         if (packageContext == null) {
85             return null;
86         }
87         Intent intent = createIntent(packageContext, sourceId, intentAction, isQuietModeEnabled);
88         if (intent == null) {
89             return null;
90         }
91         return getActivityPendingIntent(
92                 packageContext, DEFAULT_REQUEST_CODE, intent, PendingIntent.FLAG_IMMUTABLE);
93     }
94 
95     @Nullable
createIntent( Context packageContext, String sourceId, String intentAction, boolean isQuietModeEnabled)96     private Intent createIntent(
97             Context packageContext,
98             String sourceId,
99             String intentAction,
100             boolean isQuietModeEnabled) {
101         Intent intent = new Intent(intentAction);
102 
103         if (shouldAddSettingsHomepageExtra(sourceId)) {
104             // Identify this intent as coming from Settings. Because this intent is actually coming
105             // from Safety Center, which is served by PermissionController, this is useful to
106             // indicate that it is presented as part of the Settings app.
107             //
108             // In particular, the AOSP Settings app uses this to ensure that two-pane mode works
109             // correctly.
110             intent.putExtra(IS_SETTINGS_HOMEPAGE, true);
111             // Given we've added an extra to this intent, set an ID on it to ensure that it is not
112             // considered equal to the same intent without the extra. PendingIntents are cached
113             // using Intent equality as the key, and we want to make sure the extra is propagated.
114             intent.setIdentifier("with_settings_homepage_extra");
115         }
116 
117         if (intentResolvesToActivity(packageContext, intent)) {
118             return intent;
119         }
120 
121         // If the intent resolves for the package provided, then we make the assumption that it is
122         // the desired app and make the intent explicit. This is to workaround implicit internal
123         // intents that may not be exported which will stop working on Android U+.
124         Intent explicitIntent = new Intent(intent).setPackage(packageContext.getPackageName());
125         if (intentResolvesToActivity(packageContext, explicitIntent)) {
126             return explicitIntent;
127         }
128 
129         // resolveActivity does not return any activity when the work profile is in quiet mode, even
130         // though it opens the quiet mode dialog and/or the original intent would otherwise resolve
131         // when quiet mode is turned off. So, we assume that the explicit intent will always resolve
132         // to this dialog. This heuristic is preferable on U+ as it has a higher chance of resolving
133         // once the work profile is enabled considering the implicit internal intent restriction.
134         if (isQuietModeEnabled) {
135             // TODO(b/266538628): Find a way to fix this, this heuristic isn't ideal.
136             return explicitIntent;
137         }
138 
139         return null;
140     }
141 
shouldAddSettingsHomepageExtra(String sourceId)142     private boolean shouldAddSettingsHomepageExtra(String sourceId) {
143         return Arrays.asList(
144                         mSafetyCenterResourcesApk
145                                 .getStringByName("config_useSettingsHomepageIntentExtra")
146                                 .split(","))
147                 .contains(sourceId);
148     }
149 
intentResolvesToActivity(Context packageContext, Intent intent)150     private static boolean intentResolvesToActivity(Context packageContext, Intent intent) {
151         ResolveInfo resolveInfo = resolveActivity(packageContext, intent);
152         if (resolveInfo == null) {
153             return false;
154         }
155         ActivityInfo activityInfo = resolveInfo.activityInfo;
156         if (activityInfo == null) {
157             return false;
158         }
159         boolean intentIsImplicit = intent.getPackage() == null && intent.getComponent() == null;
160         if (intentIsImplicit) {
161             return activityInfo.exported;
162         }
163         return true;
164     }
165 
166     @Nullable
resolveActivity(Context packageContext, Intent intent)167     private static ResolveInfo resolveActivity(Context packageContext, Intent intent) {
168         PackageManager packageManager = packageContext.getPackageManager();
169         // This call requires the INTERACT_ACROSS_USERS permission as the `packageContext` could
170         // belong to another user.
171         final long callingId = Binder.clearCallingIdentity();
172         try {
173             return packageManager.resolveActivity(intent, ResolveInfoFlags.of(0));
174         } finally {
175             Binder.restoreCallingIdentity(callingId);
176         }
177     }
178 
179     /**
180      * Creates a {@link PendingIntent} to start an Activity from the given {@code packageContext}.
181      *
182      * <p>This function can only return {@code null} if the {@link PendingIntent#FLAG_NO_CREATE}
183      * flag is passed in.
184      */
185     @Nullable
getNullableActivityPendingIntent( Context packageContext, int requestCode, Intent intent, int flags)186     public static PendingIntent getNullableActivityPendingIntent(
187             Context packageContext, int requestCode, Intent intent, int flags) {
188         // This call requires Binder identity to be cleared for getIntentSender() to be allowed to
189         // send as another package.
190         final long callingId = Binder.clearCallingIdentity();
191         try {
192             return PendingIntent.getActivity(packageContext, requestCode, intent, flags);
193         } finally {
194             Binder.restoreCallingIdentity(callingId);
195         }
196     }
197 
198     /**
199      * Creates a {@link PendingIntent} to start an Activity from the given {@code packageContext}.
200      *
201      * <p>{@code flags} must not include {@link PendingIntent#FLAG_NO_CREATE}
202      */
getActivityPendingIntent( Context packageContext, int requestCode, Intent intent, int flags)203     public static PendingIntent getActivityPendingIntent(
204             Context packageContext, int requestCode, Intent intent, int flags) {
205         if ((flags & PendingIntent.FLAG_NO_CREATE) != 0) {
206             throw new IllegalArgumentException("flags must not include FLAG_NO_CREATE");
207         }
208         return requireNonNull(
209                 getNullableActivityPendingIntent(packageContext, requestCode, intent, flags));
210     }
211 
212     /**
213      * Creates a non-protected broadcast {@link PendingIntent} which can only be received by the
214      * system. Use this method to create PendingIntents to be received by Context-registered
215      * receivers, for example for notification-related callbacks.
216      *
217      * <p>{@code flags} must include {@link PendingIntent#FLAG_IMMUTABLE} and must not include
218      * {@link PendingIntent#FLAG_NO_CREATE}
219      */
getNonProtectedSystemOnlyBroadcastPendingIntent( Context context, int requestCode, Intent intent, int flags)220     public static PendingIntent getNonProtectedSystemOnlyBroadcastPendingIntent(
221             Context context, int requestCode, Intent intent, int flags) {
222         if ((flags & PendingIntent.FLAG_IMMUTABLE) == 0) {
223             throw new IllegalArgumentException("flags must include FLAG_IMMUTABLE");
224         }
225         if ((flags & PendingIntent.FLAG_NO_CREATE) != 0) {
226             throw new IllegalArgumentException("flags must not include FLAG_NO_CREATE");
227         }
228         intent.setPackage("android");
229         // This call is needed to be allowed to send the broadcast as the "android" package.
230         final long callingId = Binder.clearCallingIdentity();
231         try {
232             return PendingIntent.getBroadcast(context, requestCode, intent, flags);
233         } finally {
234             Binder.restoreCallingIdentity(callingId);
235         }
236     }
237 
238     /** Creates a {@link Context} for the given {@code packageName} and {@code userId}. */
239     @Nullable
createPackageContextAsUser( Context context, String packageName, @UserIdInt int userId)240     public static Context createPackageContextAsUser(
241             Context context, String packageName, @UserIdInt int userId) {
242         // This call requires the INTERACT_ACROSS_USERS permission.
243         final long callingId = Binder.clearCallingIdentity();
244         try {
245             return context.createPackageContextAsUser(
246                     packageName, /* flags= */ 0, UserHandle.of(userId));
247         } catch (PackageManager.NameNotFoundException e) {
248             Log.w(TAG, "Package name " + packageName + " not found", e);
249             return null;
250         } finally {
251             Binder.restoreCallingIdentity(callingId);
252         }
253     }
254 }
255