468 lines
17 KiB
Java
468 lines
17 KiB
Java
/*
|
|
* Copyright (C) 2013 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.dialer.app.calllog;
|
|
|
|
import android.Manifest;
|
|
import android.content.ContentResolver;
|
|
import android.content.ContentUris;
|
|
import android.content.ContentValues;
|
|
import android.content.Context;
|
|
import android.database.Cursor;
|
|
import android.net.Uri;
|
|
import android.os.Build;
|
|
import android.provider.CallLog.Calls;
|
|
import android.provider.VoicemailContract.Voicemails;
|
|
import android.support.annotation.NonNull;
|
|
import android.support.annotation.Nullable;
|
|
import android.support.annotation.VisibleForTesting;
|
|
import android.support.annotation.WorkerThread;
|
|
import android.support.v4.os.UserManagerCompat;
|
|
import android.telephony.PhoneNumberUtils;
|
|
import android.text.TextUtils;
|
|
import com.android.dialer.app.R;
|
|
import com.android.dialer.calllogutils.PhoneNumberDisplayUtil;
|
|
import com.android.dialer.common.LogUtil;
|
|
import com.android.dialer.common.database.Selection;
|
|
import com.android.dialer.compat.android.provider.VoicemailCompat;
|
|
import com.android.dialer.configprovider.ConfigProviderComponent;
|
|
import com.android.dialer.location.GeoUtil;
|
|
import com.android.dialer.phonenumbercache.ContactInfo;
|
|
import com.android.dialer.phonenumbercache.ContactInfoHelper;
|
|
import com.android.dialer.phonenumberutil.PhoneNumberHelper;
|
|
import com.android.dialer.util.PermissionsUtil;
|
|
import java.util.ArrayList;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.concurrent.TimeUnit;
|
|
|
|
/** Helper class operating on call log notifications. */
|
|
public class CallLogNotificationsQueryHelper {
|
|
|
|
@VisibleForTesting
|
|
static final String CONFIG_NEW_VOICEMAIL_NOTIFICATION_THRESHOLD_OFFSET =
|
|
"new_voicemail_notification_threshold";
|
|
|
|
private final Context context;
|
|
private final NewCallsQuery newCallsQuery;
|
|
private final ContactInfoHelper contactInfoHelper;
|
|
private final String currentCountryIso;
|
|
|
|
CallLogNotificationsQueryHelper(
|
|
Context context,
|
|
NewCallsQuery newCallsQuery,
|
|
ContactInfoHelper contactInfoHelper,
|
|
String countryIso) {
|
|
this.context = context;
|
|
this.newCallsQuery = newCallsQuery;
|
|
this.contactInfoHelper = contactInfoHelper;
|
|
currentCountryIso = countryIso;
|
|
}
|
|
|
|
/** Returns an instance of {@link CallLogNotificationsQueryHelper}. */
|
|
public static CallLogNotificationsQueryHelper getInstance(Context context) {
|
|
ContentResolver contentResolver = context.getContentResolver();
|
|
String countryIso = GeoUtil.getCurrentCountryIso(context);
|
|
return new CallLogNotificationsQueryHelper(
|
|
context,
|
|
createNewCallsQuery(context, contentResolver),
|
|
new ContactInfoHelper(context, countryIso),
|
|
countryIso);
|
|
}
|
|
|
|
public static void markAllMissedCallsInCallLogAsRead(@NonNull Context context) {
|
|
markMissedCallsInCallLogAsRead(context, null);
|
|
}
|
|
|
|
public static void markSingleMissedCallInCallLogAsRead(
|
|
@NonNull Context context, @Nullable Uri callUri) {
|
|
if (callUri == null) {
|
|
LogUtil.e(
|
|
"CallLogNotificationsQueryHelper.markSingleMissedCallInCallLogAsRead",
|
|
"call URI is null, unable to mark call as read");
|
|
} else {
|
|
markMissedCallsInCallLogAsRead(context, callUri);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If callUri is null then calls with a matching callUri are marked as read, otherwise all calls
|
|
* are marked as read.
|
|
*/
|
|
@WorkerThread
|
|
private static void markMissedCallsInCallLogAsRead(Context context, @Nullable Uri callUri) {
|
|
if (!UserManagerCompat.isUserUnlocked(context)) {
|
|
LogUtil.e("CallLogNotificationsQueryHelper.markMissedCallsInCallLogAsRead", "locked");
|
|
return;
|
|
}
|
|
if (!PermissionsUtil.hasPhonePermissions(context)) {
|
|
LogUtil.e(
|
|
"CallLogNotificationsQueryHelper.markMissedCallsInCallLogAsRead", "no phone permission");
|
|
return;
|
|
}
|
|
if (!PermissionsUtil.hasCallLogWritePermissions(context)) {
|
|
LogUtil.e(
|
|
"CallLogNotificationsQueryHelper.markMissedCallsInCallLogAsRead",
|
|
"no call log write permission");
|
|
return;
|
|
}
|
|
|
|
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 {
|
|
context
|
|
.getContentResolver()
|
|
.update(
|
|
callUri == null ? Calls.CONTENT_URI : callUri,
|
|
values,
|
|
where.toString(),
|
|
new String[] {Integer.toString(Calls.MISSED_TYPE)});
|
|
} catch (IllegalArgumentException e) {
|
|
LogUtil.e(
|
|
"CallLogNotificationsQueryHelper.markMissedCallsInCallLogAsRead",
|
|
"contacts provider update command failed",
|
|
e);
|
|
}
|
|
}
|
|
|
|
/** Create a new instance of {@link NewCallsQuery}. */
|
|
public static NewCallsQuery createNewCallsQuery(
|
|
Context context, ContentResolver contentResolver) {
|
|
|
|
return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
|
|
}
|
|
|
|
NewCallsQuery getNewCallsQuery() {
|
|
return newCallsQuery;
|
|
}
|
|
|
|
/**
|
|
* Get all voicemails with the "new" flag set to 1.
|
|
*
|
|
* @return A list of NewCall objects where each object represents a new voicemail.
|
|
*/
|
|
@Nullable
|
|
public List<NewCall> getNewVoicemails() {
|
|
return newCallsQuery.query(
|
|
Calls.VOICEMAIL_TYPE,
|
|
System.currentTimeMillis()
|
|
- ConfigProviderComponent.get(context)
|
|
.getConfigProvider()
|
|
.getLong(
|
|
CONFIG_NEW_VOICEMAIL_NOTIFICATION_THRESHOLD_OFFSET, TimeUnit.DAYS.toMillis(7)));
|
|
}
|
|
|
|
/**
|
|
* Get all missed calls with the "new" flag set to 1.
|
|
*
|
|
* @return A list of NewCall objects where each object represents a new missed call.
|
|
*/
|
|
@Nullable
|
|
public List<NewCall> getNewMissedCalls() {
|
|
return newCallsQuery.query(Calls.MISSED_TYPE);
|
|
}
|
|
|
|
/**
|
|
* Given a number and number information (presentation and country ISO), get the best name for
|
|
* display. If the name is empty but we have a special presentation, display that. Otherwise
|
|
* attempt to look it up in the database or the cache. If that fails, fall back to displaying the
|
|
* number.
|
|
*/
|
|
public String getName(
|
|
@Nullable String number, int numberPresentation, @Nullable String countryIso) {
|
|
return getContactInfo(number, numberPresentation, countryIso).name;
|
|
}
|
|
|
|
/**
|
|
* Given a number and number information (presentation and country ISO), get {@link ContactInfo}.
|
|
* If the name is empty but we have a special presentation, display that. Otherwise attempt to
|
|
* look it up in the cache. If that fails, fall back to displaying the number.
|
|
*/
|
|
public ContactInfo getContactInfo(
|
|
@Nullable String number, int numberPresentation, @Nullable String countryIso) {
|
|
if (countryIso == null) {
|
|
countryIso = currentCountryIso;
|
|
}
|
|
|
|
number = (number == null) ? "" : number;
|
|
ContactInfo contactInfo = new ContactInfo();
|
|
contactInfo.number = number;
|
|
contactInfo.formattedNumber = PhoneNumberHelper.formatNumber(context, number, countryIso);
|
|
// contactInfo.normalizedNumber is not PhoneNumberUtils.normalizeNumber. Read ContactInfo.
|
|
contactInfo.normalizedNumber = PhoneNumberUtils.formatNumberToE164(number, countryIso);
|
|
|
|
// 1. Special number representation.
|
|
contactInfo.name =
|
|
PhoneNumberDisplayUtil.getDisplayName(context, number, numberPresentation, false)
|
|
.toString();
|
|
if (!TextUtils.isEmpty(contactInfo.name)) {
|
|
return contactInfo;
|
|
}
|
|
|
|
// 2. Look it up in the cache.
|
|
ContactInfo cachedContactInfo = contactInfoHelper.lookupNumber(number, countryIso);
|
|
|
|
if (cachedContactInfo != null && !TextUtils.isEmpty(cachedContactInfo.name)) {
|
|
return cachedContactInfo;
|
|
}
|
|
|
|
if (!TextUtils.isEmpty(contactInfo.formattedNumber)) {
|
|
// 3. If we cannot lookup the contact, use the formatted number instead.
|
|
contactInfo.name = contactInfo.formattedNumber;
|
|
} else if (!TextUtils.isEmpty(number)) {
|
|
// 4. If number can't be formatted, use number.
|
|
contactInfo.name = number;
|
|
} else {
|
|
// 5. Otherwise, it's unknown number.
|
|
contactInfo.name = context.getResources().getString(R.string.unknown);
|
|
}
|
|
return contactInfo;
|
|
}
|
|
|
|
/** Allows determining the new calls for which a notification should be generated. */
|
|
public interface NewCallsQuery {
|
|
|
|
long NO_THRESHOLD = Long.MAX_VALUE;
|
|
|
|
/** Returns the new calls of a certain type for which a notification should be generated. */
|
|
@Nullable
|
|
List<NewCall> query(int type);
|
|
|
|
/**
|
|
* Returns the new calls of a certain type for which a notification should be generated.
|
|
*
|
|
* @param thresholdMillis New calls added before this timestamp will be considered old, or
|
|
* {@link #NO_THRESHOLD} if threshold is not checked.
|
|
*/
|
|
@Nullable
|
|
List<NewCall> query(int type, long thresholdMillis);
|
|
|
|
/** Returns a {@link NewCall} pointed by the {@code callsUri} */
|
|
@Nullable
|
|
NewCall queryUnreadVoicemail(Uri callsUri);
|
|
}
|
|
|
|
/** Information about a new voicemail. */
|
|
public static final class NewCall {
|
|
|
|
public final Uri callsUri;
|
|
@Nullable public final Uri voicemailUri;
|
|
public final String number;
|
|
public final int numberPresentation;
|
|
public final String accountComponentName;
|
|
public final String accountId;
|
|
public final String transcription;
|
|
public final String countryIso;
|
|
public final long dateMs;
|
|
public final int transcriptionState;
|
|
|
|
public NewCall(
|
|
Uri callsUri,
|
|
@Nullable Uri voicemailUri,
|
|
String number,
|
|
int numberPresentation,
|
|
String accountComponentName,
|
|
String accountId,
|
|
String transcription,
|
|
String countryIso,
|
|
long dateMs,
|
|
int transcriptionState) {
|
|
this.callsUri = callsUri;
|
|
this.voicemailUri = voicemailUri;
|
|
this.number = number;
|
|
this.numberPresentation = numberPresentation;
|
|
this.accountComponentName = accountComponentName;
|
|
this.accountId = accountId;
|
|
this.transcription = transcription;
|
|
this.countryIso = countryIso;
|
|
this.dateMs = dateMs;
|
|
this.transcriptionState = transcriptionState;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Default implementation of {@link NewCallsQuery} that looks up the list of new calls to notify
|
|
* about in the call log.
|
|
*/
|
|
private static final class DefaultNewCallsQuery implements NewCallsQuery {
|
|
|
|
private static final String[] PROJECTION = {
|
|
Calls._ID,
|
|
Calls.NUMBER,
|
|
Calls.VOICEMAIL_URI,
|
|
Calls.NUMBER_PRESENTATION,
|
|
Calls.PHONE_ACCOUNT_COMPONENT_NAME,
|
|
Calls.PHONE_ACCOUNT_ID,
|
|
Calls.TRANSCRIPTION,
|
|
Calls.COUNTRY_ISO,
|
|
Calls.DATE
|
|
};
|
|
|
|
private static final String[] PROJECTION_O;
|
|
|
|
static {
|
|
List<String> list = new ArrayList<>();
|
|
list.addAll(Arrays.asList(PROJECTION));
|
|
list.add(VoicemailCompat.TRANSCRIPTION_STATE);
|
|
PROJECTION_O = list.toArray(new String[list.size()]);
|
|
}
|
|
|
|
private static final int ID_COLUMN_INDEX = 0;
|
|
private static final int NUMBER_COLUMN_INDEX = 1;
|
|
private static final int VOICEMAIL_URI_COLUMN_INDEX = 2;
|
|
private static final int NUMBER_PRESENTATION_COLUMN_INDEX = 3;
|
|
private static final int PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX = 4;
|
|
private static final int PHONE_ACCOUNT_ID_COLUMN_INDEX = 5;
|
|
private static final int TRANSCRIPTION_COLUMN_INDEX = 6;
|
|
private static final int COUNTRY_ISO_COLUMN_INDEX = 7;
|
|
private static final int DATE_COLUMN_INDEX = 8;
|
|
private static final int TRANSCRIPTION_STATE_COLUMN_INDEX = 9;
|
|
|
|
private final ContentResolver contentResolver;
|
|
private final Context context;
|
|
|
|
private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
|
|
this.context = context;
|
|
this.contentResolver = contentResolver;
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
public List<NewCall> query(int type) {
|
|
return query(type, NO_THRESHOLD);
|
|
}
|
|
|
|
@Override
|
|
@Nullable
|
|
@SuppressWarnings("MissingPermission")
|
|
public List<NewCall> query(int type, long thresholdMillis) {
|
|
if (!PermissionsUtil.hasPermission(context, Manifest.permission.READ_CALL_LOG)) {
|
|
LogUtil.w(
|
|
"CallLogNotificationsQueryHelper.DefaultNewCallsQuery.query",
|
|
"no READ_CALL_LOG permission, returning null for calls lookup.");
|
|
return null;
|
|
}
|
|
// A call is "new" when:
|
|
// NEW is 1. usually set when a new row is inserted
|
|
// TYPE matches the query type.
|
|
// IS_READ is not 1. A call might be backed up and restored, so it will be "new" to the
|
|
// call log, but the user has already read it on another device.
|
|
Selection.Builder selectionBuilder =
|
|
Selection.builder()
|
|
.and(Selection.column(Calls.NEW).is("= 1"))
|
|
.and(Selection.column(Calls.TYPE).is("=", type))
|
|
.and(Selection.column(Calls.IS_READ).is("IS NOT 1"));
|
|
|
|
if (type == Calls.VOICEMAIL_TYPE) {
|
|
selectionBuilder.and(Selection.column(Voicemails.DELETED).is(" = 0"));
|
|
}
|
|
|
|
if (thresholdMillis != NO_THRESHOLD) {
|
|
selectionBuilder =
|
|
selectionBuilder.and(
|
|
Selection.column(Calls.DATE)
|
|
.is("IS NULL")
|
|
.buildUpon()
|
|
.or(Selection.column(Calls.DATE).is(">=", thresholdMillis))
|
|
.build());
|
|
}
|
|
Selection selection = selectionBuilder.build();
|
|
try (Cursor cursor =
|
|
contentResolver.query(
|
|
Calls.CONTENT_URI_WITH_VOICEMAIL,
|
|
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ? PROJECTION_O : PROJECTION,
|
|
selection.getSelection(),
|
|
selection.getSelectionArgs(),
|
|
Calls.DEFAULT_SORT_ORDER)) {
|
|
if (cursor == null) {
|
|
return null;
|
|
}
|
|
List<NewCall> newCalls = new ArrayList<>();
|
|
while (cursor.moveToNext()) {
|
|
newCalls.add(createNewCallsFromCursor(cursor));
|
|
}
|
|
return newCalls;
|
|
} catch (RuntimeException e) {
|
|
LogUtil.w(
|
|
"CallLogNotificationsQueryHelper.DefaultNewCallsQuery.query",
|
|
"exception when querying Contacts Provider for calls lookup");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
@SuppressWarnings("missingPermission")
|
|
public NewCall queryUnreadVoicemail(Uri voicemailUri) {
|
|
if (!PermissionsUtil.hasPermission(context, Manifest.permission.READ_CALL_LOG)) {
|
|
LogUtil.w(
|
|
"CallLogNotificationsQueryHelper.DefaultNewCallsQuery.query",
|
|
"No READ_CALL_LOG permission, returning null for calls lookup.");
|
|
return null;
|
|
}
|
|
Selection selection =
|
|
Selection.column(Calls.VOICEMAIL_URI)
|
|
.is("=", voicemailUri)
|
|
.buildUpon()
|
|
.and(Selection.column(Calls.IS_READ).is("IS NOT", 1))
|
|
.build();
|
|
try (Cursor cursor =
|
|
contentResolver.query(
|
|
Calls.CONTENT_URI_WITH_VOICEMAIL,
|
|
(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) ? PROJECTION_O : PROJECTION,
|
|
selection.getSelection(),
|
|
selection.getSelectionArgs(),
|
|
null)) {
|
|
if (cursor == null) {
|
|
return null;
|
|
}
|
|
if (!cursor.moveToFirst()) {
|
|
return null;
|
|
}
|
|
return createNewCallsFromCursor(cursor);
|
|
}
|
|
}
|
|
|
|
/** Returns an instance of {@link NewCall} created by using the values of the cursor. */
|
|
private NewCall createNewCallsFromCursor(Cursor cursor) {
|
|
String voicemailUriString = cursor.getString(VOICEMAIL_URI_COLUMN_INDEX);
|
|
Uri callsUri =
|
|
ContentUris.withAppendedId(
|
|
Calls.CONTENT_URI_WITH_VOICEMAIL, cursor.getLong(ID_COLUMN_INDEX));
|
|
Uri voicemailUri = voicemailUriString == null ? null : Uri.parse(voicemailUriString);
|
|
return new NewCall(
|
|
callsUri,
|
|
voicemailUri,
|
|
cursor.getString(NUMBER_COLUMN_INDEX),
|
|
cursor.getInt(NUMBER_PRESENTATION_COLUMN_INDEX),
|
|
cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME_COLUMN_INDEX),
|
|
cursor.getString(PHONE_ACCOUNT_ID_COLUMN_INDEX),
|
|
cursor.getString(TRANSCRIPTION_COLUMN_INDEX),
|
|
cursor.getString(COUNTRY_ISO_COLUMN_INDEX),
|
|
cursor.getLong(DATE_COLUMN_INDEX),
|
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
|
|
? cursor.getInt(TRANSCRIPTION_STATE_COLUMN_INDEX)
|
|
: VoicemailCompat.TRANSCRIPTION_NOT_STARTED);
|
|
}
|
|
}
|
|
}
|