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 }