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