Update dialer sources.
Test: Built package and system image.
This change clobbers the old source, and is an export
from an internal Google repository.
The internal repository was forked form Android in March,
and this change includes modifications since then, to
near the v8 release.
Since the fork, we've moved code from monolithic to independent modules. In addition,
we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make.
New dependencies have been added:
- Dagger
- Auto-Value
- Glide
- Libshortcutbadger
Going forward, development will still be in Google3, and the Gerrit release
will become an automated export, with the next drop happening in ~ two weeks.
Android.mk includes local modifications from ToT.
Abridged changelog:
Bug fixes
● Not able to mute, add a call when using Phone app in multiwindow mode
● Double tap on keypad triggering multiple key and tones
● Reported spam numbers not showing as spam in the call log
● Crash when user tries to block number while Phone app is not set as default
● Crash when user picks a number from search auto-complete list
Visual Voicemail (VVM) improvements
● Share Voicemail audio via standard exporting mechanisms that support file attachment
(email, MMS, etc.)
● Make phone number, email and web sites in VVM transcript clickable
● Set PIN before declining VVM Terms of Service {Carrier}
● Set client type for outbound visual voicemail SMS {Carrier}
New incoming call and incall UI on older devices
(Android M)
● Updated Phone app icon
● New incall UI (large buttons, button labels)
● New and animated Answer/Reject gestures
Accessibility
● Add custom answer/decline call buttons on answer screen for touch exploration
accessibility services
● Increase size of touch target
● Add verbal feedback when a Voicemail fails to load
● Fix pressing of Phone buttons while in a phone call using Switch Access
● Fix selecting and opening contacts in talkback mode
● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text
Other
● Backup & Restore for App Preferences
● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is
connected
● Rename “Dialpad” to “Keypad”
● Show "Private number" for restricted calls
● Delete unused items (vcard, add contact, call history) from Phone menu
Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
diff --git a/java/com/android/incallui/CallerInfoUtils.java b/java/com/android/incallui/CallerInfoUtils.java
new file mode 100644
index 0000000..9f57fba
--- /dev/null
+++ b/java/com/android/incallui/CallerInfoUtils.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2016 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.incallui;
+
+import android.Manifest.permission;
+import android.content.Context;
+import android.content.Loader;
+import android.content.Loader.OnLoadCompleteListener;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.support.v4.content.ContextCompat;
+import android.telecom.PhoneAccount;
+import android.telecom.TelecomManager;
+import android.text.TextUtils;
+import com.android.contacts.common.model.Contact;
+import com.android.contacts.common.model.ContactLoader;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService;
+import com.android.dialer.phonenumbercache.CachedNumberLookupService.CachedContactInfo;
+import com.android.dialer.phonenumbercache.ContactInfo;
+import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.util.PermissionsUtil;
+import com.android.incallui.call.DialerCall;
+import java.util.Arrays;
+
+/** Utility methods for contact and caller info related functionality */
+public class CallerInfoUtils {
+
+ private static final String TAG = CallerInfoUtils.class.getSimpleName();
+
+ private static final int QUERY_TOKEN = -1;
+
+ public CallerInfoUtils() {}
+
+ /**
+ * This is called to get caller info for a call. This will return a CallerInfo object immediately
+ * based off information in the call, but more information is returned to the
+ * OnQueryCompleteListener (which contains information about the phone number label, user's name,
+ * etc).
+ */
+ public static CallerInfo getCallerInfoForCall(
+ Context context,
+ DialerCall call,
+ Object cookie,
+ CallerInfoAsyncQuery.OnQueryCompleteListener listener) {
+ CallerInfo info = buildCallerInfo(context, call);
+
+ // TODO: Have phoneapp send a Uri when it knows the contact that triggered this call.
+
+ if (info.numberPresentation == TelecomManager.PRESENTATION_ALLOWED) {
+ if (PermissionsUtil.hasContactsPermissions(context)) {
+ // Start the query with the number provided from the call.
+ LogUtil.d(
+ "CallerInfoUtils.getCallerInfoForCall",
+ "Actually starting CallerInfoAsyncQuery.startQuery()...");
+
+ //noinspection MissingPermission
+ CallerInfoAsyncQuery.startQuery(QUERY_TOKEN, context, info, listener, cookie);
+ } else {
+ LogUtil.w(
+ "CallerInfoUtils.getCallerInfoForCall",
+ "Dialer doesn't have permission to read contacts."
+ + " Not calling CallerInfoAsyncQuery.startQuery().");
+ }
+ }
+ return info;
+ }
+
+ public static CallerInfo buildCallerInfo(Context context, DialerCall call) {
+ CallerInfo info = new CallerInfo();
+
+ // Store CNAP information retrieved from the Connection (we want to do this
+ // here regardless of whether the number is empty or not).
+ info.cnapName = call.getCnapName();
+ info.name = info.cnapName;
+ info.numberPresentation = call.getNumberPresentation();
+ info.namePresentation = call.getCnapNamePresentation();
+ info.callSubject = call.getCallSubject();
+
+ String number = call.getNumber();
+ if (!TextUtils.isEmpty(number)) {
+ // Don't split it if it's a SIP number.
+ if (!PhoneNumberHelper.isUriNumber(number)) {
+ final String[] numbers = number.split("&");
+ number = numbers[0];
+ if (numbers.length > 1) {
+ info.forwardingNumber = numbers[1];
+ }
+ number = modifyForSpecialCnapCases(context, info, number, info.numberPresentation);
+ }
+ info.phoneNumber = number;
+ }
+
+ // Because the InCallUI is immediately launched before the call is connected, occasionally
+ // a voicemail call will be passed to InCallUI as a "voicemail:" URI without a number.
+ // This call should still be handled as a voicemail call.
+ if ((call.getHandle() != null
+ && PhoneAccount.SCHEME_VOICEMAIL.equals(call.getHandle().getScheme()))
+ || isVoiceMailNumber(context, call)) {
+ info.markAsVoiceMail(context);
+ }
+
+ ContactInfoCache.getInstance(context).maybeInsertCnapInformationIntoCache(context, call, info);
+
+ return info;
+ }
+
+ /**
+ * Creates a new {@link CachedContactInfo} from a {@link CallerInfo}
+ *
+ * @param lookupService the {@link CachedNumberLookupService} used to build a new {@link
+ * CachedContactInfo}
+ * @param {@link CallerInfo} object
+ * @return a CachedContactInfo object created from this CallerInfo
+ * @throws NullPointerException if lookupService or ci are null
+ */
+ public static CachedContactInfo buildCachedContactInfo(
+ CachedNumberLookupService lookupService, CallerInfo ci) {
+ ContactInfo info = new ContactInfo();
+ info.name = ci.name;
+ info.type = ci.numberType;
+ info.label = ci.phoneLabel;
+ info.number = ci.phoneNumber;
+ info.normalizedNumber = ci.normalizedNumber;
+ info.photoUri = ci.contactDisplayPhotoUri;
+ info.userType = ci.userType;
+
+ CachedContactInfo cacheInfo = lookupService.buildCachedContactInfo(info);
+ cacheInfo.setLookupKey(ci.lookupKeyOrNull);
+ return cacheInfo;
+ }
+
+ public static boolean isVoiceMailNumber(Context context, DialerCall call) {
+ if (ContextCompat.checkSelfPermission(context, permission.READ_PHONE_STATE)
+ != PackageManager.PERMISSION_GRANTED) {
+ return false;
+ }
+ return TelecomUtil.isVoicemailNumber(context, call.getAccountHandle(), call.getNumber());
+ }
+
+ /**
+ * Handles certain "corner cases" for CNAP. When we receive weird phone numbers from the network
+ * to indicate different number presentations, convert them to expected number and presentation
+ * values within the CallerInfo object.
+ *
+ * @param number number we use to verify if we are in a corner case
+ * @param presentation presentation value used to verify if we are in a corner case
+ * @return the new String that should be used for the phone number
+ */
+ /* package */
+ static String modifyForSpecialCnapCases(
+ Context context, CallerInfo ci, String number, int presentation) {
+ // Obviously we return number if ci == null, but still return number if
+ // number == null, because in these cases the correct string will still be
+ // displayed/logged after this function returns based on the presentation value.
+ if (ci == null || number == null) {
+ return number;
+ }
+
+ LogUtil.d(
+ "CallerInfoUtils.modifyForSpecialCnapCases",
+ "modifyForSpecialCnapCases: initially, number="
+ + toLogSafePhoneNumber(number)
+ + ", presentation="
+ + presentation
+ + " ci "
+ + ci);
+
+ // "ABSENT NUMBER" is a possible value we could get from the network as the
+ // phone number, so if this happens, change it to "Unknown" in the CallerInfo
+ // and fix the presentation to be the same.
+ final String[] absentNumberValues = context.getResources().getStringArray(R.array.absent_num);
+ if (Arrays.asList(absentNumberValues).contains(number)
+ && presentation == TelecomManager.PRESENTATION_ALLOWED) {
+ number = context.getString(R.string.unknown);
+ ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN;
+ }
+
+ // Check for other special "corner cases" for CNAP and fix them similarly. Corner
+ // cases only apply if we received an allowed presentation from the network, so check
+ // if we think we have an allowed presentation, or if the CallerInfo presentation doesn't
+ // match the presentation passed in for verification (meaning we changed it previously
+ // because it's a corner case and we're being called from a different entry point).
+ if (ci.numberPresentation == TelecomManager.PRESENTATION_ALLOWED
+ || (ci.numberPresentation != presentation
+ && presentation == TelecomManager.PRESENTATION_ALLOWED)) {
+ // For all special strings, change number & numberPrentation.
+ if (isCnapSpecialCaseRestricted(number)) {
+ number = PhoneNumberHelper.getDisplayNameForRestrictedNumber(context).toString();
+ ci.numberPresentation = TelecomManager.PRESENTATION_RESTRICTED;
+ } else if (isCnapSpecialCaseUnknown(number)) {
+ number = context.getString(R.string.unknown);
+ ci.numberPresentation = TelecomManager.PRESENTATION_UNKNOWN;
+ }
+ LogUtil.d(
+ "CallerInfoUtils.modifyForSpecialCnapCases",
+ "SpecialCnap: number="
+ + toLogSafePhoneNumber(number)
+ + "; presentation now="
+ + ci.numberPresentation);
+ }
+ LogUtil.d(
+ "CallerInfoUtils.modifyForSpecialCnapCases",
+ "returning number string=" + toLogSafePhoneNumber(number));
+ return number;
+ }
+
+ private static boolean isCnapSpecialCaseRestricted(String n) {
+ return n.equals("PRIVATE") || n.equals("P") || n.equals("RES") || n.equals("PRIVATENUMBER");
+ }
+
+ private static boolean isCnapSpecialCaseUnknown(String n) {
+ return n.equals("UNAVAILABLE") || n.equals("UNKNOWN") || n.equals("UNA") || n.equals("U");
+ }
+
+ /* package */
+ static String toLogSafePhoneNumber(String number) {
+ // For unknown number, log empty string.
+ if (number == null) {
+ return "";
+ }
+
+ // Todo: Figure out an equivalent for VDBG
+ if (false) {
+ // When VDBG is true we emit PII.
+ return number;
+ }
+
+ // Do exactly same thing as Uri#toSafeString() does, which will enable us to compare
+ // sanitized phone numbers.
+ StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < number.length(); i++) {
+ char c = number.charAt(i);
+ if (c == '-' || c == '@' || c == '.' || c == '&') {
+ builder.append(c);
+ } else {
+ builder.append('x');
+ }
+ }
+ return builder.toString();
+ }
+
+ /**
+ * Send a notification using a {@link ContactLoader} to inform the sync adapter that we are
+ * viewing a particular contact, so that it can download the high-res photo.
+ */
+ public static void sendViewNotification(Context context, Uri contactUri) {
+ final ContactLoader loader =
+ new ContactLoader(context, contactUri, true /* postViewNotification */);
+ loader.registerListener(
+ 0,
+ new OnLoadCompleteListener<Contact>() {
+ @Override
+ public void onLoadComplete(Loader<Contact> loader, Contact contact) {
+ try {
+ loader.reset();
+ } catch (RuntimeException e) {
+ LogUtil.e("CallerInfoUtils.onLoadComplete", "Error resetting loader", e);
+ }
+ }
+ });
+ loader.startLoading();
+ }
+}