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