diff options
Diffstat (limited to 'InCallUI/src/com')
11 files changed, 699 insertions, 22 deletions
diff --git a/InCallUI/src/com/android/incallui/Call.java b/InCallUI/src/com/android/incallui/Call.java index 54ec52829..3973fdfd6 100644 --- a/InCallUI/src/com/android/incallui/Call.java +++ b/InCallUI/src/com/android/incallui/Call.java @@ -33,6 +33,8 @@ import android.telecom.VideoProfile; import android.text.TextUtils; import com.android.contacts.common.CallUtil; +import com.android.contacts.common.compat.CallSdkCompat; +import com.android.contacts.common.compat.CompatUtils; import com.android.contacts.common.compat.SdkVersionOverride; import com.android.contacts.common.compat.telecom.TelecomManagerCompat; import com.android.contacts.common.testing.NeededForTesting; @@ -389,6 +391,16 @@ public class Call { private LogState mLogState = new LogState(); + private boolean mIsSpam; + + public void setSpam(boolean isSpam) { + mIsSpam = isSpam; + } + + public boolean isSpam() { + return mIsSpam; + } + /** * Used only to create mock calls for testing */ @@ -399,13 +411,30 @@ public class Call { setState(state); } + /** + * Creates a new instance of a {@link Call}. Registers a callback for + * {@link android.telecom.Call} events. + */ public Call(android.telecom.Call telecomCall) { + this(telecomCall, true /* registerCallback */); + } + + /** + * Creates a new instance of a {@link Call}. Optionally registers a callback for + * {@link android.telecom.Call} events. + * + * Intended for use when creating a {@link Call} instance for use with the + * {@link ContactInfoCache}, where we do not want to register callbacks for the new call. + */ + public Call(android.telecom.Call telecomCall, boolean registerCallback) { mTelecomCall = telecomCall; mId = ID_PREFIX + Integer.toString(sIdCounter++); - updateFromTelecomCall(); + updateFromTelecomCall(registerCallback); - mTelecomCall.registerCallback(mTelecomCallCallback); + if (registerCallback) { + mTelecomCall.registerCallback(mTelecomCallCallback); + } mTimeAddedMs = System.currentTimeMillis(); } @@ -425,7 +454,8 @@ public class Call { private void update() { Trace.beginSection("Update"); int oldState = getState(); - updateFromTelecomCall(); + // We want to potentially register a video call callback here. + updateFromTelecomCall(true /* registerCallback */); if (oldState != getState() && getState() == Call.State.DISCONNECTED) { CallList.getInstance().onDisconnect(this); } else { @@ -434,7 +464,7 @@ public class Call { Trace.endSection(); } - private void updateFromTelecomCall() { + private void updateFromTelecomCall(boolean registerCallback) { Log.d(this, "updateFromTelecomCall: " + mTelecomCall.toString()); final int translatedState = translateState(mTelecomCall.getState()); if (mState != State.BLOCKED) { @@ -443,7 +473,7 @@ public class Call { maybeCancelVideoUpgrade(mTelecomCall.getDetails().getVideoState()); } - if (mTelecomCall.getVideoCall() != null) { + if (registerCallback && mTelecomCall.getVideoCall() != null) { if (mVideoCallCallback == null) { mVideoCallCallback = new InCallVideoCallCallback(this); } @@ -875,9 +905,46 @@ public class Call { } /** + * Determines if the call is an external call. + * + * An external call is one which does not exist locally for the + * {@link android.telecom.ConnectionService} it is associated with. + * + * External calls are only supported in N and higher. + * + * @return {@code true} if the call is an external call, {@code false} otherwise. + */ + public boolean isExternalCall() { + return CompatUtils.isNCompatible() && + hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL); + } + + /** + * Determines if the external call is pullable. + * + * An external call is one which does not exist locally for the + * {@link android.telecom.ConnectionService} it is associated with. An external call may be + * "pullable", which means that the user can request it be transferred to the current device. + * + * External calls are only supported in N and higher. + * + * @return {@code true} if the call is an external call, {@code false} otherwise. + */ + public boolean isPullableExternalCall() { + return CompatUtils.isNCompatible() && + (mTelecomCall.getDetails().getCallCapabilities() + & CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) + == CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL; + } + + /** * Logging utility methods */ public void logCallInitiationType() { + if (isExternalCall()) { + return; + } + if (getState() == State.INCOMING) { getLogState().callInitiationMethod = LogState.INITIATION_INCOMING; } else if (getIntentExtras() != null) { @@ -895,11 +962,12 @@ public class Call { return String.valueOf(mId); } - return String.format(Locale.US, "[%s, %s, %s, children:%s, parent:%s, conferenceable:%s, " + - "videoState:%s, mSessionModificationState:%d, VideoSettings:%s]", + return String.format(Locale.US, "[%s, %s, %s, %s, children:%s, parent:%s, " + + "conferenceable:%s, videoState:%s, mSessionModificationState:%d, VideoSettings:%s]", mId, State.toString(getState()), Details.capabilitiesToString(mTelecomCall.getDetails().getCallCapabilities()), + Details.propertiesToString(mTelecomCall.getDetails().getCallProperties()), mChildCallIds, getParentId(), this.mTelecomCall.getConferenceableCalls(), diff --git a/InCallUI/src/com/android/incallui/CallCardFragment.java b/InCallUI/src/com/android/incallui/CallCardFragment.java index 785d3d3aa..54b37e578 100644 --- a/InCallUI/src/com/android/incallui/CallCardFragment.java +++ b/InCallUI/src/com/android/incallui/CallCardFragment.java @@ -130,6 +130,7 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr private TextView mCallTypeLabel; private ImageView mHdAudioIcon; private ImageView mForwardIcon; + private ImageView mSpamIcon; private View mCallNumberAndLabel; private TextView mElapsedTime; private Drawable mPrimaryPhotoDrawable; @@ -280,6 +281,7 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr mCallStateLabel = (TextView) view.findViewById(R.id.callStateLabel); mHdAudioIcon = (ImageView) view.findViewById(R.id.hdAudioIcon); mForwardIcon = (ImageView) view.findViewById(R.id.forwardIcon); + mSpamIcon = (ImageView) view.findViewById(R.id.spamIcon); mCallNumberAndLabel = view.findViewById(R.id.labelAndNumber); mCallTypeLabel = (TextView) view.findViewById(R.id.callTypeLabel); mElapsedTime = (TextView) view.findViewById(R.id.elapsedTime); @@ -1207,6 +1209,19 @@ public class CallCardFragment extends BaseFragment<CallCardPresenter, CallCardPr mForwardIcon.setVisibility(visible ? View.VISIBLE : View.GONE); } + /** + * Changes the visibility of the spam icon. + * + * @param visible {@code true} if the UI should show the spam icon. + */ + @Override + public void showSpamIndicator(boolean visible) { + if (visible) { + mSpamIcon.setVisibility(View.VISIBLE); + mNumberLabel.setText(R.string.label_spam_caller); + mPhoneNumber.setVisibility(View.GONE); + } + } /** * Changes the visibility of the "manage conference call" button. diff --git a/InCallUI/src/com/android/incallui/CallCardPresenter.java b/InCallUI/src/com/android/incallui/CallCardPresenter.java index 3e3926ba9..a7d4dd483 100644 --- a/InCallUI/src/com/android/incallui/CallCardPresenter.java +++ b/InCallUI/src/com/android/incallui/CallCardPresenter.java @@ -249,7 +249,6 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> Call.areSameNumber(mPrimary, primary)); final boolean secondaryChanged = !(Call.areSame(mSecondary, secondary) && Call.areSameNumber(mSecondary, secondary)); - final boolean shouldShowCallSubject = shouldShowCallSubject(mPrimary); mSecondary = secondary; Call previousPrimary = mPrimary; @@ -262,9 +261,8 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> // Refresh primary call information if either: // 1. Primary call changed. // 2. The call's ability to manage conference has changed. - if (mPrimary != null && (primaryChanged || - ui.isManageConferenceVisible() != shouldShowManageConference()) || - ui.isCallSubjectVisible() != shouldShowCallSubject) { + // 3. The call subject should be shown or hidden. + if (shouldRefreshPrimaryInfo(primaryChanged, ui, shouldShowCallSubject(mPrimary))) { // primary call has changed if (previousPrimary != null) { //clear progess spinner (if any) related to previous primary call @@ -402,6 +400,16 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> updatePrimaryDisplayInfo(); } + private boolean shouldRefreshPrimaryInfo(boolean primaryChanged, CallCardUi ui, + boolean shouldShowCallSubject) { + if (mPrimary == null) { + return false; + } + return primaryChanged || + ui.isManageConferenceVisible() != shouldShowManageConference() || + ui.isCallSubjectVisible() != shouldShowCallSubject; + } + private String getSubscriptionNumber() { // If it's an emergency call, and they're not populating the callback number, // then try to fall back to the phone sub info (to hopefully get the SIM's @@ -450,6 +458,10 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> getUi().showHdAudioIndicator(showHdAudioIndicator); } + private void maybeShowSpamIconAndLabel() { + getUi().showSpamIndicator(mPrimary.isSpam()); + } + /** * Only show the conference call button if we can manage the conference. */ @@ -818,6 +830,7 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> boolean isEmergencyCall = mPrimary.isEmergencyCall(); mEmergencyCallListener.onCallUpdated((BaseFragment) ui, isEmergencyCall); } + maybeShowSpamIconAndLabel(); } private void updateSecondaryDisplayInfo() { @@ -1155,6 +1168,7 @@ public class CallCardPresenter extends Presenter<CallCardPresenter.CallCardUi> void setProgressSpinnerVisible(boolean visible); void showHdAudioIndicator(boolean visible); void showForwardIndicator(boolean visible); + void showSpamIndicator(boolean visible); void showManageConferenceCallButton(boolean visible); boolean isManageConferenceVisible(); boolean isCallSubjectVisible(); diff --git a/InCallUI/src/com/android/incallui/CallList.java b/InCallUI/src/com/android/incallui/CallList.java index d0f3c1000..61e8aa452 100644 --- a/InCallUI/src/com/android/incallui/CallList.java +++ b/InCallUI/src/com/android/incallui/CallList.java @@ -23,8 +23,8 @@ import android.telecom.DisconnectCause; import android.telecom.PhoneAccount; import com.android.contacts.common.testing.NeededForTesting; -import com.android.dialer.database.FilteredNumberAsyncQueryHandler; import com.android.dialer.logging.Logger; +import com.android.dialer.service.ExtendedCallInfoService; import com.android.incallui.util.TelecomCallUtil; import com.google.common.base.Preconditions; @@ -69,7 +69,7 @@ public class CallList { .newHashMap(); private final Set<Call> mPendingDisconnectCalls = Collections.newSetFromMap( new ConcurrentHashMap<Call, Boolean>(8, 0.9f, 1)); - private FilteredNumberAsyncQueryHandler mFilteredQueryHandler; + private ExtendedCallInfoService mExtendedCallInfoService; /** * Static singleton accessor method. @@ -94,6 +94,17 @@ public class CallList { if (call.getState() == Call.State.INCOMING || call.getState() == Call.State.CALL_WAITING) { onIncoming(call, call.getCannedSmsResponses()); + if (mExtendedCallInfoService != null) { + String number = TelecomCallUtil.getNumber(telecomCall); + mExtendedCallInfoService.getExtendedCallInfo(number, null, + new ExtendedCallInfoService.Listener() { + @Override + public void onComplete(boolean isSpam) { + call.setSpam(isSpam); + onUpdate(call); + } + }); + } } else { onUpdate(call); } @@ -615,8 +626,8 @@ public class CallList { } }; - public void setFilteredNumberQueryHandler(FilteredNumberAsyncQueryHandler handler) { - mFilteredQueryHandler = handler; + public void setExtendedCallInfoService(ExtendedCallInfoService service) { + mExtendedCallInfoService = service; } /** diff --git a/InCallUI/src/com/android/incallui/CallerInfo.java b/InCallUI/src/com/android/incallui/CallerInfo.java index f270678e0..f3d0e0763 100644 --- a/InCallUI/src/com/android/incallui/CallerInfo.java +++ b/InCallUI/src/com/android/incallui/CallerInfo.java @@ -322,7 +322,7 @@ public class CallerInfo { info.userType = ContactsUtils.determineUserType(directoryId, contactId); info.nameAlternative = ContactInfoHelper.lookUpDisplayNameAlternative( - context, info.lookupKeyOrNull, info.userType); + context, info.lookupKeyOrNull, info.userType, directoryId); } cursor.close(); } diff --git a/InCallUI/src/com/android/incallui/ExternalCallList.java b/InCallUI/src/com/android/incallui/ExternalCallList.java new file mode 100644 index 000000000..06e0bb975 --- /dev/null +++ b/InCallUI/src/com/android/incallui/ExternalCallList.java @@ -0,0 +1,105 @@ +package com.android.incallui; + +import com.google.common.base.Preconditions; + +import com.android.contacts.common.compat.CallSdkCompat; + +import android.os.Handler; +import android.os.Looper; +import android.telecom.Call; +import android.util.ArraySet; + +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Tracks the external calls known to the InCall UI. + * + * External calls are those with {@link android.telecom.Call.Details#PROPERTY_IS_EXTERNAL_CALL}. + */ +public class ExternalCallList { + + public interface ExternalCallListener { + void onExternalCallAdded(Call call); + void onExternalCallRemoved(Call call); + void onExternalCallUpdated(Call call); + } + + /** + * Handles {@link android.telecom.Call.Callback} callbacks. + */ + private final Call.Callback mTelecomCallCallback = new Call.Callback() { + @Override + public void onDetailsChanged(Call call, Call.Details details) { + notifyExternalCallUpdated(call); + } + }; + + private final Set<Call> mExternalCalls = new ArraySet<>(); + private final Set<ExternalCallListener> mExternalCallListeners = Collections.newSetFromMap( + new ConcurrentHashMap<ExternalCallListener, Boolean>(8, 0.9f, 1)); + + /** + * Begins tracking an external call and notifies listeners of the new call. + */ + public void onCallAdded(Call telecomCall) { + Preconditions.checkArgument(telecomCall.getDetails() + .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)); + mExternalCalls.add(telecomCall); + telecomCall.registerCallback(mTelecomCallCallback, new Handler(Looper.getMainLooper())); + notifyExternalCallAdded(telecomCall); + } + + /** + * Stops tracking an external call and notifies listeners of the removal of the call. + */ + public void onCallRemoved(Call telecomCall) { + Preconditions.checkArgument(mExternalCalls.contains(telecomCall)); + mExternalCalls.remove(telecomCall); + telecomCall.unregisterCallback(mTelecomCallCallback); + notifyExternalCallRemoved(telecomCall); + } + + /** + * Adds a new listener to external call events. + */ + public void addExternalCallListener(ExternalCallListener listener) { + mExternalCallListeners.add(Preconditions.checkNotNull(listener)); + } + + /** + * Removes a listener to external call events. + */ + public void removeExternalCallListener(ExternalCallListener listener) { + Preconditions.checkArgument(mExternalCallListeners.contains(listener)); + mExternalCallListeners.remove(Preconditions.checkNotNull(listener)); + } + + /** + * Notifies listeners of the addition of a new external call. + */ + private void notifyExternalCallAdded(Call call) { + for (ExternalCallListener listener : mExternalCallListeners) { + listener.onExternalCallAdded(call); + } + } + + /** + * Notifies listeners of the removal of an external call. + */ + private void notifyExternalCallRemoved(Call call) { + for (ExternalCallListener listener : mExternalCallListeners) { + listener.onExternalCallRemoved(call); + } + } + + /** + * Notifies listeners of changes to an external call. + */ + private void notifyExternalCallUpdated(Call call) { + for (ExternalCallListener listener : mExternalCallListeners) { + listener.onExternalCallUpdated(call); + } + } +} diff --git a/InCallUI/src/com/android/incallui/ExternalCallNotifier.java b/InCallUI/src/com/android/incallui/ExternalCallNotifier.java new file mode 100644 index 000000000..40a2e02bf --- /dev/null +++ b/InCallUI/src/com/android/incallui/ExternalCallNotifier.java @@ -0,0 +1,406 @@ +/* + * 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 com.google.common.base.Preconditions; + +import com.android.contacts.common.ContactsUtils; +import com.android.contacts.common.compat.CallSdkCompat; +import com.android.contacts.common.preference.ContactsPreferences; +import com.android.contacts.common.util.BitmapUtil; +import com.android.contacts.common.util.ContactDisplayUtils; +import com.android.dialer.R; +import com.android.incallui.util.TelecomCallUtil; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.net.Uri; +import android.support.annotation.Nullable; +import android.telecom.Call; +import android.telecom.PhoneAccount; +import android.text.BidiFormatter; +import android.text.TextDirectionHeuristics; +import android.text.TextUtils; +import android.util.ArrayMap; + +import java.util.Map; + +/** + * Handles the display of notifications for "external calls". + * + * External calls are a representation of a call which is in progress on the user's other device + * (e.g. another phone, or a watch). + */ +public class ExternalCallNotifier implements ExternalCallList.ExternalCallListener { + + /** + * Tag used with the notification manager to uniquely identify external call notifications. + */ + private static final String NOTIFICATION_TAG = "EXTERNAL_CALL"; + + /** + * Represents a call and associated cached notification data. + */ + private static class NotificationInfo { + private final Call mCall; + private final int mNotificationId; + @Nullable private String mContentTitle; + @Nullable private Bitmap mLargeIcon; + @Nullable private String mPersonReference; + + public NotificationInfo(Call call, int notificationId) { + Preconditions.checkNotNull(call); + mCall = call; + mNotificationId = notificationId; + } + + public Call getCall() { + return mCall; + } + + public int getNotificationId() { + return mNotificationId; + } + + public @Nullable String getContentTitle() { + return mContentTitle; + } + + public @Nullable Bitmap getLargeIcon() { + return mLargeIcon; + } + + public @Nullable String getPersonReference() { + return mPersonReference; + } + + public void setContentTitle(@Nullable String contentTitle) { + mContentTitle = contentTitle; + } + + public void setLargeIcon(@Nullable Bitmap largeIcon) { + mLargeIcon = largeIcon; + } + + public void setPersonReference(@Nullable String personReference) { + mPersonReference = personReference; + } + } + + private final Context mContext; + private final ContactInfoCache mContactInfoCache; + private Map<Call, NotificationInfo> mNotifications = new ArrayMap<>(); + private int mNextUniqueNotificationId; + private ContactsPreferences mContactsPreferences; + + /** + * Initializes a new instance of the external call notifier. + */ + public ExternalCallNotifier(Context context, ContactInfoCache contactInfoCache) { + mContext = Preconditions.checkNotNull(context); + mContactsPreferences = ContactsPreferencesFactory.newContactsPreferences(mContext); + mContactInfoCache = Preconditions.checkNotNull(contactInfoCache); + } + + /** + * Handles the addition of a new external call by showing a new notification. + * Triggered by {@link CallList#onCallAdded(android.telecom.Call)}. + */ + @Override + public void onExternalCallAdded(android.telecom.Call call) { + Log.i(this, "onExternalCallAdded " + call); + Preconditions.checkArgument(!mNotifications.containsKey(call)); + NotificationInfo info = new NotificationInfo(call, mNextUniqueNotificationId++); + mNotifications.put(call, info); + + showNotifcation(info); + } + + /** + * Handles the removal of an external call by hiding its associated notification. + * Triggered by {@link CallList#onCallRemoved(android.telecom.Call)}. + */ + @Override + public void onExternalCallRemoved(android.telecom.Call call) { + Log.i(this, "onExternalCallRemoved " + call); + + dismissNotification(call); + } + + /** + * Handles updates to an external call. + */ + @Override + public void onExternalCallUpdated(Call call) { + Preconditions.checkArgument(mNotifications.containsKey(call)); + postNotification(mNotifications.get(call)); + } + + /** + * Initiates a call pull given a notification ID. + * + * @param notificationId The notification ID associated with the external call which is to be + * pulled. + */ + public void pullExternalCall(int notificationId) { + for (NotificationInfo info : mNotifications.values()) { + if (info.getNotificationId() == notificationId) { + CallSdkCompat.pullExternalCall(info.getCall()); + return; + } + } + } + + /** + * Shows a notification for a new external call. Performs a contact cache lookup to find any + * associated photo and information for the call. + */ + private void showNotifcation(final NotificationInfo info) { + // We make a call to the contact info cache to query for supplemental data to what the + // call provides. This includes the contact name and photo. + // This callback will always get called immediately and synchronously with whatever data + // it has available, and may make a subsequent call later (same thread) if it had to + // call into the contacts provider for more data. + com.android.incallui.Call incallCall = new com.android.incallui.Call(info.getCall(), + false /* registerCallback */); + + mContactInfoCache.findInfo(incallCall, false /* isIncoming */, + new ContactInfoCache.ContactInfoCacheCallback() { + @Override + public void onContactInfoComplete(String callId, + ContactInfoCache.ContactCacheEntry entry) { + + // Ensure notification still exists as the external call could have been + // removed during async contact info lookup. + if (mNotifications.containsKey(info.getCall())) { + saveContactInfo(info, entry); + } + } + + @Override + public void onImageLoadComplete(String callId, + ContactInfoCache.ContactCacheEntry entry) { + + // Ensure notification still exists as the external call could have been + // removed during async contact info lookup. + if (mNotifications.containsKey(info.getCall())) { + savePhoto(info, entry); + } + } + + @Override + public void onContactInteractionsInfoComplete(String callId, + ContactInfoCache.ContactCacheEntry entry) { + } + }); + } + + /** + * Dismisses a notification for an external call. + */ + private void dismissNotification(Call call) { + Preconditions.checkArgument(mNotifications.containsKey(call)); + + NotificationManager notificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(NOTIFICATION_TAG, mNotifications.get(call).getNotificationId()); + + mNotifications.remove(call); + } + + /** + * Attempts to build a large icon to use for the notification based on the contact info and + * post the updated notification to the notification manager. + */ + private void savePhoto(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) { + Bitmap largeIcon = getLargeIconToDisplay(mContext, entry, info.getCall()); + if (largeIcon != null) { + largeIcon = getRoundedIcon(mContext, largeIcon); + } + info.setLargeIcon(largeIcon); + postNotification(info); + } + + /** + * Builds and stores the contact information the notification will display and posts the updated + * notification to the notification manager. + */ + private void saveContactInfo(NotificationInfo info, ContactInfoCache.ContactCacheEntry entry) { + info.setContentTitle(getContentTitle(mContext, mContactsPreferences, + entry, info.getCall())); + info.setPersonReference(getPersonReference(entry, info.getCall())); + postNotification(info); + } + + /** + * Rebuild an existing or show a new notification given {@link NotificationInfo}. + */ + private void postNotification(NotificationInfo info) { + Log.i(this, "postNotification : " + info.getContentTitle()); + Notification.Builder builder = new Notification.Builder(mContext); + // Set notification as ongoing since calls are long-running versus a point-in-time notice. + builder.setOngoing(true); + // Make the notification prioritized over the other normal notifications. + builder.setPriority(Notification.PRIORITY_HIGH); + // Set the content ("Ongoing call on another device") + builder.setContentText(mContext.getString(R.string.notification_external_call)); + builder.setSmallIcon(R.drawable.ic_call_white_24dp); + builder.setContentTitle(info.getContentTitle()); + builder.setLargeIcon(info.getLargeIcon()); + builder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); + builder.addPerson(info.getPersonReference()); + + // Where the external call supports being transferred to the local device, add an action + // to the notification to initiate the call pull process. + if ((info.getCall().getDetails().getCallCapabilities() + & CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) + == CallSdkCompat.Details.CAPABILITY_CAN_PULL_CALL) { + + Intent intent = new Intent( + NotificationBroadcastReceiver.ACTION_PULL_EXTERNAL_CALL, null, mContext, + NotificationBroadcastReceiver.class); + intent.putExtra(NotificationBroadcastReceiver.EXTRA_NOTIFICATION_ID, + info.getNotificationId()); + + builder.addAction(new Notification.Action.Builder(R.drawable.ic_call_white_24dp, + mContext.getText(R.string.notification_transfer_call), + PendingIntent.getBroadcast(mContext, 0, intent, 0)).build()); + } + + /** + * This builder is used for the notification shown when the device is locked and the user + * has set their notification settings to 'hide sensitive content' + * {@see Notification.Builder#setPublicVersion}. + */ + Notification.Builder publicBuilder = new Notification.Builder(mContext); + publicBuilder.setSmallIcon(R.drawable.ic_call_white_24dp); + publicBuilder.setColor(mContext.getResources().getColor(R.color.dialer_theme_color)); + + builder.setPublicVersion(publicBuilder.build()); + Notification notification = builder.build(); + + NotificationManager notificationManager = + (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(NOTIFICATION_TAG, info.getNotificationId(), notification); + } + + /** + * Finds a large icon to display in a notification for a call. For conference calls, a + * conference call icon is used, otherwise if contact info is specified, the user's contact + * photo or avatar is used. + * + * @param context The context. + * @param contactInfo The contact cache info. + * @param call The call. + * @return The large icon to use for the notification. + */ + private @Nullable Bitmap getLargeIconToDisplay(Context context, + ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) { + + Bitmap largeIcon = null; + if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) && + !call.getDetails() + .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) { + + largeIcon = BitmapFactory.decodeResource(context.getResources(), + R.drawable.img_conference); + } + if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { + largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); + } + return largeIcon; + } + + /** + * Given a bitmap, returns a rounded version of the icon suitable for display in a notification. + * + * @param context The context. + * @param bitmap The bitmap to round. + * @return The rounded bitmap. + */ + private @Nullable Bitmap getRoundedIcon(Context context, @Nullable Bitmap bitmap) { + if (bitmap == null) { + return null; + } + final int height = (int) context.getResources().getDimension( + android.R.dimen.notification_large_icon_height); + final int width = (int) context.getResources().getDimension( + android.R.dimen.notification_large_icon_width); + return BitmapUtil.getRoundedBitmap(bitmap, width, height); + } + + /** + * Builds a notification content title for a call. If the call is a conference call, it is + * identified as such. Otherwise an attempt is made to show an associated contact name or + * phone number. + * + * @param context The context. + * @param contactsPreferences Contacts preferences, used to determine the preferred formatting + * for contact names. + * @param contactInfo The contact info which was looked up in the contact cache. + * @param call The call to generate a title for. + * @return The content title. + */ + private @Nullable String getContentTitle(Context context, + @Nullable ContactsPreferences contactsPreferences, + ContactInfoCache.ContactCacheEntry contactInfo, android.telecom.Call call) { + + if (call.getDetails().hasProperty(android.telecom.Call.Details.PROPERTY_CONFERENCE) && + !call.getDetails() + .hasProperty(android.telecom.Call.Details.PROPERTY_GENERIC_CONFERENCE)) { + + return context.getResources().getString(R.string.card_title_conf_call); + } + + String preferredName = ContactDisplayUtils.getPreferredDisplayName(contactInfo.namePrimary, + contactInfo.nameAlternative, contactsPreferences); + if (TextUtils.isEmpty(preferredName)) { + return TextUtils.isEmpty(contactInfo.number) ? null : BidiFormatter.getInstance() + .unicodeWrap(contactInfo.number, TextDirectionHeuristics.LTR); + } + return preferredName; + } + + /** + * Gets a "person reference" for a notification, used by the system to determine whether the + * notification should be allowed past notification interruption filters. + * + * @param contactInfo The contact info from cache. + * @param call The call. + * @return the person reference. + */ + private String getPersonReference(ContactInfoCache.ContactCacheEntry contactInfo, + Call call) { + + String number = TelecomCallUtil.getNumber(call); + // Query {@link Contacts#CONTENT_LOOKUP_URI} directly with work lookup key is not allowed. + // So, do not pass {@link Contacts#CONTENT_LOOKUP_URI} to NotificationManager to avoid + // NotificationManager using it. + if (contactInfo.lookupUri != null && contactInfo.userType != ContactsUtils.USER_TYPE_WORK) { + return contactInfo.lookupUri.toString(); + } else if (!TextUtils.isEmpty(number)) { + return Uri.fromParts(PhoneAccount.SCHEME_TEL, number, null).toString(); + } + return ""; + } +} diff --git a/InCallUI/src/com/android/incallui/InCallPresenter.java b/InCallUI/src/com/android/incallui/InCallPresenter.java index 0109d7ee6..189049c47 100644 --- a/InCallUI/src/com/android/incallui/InCallPresenter.java +++ b/InCallUI/src/com/android/incallui/InCallPresenter.java @@ -41,6 +41,7 @@ import android.view.Window; import android.view.WindowManager; import com.android.contacts.common.GeoUtil; +import com.android.contacts.common.compat.CallSdkCompat; import com.android.contacts.common.compat.CompatUtils; import com.android.contacts.common.compat.telecom.TelecomManagerCompat; import com.android.contacts.common.interactions.TouchPointManager; @@ -109,9 +110,11 @@ public class InCallPresenter implements CallList.Listener, private AudioModeProvider mAudioModeProvider; private StatusBarNotifier mStatusBarNotifier; + private ExternalCallNotifier mExternalCallNotifier; private ContactInfoCache mContactInfoCache; private Context mContext; private CallList mCallList; + private ExternalCallList mExternalCallList; private InCallActivity mInCallActivity; private InCallState mInCallState = InCallState.NO_CALLS; private ProximitySensor mProximitySensor; @@ -299,8 +302,10 @@ public class InCallPresenter implements CallList.Listener, public void setUp(Context context, CallList callList, + ExternalCallList externalCallList, AudioModeProvider audioModeProvider, StatusBarNotifier statusBarNotifier, + ExternalCallNotifier externalCallNotifier, ContactInfoCache contactInfoCache, ProximitySensor proximitySensor) { if (mServiceConnected) { @@ -318,6 +323,7 @@ public class InCallPresenter implements CallList.Listener, mContactInfoCache = contactInfoCache; mStatusBarNotifier = statusBarNotifier; + mExternalCallNotifier = externalCallNotifier; addListener(mStatusBarNotifier); mAudioModeProvider = audioModeProvider; @@ -329,6 +335,8 @@ public class InCallPresenter implements CallList.Listener, addInCallUiListener(mAnswerPresenter); mCallList = callList; + mExternalCallList = externalCallList; + externalCallList.addExternalCallListener(mExternalCallNotifier); // This only gets called by the service so this is okay. mServiceConnected = true; @@ -343,7 +351,8 @@ public class InCallPresenter implements CallList.Listener, mFilteredQueryHandler = new FilteredNumberAsyncQueryHandler(context.getContentResolver()); mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); mTelephonyManager.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE); - mCallList.setFilteredNumberQueryHandler(mFilteredQueryHandler); + mCallList.setExtendedCallInfoService( + com.android.dialerbind.ObjectFactory.newExtendedCallInfoService(context)); Log.d(this, "Finished InCallPresenter.setUp"); } @@ -501,7 +510,12 @@ public class InCallPresenter implements CallList.Listener, if (shouldAttemptBlocking(call)) { maybeBlockCall(call); } else { - mCallList.onCallAdded(call); + if (call.getDetails() + .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) { + mExternalCallList.onCallAdded(call); + } else { + mCallList.onCallAdded(call); + } } // Since a call has been added we are no longer waiting for Telecom to send us a call. @@ -590,8 +604,13 @@ public class InCallPresenter implements CallList.Listener, } public void onCallRemoved(android.telecom.Call call) { - mCallList.onCallRemoved(call); - call.unregisterCallback(mCallCallback); + if (call.getDetails() + .hasProperty(CallSdkCompat.Details.PROPERTY_IS_EXTERNAL_CALL)) { + mExternalCallList.onCallRemoved(call); + } else { + mCallList.onCallRemoved(call); + call.unregisterCallback(mCallCallback); + } } public void onCanAddCallChanged(boolean canAddCall) { @@ -1506,6 +1525,9 @@ public class InCallPresenter implements CallList.Listener, if (mStatusBarNotifier != null) { removeListener(mStatusBarNotifier); } + if (mExternalCallNotifier != null && mExternalCallList != null) { + mExternalCallList.removeExternalCallListener(mExternalCallNotifier); + } mStatusBarNotifier = null; if (mCallList != null) { @@ -1759,7 +1781,14 @@ public class InCallPresenter implements CallList.Listener, if (call == null) { return getColorsFromPhoneAccountHandle(mPendingPhoneAccountHandle); } else { - return getColorsFromPhoneAccountHandle(call.getAccountHandle()); + if (call.isSpam()) { + Resources resources = mContext.getResources(); + return new InCallUIMaterialColorMapUtils( + resources).calculatePrimaryAndSecondaryColor( + resources.getColor(R.color.incall_call_spam_background_color)); + } else { + return getColorsFromPhoneAccountHandle(call.getAccountHandle()); + } } } @@ -1808,6 +1837,10 @@ public class InCallPresenter implements CallList.Listener, return mAnswerPresenter; } + ExternalCallNotifier getExternalCallNotifier() { + return mExternalCallNotifier; + } + /** * Private constructor. Must use getInstance() to get this singleton. */ diff --git a/InCallUI/src/com/android/incallui/InCallServiceImpl.java b/InCallUI/src/com/android/incallui/InCallServiceImpl.java index 86936973e..1414bc51d 100644 --- a/InCallUI/src/com/android/incallui/InCallServiceImpl.java +++ b/InCallUI/src/com/android/incallui/InCallServiceImpl.java @@ -64,8 +64,10 @@ public class InCallServiceImpl extends InCallService { InCallPresenter.getInstance().setUp( getApplicationContext(), CallList.getInstance(), + new ExternalCallList(), AudioModeProvider.getInstance(), new StatusBarNotifier(context, contactInfoCache), + new ExternalCallNotifier(context, contactInfoCache), contactInfoCache, new ProximitySensor( context, diff --git a/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java b/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java index 2543b783d..27f71159d 100644 --- a/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java +++ b/InCallUI/src/com/android/incallui/NotificationBroadcastReceiver.java @@ -45,6 +45,10 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver { "com.android.incallui.ACTION_ACCEPT_VIDEO_UPGRADE_REQUEST"; public static final String ACTION_DECLINE_VIDEO_UPGRADE_REQUEST = "com.android.incallui.ACTION_DECLINE_VIDEO_UPGRADE_REQUEST"; + public static final String ACTION_PULL_EXTERNAL_CALL = + "com.android.incallui.ACTION_PULL_EXTERNAL_CALL"; + public static final String EXTRA_NOTIFICATION_ID = + "com.android.incallui.extra.EXTRA_NOTIFICATION_ID"; @Override public void onReceive(Context context, Intent intent) { @@ -68,6 +72,10 @@ public class NotificationBroadcastReceiver extends BroadcastReceiver { VideoProfile.STATE_BIDIRECTIONAL, context); } else if (action.equals(ACTION_DECLINE_VIDEO_UPGRADE_REQUEST)) { InCallPresenter.getInstance().declineUpgradeRequest(context); + } else if (action.equals(ACTION_PULL_EXTERNAL_CALL)) { + int notificationId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1); + InCallPresenter.getInstance().getExternalCallNotifier() + .pullExternalCall(notificationId); } } diff --git a/InCallUI/src/com/android/incallui/StatusBarNotifier.java b/InCallUI/src/com/android/incallui/StatusBarNotifier.java index 7d212aa8e..1ed3ae7dc 100644 --- a/InCallUI/src/com/android/incallui/StatusBarNotifier.java +++ b/InCallUI/src/com/android/incallui/StatusBarNotifier.java @@ -33,7 +33,9 @@ import android.content.Context; import android.content.Intent; import android.graphics.Bitmap; import android.graphics.BitmapFactory; +import android.graphics.Canvas; import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; import android.media.AudioAttributes; import android.net.Uri; import android.provider.ContactsContract.Contacts; @@ -52,6 +54,7 @@ import com.android.contacts.common.testing.NeededForTesting; import com.android.contacts.common.util.BitmapUtil; import com.android.contacts.common.util.ContactDisplayUtils; import com.android.dialer.R; +import com.android.dialer.service.ExtendedCallInfoService; import com.android.incallui.ContactInfoCache.ContactCacheEntry; import com.android.incallui.ContactInfoCache.ContactInfoCacheCallback; import com.android.incallui.InCallPresenter.InCallState; @@ -239,6 +242,10 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener, } final int callState = call.getState(); + // Dont' show as spam if the number is in local contact. + if (contactInfo.contactLookupResult == Call.LogState.LOOKUP_LOCAL_CONTACT) { + call.setSpam(false); + } // Check if data has changed; if nothing is different, don't issue another notification. final int iconResId = getIconToDisplay(call); @@ -463,6 +470,10 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener, if (contactInfo.photo != null && (contactInfo.photo instanceof BitmapDrawable)) { largeIcon = ((BitmapDrawable) contactInfo.photo).getBitmap(); } + if (call.isSpam()) { + Drawable drawable = mContext.getResources().getDrawable(R.drawable.blocked_contact); + largeIcon = CallCardFragment.drawableToBitmap(drawable); + } return largeIcon; } @@ -524,7 +535,11 @@ public class StatusBarNotifier implements InCallPresenter.InCallStateListener, if (call.hasProperty(Details.PROPERTY_WIFI)) { resId = R.string.notification_incoming_call_wifi; } else { - resId = R.string.notification_incoming_call; + if (call.isSpam()) { + resId = R.string.notification_incoming_spam_call; + } else { + resId = R.string.notification_incoming_call; + } } } else if (call.getState() == Call.State.ONHOLD) { resId = R.string.notification_on_hold; |