Merge "Remove the work badge icon for Google caller id in call log" into ub-contactsdialer-b-dev
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 67161ad..1be1910 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -271,6 +271,12 @@
android:exported="false"
/>
+ <receiver android:name=".calllog.MissedCallNotificationReceiver">
+ <intent-filter>
+ <action android:name="android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION" />
+ </intent-filter>
+ </receiver>
+
<!-- Service to update a contact -->
<service
android:name=".contact.ContactUpdateService"
diff --git a/res/drawable-hdpi/ic_phone_24dp.png b/res/drawable-hdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..b27dfba
--- /dev/null
+++ b/res/drawable-hdpi/ic_phone_24dp.png
Binary files differ
diff --git a/res/drawable-mdpi/ic_phone_24dp.png b/res/drawable-mdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..c1766b8
--- /dev/null
+++ b/res/drawable-mdpi/ic_phone_24dp.png
Binary files differ
diff --git a/res/drawable-xhdpi/ic_phone_24dp.png b/res/drawable-xhdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..83167f4
--- /dev/null
+++ b/res/drawable-xhdpi/ic_phone_24dp.png
Binary files differ
diff --git a/res/drawable-xxhdpi/ic_phone_24dp.png b/res/drawable-xxhdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..8fff728
--- /dev/null
+++ b/res/drawable-xxhdpi/ic_phone_24dp.png
Binary files differ
diff --git a/res/drawable-xxxhdpi/ic_phone_24dp.png b/res/drawable-xxxhdpi/ic_phone_24dp.png
new file mode 100644
index 0000000..30d141d
--- /dev/null
+++ b/res/drawable-xxxhdpi/ic_phone_24dp.png
Binary files differ
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 2838e68..7e87b2f 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -99,6 +99,28 @@
<!-- Title of the "Clearing call log" progress-dialog [CHAR LIMIT=35] -->
<string name="clearCallLogProgress_title">Clearing call history\u2026</string>
+ <!-- Title used for the activity for placing a call. This name appears
+ in activity disambig dialogs -->
+ <string name="userCallActivityLabel" product="default">Phone</string>
+
+ <!-- Notification strings -->
+ <!-- Missed call notification label, used when there's exactly one missed call -->
+ <string name="notification_missedCallTitle">Missed call</string>
+ <!-- Missed call notification label, used when there are two or more missed calls -->
+ <string name="notification_missedCallsTitle">Missed calls</string>
+ <!-- Missed call notification message used when there are multiple missed calls -->
+ <string name="notification_missedCallsMsg"><xliff:g id="num_missed_calls">%s</xliff:g> missed calls</string>
+ <!-- Message for "call back" Action, which is displayed in the missed call notificaiton.
+ The user will be able to call back to the person or the phone number.
+ [CHAR LIMIT=18] -->
+ <string name="notification_missedCall_call_back">Call back</string>
+ <!-- Message for "reply via sms" action, which is displayed in the missed call notification.
+ The user will be able to send text messages using the phone number.
+ [CHAR LIMIT=18] -->
+ <string name="notification_missedCall_message">Message</string>
+ <!-- DO NOT TRANSLATE. Hardcoded number used for restricted incoming phone numbers. -->
+ <string name="handle_restricted">RESTRICTED</string>
+
<!-- Title of the notification of new voicemails. [CHAR LIMIT=30] -->
<plurals name="notification_voicemail_title">
<item quantity="one">Voicemail</item>
diff --git a/src-N/com/android/dialer/compat/UserManagerSdkCompat.java b/src-N/com/android/dialer/compat/UserManagerSdkCompat.java
new file mode 100644
index 0000000..9a08d4e
--- /dev/null
+++ b/src-N/com/android/dialer/compat/UserManagerSdkCompat.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.content.Context;
+
+/**
+ * UserManagerCompat respecting Sdk requirements
+ */
+public class UserManagerSdkCompat {
+
+ /**
+ * Return whether the calling user is running in an "unlocked" state. A user
+ * is unlocked only after they've entered their credentials (such as a lock
+ * pattern or PIN), and credential-encrypted private app data storage is
+ * available.
+ *
+ * @param context the current context
+ * @return {@code true} if the user is unlocked or context is null, {@code false} otherwise
+ * @throws NullPointerException if context is null
+ */
+ public static boolean isUserUnlocked(Context context) {
+ return android.support.v4.os.UserManagerCompat.isUserUnlocked(context);
+ }
+
+}
diff --git a/src-pre-N/com/android/dialer/compat/UserManagerSdkCompat.java b/src-pre-N/com/android/dialer/compat/UserManagerSdkCompat.java
new file mode 100644
index 0000000..c79ac2f
--- /dev/null
+++ b/src-pre-N/com/android/dialer/compat/UserManagerSdkCompat.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.content.Context;
+import android.util.Log;
+
+/**
+ * UserManagerCompat respecting Sdk requirements
+ */
+public class UserManagerSdkCompat {
+
+ /**
+ * @return {@code true}
+ */
+ public static boolean isUserUnlocked(Context context) {
+ Log.wtf("UserManagerSdkCompat", "Not implemented");
+ return true;
+ }
+
+}
diff --git a/src/com/android/dialer/DialtactsActivity.java b/src/com/android/dialer/DialtactsActivity.java
index bcc45a0..72753be 100644
--- a/src/com/android/dialer/DialtactsActivity.java
+++ b/src/com/android/dialer/DialtactsActivity.java
@@ -1038,6 +1038,7 @@
public boolean onTouch(View v, MotionEvent event) {
// Show the FAB when the user touches the lists fragment and the soft
// keyboard is hidden.
+ hideDialpadFragment(true, false);
showFabInSearchUi();
return false;
}
diff --git a/src/com/android/dialer/calllog/CallLogNotificationsHelper.java b/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
index 367cb78..64ccd5f 100644
--- a/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
+++ b/src/com/android/dialer/calllog/CallLogNotificationsHelper.java
@@ -16,14 +16,127 @@
package com.android.dialer.calllog;
-import android.content.Context;
+import static android.Manifest.permission.READ_CALL_LOG;
+import static android.Manifest.permission.READ_CONTACTS;
+import android.content.ContentResolver;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.provider.CallLog.Calls;
+import android.provider.ContactsContract.PhoneLookup;
+import android.support.annotation.Nullable;
+import android.telephony.PhoneNumberUtils;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.GeoUtil;
+import com.android.contacts.common.util.PermissionsUtil;
+import com.android.dialer.R;
import com.android.dialer.util.TelecomUtil;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Helper class operating on call log notifications.
*/
public class CallLogNotificationsHelper {
+ private static final String TAG = "CallLogNotifHelper";
+ private static CallLogNotificationsHelper sInstance;
+
+ /** Returns the singleton instance of the {@link CallLogNotificationsHelper}. */
+ public static CallLogNotificationsHelper getInstance(Context context) {
+ if (sInstance == null) {
+ ContentResolver contentResolver = context.getContentResolver();
+ String countryIso = GeoUtil.getCurrentCountryIso(context);
+ sInstance = new CallLogNotificationsHelper(context,
+ createNewCallsQuery(context, contentResolver),
+ createNameLookupQuery(context, contentResolver),
+ new ContactInfoHelper(context, countryIso),
+ countryIso);
+ }
+ return sInstance;
+ }
+
+ private final Context mContext;
+ private final NewCallsQuery mNewCallsQuery;
+ private final NameLookupQuery mNameLookupQuery;
+ private final ContactInfoHelper mContactInfoHelper;
+ private final String mCurrentCountryIso;
+
+ CallLogNotificationsHelper(Context context, NewCallsQuery newCallsQuery,
+ NameLookupQuery nameLookupQuery, ContactInfoHelper contactInfoHelper,
+ String countryIso) {
+ mContext = context;
+ mNewCallsQuery = newCallsQuery;
+ mNameLookupQuery = nameLookupQuery;
+ mContactInfoHelper = contactInfoHelper;
+ mCurrentCountryIso = countryIso;
+ }
+
+ /**
+ * 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 mNewCallsQuery.query(Calls.VOICEMAIL_TYPE);
+ }
+
+ /**
+ * 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 mNewCallsQuery.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) {
+ String name = PhoneNumberDisplayUtil.getDisplayName(
+ mContext,
+ number,
+ numberPresentation,
+ false).toString();
+ if (!TextUtils.isEmpty(name)) {
+ return name;
+ }
+
+ // Look it up in the database.
+ name = mNameLookupQuery.query(number);
+ if (!TextUtils.isEmpty(name)) {
+ return name;
+ }
+
+ if (countryIso == null) {
+ countryIso = mCurrentCountryIso;
+ }
+
+ // Look it up in the cache
+ ContactInfo contactInfo = mContactInfoHelper.lookupNumber(number, countryIso);
+
+ if (contactInfo != null && !TextUtils.isEmpty(contactInfo.name)) {
+ return contactInfo.name;
+ }
+
+ if (!TextUtils.isEmpty(number)) {
+ // If we cannot lookup the contact, use the number instead.
+ return PhoneNumberUtils.formatNumber(number, countryIso);
+ }
+ return mContext.getResources().getString(R.string.unknown);
+ }
+
/** Removes the missed call notifications. */
public static void removeMissedCallNotifications(Context context) {
TelecomUtil.cancelMissedCallsNotification(context);
@@ -33,4 +146,188 @@
public static void updateVoicemailNotifications(Context context) {
CallLogNotificationsService.updateVoicemailNotifications(context, null);
}
+
+ /** Information about a new voicemail. */
+ public static final class NewCall {
+ public final Uri callsUri;
+ 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 NewCall(
+ Uri callsUri,
+ Uri voicemailUri,
+ String number,
+ int numberPresentation,
+ String accountComponentName,
+ String accountId,
+ String transcription,
+ String countryIso,
+ long dateMs) {
+ 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;
+ }
+ }
+
+ /** Allows determining the new calls for which a notification should be generated. */
+ public interface NewCallsQuery {
+ /**
+ * Returns the new calls of a certain type for which a notification should be generated.
+ */
+ @Nullable
+ public List<NewCall> query(int type);
+ }
+
+ /** Create a new instance of {@link NewCallsQuery}. */
+ public static NewCallsQuery createNewCallsQuery(Context context,
+ ContentResolver contentResolver) {
+
+ return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
+ }
+
+ /**
+ * 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 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 final ContentResolver mContentResolver;
+ private final Context mContext;
+
+ private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
+ mContext = context;
+ mContentResolver = contentResolver;
+ }
+
+ @Override
+ @Nullable
+ public List<NewCall> query(int type) {
+ if (!PermissionsUtil.hasPermission(mContext, READ_CALL_LOG)) {
+ Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
+ return null;
+ }
+ final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
+ final String[] selectionArgs = new String[]{ Integer.toString(type) };
+ try (Cursor cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL,
+ PROJECTION, selection, selectionArgs, 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) {
+ Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
+ return null;
+ }
+ }
+
+ /** 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));
+ }
+ }
+
+ /** Allows determining the name associated with a given phone number. */
+ public interface NameLookupQuery {
+ /**
+ * Returns the name associated with the given number in the contacts database, or null if
+ * the number does not correspond to any of the contacts.
+ * <p>
+ * If there are multiple contacts with the same phone number, it will return the name of one
+ * of the matching contacts.
+ */
+ @Nullable
+ public String query(@Nullable String number);
+ }
+
+ /** Create a new instance of {@link NameLookupQuery}. */
+ public static NameLookupQuery createNameLookupQuery(Context context,
+ ContentResolver contentResolver) {
+ return new DefaultNameLookupQuery(context.getApplicationContext(), contentResolver);
+ }
+
+ /**
+ * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
+ * contacts database.
+ */
+ private static final class DefaultNameLookupQuery implements NameLookupQuery {
+ private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
+ private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
+
+ private final ContentResolver mContentResolver;
+ private final Context mContext;
+
+ private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) {
+ mContext = context;
+ mContentResolver = contentResolver;
+ }
+
+ @Override
+ @Nullable
+ public String query(@Nullable String number) {
+ if (!PermissionsUtil.hasPermission(mContext, READ_CONTACTS)) {
+ Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup.");
+ return null;
+ }
+ try (Cursor cursor = mContentResolver.query(
+ Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
+ PROJECTION, null, null, null)) {
+ if (cursor == null || !cursor.moveToFirst()) {
+ return null;
+ }
+ return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
+ } catch (RuntimeException e) {
+ Log.w(TAG, "Exception when querying Contacts Provider for name lookup");
+ return null;
+ }
+ }
+ }
}
diff --git a/src/com/android/dialer/calllog/CallLogNotificationsService.java b/src/com/android/dialer/calllog/CallLogNotificationsService.java
index d2a494d..4ff9576 100644
--- a/src/com/android/dialer/calllog/CallLogNotificationsService.java
+++ b/src/com/android/dialer/calllog/CallLogNotificationsService.java
@@ -26,15 +26,16 @@
import com.android.dialer.util.TelecomUtil;
/**
- * Provides operations for managing notifications.
+ * Provides operations for managing call-related notifications.
* <p>
* It handles the following actions:
* <ul>
- * <li>{@link #ACTION_MARK_NEW_VOICEMAILS_AS_OLD}: marks all the new voicemails in the call log as
- * old; this is called when a notification is dismissed.</li>
- * <li>{@link #ACTION_UPDATE_NOTIFICATIONS}: updates the content of the new items notification; it
- * may include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}, containing the URI of the new
- * voicemail that has triggered this update (if any).</li>
+ * <li>Updating voicemail notifications</li>
+ * <li>Marking new voicemails as old</li>
+ * <li>Updating missed call notifications</li>
+ * <li>Marking new missed calls as old</li>
+ * <li>Calling back from a missed call</li>
+ * <li>Sending an SMS from a missed call</li>
* </ul>
*/
public class CallLogNotificationsService extends IntentService {
@@ -45,21 +46,62 @@
"com.android.dialer.calllog.ACTION_MARK_NEW_VOICEMAILS_AS_OLD";
/**
- * Action to update the notifications.
+ * Action to update voicemail notifications.
* <p>
* May include an optional extra {@link #EXTRA_NEW_VOICEMAIL_URI}.
*/
- public static final String ACTION_UPDATE_NOTIFICATIONS =
- "com.android.dialer.calllog.UPDATE_NOTIFICATIONS";
+ public static final String ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS =
+ "com.android.dialer.calllog.UPDATE_VOICEMAIL_NOTIFICATIONS";
/**
- * Extra to included with {@link #ACTION_UPDATE_NOTIFICATIONS} to identify the new voicemail
- * that triggered an update.
+ * Extra to included with {@link #ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS} to identify the new
+ * voicemail that triggered an update.
* <p>
* It must be a {@link Uri}.
*/
public static final String EXTRA_NEW_VOICEMAIL_URI = "NEW_VOICEMAIL_URI";
+ /**
+ * Action to update the missed call notifications.
+ * <p>
+ * Includes optional extras {@link #EXTRA_MISSED_CALL_NUMBER} and
+ * {@link #EXTRA_MISSED_CALL_COUNT}.
+ */
+ public static final String ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS =
+ "com.android.dialer.calllog.UPDATE_MISSED_CALL_NOTIFICATIONS";
+
+ /** Action to mark all the new missed calls as old. */
+ public static final String ACTION_MARK_NEW_MISSED_CALLS_AS_OLD =
+ "com.android.dialer.calllog.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD";
+
+ /** Action to call back a missed call. */
+ public static final String ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION =
+ "com.android.dialer.calllog.CALL_BACK_FROM_MISSED_CALL_NOTIFICATION";
+
+ public static final String ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION =
+ "com.android.dialer.calllog.SEND_SMS_FROM_MISSED_CALL_NOTIFICATION";
+
+ /**
+ * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS},
+ * {@link #ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION} and
+ * {@link #ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION} to identify the number to display,
+ * call or text back.
+ * <p>
+ * It must be a {@link String}.
+ */
+ public static final String EXTRA_MISSED_CALL_NUMBER = "MISSED_CALL_NUMBER";
+
+ /**
+ * Extra to be included with {@link #ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS} to represent the
+ * number of missed calls.
+ * <p>
+ * It must be a {@link Integer}
+ */
+ public static final String EXTRA_MISSED_CALL_COUNT =
+ "MISSED_CALL_COUNT";
+
+ public static final int UNKNOWN_MISSED_CALL_COUNT = -1;
+
private VoicemailQueryHandler mVoicemailQueryHandler;
public CallLogNotificationsService() {
@@ -77,16 +119,38 @@
return;
}
- if (ACTION_MARK_NEW_VOICEMAILS_AS_OLD.equals(intent.getAction())) {
- if (mVoicemailQueryHandler == null) {
- mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver());
- }
- mVoicemailQueryHandler.markNewVoicemailsAsOld();
- } else if (ACTION_UPDATE_NOTIFICATIONS.equals(intent.getAction())) {
- Uri voicemailUri = (Uri) intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI);
- DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri);
- } else {
- Log.d(TAG, "onHandleIntent: could not handle: " + intent);
+ String action = intent.getAction();
+ switch (action) {
+ case ACTION_MARK_NEW_VOICEMAILS_AS_OLD:
+ if (mVoicemailQueryHandler == null) {
+ mVoicemailQueryHandler = new VoicemailQueryHandler(this, getContentResolver());
+ }
+ mVoicemailQueryHandler.markNewVoicemailsAsOld();
+ break;
+ case ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS:
+ Uri voicemailUri = (Uri) intent.getParcelableExtra(EXTRA_NEW_VOICEMAIL_URI);
+ DefaultVoicemailNotifier.getInstance(this).updateNotification(voicemailUri);
+ break;
+ case ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS:
+ int count = intent.getIntExtra(EXTRA_MISSED_CALL_COUNT,
+ UNKNOWN_MISSED_CALL_COUNT);
+ String number = intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER);
+ MissedCallNotifier.getInstance(this).updateMissedCallNotification(count, number);
+ break;
+ case ACTION_MARK_NEW_MISSED_CALLS_AS_OLD:
+ CallLogNotificationsHelper.removeMissedCallNotifications(this);
+ break;
+ case ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION:
+ MissedCallNotifier.getInstance(this).callBackFromMissedCall(
+ intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+ break;
+ case ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION:
+ MissedCallNotifier.getInstance(this).sendSmsFromMissedCall(
+ intent.getStringExtra(EXTRA_MISSED_CALL_NUMBER));
+ break;
+ default:
+ Log.d(TAG, "onHandleIntent: could not handle: " + intent);
+ break;
}
}
@@ -100,7 +164,8 @@
public static void updateVoicemailNotifications(Context context, Uri voicemailUri) {
if (TelecomUtil.hasReadWriteVoicemailPermissions(context)) {
Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
- serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+ serviceIntent.setAction(
+ CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
// If voicemailUri is null, then notifications for all voicemails will be updated.
if (voicemailUri != null) {
serviceIntent.putExtra(
@@ -109,4 +174,21 @@
context.startService(serviceIntent);
}
}
+
+ /**
+ * Updates notifications for any new missed calls.
+ *
+ * @param context A valid context.
+ * @param count The number of new missed calls.
+ * @param number The phone number of the newest missed call.
+ */
+ public static void updateMissedCallNotifications(Context context, int count,
+ String number) {
+ Intent serviceIntent = new Intent(context, CallLogNotificationsService.class);
+ serviceIntent.setAction(
+ CallLogNotificationsService.ACTION_UPDATE_MISSED_CALL_NOTIFICATIONS);
+ serviceIntent.putExtra(EXTRA_MISSED_CALL_COUNT, count);
+ serviceIntent.putExtra(EXTRA_MISSED_CALL_NUMBER, number);
+ context.startService(serviceIntent);
+ }
}
diff --git a/src/com/android/dialer/calllog/ContactInfoHelper.java b/src/com/android/dialer/calllog/ContactInfoHelper.java
index e075b9b..7f08fdc 100644
--- a/src/com/android/dialer/calllog/ContactInfoHelper.java
+++ b/src/com/android/dialer/calllog/ContactInfoHelper.java
@@ -19,7 +19,6 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteFullException;
import android.net.Uri;
-import android.os.Build;
import android.provider.CallLog.Calls;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
@@ -32,7 +31,6 @@
import android.util.Log;
import com.android.contacts.common.ContactsUtils;
-import com.android.contacts.common.compat.ContactsCompat;
import com.android.contacts.common.util.Constants;
import com.android.contacts.common.util.PermissionsUtil;
import com.android.contacts.common.util.PhoneNumberHelper;
@@ -42,7 +40,6 @@
import com.android.dialer.service.CachedNumberLookupService.CachedContactInfo;
import com.android.dialer.util.TelecomUtil;
import com.android.dialerbind.ObjectFactory;
-import com.android.incallui.CallerInfo;
import org.json.JSONException;
import org.json.JSONObject;
diff --git a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
index 2b894a1..db82295 100644
--- a/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
+++ b/src/com/android/dialer/calllog/DefaultVoicemailNotifier.java
@@ -19,42 +19,41 @@
import static android.Manifest.permission.READ_CALL_LOG;
import static android.Manifest.permission.READ_CONTACTS;
+import com.android.contacts.common.ContactsUtils;
+import com.android.contacts.common.compat.TelephonyManagerCompat;
import com.google.common.collect.Maps;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
-import android.database.Cursor;
import android.net.Uri;
-import android.provider.CallLog.Calls;
-import android.provider.ContactsContract.PhoneLookup;
-import android.telephony.PhoneNumberUtils;
+import android.telecom.PhoneAccountHandle;
+import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
-import com.android.common.io.MoreCloseables;
import com.android.contacts.common.util.ContactDisplayUtils;
-import com.android.contacts.common.util.PermissionsUtil;
import com.android.dialer.DialtactsActivity;
import com.android.dialer.R;
+import com.android.dialer.calllog.CallLogNotificationsHelper.NewCall;
import com.android.dialer.filterednumber.FilteredNumbersUtil;
import com.android.dialer.list.ListsFragment;
import java.util.Iterator;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
- * VoicemailNotifier that shows a notification in the status bar.
+ * Shows a voicemail notification in the status bar.
*/
public class DefaultVoicemailNotifier {
- public static final String TAG = "DefaultVoicemailNotifier";
+ public static final String TAG = "VoicemailNotifier";
/** The tag used to identify notifications from this class. */
private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
@@ -65,30 +64,18 @@
private static DefaultVoicemailNotifier sInstance;
private final Context mContext;
- private final NotificationManager mNotificationManager;
- private final NewCallsQuery mNewCallsQuery;
- private final NameLookupQuery mNameLookupQuery;
/** Returns the singleton instance of the {@link DefaultVoicemailNotifier}. */
- public static synchronized DefaultVoicemailNotifier getInstance(Context context) {
+ public static DefaultVoicemailNotifier getInstance(Context context) {
if (sInstance == null) {
- NotificationManager notificationManager =
- (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
ContentResolver contentResolver = context.getContentResolver();
- sInstance = new DefaultVoicemailNotifier(context, notificationManager,
- createNewCallsQuery(context, contentResolver),
- createNameLookupQuery(context, contentResolver));
+ sInstance = new DefaultVoicemailNotifier(context);
}
return sInstance;
}
- private DefaultVoicemailNotifier(Context context,
- NotificationManager notificationManager, NewCallsQuery newCallsQuery,
- NameLookupQuery nameLookupQuery) {
+ private DefaultVoicemailNotifier(Context context) {
mContext = context;
- mNotificationManager = notificationManager;
- mNewCallsQuery = newCallsQuery;
- mNameLookupQuery = nameLookupQuery;
}
/**
@@ -102,16 +89,17 @@
public void updateNotification(Uri newCallUri) {
// Lookup the list of new voicemails to include in the notification.
// TODO: Move this into a service, to avoid holding the receiver up.
- final List<NewCall> newCalls = mNewCallsQuery.query();
+ final List<NewCall> newCalls =
+ CallLogNotificationsHelper.getInstance(mContext).getNewVoicemails();
if (newCalls == null) {
// Query failed, just return.
return;
}
- if (newCalls.size() == 0) {
+ if (newCalls.isEmpty()) {
// No voicemails to notify about: clear the notification.
- clearNotification();
+ getNotificationManager().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
return;
}
@@ -145,19 +133,8 @@
// Check if we already know the name associated with this number.
String name = names.get(newCall.number);
if (name == null) {
- name = PhoneNumberDisplayUtil.getDisplayName(
- mContext,
- newCall.number,
- newCall.numberPresentation,
- /* isVoicemail */ false).toString();
- // If we cannot lookup the contact, use the number instead.
- if (TextUtils.isEmpty(name)) {
- // Look it up in the database.
- name = mNameLookupQuery.query(newCall.number);
- if (TextUtils.isEmpty(name) && !TextUtils.isEmpty(newCall.number)) {
- name = PhoneNumberUtils.formatNumber(newCall.number, newCall.countryIso);
- }
- }
+ name = CallLogNotificationsHelper.getInstance(mContext).getName(newCall.number,
+ newCall.numberPresentation, newCall.countryIso);
names.put(newCall.number, name);
// This is a new caller. Add it to the back of the list of callers.
if (TextUtils.isEmpty(callers)) {
@@ -174,6 +151,7 @@
}
}
+ // All the potential new voicemails have been removed, e.g. if they were spam.
if (newCalls.isEmpty()) {
return;
}
@@ -194,13 +172,31 @@
// TODO: Use the photo of contact if all calls are from the same person.
final int icon = android.R.drawable.stat_notify_voicemail;
+ Uri ringtoneUri = null;
+ int notificationDefaults = 0;
+ if (callToNotify != null) {
+ PhoneAccountHandle accountHandle = new PhoneAccountHandle(
+ ComponentName.unflattenFromString(callToNotify.accountComponentName),
+ callToNotify.accountId);
+ ringtoneUri = TelephonyManagerCompat
+ .getVoicemailRingtoneUri(getTelephonyManager(), accountHandle);
+ if (ContactsUtils.FLAG_N_FEATURE) {
+ notificationDefaults = TelephonyManagerCompat.isVoicemailVibrationEnabled(
+ getTelephonyManager(), accountHandle)
+ ? Notification.DEFAULT_VIBRATE : 0;
+ } else {
+ notificationDefaults = Notification.DEFAULT_ALL;
+ }
+ }
+
Notification.Builder notificationBuilder = new Notification.Builder(mContext)
.setSmallIcon(icon)
.setContentTitle(title)
.setContentText(callers)
.setStyle(new Notification.BigTextStyle().bigText(transcription))
.setColor(resources.getColor(R.color.dialer_theme_color))
- .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0)
+ .setSound(ringtoneUri)
+ .setDefaults(notificationDefaults)
.setDeleteIntent(createMarkNewVoicemailsAsOldIntent())
.setAutoCancel(true);
@@ -221,7 +217,8 @@
notificationBuilder.setTicker(msg);
}
- mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notificationBuilder.build());
+ getNotificationManager().notify(NOTIFICATION_TAG, NOTIFICATION_ID,
+ notificationBuilder.build());
}
/** Creates a pending intent that marks all new voicemails as old. */
@@ -231,194 +228,11 @@
return PendingIntent.getService(mContext, 0, intent, 0);
}
- public void clearNotification() {
- mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
+ private NotificationManager getNotificationManager() {
+ return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
}
- /** Information about a new voicemail. */
- private static final class NewCall {
- public final Uri callsUri;
- 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 NewCall(
- Uri callsUri,
- Uri voicemailUri,
- String number,
- int numberPresentation,
- String accountComponentName,
- String accountId,
- String transcription,
- String countryIso,
- long dateMs) {
- 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;
- }
- }
-
- /** Allows determining the new calls for which a notification should be generated. */
- public interface NewCallsQuery {
- /**
- * Returns the new calls for which a notification should be generated.
- */
- public List<NewCall> query();
- }
-
- /** Create a new instance of {@link NewCallsQuery}. */
- public static NewCallsQuery createNewCallsQuery(Context context,
- ContentResolver contentResolver) {
- return new DefaultNewCallsQuery(context.getApplicationContext(), contentResolver);
- }
-
- /**
- * 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 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 final ContentResolver mContentResolver;
- private final Context mContext;
-
- private DefaultNewCallsQuery(Context context, ContentResolver contentResolver) {
- mContext = context;
- mContentResolver = contentResolver;
- }
-
- @Override
- public List<NewCall> query() {
- if (!PermissionsUtil.hasPermission(mContext, READ_CALL_LOG)) {
- Log.w(TAG, "No READ_CALL_LOG permission, returning null for calls lookup.");
- return null;
- }
- final String selection = String.format("%s = 1 AND %s = ?", Calls.NEW, Calls.TYPE);
- final String[] selectionArgs = new String[]{ Integer.toString(Calls.VOICEMAIL_TYPE) };
- Cursor cursor = null;
- try {
- cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION,
- selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
- if (cursor == null) {
- return null;
- }
- List<NewCall> newCalls = new LinkedList<NewCall>();
- while (cursor.moveToNext()) {
- newCalls.add(createNewCallsFromCursor(cursor));
- }
- return newCalls;
- } catch (RuntimeException e) {
- Log.w(TAG, "Exception when querying Contacts Provider for calls lookup");
- return null;
- } finally {
- MoreCloseables.closeQuietly(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));
- }
- }
-
- /** Allows determining the name associated with a given phone number. */
- public interface NameLookupQuery {
- /**
- * Returns the name associated with the given number in the contacts database, or null if
- * the number does not correspond to any of the contacts.
- * <p>
- * If there are multiple contacts with the same phone number, it will return the name of one
- * of the matching contacts.
- */
- public String query(String number);
- }
-
- /** Create a new instance of {@link NameLookupQuery}. */
- public static NameLookupQuery createNameLookupQuery(Context context,
- ContentResolver contentResolver) {
- return new DefaultNameLookupQuery(context.getApplicationContext(), contentResolver);
- }
-
- /**
- * Default implementation of {@link NameLookupQuery} that looks up the name of a contact in the
- * contacts database.
- */
- private static final class DefaultNameLookupQuery implements NameLookupQuery {
- private static final String[] PROJECTION = { PhoneLookup.DISPLAY_NAME };
- private static final int DISPLAY_NAME_COLUMN_INDEX = 0;
-
- private final ContentResolver mContentResolver;
- private final Context mContext;
-
- private DefaultNameLookupQuery(Context context, ContentResolver contentResolver) {
- mContext = context;
- mContentResolver = contentResolver;
- }
-
- @Override
- public String query(String number) {
- if (!PermissionsUtil.hasPermission(mContext, READ_CONTACTS)) {
- Log.w(TAG, "No READ_CONTACTS permission, returning null for name lookup.");
- return null;
- }
- Cursor cursor = null;
- try {
- cursor = mContentResolver.query(
- Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, Uri.encode(number)),
- PROJECTION, null, null, null);
- if (cursor == null || !cursor.moveToFirst()) return null;
- return cursor.getString(DISPLAY_NAME_COLUMN_INDEX);
- } catch (RuntimeException e) {
- Log.w(TAG, "Exception when querying Contacts Provider for name lookup");
- return null;
- } finally {
- if (cursor != null) {
- cursor.close();
- }
- }
- }
+ private TelephonyManager getTelephonyManager() {
+ return (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
}
}
diff --git a/src/com/android/dialer/calllog/MissedCallNotificationReceiver.java b/src/com/android/dialer/calllog/MissedCallNotificationReceiver.java
new file mode 100644
index 0000000..86d6cb9
--- /dev/null
+++ b/src/com/android/dialer/calllog/MissedCallNotificationReceiver.java
@@ -0,0 +1,53 @@
+/*
+ * 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.dialer.calllog;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.telecom.TelecomManager;
+import android.util.Log;
+
+import com.android.dialer.calllog.CallLogNotificationsService;
+
+/**
+ * Receives broadcasts that should trigger a refresh of the missed call notification. This includes
+ * both an explicit broadcast from Telecom and a reboot.
+ */
+public class MissedCallNotificationReceiver extends BroadcastReceiver {
+ //TODO: Use compat class for these methods.
+ public static final String ACTION_SHOW_MISSED_CALLS_NOTIFICATION =
+ "android.telecom.action.SHOW_MISSED_CALLS_NOTIFICATION";
+
+ public static final String EXTRA_NOTIFICATION_COUNT =
+ "android.telecom.extra.NOTIFICATION_COUNT";
+
+ public static final String EXTRA_NOTIFICATION_PHONE_NUMBER =
+ "android.telecom.extra.NOTIFICATION_PHONE_NUMBER";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (!ACTION_SHOW_MISSED_CALLS_NOTIFICATION.equals(action)) {
+ return;
+ }
+
+ int count = intent.getIntExtra(EXTRA_NOTIFICATION_COUNT,
+ CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT);
+ String number = intent.getStringExtra(EXTRA_NOTIFICATION_PHONE_NUMBER);
+ CallLogNotificationsService.updateMissedCallNotifications(context, count, number);
+ }
+}
diff --git a/src/com/android/dialer/calllog/MissedCallNotifier.java b/src/com/android/dialer/calllog/MissedCallNotifier.java
new file mode 100644
index 0000000..8811baf
--- /dev/null
+++ b/src/com/android/dialer/calllog/MissedCallNotifier.java
@@ -0,0 +1,271 @@
+/*
+ * 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.dialer.calllog;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.provider.CallLog.Calls;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.contacts.common.util.PhoneNumberHelper;
+import com.android.dialer.calllog.CallLogNotificationsHelper.NewCall;
+import com.android.dialer.DialtactsActivity;
+import com.android.dialer.list.ListsFragment;
+import com.android.dialer.util.DialerUtils;
+import com.android.dialer.util.IntentUtil;
+import com.android.dialer.util.IntentUtil.CallIntentBuilder;
+import com.android.dialer.R;
+
+import java.util.List;
+
+/**
+ * Creates a notification for calls that the user missed (neither answered nor rejected).
+ *
+ */
+public class MissedCallNotifier {
+ public static final String TAG = "MissedCallNotifier";
+
+ /** The tag used to identify notifications from this class. */
+ private static final String NOTIFICATION_TAG = "MissedCallNotifier";
+ /** The identifier of the notification of new missed calls. */
+ private static final int NOTIFICATION_ID = 1;
+ /** Preference file key for number of missed calls. */
+ private static final String MISSED_CALL_COUNT = "missed_call_count";
+
+ private static MissedCallNotifier sInstance;
+ private Context mContext;
+
+ /** Returns the singleton instance of the {@link MissedCallNotifier}. */
+ public static MissedCallNotifier getInstance(Context context) {
+ if (sInstance == null) {
+ sInstance = new MissedCallNotifier(context);
+ }
+ return sInstance;
+ }
+
+ private MissedCallNotifier(Context context) {
+ mContext = context;
+ }
+
+ public void updateMissedCallNotification(int count, String number) {
+ final int titleResId;
+ final String expandedText; // The text in the notification's line 1 and 2.
+
+ final List<NewCall> newCalls =
+ CallLogNotificationsHelper.getInstance(mContext).getNewMissedCalls();
+
+ if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
+ if (newCalls == null) {
+ // If the intent did not contain a count, and we are unable to get a count from the
+ // call log, then no notification can be shown.
+ return;
+ }
+ count = newCalls.size();
+ }
+
+ if (count == 0) {
+ // No voicemails to notify about: clear the notification.
+ clearMissedCalls();
+ return;
+ }
+
+ // The call log has been updated, use that information preferentially.
+ boolean useCallLog = newCalls != null && newCalls.size() == count;
+ NewCall newestCall = useCallLog ? newCalls.get(0) : null;
+ long timeMs = useCallLog ? newestCall.dateMs : System.currentTimeMillis();
+
+ // Display the first line of the notification:
+ // 1 missed call: <caller name || handle>
+ // More than 1 missed call: <number of calls> + "missed calls"
+ if (count == 1) {
+ titleResId = R.string.notification_missedCallTitle;
+
+ //TODO: look up caller ID that is not in contacts.
+ expandedText = CallLogNotificationsHelper.getInstance(mContext)
+ .getName(useCallLog ? newestCall.number : number,
+ useCallLog ? newestCall.numberPresentation
+ : Calls.PRESENTATION_ALLOWED,
+ useCallLog ? newestCall.countryIso : null);
+ } else {
+ titleResId = R.string.notification_missedCallsTitle;
+ expandedText =
+ mContext.getString(R.string.notification_missedCallsMsg, count);
+ }
+
+ // Create a public viewable version of the notification, suitable for display when sensitive
+ // notification content is hidden.
+ Notification.Builder publicBuilder = new Notification.Builder(mContext);
+ publicBuilder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
+ .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+ // 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(mContext.getText(titleResId))
+ .setContentIntent(createCallLogPendingIntent())
+ .setAutoCancel(true)
+ .setWhen(timeMs)
+ .setDeleteIntent(createClearMissedCallsPendingIntent());
+
+ // Create the notification suitable for display when sensitive information is showing.
+ Notification.Builder builder = new Notification.Builder(mContext);
+ builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
+ .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
+ .setContentTitle(mContext.getText(titleResId))
+ .setContentText(expandedText)
+ .setContentIntent(createCallLogPendingIntent())
+ .setAutoCancel(true)
+ .setWhen(timeMs)
+ .setDeleteIntent(createClearMissedCallsPendingIntent())
+ // 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());
+
+ // Add additional actions when there is only 1 missed call, like call-back and SMS.
+ if (count == 1) {
+ if (!TextUtils.isEmpty(number)
+ && !TextUtils.equals(
+ number, mContext.getString(R.string.handle_restricted))) {
+ builder.addAction(R.drawable.ic_phone_24dp,
+ mContext.getString(R.string.notification_missedCall_call_back),
+ createCallBackPendingIntent(number));
+
+ if (!PhoneNumberHelper.isUriNumber(number)) {
+ builder.addAction(R.drawable.ic_message_24dp,
+ mContext.getString(R.string.notification_missedCall_message),
+ createSendSmsFromNotificationPendingIntent(number));
+ }
+ }
+ //TODO: add photo
+ }
+
+ Notification notification = builder.build();
+ configureLedOnNotification(notification);
+
+ Log.i(TAG, "Adding missed call notification.");
+ getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
+ }
+
+ private void clearMissedCalls() {
+ AsyncTask.execute(new Runnable() {
+ @Override
+ public void run() {
+ // 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 {
+ mContext.getContentResolver().update(Calls.CONTENT_URI, values,
+ where.toString(), new String[]{ Integer.toString(Calls.
+ MISSED_TYPE) });
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "ContactsProvider update command failed", e);
+ }
+ getNotificationMgr().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
+ }
+ });
+ }
+
+ /**
+ * Trigger an intent to make a call from a missed call number.
+ */
+ public void callBackFromMissedCall(String number) {
+ closeSystemDialogs(mContext);
+ CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+ DialerUtils.startActivityWithErrorToast(
+ mContext,
+ new CallIntentBuilder(number)
+ .build()
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ }
+
+ /**
+ * Trigger an intent to send an sms from a missed call number.
+ */
+ public void sendSmsFromMissedCall(String number) {
+ closeSystemDialogs(mContext);
+ CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
+ DialerUtils.startActivityWithErrorToast(
+ mContext,
+ IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ }
+
+ /**
+ * Creates a new pending intent that sends the user to the call log.
+ *
+ * @return The pending intent.
+ */
+ private PendingIntent createCallLogPendingIntent() {
+ Intent contentIntent = new Intent(mContext, DialtactsActivity.class);
+ contentIntent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_HISTORY);
+ return PendingIntent.getActivity(
+ mContext, 0, contentIntent,PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+
+ /** Creates a pending intent that marks all new missed calls as old. */
+ private PendingIntent createClearMissedCallsPendingIntent() {
+ Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD);
+ return PendingIntent.getService(mContext, 0, intent, 0);
+ }
+
+ private PendingIntent createCallBackPendingIntent(String number) {
+ Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ intent.setAction(
+ CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION);
+ intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+ return PendingIntent.getService(mContext, 0, intent, 0);
+ }
+
+ private PendingIntent createSendSmsFromNotificationPendingIntent(String number) {
+ Intent intent = new Intent(mContext, CallLogNotificationsService.class);
+ intent.setAction(
+ CallLogNotificationsService.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION);
+ intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
+ return PendingIntent.getService(mContext, 0, intent, 0);
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Closes open system dialogs and the notification shade.
+ */
+ private void closeSystemDialogs(Context context) {
+ context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ }
+
+ private NotificationManager getNotificationMgr() {
+ return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+}
diff --git a/src/com/android/dialer/calllog/VoicemailQueryHandler.java b/src/com/android/dialer/calllog/VoicemailQueryHandler.java
index 26f9bd1..c6e644c 100644
--- a/src/com/android/dialer/calllog/VoicemailQueryHandler.java
+++ b/src/com/android/dialer/calllog/VoicemailQueryHandler.java
@@ -59,7 +59,8 @@
if (token == UPDATE_MARK_VOICEMAILS_AS_OLD_TOKEN) {
if (mContext != null) {
Intent serviceIntent = new Intent(mContext, CallLogNotificationsService.class);
- serviceIntent.setAction(CallLogNotificationsService.ACTION_UPDATE_NOTIFICATIONS);
+ serviceIntent.setAction(
+ CallLogNotificationsService.ACTION_UPDATE_VOICEMAIL_NOTIFICATIONS);
mContext.startService(serviceIntent);
} else {
Log.w(TAG, "Unknown update completed: ignoring: " + token);
diff --git a/src/com/android/dialer/compat/UserManagerCompat.java b/src/com/android/dialer/compat/UserManagerCompat.java
index a1fdcc6..5767033 100644
--- a/src/com/android/dialer/compat/UserManagerCompat.java
+++ b/src/com/android/dialer/compat/UserManagerCompat.java
@@ -1,3 +1,18 @@
+/*
+ * Copyright (C) 2015 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.compat;
import android.content.Context;
@@ -34,4 +49,23 @@
// Adapted from {@link UserManager} and {@link UserHandle}.
return (Process.myUid() / PER_USER_RANGE) == USER_SYSTEM;
}
+
+ /**
+ * Return whether the calling user is running in an "unlocked" state. A user
+ * is unlocked only after they've entered their credentials (such as a lock
+ * pattern or PIN), and credential-encrypted private app data storage is
+ * available.
+ *
+ * TODO b/26688153
+ *
+ * @param context the current context
+ * @return {@code true} if the user is unlocked, {@code false} otherwise
+ * @throws NullPointerException if context is null
+ */
+ public static boolean isUserUnlocked(Context context) {
+ if (CompatUtils.isNCompatible()) {
+ return UserManagerSdkCompat.isUserUnlocked(context);
+ }
+ return true;
+ }
}
diff --git a/src/com/android/dialer/settings/AppCompatPreferenceActivity.java b/src/com/android/dialer/settings/AppCompatPreferenceActivity.java
deleted file mode 100644
index 4e5d9c9..0000000
--- a/src/com/android/dialer/settings/AppCompatPreferenceActivity.java
+++ /dev/null
@@ -1,155 +0,0 @@
-/*
- * Copyright (C) 2015 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.settings;
-
-import android.content.res.Configuration;
-import android.os.Bundle;
-import android.preference.PreferenceActivity;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AppCompatDelegate;
-import android.support.v7.widget.Toolbar;
-import android.view.MenuInflater;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls
- * to be used with AppCompat.
- */
-public class AppCompatPreferenceActivity extends PreferenceActivity {
- private AppCompatDelegate mDelegate;
-
- private boolean mIsSafeToCommitTransactions;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- getDelegate().installViewFactory();
- getDelegate().onCreate(savedInstanceState);
- super.onCreate(savedInstanceState);
- mIsSafeToCommitTransactions = true;
- }
-
- @Override
- protected void onPostCreate(Bundle savedInstanceState) {
- super.onPostCreate(savedInstanceState);
- getDelegate().onPostCreate(savedInstanceState);
- }
-
- public ActionBar getSupportActionBar() {
- return getDelegate().getSupportActionBar();
- }
-
- public void setSupportActionBar(Toolbar toolbar) {
- getDelegate().setSupportActionBar(toolbar);
- }
-
- @Override
- public MenuInflater getMenuInflater() {
- return getDelegate().getMenuInflater();
- }
-
- @Override
- public void setContentView(int layoutResID) {
- getDelegate().setContentView(layoutResID);
- }
-
- @Override
- public void setContentView(View view) {
- getDelegate().setContentView(view);
- }
-
- @Override
- public void setContentView(View view, ViewGroup.LayoutParams params) {
- getDelegate().setContentView(view, params);
- }
-
- @Override
- public void addContentView(View view, ViewGroup.LayoutParams params) {
- getDelegate().addContentView(view, params);
- }
-
- @Override
- protected void onPostResume() {
- super.onPostResume();
- getDelegate().onPostResume();
- }
-
- @Override
- protected void onTitleChanged(CharSequence title, int color) {
- super.onTitleChanged(title, color);
- getDelegate().setTitle(title);
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- getDelegate().onConfigurationChanged(newConfig);
- }
-
- @Override
- protected void onStop() {
- super.onStop();
- getDelegate().onStop();
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- getDelegate().onDestroy();
- }
-
- @Override
- public void invalidateOptionsMenu() {
- getDelegate().invalidateOptionsMenu();
- }
-
- private AppCompatDelegate getDelegate() {
- if (mDelegate == null) {
- mDelegate = AppCompatDelegate.create(this, null);
- }
- return mDelegate;
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- mIsSafeToCommitTransactions = true;
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- mIsSafeToCommitTransactions = true;
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- mIsSafeToCommitTransactions = false;
- }
-
- /**
- * Returns true if it is safe to commit {@link FragmentTransaction}s at this time, based on
- * whether {@link Activity#onSaveInstanceState} has been called or not.
- *
- * Make sure that the current activity calls into
- * {@link super.onSaveInstanceState(Bundle outState)} (if that method is overridden),
- * so the flag is properly set.
- */
- public boolean isSafeToCommitTransactions() {
- return mIsSafeToCommitTransactions;
- }
-}
diff --git a/src/com/android/dialer/settings/DialerSettingsActivity.java b/src/com/android/dialer/settings/DialerSettingsActivity.java
index a187157..b25c5a6 100644
--- a/src/com/android/dialer/settings/DialerSettingsActivity.java
+++ b/src/com/android/dialer/settings/DialerSettingsActivity.java
@@ -28,6 +28,7 @@
import android.view.MenuItem;
import android.widget.Toast;
+import com.android.contacts.common.activity.AppCompatPreferenceActivity;
import com.android.contacts.common.compat.CompatUtils;
import com.android.contacts.common.compat.SdkVersionOverride;
import com.android.contacts.common.compat.TelephonyManagerCompat;
diff --git a/tests/src/com/android/dialer/compat/UserManagerCompatTest.java b/tests/src/com/android/dialer/compat/UserManagerCompatTest.java
new file mode 100644
index 0000000..ff734a1
--- /dev/null
+++ b/tests/src/com/android/dialer/compat/UserManagerCompatTest.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2015 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.compat;
+
+import android.test.AndroidTestCase;
+
+import com.android.contacts.common.compat.CompatUtils;
+
+public class UserManagerCompatTest extends AndroidTestCase {
+
+ public void testIsUserUnlocked_N_NullContext() {
+ if (!CompatUtils.isNCompatible()) {
+ return;
+ }
+ try {
+ UserManagerCompat.isUserUnlocked(null);
+ fail("Expected NullPointerException but none was thrown");
+ } catch (NullPointerException e) {}
+ }
+
+ public void testIsUserUnlocked_M_NullContext() {
+ if (CompatUtils.isNCompatible()) {
+ return;
+ }
+ assertTrue(UserManagerCompat.isUserUnlocked(null));
+ }
+
+ public void testIsUserUnlocked() {
+ assertTrue(UserManagerCompat.isUserUnlocked(getContext()));
+ }
+}