Make TelecomManager APIs compatible with Lollipop (2/3)

+ Move TelecomManagerCompat to ContactsCommon because it is called by
  CallSubjectDialog
+ Move isDefaultDialerCompatible to CompatUtils because it is called in
  TelecomManagerCompat
+ Add invokeMethod method to CompatUtils
+ Use TELEPHONY_MANAGER_CLASS and TELECOM_MANAGER_CLASS constants
+ Add @Nullable annotations

Bug: 25776171

Change-Id: I91ebaf59fa8234e52aeac733c424bd4bdfc6d8a2
diff --git a/src/com/android/contacts/common/compat/CompatUtils.java b/src/com/android/contacts/common/compat/CompatUtils.java
index 58a4eb7..a3ac00c 100644
--- a/src/com/android/contacts/common/compat/CompatUtils.java
+++ b/src/com/android/contacts/common/compat/CompatUtils.java
@@ -22,6 +22,9 @@
 
 import com.android.contacts.common.model.CPOWrapper;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
 public final class CompatUtils {
 
     private static final String TAG = CompatUtils.class.getSimpleName();
@@ -90,6 +93,16 @@
     }
 
     /**
+     * Determines if this version is compatible with a default dialer. Can also force the version to
+     * be lower through {@link SdkVersionOverride}.
+     *
+     * @return {@code true} if default dialer is a feature on this device, {@code false} otherwise.
+     */
+    public static boolean isDefaultDialerCompatible() {
+        return isMarshmallowCompatible();
+    }
+
+    /**
      * Determines if this version is compatible with Lollipop Mr1-specific APIs. Can also force the
      * version to be lower through SdkVersionOverride.
      *
@@ -155,6 +168,7 @@
             Class.forName(className).getMethod(methodName, parameterTypes);
             return true;
         } catch (ClassNotFoundException | NoSuchMethodException e) {
+            Log.v(TAG, "Could not find method: " + className + "#" + methodName);
             return false;
         } catch (Throwable t) {
             Log.e(TAG, "Unexpected exception when checking if method: " + className + "#"
@@ -164,6 +178,39 @@
     }
 
     /**
+     * Invokes a given class's method using reflection. Can be used to call system apis that exist
+     * at runtime but not in the SDK.
+     *
+     * @param instance The instance of the class to invoke the method on.
+     * @param methodName The name of the method to invoke.
+     * @param parameterTypes The needed parameter types for the method.
+     * @param parameters The parameter values to pass into the method.
+     * @return The result of the invocation or {@code null} if instance or methodName are
+     * empty, or if the reflection fails.
+     */
+    @Nullable
+    public static Object invokeMethod(@Nullable Object instance, @Nullable String methodName,
+            Class<?>[] parameterTypes, Object[] parameters) {
+        if (instance == null || TextUtils.isEmpty(methodName)) {
+            return null;
+        }
+
+        String className = instance.getClass().getName();
+        try {
+            return Class.forName(className).getMethod(methodName, parameterTypes)
+                    .invoke(instance, parameters);
+        } catch (ClassNotFoundException | NoSuchMethodException | IllegalArgumentException
+                | IllegalAccessException | InvocationTargetException e) {
+            Log.v(TAG, "Could not invoke method: " + className + "#" + methodName);
+            return null;
+        } catch (Throwable t) {
+            Log.e(TAG, "Unexpected exception when invoking method: " + className
+                    + "#" + methodName + " at runtime", t);
+            return null;
+        }
+    }
+
+    /**
      * Determines if this version is compatible with Lollipop-specific APIs. Can also force the
      * version to be lower through SdkVersionOverride.
      *
diff --git a/src/com/android/contacts/common/compat/TelephonyManagerCompat.java b/src/com/android/contacts/common/compat/TelephonyManagerCompat.java
index d2a189d..0f55c48 100644
--- a/src/com/android/contacts/common/compat/TelephonyManagerCompat.java
+++ b/src/com/android/contacts/common/compat/TelephonyManagerCompat.java
@@ -21,6 +21,7 @@
 import android.telephony.TelephonyManager;
 
 public class TelephonyManagerCompat {
+    public static final String TELEPHONY_MANAGER_CLASS = "android.telephony.TelephonyManager";
 
     /**
      * @param telephonyManager The telephony manager instance to use for method calls.
@@ -41,7 +42,8 @@
         if (telephonyManager == null) {
             return false;
         }
-        if (CompatUtils.isLollipopMr1Compatible()) {
+        if (CompatUtils.isLollipopMr1Compatible()
+                || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isVoiceCapable")) {
             // isVoiceCapable was unhidden in L-MR1
             return telephonyManager.isVoiceCapable();
         }
@@ -64,7 +66,7 @@
             return 1;
         }
         if (CompatUtils.isMarshmallowCompatible() || CompatUtils
-                .isMethodAvailable("android.telephony.TelephonyManager", "getPhoneCount")) {
+                .isMethodAvailable(TELEPHONY_MANAGER_CLASS, "getPhoneCount")) {
             return telephonyManager.getPhoneCount();
         }
         return 1;
@@ -85,7 +87,7 @@
             return null;
         }
         if (CompatUtils.isMarshmallowCompatible()
-                || CompatUtils.isMethodAvailable("android.telephony.TelephonyManager",
+                || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS,
                         "getDeviceId", Integer.class)) {
             return telephonyManager.getDeviceId(slotId);
         }
@@ -104,8 +106,7 @@
             return false;
         }
         if (CompatUtils.isMarshmallowCompatible()
-                || CompatUtils.isMethodAvailable("android.telephony.TelephonyManager",
-                        "isTtyModeSupported")) {
+                || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS, "isTtyModeSupported")) {
             return telephonyManager.isTtyModeSupported();
         }
         return false;
@@ -124,8 +125,8 @@
             return false;
         }
         if (CompatUtils.isMarshmallowCompatible()
-                || CompatUtils.isMethodAvailable("android.telephony.TelephonyManager",
-                        "isTtyModeSupported")) {
+                || CompatUtils.isMethodAvailable(TELEPHONY_MANAGER_CLASS,
+                        "isHearingAidCompatibilitySupported")) {
             return telephonyManager.isHearingAidCompatibilitySupported();
         }
         return false;
diff --git a/src/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java b/src/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
index 10c2ac3..035b1bd 100644
--- a/src/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
+++ b/src/com/android/contacts/common/compat/telecom/TelecomManagerCompat.java
@@ -17,19 +17,29 @@
 
 import android.app.Activity;
 import android.content.Intent;
+import android.content.Context;
+import android.net.Uri;
 import android.support.annotation.Nullable;
+import android.telecom.PhoneAccount;
+
 import android.telecom.PhoneAccountHandle;
 import android.telecom.TelecomManager;
 import android.telephony.PhoneNumberUtils;
 import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
 
 import com.android.contacts.common.compat.CompatUtils;
 
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
 /**
- * Compatibility class for {@link android.telecom.TelecomManager}
+ * Compatibility class for {@link android.telecom.TelecomManager}.
  */
 public class TelecomManagerCompat {
-
+    public static final String TELECOM_MANAGER_CLASS = "android.telecom.TelecomManager";
     /**
      * Places a new outgoing call to the provided address using the system telecom service with
      * the specified intent.
@@ -37,9 +47,12 @@
      * @param activity {@link Activity} used to start another activity for the given intent
      * @param telecomManager the {@link TelecomManager} used to place a call, if possible
      * @param intent the intent for the call
-     * @throws NullPointerException if activity, telecomManager, or intent are null
      */
-    public static void placeCall(Activity activity, TelecomManager telecomManager, Intent intent) {
+    public static void placeCall(@Nullable Activity activity,
+            @Nullable TelecomManager telecomManager, @Nullable Intent intent) {
+        if (activity == null || telecomManager == null || intent == null) {
+            return;
+        }
         if (CompatUtils.isMarshmallowCompatible()) {
             telecomManager.placeCall(intent.getData(), intent.getExtras());
             return;
@@ -48,6 +61,86 @@
     }
 
     /**
+     * Get the URI for running an adn query.
+     *
+     * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+     * @param accountHandle The handle for the account to derive an adn query URI for or
+     * {@code null} to return a URI which will use the default account.
+     * @return The URI (with the content:// scheme) specific to the specified {@link PhoneAccount}
+     * for the the content retrieve.
+     */
+    public static Uri getAdnUriForPhoneAccount(@Nullable TelecomManager telecomManager,
+            PhoneAccountHandle accountHandle) {
+        if (telecomManager != null && (CompatUtils.isMarshmallowCompatible()
+                || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "getAdnUriForPhoneAccount",
+                        PhoneAccountHandle.class))) {
+            return telecomManager.getAdnUriForPhoneAccount(accountHandle);
+        }
+        return Uri.parse("content://icc/adn");
+    }
+
+    /**
+     * Returns a list of {@link PhoneAccountHandle}s which can be used to make and receive phone
+     * calls. The returned list includes only those accounts which have been explicitly enabled
+     * by the user.
+     *
+     * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+     * @return A list of PhoneAccountHandle objects.
+     */
+    public static List<PhoneAccountHandle> getCallCapablePhoneAccounts(
+            @Nullable TelecomManager telecomManager) {
+        if (telecomManager != null && (CompatUtils.isMarshmallowCompatible()
+                || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS,
+                        "getCallCapablePhoneAccounts"))) {
+            return telecomManager.getCallCapablePhoneAccounts();
+        }
+        return new ArrayList<>();
+    }
+
+    /**
+     * Used to determine the currently selected default dialer package.
+     *
+     * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+     * @return package name for the default dialer package or null if no package has been
+     *         selected as the default dialer.
+     */
+    @Nullable
+    public static String getDefaultDialerPackage(@Nullable TelecomManager telecomManager) {
+        if (telecomManager != null && CompatUtils.isDefaultDialerCompatible()) {
+            return telecomManager.getDefaultDialerPackage();
+        }
+        return null;
+    }
+
+    /**
+     * Return the {@link PhoneAccount} which will be used to place outgoing calls to addresses with
+     * the specified {@code uriScheme}. This PhoneAccount will always be a member of the
+     * list which is returned from invoking {@link TelecomManager#getCallCapablePhoneAccounts()}.
+     * The specific account returned depends on the following priorities:
+     *
+     * 1. If the user-selected default PhoneAccount supports the specified scheme, it will
+     * be returned.
+     * 2. If there exists only one PhoneAccount that supports the specified scheme, it
+     * will be returned.
+     *
+     * If no PhoneAccount fits the criteria above, this method will return {@code null}.
+     *
+     * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+     * @param uriScheme The URI scheme.
+     * @return The {@link PhoneAccountHandle} corresponding to the account to be used.
+     */
+    @Nullable
+    public static PhoneAccountHandle getDefaultOutgoingPhoneAccount(
+            @Nullable TelecomManager telecomManager, @Nullable String uriScheme) {
+        if (telecomManager != null && (CompatUtils.isMarshmallowCompatible()
+                || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS,
+                        "getDefaultOutgoingPhoneAccount", String.class))) {
+            return telecomManager.getDefaultOutgoingPhoneAccount(uriScheme);
+        }
+        return null;
+    }
+
+    /**
      * Return the line 1 phone number for given phone account.
      *
      * @param telecomManager the {@link TelecomManager} to use in the event that
@@ -56,14 +149,18 @@
      *    is unavailable
      * @param phoneAccountHandle the phoneAccountHandle upon which to check the line one number
      * @return the line one number
-     * @throws NullPointerException if telecomManager or telephonyManager are null
      */
-    public static String getLine1Number(TelecomManager telecomManager,
-            TelephonyManager telephonyManager, @Nullable PhoneAccountHandle phoneAccountHandle) {
-        if (CompatUtils.isMarshmallowCompatible()) {
+    @Nullable
+    public static String getLine1Number(@Nullable TelecomManager telecomManager,
+            @Nullable TelephonyManager telephonyManager,
+            @Nullable PhoneAccountHandle phoneAccountHandle) {
+        if (telecomManager != null && CompatUtils.isMarshmallowCompatible()) {
             return telecomManager.getLine1Number(phoneAccountHandle);
         }
-        return telephonyManager.getLine1Number();
+        if (telephonyManager != null) {
+            return telephonyManager.getLine1Number();
+        }
+        return null;
     }
 
     /**
@@ -73,26 +170,98 @@
      * @param telecomManager the {@link TelecomManager} to use
      * @param accountHandle The handle for the account to check the voicemail number against
      * @param number The number to look up.
-     * @throws NullPointerException if telecomManager is null
      */
-    public static boolean isVoiceMailNumber(TelecomManager telecomManager,
+    public static boolean isVoiceMailNumber(@Nullable TelecomManager telecomManager,
             @Nullable PhoneAccountHandle accountHandle, @Nullable String number) {
-        if (CompatUtils.isMarshmallowCompatible()) {
+        if (telecomManager != null && (CompatUtils.isMarshmallowCompatible()
+                || CompatUtils.isMethodAvailable(TELECOM_MANAGER_CLASS, "isVoiceMailNumber",
+                        PhoneAccountHandle.class, String.class))) {
             return telecomManager.isVoiceMailNumber(accountHandle, number);
         }
         return PhoneNumberUtils.isVoiceMailNumber(number);
     }
 
     /**
+     * Return the {@link PhoneAccount} for a specified {@link PhoneAccountHandle}. Object includes
+     * resources which can be used in a user interface.
+     *
+     * @param telecomManager the {@link TelecomManager} used for method calls, if possible.
+     * @param accountHandle The PhoneAccountHandle.
+     * @return The PhoneAccount object or null if it doesn't exist.
+     */
+    @Nullable
+    public static PhoneAccount getPhoneAccount(@Nullable TelecomManager telecomManager,
+            @Nullable PhoneAccountHandle accountHandle) {
+        if (telecomManager != null && (CompatUtils.isMethodAvailable(
+                TELECOM_MANAGER_CLASS, "getPhoneAccount", PhoneAccountHandle.class))) {
+            return telecomManager.getPhoneAccount(accountHandle);
+        }
+        return null;
+    }
+
+    /**
+     * Return the voicemail number for a given phone account.
+     *
+     * @param telecomManager The {@link TelecomManager} object to use for retrieving the voicemail
+     * number if accountHandle is specified.
+     * @param telephonyManager The {@link TelephonyManager} object to use for retrieving the
+     * voicemail number if accountHandle is null.
+     * @param accountHandle The handle for the phone account.
+     * @return The voicemail number for the phone account, and {@code null} if one has not been
+     *         configured.
+     */
+    @Nullable
+    public static String getVoiceMailNumber(@Nullable TelecomManager telecomManager,
+            @Nullable TelephonyManager telephonyManager,
+            @Nullable PhoneAccountHandle accountHandle) {
+        if (telecomManager != null && (CompatUtils.isMethodAvailable(
+                TELECOM_MANAGER_CLASS, "getVoiceMailNumber", PhoneAccountHandle.class))) {
+            return telecomManager.getVoiceMailNumber(accountHandle);
+        } else if (telephonyManager != null){
+            return telephonyManager.getVoiceMailNumber();
+        }
+        return null;
+    }
+
+    /**
+     * Processes the specified dial string as an MMI code.
+     * MMI codes are any sequence of characters entered into the dialpad that contain a "*" or "#".
+     * Some of these sequences launch special behavior through handled by Telephony.
+     *
+     * @param telecomManager The {@link TelecomManager} object to use for handling MMI.
+     * @param dialString The digits to dial.
+     * @return {@code true} if the digits were processed as an MMI code, {@code false} otherwise.
+     */
+    public static boolean handleMmi(@Nullable TelecomManager telecomManager,
+            @Nullable String dialString, @Nullable PhoneAccountHandle accountHandle) {
+        if (telecomManager == null || TextUtils.isEmpty(dialString)) {
+            return false;
+        }
+        if (CompatUtils.isMarshmallowCompatible()) {
+            return telecomManager.handleMmi(dialString, accountHandle);
+        }
+
+        Object handleMmiResult = CompatUtils.invokeMethod(
+                telecomManager,
+                "handleMmi",
+                new Class<?>[] {PhoneAccountHandle.class, String.class},
+                new Object[] {accountHandle, dialString});
+        if (handleMmiResult != null) {
+            return (boolean) handleMmiResult;
+        }
+
+        return telecomManager.handleMmi(dialString);
+    }
+
+    /**
      * Silences the ringer if a ringing call exists. Noop if {@link TelecomManager#silenceRinger()}
      * is unavailable.
      *
-     * @param telecomManager the {@link TelecomManager} to use to silence the ringer
-     * @throws NullPointerException if telecomManager is null
+     * @param telecomManager the TelecomManager to use to silence the ringer
      */
-    public static void silenceRinger(TelecomManager telecomManager) {
-        if (CompatUtils.isMarshmallowCompatible() || CompatUtils
-                .isMethodAvailable("android.telecom.TelecomManager", "silenceRinger")) {
+    public static void silenceRinger(@Nullable TelecomManager telecomManager) {
+        if (telecomManager != null && (CompatUtils.isMarshmallowCompatible() || CompatUtils
+                .isMethodAvailable(TELECOM_MANAGER_CLASS, "silenceRinger"))) {
             telecomManager.silenceRinger();
         }
     }
diff --git a/src/com/android/contacts/common/dialog/CallSubjectDialog.java b/src/com/android/contacts/common/dialog/CallSubjectDialog.java
index ecaa1a5..3c08b37 100644
--- a/src/com/android/contacts/common/dialog/CallSubjectDialog.java
+++ b/src/com/android/contacts/common/dialog/CallSubjectDialog.java
@@ -48,6 +48,7 @@
 import com.android.contacts.common.ContactPhotoManager;
 import com.android.contacts.common.R;
 import com.android.contacts.common.compat.CompatUtils;
+import com.android.contacts.common.compat.telecom.TelecomManagerCompat;
 import com.android.contacts.common.util.UriUtils;
 import com.android.phone.common.animation.AnimUtils;
 
@@ -161,9 +162,10 @@
             Intent intent = CallUtil.getCallWithSubjectIntent(mNumber, mPhoneAccountHandle,
                     subject);
 
-            final TelecomManager tm =
-                    (TelecomManager) getSystemService(Context.TELECOM_SERVICE);
-            tm.placeCall(intent.getData(), intent.getExtras());
+            TelecomManagerCompat.placeCall(
+                    CallSubjectDialog.this,
+                    (TelecomManager) getSystemService(Context.TELECOM_SERVICE),
+                    intent);
 
             mSubjectHistory.add(subject);
             saveSubjectHistory(mSubjectHistory);
diff --git a/tests/src/com/android/contacts/common/compat/CompatUtilsTest.java b/tests/src/com/android/contacts/common/compat/CompatUtilsTest.java
index eaacbba..3386a00 100644
--- a/tests/src/com/android/contacts/common/compat/CompatUtilsTest.java
+++ b/tests/src/com/android/contacts/common/compat/CompatUtilsTest.java
@@ -80,15 +80,58 @@
                 Boolean.TYPE));
     }
 
+    public void testInvokeMethod_NullMethodName() {
+        assertNull(CompatUtils.invokeMethod(new BaseClass(), null, null, null));
+    }
+
+    public void testInvokeMethod_EmptyMethodName() {
+        assertNull(CompatUtils.invokeMethod(new BaseClass(), "", null, null));
+    }
+
+    public void testInvokeMethod_NullClassInstance() {
+        assertNull(CompatUtils.invokeMethod(null, "", null, null));
+    }
+
+    public void testInvokeMethod_NonexistentMethod() {
+        assertNull(CompatUtils.invokeMethod(new BaseClass(), "derivedMethod", null, null));
+    }
+
+    public void testInvokeMethod_MethodWithNoParameters() {
+        assertEquals(1, CompatUtils.invokeMethod(new DerivedClass(), "overloadedMethod", null, null));
+    }
+
+    public void testInvokeMethod_MethodWithNoParameters_WithParameters() {
+        assertNull(CompatUtils.invokeMethod(new DerivedClass(), "derivedMethod",
+                new Class<?>[] {Integer.TYPE}, new Object[] {1}));
+    }
+
+    public void testInvokeMethod_MethodWithParameters_WithEmptyParameterList() {
+        assertNull(CompatUtils.invokeMethod(new DerivedClass(), "overloadedMethod",
+                new Class<?>[] {Integer.TYPE}, new Object[] {}));
+    }
+
+    public void testInvokeMethod_InvokeSimpleMethod() {
+        assertEquals(2, CompatUtils.invokeMethod(new DerivedClass(), "overloadedMethod",
+                new Class<?>[] {Integer.TYPE}, new Object[] {2}));
+    }
+
     private class BaseClass {
         public void baseMethod() {}
     }
 
     private class DerivedClass extends BaseClass {
-        public void derivedMethod() {}
+        public int derivedMethod() {
+            // This method needs to return something to differentiate a successful invocation from
+            // an unsuccessful one.
+            return 0;
+        }
 
-        public void overloadedMethod() {}
+        public int overloadedMethod() {
+            return 1;
+        }
 
-        public void overloadedMethod(int i) {}
+        public int overloadedMethod(int i) {
+            return i;
+        }
     }
 }