1 /*
2  * Copyright (C) 2018 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.server.telecom;
18 
19 import android.content.ComponentName;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.content.ServiceConnection;
23 import android.content.pm.PackageManager;
24 import android.content.pm.ResolveInfo;
25 import android.content.pm.ServiceInfo;
26 import android.net.Uri;
27 import android.os.Handler;
28 import android.os.IBinder;
29 import android.os.RemoteException;
30 import android.telecom.Log;
31 import android.telecom.Logging.Session;
32 import android.telecom.PhoneAccountHandle;
33 import android.telecom.PhoneAccountSuggestion;
34 import android.telecom.PhoneAccountSuggestionService;
35 import android.telephony.PhoneNumberUtils;
36 import android.text.TextUtils;
37 
38 import com.android.internal.telecom.IPhoneAccountSuggestionCallback;
39 import com.android.internal.telecom.IPhoneAccountSuggestionService;
40 
41 import java.util.List;
42 import java.util.concurrent.CompletableFuture;
43 import java.util.stream.Collectors;
44 import java.util.stream.Stream;
45 
46 public class PhoneAccountSuggestionHelper {
47     private static final String TAG = PhoneAccountSuggestionHelper.class.getSimpleName();
48     private static ComponentName sOverrideComponent;
49 
50     /**
51      * @return A future (possible already complete) that contains a list of suggestions.
52      */
53     public static CompletableFuture<List<PhoneAccountSuggestion>>
bindAndGetSuggestions(Context context, Uri handle, List<PhoneAccountHandle> availablePhoneAccounts)54     bindAndGetSuggestions(Context context, Uri handle,
55             List<PhoneAccountHandle> availablePhoneAccounts) {
56         // Use the default list if there's no handle
57         if (handle == null) {
58             return CompletableFuture.completedFuture(getDefaultSuggestions(availablePhoneAccounts));
59         }
60         String number = PhoneNumberUtils.extractNetworkPortion(handle.getSchemeSpecificPart());
61 
62         // Use the default list if there's no service on the device.
63         ServiceInfo suggestionServiceInfo = getSuggestionServiceInfo(context);
64         if (suggestionServiceInfo == null) {
65             return CompletableFuture.completedFuture(getDefaultSuggestions(availablePhoneAccounts));
66         }
67 
68         Intent bindIntent = new Intent();
69         bindIntent.setComponent(new ComponentName(suggestionServiceInfo.packageName,
70                 suggestionServiceInfo.name));
71 
72         final CompletableFuture<List<PhoneAccountSuggestion>> future = new CompletableFuture<>();
73 
74         final Session logSession = Log.createSubsession();
75         ServiceConnection serviceConnection = new ServiceConnection() {
76             @Override
77             public void onServiceConnected(ComponentName name, IBinder _service) {
78                 Log.continueSession(logSession, "PASH.oSC");
79                 try {
80                     IPhoneAccountSuggestionService service =
81                             IPhoneAccountSuggestionService.Stub.asInterface(_service);
82                     // Set up the callback to complete the future once the remote side comes
83                     // back with suggestions
84                     IPhoneAccountSuggestionCallback callback =
85                             new IPhoneAccountSuggestionCallback.Stub() {
86                                 @Override
87                                 public void suggestPhoneAccounts(String suggestResultNumber,
88                                         List<PhoneAccountSuggestion> suggestions) {
89                                     if (TextUtils.equals(number, suggestResultNumber)) {
90                                         if (suggestions == null) {
91                                             future.complete(
92                                                     getDefaultSuggestions(availablePhoneAccounts));
93                                         } else {
94                                             future.complete(
95                                                     addDefaultsToProvidedSuggestions(
96                                                             suggestions, availablePhoneAccounts));
97                                         }
98                                     }
99                                 }
100                             };
101                     try {
102                         service.onAccountSuggestionRequest(callback, number);
103                     } catch (RemoteException e) {
104                         Log.w(TAG, "Cancelling suggestion process due to remote exception");
105                         future.complete(getDefaultSuggestions(availablePhoneAccounts));
106                     }
107                 } finally {
108                     Log.endSession();
109                 }
110             }
111 
112             @Override
113             public void onServiceDisconnected(ComponentName name) {
114                 // No locking needed -- CompletableFuture only lets one thread call complete.
115                 Log.continueSession(logSession, "PASH.oSD");
116                 try {
117                     if (!future.isDone()) {
118                         Log.w(TAG, "Cancelling suggestion process due to service disconnect");
119                     }
120                     future.complete(getDefaultSuggestions(availablePhoneAccounts));
121                 } finally {
122                     Log.endSession();
123                 }
124             }
125         };
126 
127         if (!context.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE)) {
128             Log.i(TAG, "Cancelling suggestion process due to bind failure.");
129             future.complete(getDefaultSuggestions(availablePhoneAccounts));
130         }
131 
132         // Set up a timeout so that we're not waiting forever for the suggestion service.
133         Handler handler = new Handler();
134         handler.postDelayed(() -> {
135                     // No locking needed -- CompletableFuture only lets one thread call complete.
136                     Log.continueSession(logSession, "PASH.timeout");
137                     try {
138                         if (!future.isDone()) {
139                             Log.w(TAG, "Cancelling suggestion process due to timeout");
140                         }
141                         future.complete(getDefaultSuggestions(availablePhoneAccounts));
142                     } finally {
143                         Log.endSession();
144                     }
145                 },
146                 Timeouts.getPhoneAccountSuggestionServiceTimeout(context.getContentResolver()));
147         return future;
148     }
149 
addDefaultsToProvidedSuggestions( List<PhoneAccountSuggestion> providedSuggestions, List<PhoneAccountHandle> availableAccountHandles)150     private static List<PhoneAccountSuggestion> addDefaultsToProvidedSuggestions(
151             List<PhoneAccountSuggestion> providedSuggestions,
152             List<PhoneAccountHandle> availableAccountHandles) {
153         List<PhoneAccountHandle> handlesInSuggestions = providedSuggestions.stream()
154                 .map(PhoneAccountSuggestion::getPhoneAccountHandle)
155                 .collect(Collectors.toList());
156         List<PhoneAccountHandle> handlesToFillIn = availableAccountHandles.stream()
157                 .filter(handle -> !handlesInSuggestions.contains(handle))
158                 .collect(Collectors.toList());
159         List<PhoneAccountSuggestion> suggestionsToAppend = getDefaultSuggestions(handlesToFillIn);
160         return Stream.concat(suggestionsToAppend.stream(), providedSuggestions.stream())
161                 .collect( Collectors.toList());
162     }
163 
getSuggestionServiceInfo(Context context)164     private static ServiceInfo getSuggestionServiceInfo(Context context) {
165         PackageManager packageManager = context.getPackageManager();
166         Intent queryIntent = new Intent();
167         queryIntent.setAction(PhoneAccountSuggestionService.SERVICE_INTERFACE);
168 
169         List<ResolveInfo> services;
170         if (sOverrideComponent == null) {
171             services = packageManager.queryIntentServices(queryIntent,
172                     PackageManager.MATCH_SYSTEM_ONLY);
173         } else {
174             Log.i(TAG, "Using override component %s", sOverrideComponent);
175             queryIntent.setComponent(sOverrideComponent);
176             services = packageManager.queryIntentServices(queryIntent,
177                     PackageManager.MATCH_ALL);
178         }
179 
180         if (services == null || services.size() == 0) {
181             Log.i(TAG, "No acct suggestion services found. Using defaults.");
182             return null;
183         }
184 
185         if (services.size() > 1) {
186             Log.w(TAG, "More than acct suggestion service found, cannot get unique service");
187             return null;
188         }
189         return services.get(0).serviceInfo;
190     }
191 
setOverrideServiceName(String flattenedComponentName)192     static void setOverrideServiceName(String flattenedComponentName) {
193         try {
194             sOverrideComponent = TextUtils.isEmpty(flattenedComponentName)
195                     ? null : ComponentName.unflattenFromString(flattenedComponentName);
196         } catch (Exception e) {
197             sOverrideComponent = null;
198             throw e;
199         }
200     }
201 
getDefaultSuggestions( List<PhoneAccountHandle> phoneAccountHandles)202     private static List<PhoneAccountSuggestion> getDefaultSuggestions(
203             List<PhoneAccountHandle> phoneAccountHandles) {
204         return phoneAccountHandles.stream().map(phoneAccountHandle ->
205                 new PhoneAccountSuggestion(phoneAccountHandle,
206                         PhoneAccountSuggestion.REASON_NONE, false)
207         ).collect(Collectors.toList());
208     }
209 }