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.role.controller.behavior.v35;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.pm.PackageManager;
23 import android.content.pm.ResolveInfo;
24 import android.content.pm.ServiceInfo;
25 import android.nfc.cardemulation.ApduServiceInfo;
26 import android.nfc.cardemulation.CardEmulation;
27 import android.nfc.cardemulation.HostApduService;
28 import android.nfc.cardemulation.OffHostApduService;
29 import android.os.Build;
30 import android.os.UserHandle;
31 import android.permission.flags.Flags;
32 import android.service.quickaccesswallet.QuickAccessWalletService;
33 import android.util.ArraySet;
34 import android.util.Log;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 import androidx.annotation.RequiresApi;
39 
40 import com.android.modules.utils.build.SdkLevel;
41 import com.android.role.controller.model.Role;
42 import com.android.role.controller.model.RoleBehavior;
43 import com.android.role.controller.util.CollectionUtils;
44 import com.android.role.controller.util.UserUtils;
45 
46 import org.xmlpull.v1.XmlPullParserException;
47 
48 import java.io.IOException;
49 import java.util.ArrayList;
50 import java.util.Collections;
51 import java.util.List;
52 import java.util.Set;
53 
54 /**
55  * Handles the behavior of the wallet role.
56  */
57 @RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
58 public class WalletRoleBehavior implements RoleBehavior {
59 
60     private static final String LOG_TAG = WalletRoleBehavior.class.getSimpleName();
61 
62     @Override
isAvailableAsUser(@onNull Role role, @NonNull UserHandle user, @NonNull Context context)63     public boolean isAvailableAsUser(@NonNull Role role, @NonNull UserHandle user,
64             @NonNull Context context) {
65         return SdkLevel.isAtLeastV() && Flags.walletRoleEnabled()
66                 && !UserUtils.isProfile(user, context);
67     }
68 
69     @Nullable
70     @Override
getDefaultHoldersAsUser(@onNull Role role, @NonNull UserHandle user, @NonNull Context context)71     public List<String> getDefaultHoldersAsUser(@NonNull Role role, @NonNull UserHandle user,
72             @NonNull Context context) {
73         Context userContext = UserUtils.getUserContext(context, user);
74         ComponentName preferredPaymentService =
75                 CardEmulation.getPreferredPaymentService(userContext);
76         if (preferredPaymentService != null) {
77             return Collections.singletonList(preferredPaymentService.getPackageName());
78         }
79 
80         return null;
81     }
82 
83     @Nullable
84     @Override
getFallbackHolderAsUser(@onNull Role role, @NonNull UserHandle user, @NonNull Context context)85     public String getFallbackHolderAsUser(@NonNull Role role, @NonNull UserHandle user,
86             @NonNull Context context) {
87         return CollectionUtils.firstOrNull(role.getDefaultHoldersAsUser(user, context));
88     }
89 
90     @Nullable
91     @Override
isPackageQualifiedAsUser(@onNull Role role, @NonNull String packageName, @NonNull UserHandle user, @NonNull Context context)92     public Boolean isPackageQualifiedAsUser(@NonNull Role role, @NonNull String packageName,
93             @NonNull UserHandle user, @NonNull Context context) {
94         return !getQualifyingPackageNamesInternal(packageName, user, context).isEmpty();
95     }
96 
97     @Nullable
98     @Override
getQualifyingPackagesAsUser(@onNull Role role, @NonNull UserHandle user, @NonNull Context context)99     public List<String> getQualifyingPackagesAsUser(@NonNull Role role, @NonNull UserHandle user,
100             @NonNull Context context) {
101         return new ArrayList<>(getQualifyingPackageNamesInternal(null, user, context));
102     }
103 
104     @NonNull
getQualifyingPackageNamesInternal(@ullable String packageName, @NonNull UserHandle user, @NonNull Context context)105     private static Set<String> getQualifyingPackageNamesInternal(@Nullable String packageName,
106             @NonNull UserHandle user, @NonNull Context context) {
107         Set<String> packageNames = resolvePackageNames(QuickAccessWalletService.SERVICE_INTERFACE,
108                 packageName, user, context);
109         if (isNfcHostCardEmulationSupported(context)) {
110             packageNames.addAll(getQualifyingApduServicesAsUser(packageName, false, user,
111                     context));
112             packageNames.addAll(getQualifyingApduServicesAsUser(packageName, true, user,
113                     context));
114         }
115         return packageNames;
116     }
117 
118     @NonNull
resolvePackageNames(@onNull String action, @Nullable String packageName, @NonNull UserHandle user, @NonNull Context context)119     private static Set<String> resolvePackageNames(@NonNull String action,
120             @Nullable String packageName, @NonNull UserHandle user, @NonNull Context context) {
121         Intent intent = new Intent(action).setPackage(packageName);
122         PackageManager packageManager = context.getPackageManager();
123         List<ResolveInfo> resolveInfos = packageManager
124                 .queryIntentServicesAsUser(intent, PackageManager.MATCH_DIRECT_BOOT_AWARE
125                         | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, user);
126         Set<String> packageNames = new ArraySet<>();
127         int resolveInfosSize = resolveInfos.size();
128         for (int i = 0; i < resolveInfosSize; i++) {
129             ServiceInfo serviceInfo = resolveInfos.get(i).serviceInfo;
130             if (!serviceInfo.exported) {
131                 continue;
132             }
133             packageNames.add(serviceInfo.packageName);
134         }
135         return packageNames;
136     }
137 
138     @NonNull
getQualifyingApduServicesAsUser(@ullable String packageName, boolean onHost, @NonNull UserHandle user, @NonNull Context context)139     private static Set<String> getQualifyingApduServicesAsUser(@Nullable String packageName,
140             boolean onHost, @NonNull UserHandle user, @NonNull Context context) {
141         Context userContext = UserUtils.getUserContext(context, user);
142         PackageManager userPackageManager = userContext.getPackageManager();
143         Intent intent = new Intent(
144                 onHost ? HostApduService.SERVICE_INTERFACE : OffHostApduService.SERVICE_INTERFACE)
145                 .setPackage(packageName);
146         List<ResolveInfo> resolveInfos = userPackageManager.queryIntentServices(intent,
147                 PackageManager.MATCH_DIRECT_BOOT_AWARE
148                         | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.GET_META_DATA);
149         Set<String> packageNames = new ArraySet<>();
150         int resolveInfosSize = resolveInfos.size();
151         for (int i = 0; i < resolveInfosSize; i++) {
152             ResolveInfo resolveInfo = resolveInfos.get(i);
153             ServiceInfo serviceInfo = resolveInfo.serviceInfo;
154             if (!serviceInfo.exported) {
155                 continue;
156             }
157             ApduServiceInfo apduServiceInfo;
158             try {
159                 apduServiceInfo = new ApduServiceInfo(userPackageManager, resolveInfo, onHost);
160             } catch (IOException | XmlPullParserException e) {
161                 Log.w(LOG_TAG, "Unable to create ApduServiceInfo for " + resolveInfo, e);
162                 continue;
163             }
164             if (apduServiceInfo.hasCategory(CardEmulation.CATEGORY_PAYMENT)) {
165                 packageNames.add(resolveInfo.serviceInfo.packageName);
166             }
167         }
168         return packageNames;
169     }
170 
isNfcHostCardEmulationSupported(@onNull Context context)171     private static boolean isNfcHostCardEmulationSupported(@NonNull Context context) {
172         return context.getPackageManager().hasSystemFeature(
173                 PackageManager.FEATURE_NFC_HOST_CARD_EMULATION);
174     }
175 }
176