blob: 438ee683018b79031af1ba67dce694767b8eb518 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.telecom;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.telecom.Log;
import android.telecom.Logging.Session;
import android.telecom.PhoneAccountHandle;
import android.telecom.PhoneAccountSuggestion;
import android.telecom.PhoneAccountSuggestionService;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import com.android.internal.telecom.IPhoneAccountSuggestionCallback;
import com.android.internal.telecom.IPhoneAccountSuggestionService;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class PhoneAccountSuggestionHelper {
private static final String TAG = PhoneAccountSuggestionHelper.class.getSimpleName();
private static ComponentName sOverrideComponent;
/**
* @return A future (possible already complete) that contains a list of suggestions.
*/
public static CompletableFuture<List<PhoneAccountSuggestion>>
bindAndGetSuggestions(Context context, Uri handle,
List<PhoneAccountHandle> availablePhoneAccounts) {
// Use the default list if there's no handle
if (handle == null) {
return CompletableFuture.completedFuture(getDefaultSuggestions(availablePhoneAccounts));
}
String number = PhoneNumberUtils.extractNetworkPortion(handle.getSchemeSpecificPart());
// Use the default list if there's no service on the device.
ServiceInfo suggestionServiceInfo = getSuggestionServiceInfo(context);
if (suggestionServiceInfo == null) {
return CompletableFuture.completedFuture(getDefaultSuggestions(availablePhoneAccounts));
}
Intent bindIntent = new Intent();
bindIntent.setComponent(new ComponentName(suggestionServiceInfo.packageName,
suggestionServiceInfo.name));
final CompletableFuture<List<PhoneAccountSuggestion>> future = new CompletableFuture<>();
final Session logSession = Log.createSubsession();
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder _service) {
Log.continueSession(logSession, "PASH.oSC");
try {
IPhoneAccountSuggestionService service =
IPhoneAccountSuggestionService.Stub.asInterface(_service);
// Set up the callback to complete the future once the remote side comes
// back with suggestions
IPhoneAccountSuggestionCallback callback =
new IPhoneAccountSuggestionCallback.Stub() {
@Override
public void suggestPhoneAccounts(String suggestResultNumber,
List<PhoneAccountSuggestion> suggestions) {
if (TextUtils.equals(number, suggestResultNumber)) {
if (suggestions == null) {
future.complete(
getDefaultSuggestions(availablePhoneAccounts));
} else {
future.complete(
addDefaultsToProvidedSuggestions(
suggestions, availablePhoneAccounts));
}
}
}
};
try {
service.onAccountSuggestionRequest(callback, number);
} catch (RemoteException e) {
Log.w(TAG, "Cancelling suggestion process due to remote exception");
future.complete(getDefaultSuggestions(availablePhoneAccounts));
}
} finally {
Log.endSession();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
// No locking needed -- CompletableFuture only lets one thread call complete.
Log.continueSession(logSession, "PASH.oSD");
try {
if (!future.isDone()) {
Log.w(TAG, "Cancelling suggestion process due to service disconnect");
}
future.complete(getDefaultSuggestions(availablePhoneAccounts));
} finally {
Log.endSession();
}
}
};
if (!context.bindService(bindIntent, serviceConnection, Context.BIND_AUTO_CREATE)) {
Log.i(TAG, "Cancelling suggestion process due to bind failure.");
future.complete(getDefaultSuggestions(availablePhoneAccounts));
}
// Set up a timeout so that we're not waiting forever for the suggestion service.
Handler handler = new Handler();
handler.postDelayed(() -> {
// No locking needed -- CompletableFuture only lets one thread call complete.
Log.continueSession(logSession, "PASH.timeout");
try {
if (!future.isDone()) {
Log.w(TAG, "Cancelling suggestion process due to timeout");
}
future.complete(getDefaultSuggestions(availablePhoneAccounts));
} finally {
Log.endSession();
}
},
Timeouts.getPhoneAccountSuggestionServiceTimeout(context.getContentResolver()));
return future;
}
private static List<PhoneAccountSuggestion> addDefaultsToProvidedSuggestions(
List<PhoneAccountSuggestion> providedSuggestions,
List<PhoneAccountHandle> availableAccountHandles) {
List<PhoneAccountHandle> handlesInSuggestions = providedSuggestions.stream()
.map(PhoneAccountSuggestion::getPhoneAccountHandle)
.collect(Collectors.toList());
List<PhoneAccountHandle> handlesToFillIn = availableAccountHandles.stream()
.filter(handle -> !handlesInSuggestions.contains(handle))
.collect(Collectors.toList());
List<PhoneAccountSuggestion> suggestionsToAppend = getDefaultSuggestions(handlesToFillIn);
return Stream.concat(suggestionsToAppend.stream(), providedSuggestions.stream())
.collect( Collectors.toList());
}
private static ServiceInfo getSuggestionServiceInfo(Context context) {
PackageManager packageManager = context.getPackageManager();
Intent queryIntent = new Intent();
queryIntent.setAction(PhoneAccountSuggestionService.SERVICE_INTERFACE);
List<ResolveInfo> services;
if (sOverrideComponent == null) {
services = packageManager.queryIntentServices(queryIntent,
PackageManager.MATCH_SYSTEM_ONLY);
} else {
Log.i(TAG, "Using override component %s", sOverrideComponent);
queryIntent.setComponent(sOverrideComponent);
services = packageManager.queryIntentServices(queryIntent,
PackageManager.MATCH_ALL);
}
if (services == null || services.size() == 0) {
Log.i(TAG, "No acct suggestion services found. Using defaults.");
return null;
}
if (services.size() > 1) {
Log.w(TAG, "More than acct suggestion service found, cannot get unique service");
return null;
}
return services.get(0).serviceInfo;
}
static void setOverrideServiceName(String flattenedComponentName) {
try {
sOverrideComponent = TextUtils.isEmpty(flattenedComponentName)
? null : ComponentName.unflattenFromString(flattenedComponentName);
} catch (Exception e) {
sOverrideComponent = null;
throw e;
}
}
private static List<PhoneAccountSuggestion> getDefaultSuggestions(
List<PhoneAccountHandle> phoneAccountHandles) {
return phoneAccountHandles.stream().map(phoneAccountHandle ->
new PhoneAccountSuggestion(phoneAccountHandle,
PhoneAccountSuggestion.REASON_NONE, false)
).collect(Collectors.toList());
}
}