blob: 25ce0ca5a2cddb1880567009e5417c7e302afb05 [file] [log] [blame]
/*
* 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.annotation.Nullable;
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;
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.flags.FeatureFlags;
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,
FeatureFlags featureFlags);
}
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<UserHandle, Integer> mMissedCallCounts;
private Set<UserHandle> mUsersToLoadAfterBootComplete = new ArraySet<>();
private FeatureFlags mFeatureFlags;
public MissedCallNotifierImpl(Context context, PhoneAccountRegistrar phoneAccountRegistrar,
DefaultDialerCache defaultDialerCache,
DeviceIdleControllerAdapter deviceIdleControllerAdapter,
FeatureFlags featureFlags) {
this(context, phoneAccountRegistrar, defaultDialerCache,
new DefaultNotificationBuilderFactory(), deviceIdleControllerAdapter, featureFlags);
}
public MissedCallNotifierImpl(Context context,
PhoneAccountRegistrar phoneAccountRegistrar,
DefaultDialerCache defaultDialerCache,
NotificationBuilderFactory notificationBuilderFactory,
DeviceIdleControllerAdapter deviceIdleControllerAdapter,
FeatureFlags featureFlags) {
mContext = context;
mPhoneAccountRegistrar = phoneAccountRegistrar;
mNotificationManager =
(NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
mDeviceIdleControllerAdapter = deviceIdleControllerAdapter;
mDefaultDialerCache = defaultDialerCache;
mNotificationBuilderFactory = notificationBuilderFactory;
mMissedCallCounts = new ArrayMap<>();
mFeatureFlags = featureFlags;
}
/** 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<ResolveInfo> 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, @Nullable Uri uri) {
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_CALL_LOG_URI, uri)
.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, @Nullable Uri uri) {
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, uri);
}
private void showMissedCallNotification(@NonNull CallInfo callInfo, UserHandle userHandle,
@Nullable Uri uri) {
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, uri);
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: <caller name || handle>
// More than 1 missed call: <number of calls> + "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,
/* missedCallCount= */ 0, /* uri= */ null);
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 Uri uri;
if (mFeatureFlags.addCallUriForMissedCalls()){
uri = Calls.CONTENT_URI.buildUpon().appendPath(
Long.toString(cursor.getInt(CALL_LOG_COLUMN_ID))).build();
}else{
uri = null;
}
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,
/* uri= */ uri);
}
}
@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,
/* uri= */ uri);
}
}
);
}
} 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;
}
}
}