Merge "Mark flaky tests flaky until we can figure out why"
diff --git a/src/com/android/server/telecom/AsyncRingtonePlayer.java b/src/com/android/server/telecom/AsyncRingtonePlayer.java
index 0940429..439eb63 100644
--- a/src/com/android/server/telecom/AsyncRingtonePlayer.java
+++ b/src/com/android/server/telecom/AsyncRingtonePlayer.java
@@ -170,6 +170,10 @@
             handleRepeat();
         } else {
             mRingtone.setLooping(true);
+            if (mRingtone.isPlaying()) {
+                Log.d(this, "Ringtone already playing.");
+                return;
+            }
             mRingtone.play();
             Log.i(this, "Play ringtone, looping.");
         }
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 7ff4e51..de22352 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -148,6 +148,7 @@
         void performAction();
     }
 
+    /** An executor that starts a log session before executing a runnable */
     private class LoggedHandlerExecutor implements Executor {
         private Handler mHandler;
         private String mSessionName;
@@ -1336,15 +1337,8 @@
                         return CompletableFuture.completedFuture(
                                 Collections.singletonList(suggestion));
                     }
-                    // todo: call onsuggestphoneaccount and bring back the list of suggestions
-                    // from there. For now just map all the accounts to suggest_none
-                    List<PhoneAccountSuggestion> suggestions =
-                            potentialPhoneAccounts.stream().map(phoneAccountHandle ->
-                                    new PhoneAccountSuggestion(phoneAccountHandle,
-                                            PhoneAccountSuggestion.REASON_NONE, false)
-                            ).collect(Collectors.toList());
-
-                    return CompletableFuture.completedFuture(suggestions);
+                    return PhoneAccountSuggestionHelper.bindAndGetSuggestions(mContext,
+                            finalCall.getHandle(), potentialPhoneAccounts);
                 }, new LoggedHandlerExecutor(outgoingCallHandler, "CM.cOCSS"));
 
 
@@ -1430,6 +1424,9 @@
                             newExtras.putParcelableList(
                                     android.telecom.Call.AVAILABLE_PHONE_ACCOUNTS,
                                     accountsFromSuggestions);
+                            newExtras.putParcelableList(
+                                    android.telecom.Call.EXTRA_SUGGESTED_PHONE_ACCOUNTS,
+                                    accountSuggestions);
                             // Set a future in place so that we can proceed once the dialer replies.
                             mPendingAccountSelection = new CompletableFuture<>();
                             callToPlace.setIntentExtras(newExtras);
diff --git a/src/com/android/server/telecom/PhoneAccountSuggestionHelper.java b/src/com/android/server/telecom/PhoneAccountSuggestionHelper.java
new file mode 100644
index 0000000..438ee68
--- /dev/null
+++ b/src/com/android/server/telecom/PhoneAccountSuggestionHelper.java
@@ -0,0 +1,209 @@
+/*
+ * 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());
+    }
+}
\ No newline at end of file
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 14d54b0..8e7303d 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -1754,6 +1754,23 @@
                 Log.endSession();
             }
         }
+
+        @Override
+        public void setTestPhoneAcctSuggestionComponent(String flattenedComponentName) {
+            try {
+                Log.startSession("TSI.sPASA");
+                enforceModifyPermission();
+                if (Binder.getCallingUid() != Process.SHELL_UID
+                        && Binder.getCallingUid() != Process.ROOT_UID) {
+                    throw new SecurityException("Shell-only API.");
+                }
+                synchronized (mLock) {
+                    PhoneAccountSuggestionHelper.setOverrideServiceName(flattenedComponentName);
+                }
+            } finally {
+                Log.endSession();
+            }
+        }
     };
 
     /**
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index 9257654..e927694 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -58,6 +58,10 @@
         public long getCarrierCallRedirectionTimeoutMillis(ContentResolver cr) {
             return Timeouts.getCarrierCallRedirectionTimeoutMillis(cr);
         }
+
+        public long getPhoneAccountSuggestionServiceTimeout(ContentResolver cr) {
+            return Timeouts.getPhoneAccountSuggestionServiceTimeout(cr);
+        }
     }
 
     /** A prefix to use for all keys so to not clobber the global namespace. */
@@ -152,6 +156,14 @@
     }
 
     /**
+     * Returns the amount of time to wait for the phone account suggestion service to reply.
+     */
+    public static long getPhoneAccountSuggestionServiceTimeout(ContentResolver contentResolver) {
+        return get(contentResolver, "phone_account_suggestion_service_timeout",
+                5000L /* 5 seconds */);
+    }
+
+    /**
      * Returns the amount of time to wait for the call screening service to allow or disallow a
      * call.
      */
diff --git a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
index 0118d1f..8772cd5 100644
--- a/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
+++ b/src/com/android/server/telecom/bluetooth/BluetoothRouteManager.java
@@ -296,7 +296,7 @@
                         }
                         break;
                     case BT_AUDIO_LOST:
-                        if (Objects.equals(mDeviceAddress, address)) {
+                        if (Objects.equals(mDeviceAddress, address) || address == null) {
                             Log.i(LOG_TAG, "Connection with device %s failed.",
                                     mDeviceAddress);
                             transitionToActualState();
@@ -406,7 +406,7 @@
                         }
                         break;
                     case BT_AUDIO_LOST:
-                        if (Objects.equals(mDeviceAddress, address)) {
+                        if (Objects.equals(mDeviceAddress, address) || address == null) {
                             Log.i(LOG_TAG, "HFP connection with device %s lost.", mDeviceAddress);
                             transitionToActualState();
                         } else {
diff --git a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
index 4325831..d2288c5 100644
--- a/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
+++ b/tests/src/com/android/server/telecom/tests/BluetoothRouteTransitionTests.java
@@ -539,6 +539,18 @@
                 .build());
 
         result.add(new BluetoothRouteTestParametersBuilder()
+                .setName("Audio disconnect comes with a null device")
+                .setInitialBluetoothState(BluetoothRouteManager.AUDIO_CONNECTED_STATE_NAME_PREFIX)
+                .setInitialDevice(DEVICE2)
+                .setConnectedDevices(DEVICE2)
+                .setMessageType(BluetoothRouteManager.BT_AUDIO_LOST)
+                .setMessageDevice(null)
+                .setExpectedListenerUpdates(ListenerUpdate.AUDIO_DISCONNECTED)
+                .setExpectedBluetoothInteraction(NONE)
+                .setExpectedFinalStateName(BluetoothRouteManager.AUDIO_OFF_STATE_NAME)
+                .build());
+
+        result.add(new BluetoothRouteTestParametersBuilder()
                 .setName("Device gets disconnected while pending. No fallback.")
                 .setInitialBluetoothState(BluetoothRouteManager.AUDIO_CONNECTING_STATE_NAME_PREFIX)
                 .setInitialDevice(DEVICE2)