1 /*
2  * Copyright 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 android.credentials.selection;
18 
19 import static android.credentials.flags.Flags.FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED;
20 import static android.credentials.flags.Flags.configurableSelectorUiEnabled;
21 
22 import android.annotation.FlaggedApi;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.SuppressLint;
26 import android.annotation.TestApi;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.pm.ActivityInfo;
31 import android.content.pm.PackageManager;
32 import android.content.res.Resources;
33 import android.os.IBinder;
34 import android.os.Parcel;
35 import android.os.ResultReceiver;
36 import android.text.TextUtils;
37 import android.util.Slog;
38 
39 import com.android.internal.annotations.VisibleForTesting;
40 
41 import java.util.ArrayList;
42 
43 /**
44  * Helpers for generating the intents and related extras parameters to launch the UI activities.
45  *
46  * @hide
47  */
48 @TestApi
49 @FlaggedApi(FLAG_CONFIGURABLE_SELECTOR_UI_ENABLED)
50 public class IntentFactory {
51 
52     /**
53      * Generate a new launch intent to the Credential Selector UI for auto-filling. This intent is
54      * invoked from the Autofill flow, when the user requests to bring up the 'All Options' page of
55      * the credential bottom-sheet. When the user clicks on the pinned entry, the intent will bring
56      * up the 'All Options' page of the bottom-sheet. The provider data list is processed by the
57      * credential autofill service for each autofill id and passed in as an auth extra.
58      *
59      * @hide
60      */
61     @NonNull
createCredentialSelectorIntentForAutofill( @onNull Context context, @NonNull RequestInfo requestInfo, @SuppressLint("ConcreteCollection") @NonNull ArrayList<DisabledProviderData> disabledProviderDataList, @NonNull ResultReceiver resultReceiver)62     public static IntentCreationResult createCredentialSelectorIntentForAutofill(
63             @NonNull Context context,
64             @NonNull RequestInfo requestInfo,
65             @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
66             @NonNull
67             ArrayList<DisabledProviderData> disabledProviderDataList,
68             @NonNull ResultReceiver resultReceiver) {
69         return createCredentialSelectorIntentInternal(context, requestInfo,
70                 disabledProviderDataList, resultReceiver);
71     }
72 
73     /**
74      * Generate a new launch intent to the Credential Selector UI.
75      *
76      * @param context                  the CredentialManager system service (only expected caller)
77      *                                 context that may be used to query existence of the key UI
78      *                                 application
79      * @param disabledProviderDataList the list of disabled provider data that when non-empty the
80      *                                 UI should accordingly generate an entry suggesting the user
81      *                                 to navigate to settings and enable them
82      * @param enabledProviderDataList  the list of enabled provider that contain options for this
83      *                                 request; the UI should render each option to the user for
84      *                                 selection
85      * @param requestInfo              the display information about the given app request
86      * @param resultReceiver           used by the UI to send the UI selection result back
87      * @hide
88      */
89     @NonNull
createCredentialSelectorIntentForCredMan( @onNull Context context, @NonNull RequestInfo requestInfo, @SuppressLint("ConcreteCollection") @NonNull ArrayList<ProviderData> enabledProviderDataList, @SuppressLint("ConcreteCollection") @NonNull ArrayList<DisabledProviderData> disabledProviderDataList, @NonNull ResultReceiver resultReceiver)90     public static IntentCreationResult createCredentialSelectorIntentForCredMan(
91             @NonNull Context context,
92             @NonNull RequestInfo requestInfo,
93             @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
94             @NonNull
95             ArrayList<ProviderData> enabledProviderDataList,
96             @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
97             @NonNull
98             ArrayList<DisabledProviderData> disabledProviderDataList,
99             @NonNull ResultReceiver resultReceiver) {
100         IntentCreationResult result = createCredentialSelectorIntentInternal(context, requestInfo,
101                 disabledProviderDataList, resultReceiver);
102         result.getIntent().putParcelableArrayListExtra(
103                 ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, enabledProviderDataList);
104         return result;
105     }
106 
107     /**
108      * Generate a new launch intent to the Credential Selector UI.
109      *
110      * @param context                  the CredentialManager system service (only expected caller)
111      *                                 context that may be used to query existence of the key UI
112      *                                 application
113      * @param disabledProviderDataList the list of disabled provider data that when non-empty the
114      *                                 UI should accordingly generate an entry suggesting the user
115      *                                 to navigate to settings and enable them
116      * @param enabledProviderDataList  the list of enabled provider that contain options for this
117      *                                 request; the UI should render each option to the user for
118      *                                 selection
119      * @param requestInfo              the display information about the given app request
120      * @param resultReceiver           used by the UI to send the UI selection result back
121      */
122     @VisibleForTesting
123     @NonNull
createCredentialSelectorIntent( @onNull Context context, @NonNull RequestInfo requestInfo, @SuppressLint("ConcreteCollection") @NonNull ArrayList<ProviderData> enabledProviderDataList, @SuppressLint("ConcreteCollection") @NonNull ArrayList<DisabledProviderData> disabledProviderDataList, @NonNull ResultReceiver resultReceiver)124     public static Intent createCredentialSelectorIntent(
125             @NonNull Context context,
126             @NonNull RequestInfo requestInfo,
127             @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
128             @NonNull
129             ArrayList<ProviderData> enabledProviderDataList,
130             @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
131             @NonNull
132             ArrayList<DisabledProviderData> disabledProviderDataList,
133             @NonNull ResultReceiver resultReceiver) {
134         return createCredentialSelectorIntentForCredMan(context, requestInfo,
135                 enabledProviderDataList, disabledProviderDataList, resultReceiver).getIntent();
136     }
137 
138     /**
139      * Creates an Intent that cancels any UI matching the given request token id.
140      */
141     @VisibleForTesting
142     @NonNull
createCancelUiIntent(@onNull Context context, @NonNull IBinder requestToken, boolean shouldShowCancellationUi, @NonNull String appPackageName)143     public static Intent createCancelUiIntent(@NonNull Context context,
144             @NonNull IBinder requestToken, boolean shouldShowCancellationUi,
145             @NonNull String appPackageName) {
146         Intent intent = new Intent();
147         IntentCreationResult.Builder intentResultBuilder = new IntentCreationResult.Builder(intent);
148         setCredentialSelectorUiComponentName(context, intent, intentResultBuilder);
149         intent.putExtra(CancelSelectionRequest.EXTRA_CANCEL_UI_REQUEST,
150                 new CancelSelectionRequest(new RequestToken(requestToken), shouldShowCancellationUi,
151                         appPackageName));
152         return intent;
153     }
154 
155     /**
156      * Generate a new launch intent to the Credential Selector UI.
157      */
158     @NonNull
createCredentialSelectorIntentInternal( @onNull Context context, @NonNull RequestInfo requestInfo, @SuppressLint("ConcreteCollection") @NonNull ArrayList<DisabledProviderData> disabledProviderDataList, @NonNull ResultReceiver resultReceiver)159     private static IntentCreationResult createCredentialSelectorIntentInternal(
160             @NonNull Context context,
161             @NonNull RequestInfo requestInfo,
162             @SuppressLint("ConcreteCollection") // Concrete collection needed for marshalling.
163             @NonNull
164             ArrayList<DisabledProviderData> disabledProviderDataList,
165             @NonNull ResultReceiver resultReceiver) {
166         Intent intent = new Intent();
167         IntentCreationResult.Builder intentResultBuilder = new IntentCreationResult.Builder(intent);
168         setCredentialSelectorUiComponentName(context, intent, intentResultBuilder);
169         intent.putParcelableArrayListExtra(
170                 ProviderData.EXTRA_DISABLED_PROVIDER_DATA_LIST, disabledProviderDataList);
171         intent.putExtra(RequestInfo.EXTRA_REQUEST_INFO, requestInfo);
172         intent.putExtra(
173                 Constants.EXTRA_RESULT_RECEIVER, toIpcFriendlyResultReceiver(resultReceiver));
174         return intentResultBuilder.build();
175     }
176 
setCredentialSelectorUiComponentName(@onNull Context context, @NonNull Intent intent, @NonNull IntentCreationResult.Builder intentResultBuilder)177     private static void setCredentialSelectorUiComponentName(@NonNull Context context,
178             @NonNull Intent intent, @NonNull IntentCreationResult.Builder intentResultBuilder) {
179         if (configurableSelectorUiEnabled()) {
180             ComponentName componentName = getOemOverrideComponentName(context, intentResultBuilder);
181 
182             ComponentName fallbackUiComponentName = null;
183             try {
184                 fallbackUiComponentName = ComponentName.unflattenFromString(
185                         Resources.getSystem().getString(
186                                 com.android.internal.R.string
187                                         .config_fallbackCredentialManagerDialogComponent));
188                 intentResultBuilder.setFallbackUiPackageName(
189                         fallbackUiComponentName.getPackageName());
190             } catch (Exception e) {
191                 Slog.w(TAG, "Fallback CredMan IU not found: " + e);
192             }
193 
194             if (componentName == null) {
195                 componentName = fallbackUiComponentName;
196             }
197 
198             intent.setComponent(componentName);
199         } else {
200             ComponentName componentName = ComponentName.unflattenFromString(Resources.getSystem()
201                     .getString(com.android.internal.R.string
202                             .config_fallbackCredentialManagerDialogComponent));
203             intent.setComponent(componentName);
204         }
205     }
206 
207     /**
208      * Returns null if there is not an enabled and valid oem override component. It means the
209      * default platform UI component name should be used instead.
210      */
211     @Nullable
getOemOverrideComponentName(@onNull Context context, @NonNull IntentCreationResult.Builder intentResultBuilder)212     private static ComponentName getOemOverrideComponentName(@NonNull Context context,
213             @NonNull IntentCreationResult.Builder intentResultBuilder) {
214         ComponentName result = null;
215         String oemComponentString =
216                 Resources.getSystem()
217                         .getString(
218                                 com.android.internal.R.string
219                                         .config_oemCredentialManagerDialogComponent);
220         if (!TextUtils.isEmpty(oemComponentString)) {
221             ComponentName oemComponentName = null;
222             try {
223                 oemComponentName = ComponentName.unflattenFromString(
224                         oemComponentString);
225             } catch (Exception e) {
226                 Slog.i(TAG, "Failed to parse OEM component name " + oemComponentString + ": " + e);
227             }
228             if (oemComponentName != null) {
229                 try {
230                     intentResultBuilder.setOemUiPackageName(oemComponentName.getPackageName());
231                     ActivityInfo info = context.getPackageManager().getActivityInfo(
232                             oemComponentName,
233                             PackageManager.ComponentInfoFlags.of(
234                                     PackageManager.MATCH_SYSTEM_ONLY));
235                     boolean oemComponentEnabled = info.enabled;
236                     int runtimeComponentEnabledState = context.getPackageManager()
237                           .getComponentEnabledSetting(oemComponentName);
238                     if (runtimeComponentEnabledState == PackageManager
239                           .COMPONENT_ENABLED_STATE_ENABLED) {
240                           oemComponentEnabled = true;
241                     } else if (runtimeComponentEnabledState == PackageManager
242                           .COMPONENT_ENABLED_STATE_DISABLED) {
243                         oemComponentEnabled = false;
244                     }
245                     if (oemComponentEnabled && info.exported) {
246                         intentResultBuilder.setOemUiUsageStatus(IntentCreationResult
247                                 .OemUiUsageStatus.SUCCESS);
248                         Slog.i(TAG,
249                                 "Found enabled oem CredMan UI component."
250                                         + oemComponentString);
251                         result = oemComponentName;
252                     } else {
253                         intentResultBuilder.setOemUiUsageStatus(IntentCreationResult
254                                 .OemUiUsageStatus.OEM_UI_CONFIG_SPECIFIED_FOUND_BUT_NOT_ENABLED);
255                         Slog.i(TAG,
256                                 "Found enabled oem CredMan UI component but it was not "
257                                         + "enabled.");
258                     }
259                 } catch (PackageManager.NameNotFoundException e) {
260                     intentResultBuilder.setOemUiUsageStatus(IntentCreationResult.OemUiUsageStatus
261                             .OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND);
262                     Slog.i(TAG, "Unable to find oem CredMan UI component: "
263                             + oemComponentString + ".");
264                 }
265             } else {
266                 intentResultBuilder.setOemUiUsageStatus(IntentCreationResult.OemUiUsageStatus
267                         .OEM_UI_CONFIG_SPECIFIED_BUT_NOT_FOUND);
268                 Slog.i(TAG, "Invalid OEM ComponentName format.");
269             }
270         } else {
271             intentResultBuilder.setOemUiUsageStatus(
272                     IntentCreationResult.OemUiUsageStatus.OEM_UI_CONFIG_NOT_SPECIFIED);
273             Slog.i(TAG, "Invalid empty OEM component name.");
274         }
275         return result;
276     }
277 
278     /**
279      * Convert an instance of a "locally-defined" ResultReceiver to an instance of {@link
280      * android.os.ResultReceiver} itself, which the receiving process will be able to unmarshall.
281      */
toIpcFriendlyResultReceiver( T resultReceiver)282     private static <T extends ResultReceiver> ResultReceiver toIpcFriendlyResultReceiver(
283             T resultReceiver) {
284         final Parcel parcel = Parcel.obtain();
285         resultReceiver.writeToParcel(parcel, 0);
286         parcel.setDataPosition(0);
287 
288         final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
289         parcel.recycle();
290 
291         return ipcFriendly;
292     }
293 
IntentFactory()294     private IntentFactory() {
295     }
296 
297     private static final String TAG = "CredManIntentHelper";
298 }
299