Merge "TestConnectionService needs more permissions!" into lmp-mr1-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 88e90ae..33c9838 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -57,7 +57,8 @@
             android:label="@string/telecommAppLabel"
             android:icon="@mipmap/ic_launcher_phone"
             android:allowBackup="false"
-            android:supportsRtl="true">
+            android:supportsRtl="true"
+            android:process="com.android.phone">
 
         <!-- CALL vs CALL_PRIVILEGED vs CALL_EMERGENCY
              We have three different intents through which a call can be initiated each with its
diff --git a/src/com/android/server/telecom/BluetoothPhoneService.java b/src/com/android/server/telecom/BluetoothPhoneService.java
index 8bf50ae..e9a00ff 100644
--- a/src/com/android/server/telecom/BluetoothPhoneService.java
+++ b/src/com/android/server/telecom/BluetoothPhoneService.java
@@ -680,7 +680,7 @@
 
         String ringingAddress = null;
         int ringingAddressType = 128;
-        if (ringingCall != null) {
+        if (ringingCall != null && ringingCall.getHandle() != null) {
             ringingAddress = ringingCall.getHandle().getSchemeSpecificPart();
             if (ringingAddress != null) {
                 ringingAddressType = PhoneNumberUtils.toaFromString(ringingAddress);
diff --git a/src/com/android/server/telecom/Call.java b/src/com/android/server/telecom/Call.java
index e2bead7..36048dd 100644
--- a/src/com/android/server/telecom/Call.java
+++ b/src/com/android/server/telecom/Call.java
@@ -89,6 +89,7 @@
         void onConnectionManagerPhoneAccountChanged(Call call);
         void onPhoneAccountChanged(Call call);
         void onConferenceableCallsChanged(Call call);
+        boolean onCanceledViaNewOutgoingCallBroadcast(Call call);
     }
 
     abstract static class ListenerBase implements Listener {
@@ -138,6 +139,10 @@
         public void onPhoneAccountChanged(Call call) {}
         @Override
         public void onConferenceableCallsChanged(Call call) {}
+        @Override
+        public boolean onCanceledViaNewOutgoingCallBroadcast(Call call) {
+            return false;
+        }
     }
 
     private static final OnQueryCompleteListener sCallerInfoQueryListener =
@@ -417,8 +422,20 @@
 
     void setHandle(Uri handle, int presentation) {
         if (!Objects.equals(handle, mHandle) || presentation != mHandlePresentation) {
-            mHandle = handle;
             mHandlePresentation = presentation;
+            if (mHandlePresentation == TelecomManager.PRESENTATION_RESTRICTED ||
+                    mHandlePresentation == TelecomManager.PRESENTATION_UNKNOWN) {
+                mHandle = null;
+            } else {
+                mHandle = handle;
+                if (mHandle != null && !PhoneAccount.SCHEME_VOICEMAIL.equals(mHandle.getScheme())
+                        && TextUtils.isEmpty(mHandle.getSchemeSpecificPart())) {
+                    // If the number is actually empty, set it to null, unless this is a
+                    // SCHEME_VOICEMAIL uri which always has an empty number.
+                    mHandle = null;
+                }
+            }
+
             mIsEmergencyCall = mHandle != null && PhoneNumberUtils.isLocalEmergencyNumber(mContext,
                     mHandle.getSchemeSpecificPart());
             startCallerInfoLookup();
@@ -758,17 +775,21 @@
         }
     }
 
+    void disconnect() {
+        disconnect(false);
+    }
+
     /**
      * Attempts to disconnect the call through the connection service.
      */
-    void disconnect() {
+    void disconnect(boolean wasViaNewOutgoingCallBroadcaster) {
         // Track that the call is now locally disconnecting.
         setLocallyDisconnecting(true);
 
         if (mState == CallState.NEW || mState == CallState.PRE_DIAL_WAIT ||
                 mState == CallState.CONNECTING) {
             Log.v(this, "Aborting call %s", this);
-            abort();
+            abort(wasViaNewOutgoingCallBroadcaster);
         } else if (mState != CallState.ABORTED && mState != CallState.DISCONNECTED) {
             if (mConnectionService == null) {
                 Log.e(this, new Exception(), "disconnect() request on a call without a"
@@ -784,11 +805,30 @@
         }
     }
 
-    void abort() {
+    void abort(boolean wasViaNewOutgoingCallBroadcaster) {
         if (mCreateConnectionProcessor != null) {
             mCreateConnectionProcessor.abort();
         } else if (mState == CallState.NEW || mState == CallState.PRE_DIAL_WAIT
                 || mState == CallState.CONNECTING) {
+            if (wasViaNewOutgoingCallBroadcaster) {
+                // If the cancelation was from NEW_OUTGOING_CALL, then we do not automatically
+                // destroy the call.  Instead, we announce the cancelation and CallsManager handles
+                // it through a timer. Since apps often cancel calls through NEW_OUTGOING_CALL and
+                // then re-dial them quickly using a gateway, allowing the first call to end
+                // causes jank. This timeout allows CallsManager to transition the first call into
+                // the second call so that in-call only ever sees a single call...eliminating the
+                // jank altogether.
+                for (Listener listener : mListeners) {
+                    if (listener.onCanceledViaNewOutgoingCallBroadcast(this)) {
+                        // The first listener to handle this wins. A return value of true means that
+                        // the listener will handle the disconnection process later and so we
+                        // should not continue it here.
+                        setLocallyDisconnecting(false);
+                        return;
+                    }
+                }
+            }
+
             handleCreateConnectionFailure(new DisconnectCause(DisconnectCause.CANCELED));
         } else {
             Log.v(this, "Cannot abort a call which isn't either PRE_DIAL_WAIT or CONNECTING");
diff --git a/src/com/android/server/telecom/CallsManager.java b/src/com/android/server/telecom/CallsManager.java
index 300f4fa..b677b85 100644
--- a/src/com/android/server/telecom/CallsManager.java
+++ b/src/com/android/server/telecom/CallsManager.java
@@ -22,6 +22,7 @@
 import android.content.pm.ResolveInfo;
 import android.net.Uri;
 import android.os.Bundle;
+import android.os.Handler;
 import android.provider.CallLog.Calls;
 import android.telecom.AudioState;
 import android.telecom.CallState;
@@ -37,7 +38,6 @@
 import android.telephony.TelephonyManager;
 
 import com.android.internal.util.IndentingPrintWriter;
-
 import com.google.common.collect.ImmutableCollection;
 import com.google.common.collect.ImmutableList;
 
@@ -97,7 +97,7 @@
     /**
      * The main call repository. Keeps an instance of all live calls. New incoming and outgoing
      * calls are added to the map and removed when the calls move to the disconnected state.
-    *
+     *
      * ConcurrentHashMap constructor params: 8 is initial table size, 0.9f is
      * load factor before resizing, 1 means we only expect a single thread to
      * access the map so make only a single shard
@@ -124,6 +124,9 @@
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final MissedCallNotifier mMissedCallNotifier;
     private final Set<Call> mLocallyDisconnectingCalls = new HashSet<>();
+    private final Set<Call> mPendingCallsToDisconnect = new HashSet<>();
+    /* Handler tied to thread in which CallManager was initialized. */
+    private final Handler mHandler = new Handler();
 
     /**
      * The call the user is currently interacting with. This is the call that should have audio
@@ -289,6 +292,22 @@
         }
     }
 
+    @Override
+    public boolean onCanceledViaNewOutgoingCallBroadcast(final Call call) {
+        mPendingCallsToDisconnect.add(call);
+        mHandler.postDelayed(new Runnable() {
+            @Override
+            public void run() {
+                if (mPendingCallsToDisconnect.remove(call)) {
+                    Log.i(this, "Delayed disconnection of call: %s", call);
+                    call.disconnect();
+                }
+            }
+        }, Timeouts.getNewOutgoingCallCancelMillis(mContext.getContentResolver()));
+
+        return true;
+    }
+
     ImmutableCollection<Call> getCalls() {
         return ImmutableList.copyOf(mCalls);
     }
@@ -390,6 +409,30 @@
         call.startCreateConnection(mPhoneAccountRegistrar);
     }
 
+    private Call getNewOutgoingCall(Uri handle) {
+        // First check to see if we can reuse any of the calls that are waiting to disconnect.
+        // See {@link Call#abort} and {@link #onCanceledViaNewOutgoingCall} for more information.
+        for (Call pendingCall : mPendingCallsToDisconnect) {
+            if (Objects.equals(pendingCall.getHandle(), handle)) {
+                mPendingCallsToDisconnect.remove(pendingCall);
+                Log.i(this, "Reusing disconnected call %s", pendingCall);
+                return pendingCall;
+            }
+        }
+
+        // Create a call with original handle. The handle may be changed when the call is attached
+        // to a connection service, but in most cases will remain the same.
+        return new Call(
+                mContext,
+                mConnectionServiceRepository,
+                handle,
+                null /* gatewayInfo */,
+                null /* connectionManagerPhoneAccount */,
+                null /* phoneAccountHandle */,
+                false /* isIncoming */,
+                false /* isConference */);
+    }
+
     /**
      * Kicks off the first steps to creating an outgoing call so that InCallUI can launch.
      *
@@ -399,17 +442,7 @@
      * @param extras The optional extras Bundle passed with the intent used for the incoming call.
      */
     Call startOutgoingCall(Uri handle, PhoneAccountHandle phoneAccountHandle, Bundle extras) {
-        // Create a call with original handle. The handle may be changed when the call is attached
-        // to a connection service, but in most cases will remain the same.
-        Call call = new Call(
-                mContext,
-                mConnectionServiceRepository,
-                handle,
-                null /* gatewayInfo */,
-                null /* connectionManagerPhoneAccount */,
-                null /* phoneAccountHandle */,
-                false /* isIncoming */,
-                false /* isConference */);
+        Call call = getNewOutgoingCall(handle);
 
         List<PhoneAccountHandle> accounts =
                 mPhoneAccountRegistrar.getCallCapablePhoneAccounts(handle.getScheme());
@@ -444,6 +477,11 @@
         // a call, or cancel this call altogether.
         if (!isPotentialInCallMMICode && !makeRoomForOutgoingCall(call, isEmergencyCall)) {
             // just cancel at this point.
+            if (mCalls.contains(call)) {
+                // This call can already exist if it is a reused call,
+                // See {@link #getNewOutgoingCall}.
+                call.disconnect();
+            }
             return null;
         }
 
@@ -463,7 +501,9 @@
         // Do not add the call if it is a potential MMI code.
         if ((isPotentialMMICode(handle) || isPotentialInCallMMICode) && !needsAccountSelection) {
             call.addListener(this);
-        } else {
+        } else if (!mCalls.contains(call)) {
+            // We check if mCalls already contains the call because we could potentially be reusing
+            // a call which was previously added (See {@link #getNewOutgoingCall}).
             addCall(call);
         }
 
diff --git a/src/com/android/server/telecom/CreateConnectionProcessor.java b/src/com/android/server/telecom/CreateConnectionProcessor.java
index fab2679..f31f423 100644
--- a/src/com/android/server/telecom/CreateConnectionProcessor.java
+++ b/src/com/android/server/telecom/CreateConnectionProcessor.java
@@ -89,6 +89,7 @@
     private DisconnectCause mLastErrorDisconnectCause;
     private final PhoneAccountRegistrar mPhoneAccountRegistrar;
     private final Context mContext;
+    private boolean mShouldUseConnectionManager = true;
 
     CreateConnectionProcessor(
             Call call, ConnectionServiceRepository repository, CreateConnectionResponse response,
@@ -185,6 +186,10 @@
     }
 
     private boolean shouldSetConnectionManager() {
+        if (!mShouldUseConnectionManager) {
+            return false;
+        }
+
         if (mAttemptRecords.size() == 0) {
             return false;
         }
@@ -223,8 +228,7 @@
             CallAttemptRecord record = new CallAttemptRecord(
                     mPhoneAccountRegistrar.getSimCallManager(),
                     mAttemptRecords.get(0).targetPhoneAccount);
-            Log.v(this, "setConnectionManager, changing %s -> %s",
-                    mAttemptRecords.get(0).targetPhoneAccount, record);
+            Log.v(this, "setConnectionManager, changing %s -> %s", mAttemptRecords.get(0), record);
             mAttemptRecords.set(0, record);
         } else {
             Log.v(this, "setConnectionManager, not changing");
@@ -307,12 +311,44 @@
             }
         }
 
+        private boolean shouldFallbackToNoConnectionManager(DisconnectCause cause) {
+            PhoneAccountHandle handle = mCall.getConnectionManagerPhoneAccount();
+            if (handle == null || !handle.equals(mPhoneAccountRegistrar.getSimCallManager())) {
+                return false;
+            }
+
+            ConnectionServiceWrapper connectionManager = mCall.getConnectionService();
+            if (connectionManager == null) {
+                return false;
+            }
+
+            if (cause.getCode() == DisconnectCause.CONNECTION_MANAGER_NOT_SUPPORTED) {
+                Log.d(CreateConnectionProcessor.this, "Connection manager declined to handle the "
+                        + "call, falling back to not using a connection manager");
+                return true;
+            }
+
+            if (!connectionManager.isServiceValid("createConnection")) {
+                Log.d(CreateConnectionProcessor.this, "Connection manager unbound while trying "
+                        + "create a connection, falling back to not using a connection manager");
+                return true;
+            }
+
+            return false;
+        }
+
         @Override
         public void handleCreateConnectionFailure(DisconnectCause errorDisconnectCause) {
             // Failure of some sort; record the reasons for failure and try again if possible
             Log.d(CreateConnectionProcessor.this, "Connection failed: (%s)", errorDisconnectCause);
             mLastErrorDisconnectCause = errorDisconnectCause;
-            attemptNextPhoneAccount();
+            if (shouldFallbackToNoConnectionManager(errorDisconnectCause)) {
+                mShouldUseConnectionManager = false;
+                // Restart from the beginning.
+                process();
+            } else {
+                attemptNextPhoneAccount();
+            }
         }
     }
 }
diff --git a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
index d645009..9100718 100644
--- a/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
+++ b/src/com/android/server/telecom/NewOutgoingCallIntentBroadcaster.java
@@ -114,7 +114,7 @@
 
             if (endEarly) {
                 if (mCall != null) {
-                    mCall.disconnect();
+                    mCall.disconnect(true /* wasViaNewOutgoingCall */);
                 }
                 return;
             }
diff --git a/src/com/android/server/telecom/PhoneAccountRegistrar.java b/src/com/android/server/telecom/PhoneAccountRegistrar.java
index bb23123..3550201 100644
--- a/src/com/android/server/telecom/PhoneAccountRegistrar.java
+++ b/src/com/android/server/telecom/PhoneAccountRegistrar.java
@@ -250,7 +250,8 @@
             // Return the registered sim call manager iff it still exists (we keep a sticky
             // setting to survive account deletion and re-addition)
             for (int i = 0; i < mState.accounts.size(); i++) {
-                if (mState.accounts.get(i).getAccountHandle().equals(mState.simCallManager)) {
+                if (mState.accounts.get(i).getAccountHandle().equals(mState.simCallManager)
+                        && !resolveComponent(mState.simCallManager.getComponentName()).isEmpty()) {
                     return mState.simCallManager;
                 }
             }
@@ -260,14 +261,9 @@
         String defaultConnectionMgr =
                 mContext.getResources().getString(R.string.default_connection_manager_component);
         if (!TextUtils.isEmpty(defaultConnectionMgr)) {
-            PackageManager pm = mContext.getPackageManager();
-
             ComponentName componentName = ComponentName.unflattenFromString(defaultConnectionMgr);
-            Intent intent = new Intent(ConnectionService.SERVICE_INTERFACE);
-            intent.setComponent(componentName);
-
             // Make sure that the component can be resolved.
-            List<ResolveInfo> resolveInfos = pm.queryIntentServices(intent, 0);
+            List<ResolveInfo> resolveInfos = resolveComponent(componentName);
             if (!resolveInfos.isEmpty()) {
                 // See if there is registered PhoneAccount by this component.
                 List<PhoneAccountHandle> handles = getAllPhoneAccountHandles();
@@ -287,6 +283,13 @@
         return null;
     }
 
+    private List<ResolveInfo> resolveComponent(ComponentName componentName) {
+        PackageManager pm = mContext.getPackageManager();
+        Intent intent = new Intent(ConnectionService.SERVICE_INTERFACE);
+        intent.setComponent(componentName);
+        return pm.queryIntentServices(intent, 0);
+    }
+
     /**
      * Retrieves a list of all {@link PhoneAccountHandle}s registered.
      *
@@ -520,7 +523,10 @@
         List<PhoneAccountHandle> accountHandles = new ArrayList<>();
         for (PhoneAccount m : mState.accounts) {
             if (m.hasCapabilities(flags) && (uriScheme == null || m.supportsUriScheme(uriScheme))) {
-                accountHandles.add(m.getAccountHandle());
+                // Also filter out unresolveable accounts
+                if (!resolveComponent(m.getAccountHandle().getComponentName()).isEmpty()) {
+                    accountHandles.add(m.getAccountHandle());
+                }
             }
         }
         return accountHandles;
diff --git a/src/com/android/server/telecom/RespondViaSmsManager.java b/src/com/android/server/telecom/RespondViaSmsManager.java
index 2ac9379..dcfa19a 100644
--- a/src/com/android/server/telecom/RespondViaSmsManager.java
+++ b/src/com/android/server/telecom/RespondViaSmsManager.java
@@ -139,8 +139,7 @@
 
     @Override
     public void onIncomingCallRejected(Call call, boolean rejectWithMessage, String textMessage) {
-        if (rejectWithMessage) {
-
+        if (rejectWithMessage && call.getHandle() != null) {
             rejectCallWithMessage(call.getContext(), call.getHandle().getSchemeSpecificPart(),
                     textMessage);
         }
diff --git a/src/com/android/server/telecom/TelecomServiceImpl.java b/src/com/android/server/telecom/TelecomServiceImpl.java
index 2656547..c3a9f9f 100644
--- a/src/com/android/server/telecom/TelecomServiceImpl.java
+++ b/src/com/android/server/telecom/TelecomServiceImpl.java
@@ -23,6 +23,7 @@
 import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
+import android.net.Uri;
 import android.os.Binder;
 import android.os.Bundle;
 import android.os.Handler;
@@ -459,6 +460,47 @@
     }
 
     /**
+     * @see android.telecom.TelecomManager#handleMmi
+     */
+    @Override
+    public boolean handlePinMmiForPhoneAccount(PhoneAccountHandle accountHandle,
+            String dialString) {
+        enforceModifyPermissionOrDefaultDialer();
+
+        // Switch identity so that TelephonyManager checks Telecom's permissions instead.
+        long token = Binder.clearCallingIdentity();
+        boolean retval = false;
+        try {
+            int subId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle);
+            retval = getTelephonyManager().handlePinMmiForSubscriber(subId, dialString);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+
+        return retval;
+    }
+
+    /**
+     * @see android.telecom.TelecomManager#getAdnUriForPhoneAccount
+     */
+    @Override
+    public Uri getAdnUriForPhoneAccount(PhoneAccountHandle accountHandle) {
+        enforceModifyPermissionOrDefaultDialer();
+
+        // Switch identity so that TelephonyManager checks Telecom's permissions instead.
+        long token = Binder.clearCallingIdentity();
+        String retval = "content://icc/adn/";
+        try {
+            long subId = mPhoneAccountRegistrar.getSubscriptionIdForPhoneAccount(accountHandle);
+            retval = retval + "subId/" + subId;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+
+        return Uri.parse(retval);
+    }
+
+    /**
      * @see android.telecom.TelecomManager#isTtySupported
      */
     @Override
diff --git a/src/com/android/server/telecom/Timeouts.java b/src/com/android/server/telecom/Timeouts.java
index 35f61ae..869e98a 100644
--- a/src/com/android/server/telecom/Timeouts.java
+++ b/src/com/android/server/telecom/Timeouts.java
@@ -52,4 +52,14 @@
     public static long getDirectToVoicemailMillis(ContentResolver contentResolver) {
         return get(contentResolver, "direct_to_voicemail_ms", 500L);
     }
+
+    /**
+     * Returns the amount of time to wait before disconnecting a call that was canceled via
+     * NEW_OUTGOING_CALL broadcast. This timeout allows apps which repost the call using a gateway
+     * to reuse the existing call, preventing the call from causing a start->end->start jank in the
+     * in-call UI.
+     */
+    public static long getNewOutgoingCallCancelMillis(ContentResolver contentResolver) {
+        return get(contentResolver, "new_outgoing_call_cancel_ms", 200L);
+    }
 }