/* * Copyright 2014, The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.telecom.ui; import static android.Manifest.permission.READ_PHONE_STATE; import static android.app.admin.DevicePolicyResources.Strings.Telecomm.NOTIFICATION_MISSED_WORK_CALL_TITLE; import android.annotation.NonNull; import android.app.BroadcastOptions; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.TaskStackBuilder; import android.app.admin.DevicePolicyManager; import android.content.AsyncQueryHandler; import android.content.ContentProvider; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.database.Cursor; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; import android.os.Bundle; import android.os.UserHandle; import android.provider.CallLog.Calls; import android.telecom.CallerInfo; import android.telecom.Log; import android.telecom.Logging.Runnable; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; import android.text.BidiFormatter; import android.text.TextDirectionHeuristics; import android.text.TextUtils; import android.util.ArrayMap; import android.util.ArraySet; import com.android.server.telecom.CallerInfoLookupHelper; import com.android.server.telecom.CallsManagerListenerBase; import com.android.server.telecom.Constants; import com.android.server.telecom.DefaultDialerCache; import com.android.server.telecom.DeviceIdleControllerAdapter; import com.android.server.telecom.MissedCallNotifier; import com.android.server.telecom.PhoneAccountRegistrar; import com.android.server.telecom.R; import com.android.server.telecom.TelecomBroadcastIntentProcessor; import com.android.server.telecom.TelecomSystem; import com.android.server.telecom.Timeouts; import com.android.server.telecom.components.TelecomBroadcastReceiver; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.Set; /** * Creates a notification for calls that the user missed (neither answered nor rejected). * * TODO: Make TelephonyManager.clearMissedCalls call into this class. */ public class MissedCallNotifierImpl extends CallsManagerListenerBase implements MissedCallNotifier { public interface MissedCallNotifierImplFactory { MissedCallNotifier makeMissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar, DefaultDialerCache defaultDialerCache, DeviceIdleControllerAdapter deviceIdleControllerAdapter); } public interface NotificationBuilderFactory { Notification.Builder getBuilder(Context context); } private static class DefaultNotificationBuilderFactory implements NotificationBuilderFactory { public DefaultNotificationBuilderFactory() {} @Override public Notification.Builder getBuilder(Context context) { return new Notification.Builder(context); } } private static final String[] CALL_LOG_PROJECTION = new String[] { Calls._ID, Calls.NUMBER, Calls.NUMBER_PRESENTATION, Calls.DATE, Calls.DURATION, Calls.TYPE, }; private static final String CALL_LOG_WHERE_CLAUSE = "type=" + Calls.MISSED_TYPE + " AND new=1" + " AND is_read=0"; public static final int CALL_LOG_COLUMN_ID = 0; public static final int CALL_LOG_COLUMN_NUMBER = 1; public static final int CALL_LOG_COLUMN_NUMBER_PRESENTATION = 2; public static final int CALL_LOG_COLUMN_DATE = 3; public static final int CALL_LOG_COLUMN_DURATION = 4; public static final int CALL_LOG_COLUMN_TYPE = 5; private static final int MISSED_CALL_NOTIFICATION_ID = 1; private static final String NOTIFICATION_TAG = MissedCallNotifierImpl.class.getSimpleName(); private static final String MISSED_CALL_POWER_SAVE_REASON = "missed-call"; private final Context mContext; private final PhoneAccountRegistrar mPhoneAccountRegistrar; private final NotificationManager mNotificationManager; private final NotificationBuilderFactory mNotificationBuilderFactory; private final DefaultDialerCache mDefaultDialerCache; private final DeviceIdleControllerAdapter mDeviceIdleControllerAdapter; private UserHandle mCurrentUserHandle; // Used to guard access to mMissedCallCounts private final Object mMissedCallCountsLock = new Object(); // Used to track the number of missed calls. private final Map mMissedCallCounts; private Set mUsersToLoadAfterBootComplete = new ArraySet<>(); public MissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar, DefaultDialerCache defaultDialerCache, DeviceIdleControllerAdapter deviceIdleControllerAdapter) { this(context, phoneAccountRegistrar, defaultDialerCache, new DefaultNotificationBuilderFactory(), deviceIdleControllerAdapter); } public MissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar, DefaultDialerCache defaultDialerCache, NotificationBuilderFactory notificationBuilderFactory, DeviceIdleControllerAdapter deviceIdleControllerAdapter) { mContext = context; mPhoneAccountRegistrar = phoneAccountRegistrar; mNotificationManager = (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE); mDeviceIdleControllerAdapter = deviceIdleControllerAdapter; mDefaultDialerCache = defaultDialerCache; mNotificationBuilderFactory = notificationBuilderFactory; mMissedCallCounts = new ArrayMap<>(); } /** Clears missed call notification and marks the call log's missed calls as read. */ @Override public void clearMissedCalls(UserHandle userHandle) { // If the default dialer is showing the missed call notification then it will modify the // call log and we don't have to do anything here. String dialerPackage = getDefaultDialerPackage(userHandle); if (!shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) { markMissedCallsAsRead(userHandle); } cancelMissedCallNotification(userHandle); } private void markMissedCallsAsRead(final UserHandle userHandle) { AsyncTask.execute(new Runnable("MCNI.mMCAR", null /*lock*/) { @Override public void loggedRun() { // Clear the list of new missed calls from the call log. ContentValues values = new ContentValues(); values.put(Calls.NEW, 0); values.put(Calls.IS_READ, 1); StringBuilder where = new StringBuilder(); where.append(Calls.NEW); where.append(" = 1 AND "); where.append(Calls.TYPE); where.append(" = ?"); try { Uri callsUri = ContentProvider .maybeAddUserId(Calls.CONTENT_URI, userHandle.getIdentifier()); mContext.getContentResolver().update(callsUri, values, where.toString(), new String[]{ Integer.toString(Calls. MISSED_TYPE) }); } catch (IllegalArgumentException e) { Log.w(this, "ContactsProvider update command failed", e); } } }.prepare()); } private String getDefaultDialerPackage(UserHandle userHandle) { String dialerPackage = mDefaultDialerCache.getDefaultDialerApplication( userHandle.getIdentifier()); if (TextUtils.isEmpty(dialerPackage)) { return null; } return dialerPackage; } /** * Returns the missed-call notification intent to send to the default dialer for the given user. * Note, the passed in userHandle is always the non-managed user for SIM calls (multi-user * calls). In this case we return the default dialer for the logged in user. This is never the * managed (work profile) dialer. * * For non-multi-user calls (3rd party phone accounts), the passed in userHandle is the user * handle of the phone account. This could be a managed user. In that case we return the default * dialer for the given user which could be a managed (work profile) dialer. */ private Intent getShowMissedCallIntentForDefaultDialer(String dialerPackage) { return new Intent(TelecomManager.ACTION_SHOW_MISSED_CALLS_NOTIFICATION) .setPackage(dialerPackage); } private boolean shouldManageNotificationThroughDefaultDialer(String dialerPackage, UserHandle userHandle) { if (TextUtils.isEmpty(dialerPackage)) return false; Intent intent = getShowMissedCallIntentForDefaultDialer(dialerPackage); if (intent == null) { return false; } List receivers = mContext.getPackageManager() .queryBroadcastReceiversAsUser(intent, 0, userHandle.getIdentifier()); return receivers.size() > 0; } /** * For dialers that manage missed call handling themselves, we must temporarily add them to the * power save exemption list, as they must perform operations such as modifying the call log and * power save restrictions can cause these types of operations to not complete (sometimes * causing ANRs). */ private Bundle exemptFromPowerSavingTemporarily(String dialerPackage, UserHandle handle) { if (TextUtils.isEmpty(dialerPackage)) { return null; } BroadcastOptions bopts = BroadcastOptions.makeBasic(); long duration = Timeouts.getDialerMissedCallPowerSaveExemptionTimeMillis( mContext.getContentResolver()); mDeviceIdleControllerAdapter.exemptAppTemporarilyForEvent(dialerPackage, duration, handle.getIdentifier(), MISSED_CALL_POWER_SAVE_REASON); bopts.setTemporaryAppWhitelistDuration(duration); return bopts.toBundle(); } private void sendNotificationThroughDefaultDialer(String dialerPackage, CallInfo callInfo, UserHandle userHandle, int missedCallCount) { Intent intent = getShowMissedCallIntentForDefaultDialer(dialerPackage) .setFlags(Intent.FLAG_RECEIVER_FOREGROUND) .putExtra(TelecomManager.EXTRA_CLEAR_MISSED_CALLS_INTENT, createClearMissedCallsPendingIntent(userHandle)) .putExtra(TelecomManager.EXTRA_NOTIFICATION_COUNT, missedCallCount) .putExtra(TelecomManager.EXTRA_NOTIFICATION_PHONE_NUMBER, callInfo == null ? null : callInfo.getPhoneNumber()) .putExtra(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, callInfo == null ? null : callInfo.getPhoneAccountHandle()); if (missedCallCount == 1 && callInfo != null) { final Uri handleUri = callInfo.getHandle(); String handle = handleUri == null ? null : handleUri.getSchemeSpecificPart(); if (!TextUtils.isEmpty(handle) && !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))) { intent.putExtra(TelecomManager.EXTRA_CALL_BACK_INTENT, createCallBackPendingIntent(handleUri, userHandle)); } } Log.i(this, "sendNotificationThroughDefaultDialer; count=%d, dialerPackage=%s", missedCallCount, intent.getPackage()); Bundle options = exemptFromPowerSavingTemporarily(dialerPackage, userHandle); mContext.sendBroadcastAsUser(intent, userHandle, READ_PHONE_STATE, options); } /** * Create a system notification for the missed call. * * @param callInfo The missed call. */ @Override public void showMissedCallNotification(@NonNull CallInfo callInfo) { final PhoneAccountHandle phoneAccountHandle = callInfo.getPhoneAccountHandle(); final PhoneAccount phoneAccount = mPhoneAccountRegistrar.getPhoneAccountUnchecked(phoneAccountHandle); UserHandle userHandle; if (phoneAccount != null && phoneAccount.hasCapabilities(PhoneAccount.CAPABILITY_MULTI_USER)) { userHandle = mCurrentUserHandle; } else { userHandle = phoneAccountHandle.getUserHandle(); } showMissedCallNotification(callInfo, userHandle); } private void showMissedCallNotification(@NonNull CallInfo callInfo, UserHandle userHandle) { int missedCallCounts; synchronized (mMissedCallCountsLock) { Integer currentCount = mMissedCallCounts.get(userHandle); missedCallCounts = currentCount == null ? 0 : currentCount; missedCallCounts++; mMissedCallCounts.put(userHandle, missedCallCounts); } Log.i(this, "showMissedCallNotification: userHandle=%d, missedCallCount=%d", userHandle.getIdentifier(), missedCallCounts); String dialerPackage = getDefaultDialerPackage(userHandle); if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) { sendNotificationThroughDefaultDialer(dialerPackage, callInfo, userHandle, missedCallCounts); return; } final String titleText; final String expandedText; // The text in the notification's line 1 and 2. // Display the first line of the notification: // 1 missed call: // More than 1 missed call: + "missed calls" if (missedCallCounts == 1) { expandedText = getNameForMissedCallNotification(callInfo); CallerInfo ci = callInfo.getCallerInfo(); if (ci != null && ci.userType == CallerInfo.USER_TYPE_WORK) { titleText = mContext.getSystemService(DevicePolicyManager.class).getResources() .getString(NOTIFICATION_MISSED_WORK_CALL_TITLE, () -> mContext.getString(R.string.notification_missedWorkCallTitle)); } else { titleText = mContext.getString(R.string.notification_missedCallTitle); } } else { titleText = mContext.getString(R.string.notification_missedCallsTitle); expandedText = mContext.getString(R.string.notification_missedCallsMsg, missedCallCounts); } // Create a public viewable version of the notification, suitable for display when sensitive // notification content is hidden. // We use user's context here to make sure notification is badged if it is a managed user. Context contextForUser = getContextForUser(userHandle); Notification.Builder publicBuilder = mNotificationBuilderFactory.getBuilder(contextForUser); publicBuilder.setSmallIcon(android.R.drawable.stat_notify_missed_call) .setColor(mContext.getResources().getColor(R.color.theme_color)) .setWhen(callInfo.getCreationTimeMillis()) .setShowWhen(true) // Show "Phone" for notification title. .setContentTitle(mContext.getText(R.string.userCallActivityLabel)) // Notification details shows that there are missed call(s), but does not reveal // the missed caller information. .setContentText(titleText) .setContentIntent(createCallLogPendingIntent(userHandle)) .setAutoCancel(true) .setDeleteIntent(createClearMissedCallsPendingIntent(userHandle)); // Create the notification suitable for display when sensitive information is showing. Notification.Builder builder = mNotificationBuilderFactory.getBuilder(contextForUser); builder.setSmallIcon(android.R.drawable.stat_notify_missed_call) .setColor(mContext.getResources().getColor(R.color.theme_color)) .setWhen(callInfo.getCreationTimeMillis()) .setShowWhen(true) .setContentTitle(titleText) .setContentText(expandedText) .setContentIntent(createCallLogPendingIntent(userHandle)) .setAutoCancel(true) .setDeleteIntent(createClearMissedCallsPendingIntent(userHandle)) // Include a public version of the notification to be shown when the missed call // notification is shown on the user's lock screen and they have chosen to hide // sensitive notification information. .setPublicVersion(publicBuilder.build()) .setChannelId(NotificationChannelManager.CHANNEL_ID_MISSED_CALLS); Uri handleUri = callInfo.getHandle(); String handle = callInfo.getHandleSchemeSpecificPart(); // Add additional actions when there is only 1 missed call, like call-back and SMS. if (missedCallCounts == 1) { Log.d(this, "Add actions with number %s.", Log.piiHandle(handle)); if (!TextUtils.isEmpty(handle) && !TextUtils.equals(handle, mContext.getString(R.string.handle_restricted))) { builder.addAction(R.drawable.ic_phone_24dp, mContext.getString(R.string.notification_missedCall_call_back), createCallBackPendingIntent(handleUri, userHandle)); if (canRespondViaSms(callInfo)) { builder.addAction(R.drawable.ic_message_24dp, mContext.getString(R.string.notification_missedCall_message), createSendSmsFromNotificationPendingIntent(handleUri, userHandle)); } } Bitmap photoIcon = callInfo.getCallerInfo() == null ? null : callInfo.getCallerInfo().cachedPhotoIcon; if (photoIcon != null) { builder.setLargeIcon(photoIcon); } else { Drawable photo = callInfo.getCallerInfo() == null ? null : callInfo.getCallerInfo().cachedPhoto; if (photo != null && photo instanceof BitmapDrawable) { builder.setLargeIcon(((BitmapDrawable) photo).getBitmap()); } } } else { Log.d(this, "Suppress actions. handle: %s, missedCalls: %d.", Log.piiHandle(handle), missedCallCounts); } Notification notification = builder.build(); configureLedOnNotification(notification); Log.i(this, "Adding missed call notification for %s.", Log.pii(callInfo.getHandle())); long token = Binder.clearCallingIdentity(); try { mNotificationManager.notifyAsUser( NOTIFICATION_TAG, MISSED_CALL_NOTIFICATION_ID, notification, userHandle); } finally { Binder.restoreCallingIdentity(token); } } /** Cancels the "missed call" notification. */ private void cancelMissedCallNotification(UserHandle userHandle) { // Reset the number of missed calls to 0. synchronized(mMissedCallCountsLock) { mMissedCallCounts.put(userHandle, 0); } String dialerPackage = getDefaultDialerPackage(userHandle); if (shouldManageNotificationThroughDefaultDialer(dialerPackage, userHandle)) { sendNotificationThroughDefaultDialer(dialerPackage, null, userHandle, 0 /* missedCallCount */); return; } long token = Binder.clearCallingIdentity(); try { mNotificationManager.cancelAsUser(NOTIFICATION_TAG, MISSED_CALL_NOTIFICATION_ID, userHandle); } finally { Binder.restoreCallingIdentity(token); } } /** * Returns the name to use in the missed call notification. */ private String getNameForMissedCallNotification(@NonNull CallInfo callInfo) { String handle = callInfo.getHandleSchemeSpecificPart(); String name = callInfo.getName(); if (!TextUtils.isEmpty(handle)) { String formattedNumber = PhoneNumberUtils.formatNumber(handle, getCurrentCountryIso(mContext)); // The formatted number will be null if there was a problem formatting it, but we can // default to using the unformatted number instead (e.g. a SIP URI may not be able to // be formatted. if (!TextUtils.isEmpty(formattedNumber)) { handle = formattedNumber; } } if (!TextUtils.isEmpty(name) && TextUtils.isGraphic(name)) { return name; } else if (!TextUtils.isEmpty(handle)) { // A handle should always be displayed LTR using {@link BidiFormatter} regardless of the // content of the rest of the notification. // TODO: Does this apply to SIP addresses? BidiFormatter bidiFormatter = BidiFormatter.getInstance(); return bidiFormatter.unicodeWrap(handle, TextDirectionHeuristics.LTR); } else { // Use "unknown" if the call is unidentifiable. return mContext.getString(R.string.unknown); } } /** * @return The ISO 3166-1 two letters country code of the country the user is in based on the * network location. If the network location does not exist, fall back to the locale * setting. */ private String getCurrentCountryIso(Context context) { // Without framework function calls, this seems to be the most accurate location service // we can rely on. final TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); String countryIso = telephonyManager.getNetworkCountryIso().toUpperCase(); if (countryIso == null) { countryIso = Locale.getDefault().getCountry(); Log.w(this, "No CountryDetector; falling back to countryIso based on locale: " + countryIso); } return countryIso; } /** * Creates a new pending intent that sends the user to the call log. * * @return The pending intent. */ private PendingIntent createCallLogPendingIntent(UserHandle userHandle) { Intent intent = new Intent(Intent.ACTION_VIEW, null); intent.setType(Calls.CONTENT_TYPE); TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(mContext); taskStackBuilder.addNextIntent(intent); return taskStackBuilder.getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE, null, userHandle); } /** * Creates an intent to be invoked when the missed call notification is cleared. */ private PendingIntent createClearMissedCallsPendingIntent(UserHandle userHandle) { return createTelecomPendingIntent( TelecomBroadcastIntentProcessor.ACTION_CLEAR_MISSED_CALLS, null, userHandle); } /** * Creates an intent to be invoked when the user opts to "call back" from the missed call * notification. * * @param handle The handle to call back. */ private PendingIntent createCallBackPendingIntent(Uri handle, UserHandle userHandle) { return createTelecomPendingIntent( TelecomBroadcastIntentProcessor.ACTION_CALL_BACK_FROM_NOTIFICATION, handle, userHandle); } /** * Creates an intent to be invoked when the user opts to "send sms" from the missed call * notification. */ private PendingIntent createSendSmsFromNotificationPendingIntent(Uri handle, UserHandle userHandle) { return createTelecomPendingIntent( TelecomBroadcastIntentProcessor.ACTION_SEND_SMS_FROM_NOTIFICATION, Uri.fromParts(Constants.SCHEME_SMSTO, handle.getSchemeSpecificPart(), null), userHandle); } /** * Creates generic pending intent from the specified parameters to be received by * {@link TelecomBroadcastIntentProcessor}. * * @param action The intent action. * @param data The intent data. */ private PendingIntent createTelecomPendingIntent(String action, Uri data, UserHandle userHandle) { Intent intent = new Intent(action, data, mContext, TelecomBroadcastReceiver.class); intent.putExtra(TelecomBroadcastIntentProcessor.EXTRA_USERHANDLE, userHandle); return PendingIntent.getBroadcast(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); } /** * Configures a notification to emit the blinky notification light. */ private void configureLedOnNotification(Notification notification) { notification.flags |= Notification.FLAG_SHOW_LIGHTS; notification.defaults |= Notification.DEFAULT_LIGHTS; } private boolean canRespondViaSms(@NonNull CallInfo callInfo) { // Only allow respond-via-sms for "tel:" calls. return callInfo.getHandle() != null && PhoneAccount.SCHEME_TEL.equals(callInfo.getHandle().getScheme()); } @Override public void reloadAfterBootComplete(final CallerInfoLookupHelper callerInfoLookupHelper, CallInfoFactory callInfoFactory) { if (!mUsersToLoadAfterBootComplete.isEmpty()) { for (UserHandle handle : mUsersToLoadAfterBootComplete) { Log.i(this, "reloadAfterBootComplete: user=%d", handle.getIdentifier()); reloadFromDatabase(callerInfoLookupHelper, callInfoFactory, handle); } mUsersToLoadAfterBootComplete.clear(); } else { Log.i(this, "reloadAfterBootComplete: no user(s) to check; skipping reload."); } } /** * Adds the missed call notification on startup if there are unread missed calls. */ @Override public void reloadFromDatabase(final CallerInfoLookupHelper callerInfoLookupHelper, CallInfoFactory callInfoFactory, final UserHandle userHandle) { Log.d(this, "reloadFromDatabase: user=%d", userHandle.getIdentifier()); if (TelecomSystem.getInstance() == null || !TelecomSystem.getInstance().isBootComplete()) { if (!mUsersToLoadAfterBootComplete.contains(userHandle)) { Log.i(this, "reloadFromDatabase: Boot not yet complete -- call log db may not be " + "available. Deferring loading until boot complete for user %d", userHandle.getIdentifier()); mUsersToLoadAfterBootComplete.add(userHandle); } return; } // instantiate query handler AsyncQueryHandler queryHandler = new AsyncQueryHandler(mContext.getContentResolver()) { @Override protected void onQueryComplete(int token, Object cookie, Cursor cursor) { Log.d(MissedCallNotifierImpl.this, "onQueryComplete()..."); if (cursor != null) { try { synchronized(mMissedCallCountsLock) { mMissedCallCounts.remove(userHandle); } while (cursor.moveToNext()) { // Get data about the missed call from the cursor final String handleString = cursor.getString(CALL_LOG_COLUMN_NUMBER); final int presentation = cursor.getInt(CALL_LOG_COLUMN_NUMBER_PRESENTATION); final long date = cursor.getLong(CALL_LOG_COLUMN_DATE); final Uri handle; if (presentation != Calls.PRESENTATION_ALLOWED || TextUtils.isEmpty(handleString)) { handle = null; } else { // TODO: Remove the assumption that numbers are SIP or TEL only. handle = Uri.fromParts(PhoneNumberUtils.isUriNumber(handleString) ? PhoneAccount.SCHEME_SIP : PhoneAccount.SCHEME_TEL, handleString, null); } callerInfoLookupHelper.startLookup(handle, new CallerInfoLookupHelper.OnQueryCompleteListener() { @Override public void onCallerInfoQueryComplete(Uri queryHandle, CallerInfo info) { if (!Objects.equals(queryHandle, handle)) { Log.w(MissedCallNotifierImpl.this, "CallerInfo query returned with " + "different handle."); return; } if (info == null || info.getContactDisplayPhotoUri() == null) { // If there is no photo or if the caller info is // null, just show the notification. CallInfo callInfo = callInfoFactory.makeCallInfo( info, null, handle, date); showMissedCallNotification(callInfo, userHandle); } } @Override public void onContactPhotoQueryComplete(Uri queryHandle, CallerInfo info) { if (!Objects.equals(queryHandle, handle)) { Log.w(MissedCallNotifierImpl.this, "CallerInfo query for photo returned " + "with different handle."); return; } CallInfo callInfo = callInfoFactory.makeCallInfo( info, null, handle, date); showMissedCallNotification(callInfo, userHandle); } } ); } } finally { cursor.close(); } } } }; // setup query spec, look for all Missed calls that are new. Uri callsUri = ContentProvider.maybeAddUserId(Calls.CONTENT_URI, userHandle.getIdentifier()); // start the query queryHandler.startQuery(0, null, callsUri, CALL_LOG_PROJECTION, CALL_LOG_WHERE_CLAUSE, null, Calls.DEFAULT_SORT_ORDER); } @Override public void setCurrentUserHandle(UserHandle currentUserHandle) { mCurrentUserHandle = currentUserHandle; } private Context getContextForUser(UserHandle user) { try { return mContext.createPackageContextAsUser(mContext.getPackageName(), 0, user); } catch (NameNotFoundException e) { // Default to mContext, not finding the package system is running as is unlikely. return mContext; } } }