Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f9bd0163 authored by Flavio Lerda's avatar Flavio Lerda Committed by Android (Google) Code Review
Browse files

Merge "Fixes for voicemail notifications."

parents 0e7ae7e1 b78b7096
Loading
Loading
Loading
Loading
+9 −1
Original line number Diff line number Diff line
@@ -647,7 +647,7 @@
                    android:resource="@xml/social_widget_info" />
        </receiver>

        <receiver android:name=".calllog.NewVoicemailReceiver">
        <receiver android:name=".calllog.CallLogReceiver">
            <intent-filter>
                <action android:name="android.intent.action.NEW_VOICEMAIL" />
                <data
@@ -655,6 +655,9 @@
                    android:host="com.android.voicemail"
                />
            </intent-filter>
            <intent-filter android:priority="100">
                 <action android:name="android.intent.action.BOOT_COMPLETED"/>
            </intent-filter>
        </receiver>

        <activity
@@ -664,5 +667,10 @@
                <action android:name="android.intent.action.APPWIDGET_PICK" />
            </intent-filter>
        </activity>

        <service
            android:name=".calllog.CallLogNotificationsService"
            android:exported="false"
        />
    </application>
</manifest>
+19 −3
Original line number Diff line number Diff line
@@ -1605,13 +1605,29 @@
    <!-- Button to view the updates from the current group on the group detail page [CHAR LIMIT=20] -->
    <string name="view_updates_from_group">View updates</string>

    <!-- Title of the notification of new voicemail. -->
    <string name="notification_voicemail_title">New voicemail</string>
    <!-- Title of the notification of new voicemails. [CHAR LIMIT=30] -->
    <plurals name="notification_voicemail_title">
        <item quantity="one">Voicemail</item>
        <item quantity="other"><xliff:g id="count">%1$d</xliff:g> Voicemails</item>
    </plurals>

    <!-- Used to build a list of names or phone numbers, to indicate the callers who left
         voicemails.
         The first argument may be one or more callers, the most recent ones.
         The second argument is an additional callers.
         This string is used to build a list of callers.

         [CHAR LIMIT=10]
     -->
    <string name="notification_voicemail_callers_list"><xliff:g id="newer_callers">%1$s</xliff:g>, <xliff:g id="older_caller">%2$s</xliff:g></string>

    <!-- Text used in the ticker to notify the user of the latest voicemail. [CHAR LIMIT=30] -->
    <string name="notification_new_voicemail_ticker">New voicemail from <xliff:g id="caller">%1$s</xliff:g></string>

    <!-- Initial display for position of current playback, do not translate. -->
    <string name="voicemail_initial_time">00:05</string>

    <!-- Message to show when there is an error playing back the voicemail. -->
    <!-- Message to show when there is an error playing back the voicemail. [CHAR LIMIT=40] -->
    <string name="voicemail_playback_error">Could not play voicemail</string>

    <!-- The separator between the call type text and the date in the call log [CHAR LIMIT=3] -->
+53 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2011 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.contacts.calllog;

import android.app.IntentService;
import android.content.Intent;
import android.util.Log;

/**
 * Provides operations for managing notifications.
 * <p>
 * At the moment, it only handle {@link #ACTION_MARK_NEW_CALLS_AS_OLD}, which marks all the new
 * items in the call log as old; this is called when a notification is dismissed.
 */
public class CallLogNotificationsService extends IntentService {
    private static final String TAG = "CallLogNotificationsService";

    // Action to mark all the new calls as old. Invoked when the notifications need to be cleared.
    public static final String ACTION_MARK_NEW_CALLS_AS_OLD =
            "com.android.contacts.ACTION_MARK_NEW_CALLS_AS_OLD";

    private CallLogQueryHandler mCallLogQueryHandler;

    public CallLogNotificationsService() {
        super("CallLogNotificationsService");
        mCallLogQueryHandler = new CallLogQueryHandler(getContentResolver(), null /*listener*/);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        if (ACTION_MARK_NEW_CALLS_AS_OLD.equals(intent.getAction())) {
            mCallLogQueryHandler.markNewCallsAsOld();
            return;
        } else {
            Log.d(TAG, "onHandleIntent: could not handle: " + intent);
        }
    }

}
+26 −7
Original line number Diff line number Diff line
@@ -18,25 +18,44 @@ package com.android.contacts.calllog;

import android.app.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.provider.VoicemailContract;
import android.util.Log;

/**
 * Receiver for new voicemail notifications.
 * Receiver for call log events.
 * <p>
 * Delegates to a {@link VoicemailNotifier}.
 * It is currently used to handle {@link VoicemailContract#ACTION_NEW_VOICEMAIL} and
 * {@link Intent#ACTION_BOOT_COMPLETED}.
 */
public class NewVoicemailReceiver extends BroadcastReceiver {
public class CallLogReceiver extends BroadcastReceiver {
    private static final String TAG = "CallLogReceiver";

    private VoicemailNotifier mNotifier;

    @Override
    public void onReceive(Context context, Intent intent) {
        getVoicemailNotifier(context).notifyNewVoicemail(intent.getData());
        if (mNotifier == null) {
            mNotifier = getNotifier(context);
        }
        if (VoicemailContract.ACTION_NEW_VOICEMAIL.equals(intent.getAction())) {
            mNotifier.notifyNewVoicemail(intent.getData());
        } else if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
            mNotifier.updateNotification();
        } else {
            Log.d(TAG, "onReceive: could not handle: " + intent);
        }
    }

    private VoicemailNotifier getVoicemailNotifier(Context context) {
    private VoicemailNotifier getNotifier(Context context) {
        NotificationManager notificationManager =
                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        ContentResolver contentResolver = context.getContentResolver();
        return new DefaultVoicemailNotifier(context, notificationManager,
                DefaultVoicemailNotifier.createVoicemailNumberQuery(context.getContentResolver()),
                DefaultVoicemailNotifier.createNameLookupQuery(context.getContentResolver()));
                DefaultVoicemailNotifier.createNewCallsQuery(contentResolver),
                DefaultVoicemailNotifier.createNameLookupQuery(contentResolver),
                DefaultVoicemailNotifier.createPhoneNumberHelper(context));
    }
}
+195 −70
Original line number Diff line number Diff line
@@ -16,23 +16,36 @@

package com.android.contacts.calllog;

import com.android.common.io.MoreCloseables;
import com.android.contacts.CallDetailActivity;
import com.android.contacts.R;
import com.google.common.collect.Maps;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
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.provider.VoicemailContract;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;

import java.util.Map;

/**
 * Implementation of {@link VoicemailNotifier} that shows a notification in the status bar.
 * Implementation of {@link VoicemailNotifier} that shows a notification in the
 * status bar.
 */
public class DefaultVoicemailNotifier implements VoicemailNotifier {
    public static final String TAG = "DefaultVoicemailNotifier";

    /** The tag used to identify notifications from this class. */
    private static final String NOTIFICATION_TAG = "DefaultVoicemailNotifier";
    /** The identifier of the notification of new voicemails. */
@@ -40,115 +53,211 @@ public class DefaultVoicemailNotifier implements VoicemailNotifier {

    private final Context mContext;
    private final NotificationManager mNotificationManager;
    private final VoicemailNumberQuery mVoicemailNumberQuery;
    private final NewCallsQuery mNewCallsQuery;
    private final NameLookupQuery mNameLookupQuery;
    private final PhoneNumberHelper mPhoneNumberHelper;

    public DefaultVoicemailNotifier(Context context, NotificationManager notificationManager,
            VoicemailNumberQuery voicemailNumberQuery, NameLookupQuery nameLookupQuery) {
    public DefaultVoicemailNotifier(Context context,
            NotificationManager notificationManager, NewCallsQuery newCallsQuery,
            NameLookupQuery nameLookupQuery, PhoneNumberHelper phoneNumberHelper) {
        mContext = context;
        mNotificationManager = notificationManager;
        mVoicemailNumberQuery = voicemailNumberQuery;
        mNewCallsQuery = newCallsQuery;
        mNameLookupQuery = nameLookupQuery;
        mPhoneNumberHelper = phoneNumberHelper;
    }

    @Override
    public void notifyNewVoicemail(Uri uri) {
        // Lookup the number that left the voicemail.
        String number = mVoicemailNumberQuery.query(uri);
        // Lookup the name of the contact associated with this number.
        String name = mNameLookupQuery.query(number);
        // Show the name of the contact if available, falling back to using the number if not.
        String displayName = name == null ? number : name;
    public void notifyNewVoicemail(Uri newVoicemailUri) {
        Log.d(TAG, "notifyNewVoicemail: " + newVoicemailUri);
        updateNotification(newVoicemailUri);
    }

    @Override
    public void updateNotification() {
        Log.d(TAG, "updateNotification");
        updateNotification(null);
    }

    /** Updates the notification and notifies of the call with the given URI. */
    private 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 NewCall[] newCalls = mNewCallsQuery.query();

        if (newCalls.length == 0) {
            Log.e(TAG, "No voicemails to notify about: clear the notification.");
            clearNotification();
            return;
        }

        Resources resources = mContext.getResources();

        // This represents a list of names to include in the notification.
        String callers = null;

        // Maps each number into a name: if a number is in the map, it has already left a more
        // recent voicemail.
        final Map<String, String> names = Maps.newHashMap();

        // Determine the call corresponding to the new voicemail we have to notify about.
        NewCall callToNotify = null;

        // Iterate over the new voicemails to determine all the information above.
        for (NewCall newCall : newCalls) {
            // Check if we already know the name associated with this number.
            String name = names.get(newCall.number);
            if (name == null) {
                // Look it up in the database.
                name = mNameLookupQuery.query(newCall.number);
                // If we cannot lookup the contact, use the number instead.
                if (name == null) {
                    name = mPhoneNumberHelper.getDisplayNumber(newCall.number, "").toString();
                    if (TextUtils.isEmpty(name)) {
                        name = newCall.number;
                    }
                }
                names.put(newCall.number, name);
                // This is a new caller. Add it to the back of the list of callers.
                if (TextUtils.isEmpty(callers)) {
                    callers = name;
                } else {
                    callers = resources.getString(
                            R.string.notification_voicemail_callers_list, callers, name);
                }
            }
            // Check if this is the new call we need to notify about.
            if (newCallUri != null && newCallUri.equals(newCall.voicemailUri)) {
                callToNotify = newCall;
            }
        }

        if (newCallUri != null && callToNotify == null) {
            Log.e(TAG, "The new call could not be found in the call log: " + newCallUri);
        }

        // Determine the title of the notification and the icon for it.
        final String title = resources.getQuantityString(
                R.plurals.notification_voicemail_title, newCalls.length, newCalls.length);
        // TODO: Use the photo of contact if all calls are from the same person.
        final int icon = android.R.drawable.stat_notify_voicemail;

        Notification notification = new Notification.Builder(mContext)
                .setSmallIcon(android.R.drawable.stat_notify_voicemail)
                .setContentTitle(mContext.getString(R.string.notification_voicemail_title))
                .setContentText(displayName)
                .setDefaults(Notification.DEFAULT_ALL)
                .setSmallIcon(icon)
                .setContentTitle(title)
                .setContentText(callers)
                .setDefaults(callToNotify != null ? Notification.DEFAULT_ALL : 0)
                .setDeleteIntent(createMarkNewCallsAsOld())
                .setAutoCancel(true)
                .getNotification();

        // Open the voicemail when clicking on the notification.
        notification.contentIntent =
                PendingIntent.getActivity(mContext, 0, new Intent(Intent.ACTION_VIEW, uri), 0);
        // Determine the intent to fire when the notification is clicked on.
        final Intent contentIntent;
        if (newCalls.length == 1) {
            // Open the voicemail directly.
            Log.d(TAG, "Opening voicemail directly on select");
            contentIntent = new Intent(mContext, CallDetailActivity.class);
            contentIntent.setData(newCalls[0].callsUri);
            contentIntent.putExtra(CallDetailActivity.EXTRA_VOICEMAIL_URI,
                    newCalls[0].voicemailUri);
        } else {
            // Open the call log.
            Log.d(TAG, "Opening call log on select");
            contentIntent = new Intent(Intent.ACTION_VIEW, Calls.CONTENT_URI);
        }
        notification.contentIntent = PendingIntent.getActivity(mContext, 0, contentIntent, 0);

        // The text to show in the ticker, describing the new event.
        if (callToNotify != null) {
            notification.tickerText = resources.getString(
                    R.string.notification_new_voicemail_ticker, names.get(callToNotify.number));
        }

        mNotificationManager.notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
    }

    /** Creates a pending intent that marks all new calls as old. */
    private PendingIntent createMarkNewCallsAsOld() {
        Intent intent = new Intent(mContext, CallLogNotificationsService.class);
        intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_CALLS_AS_OLD);
        return PendingIntent.getService(mContext, 0, intent, 0);
    }

    @Override
    public void clearNewVoicemailNotification() {
    public void clearNotification() {
        mNotificationManager.cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
    }

    /** Allows determining the number associated with a given voicemail. */
    public interface VoicemailNumberQuery {
    /** Information about a new voicemail. */
    private static final class NewCall {
        public final Uri callsUri;
        public final Uri voicemailUri;
        public final String number;

        public NewCall(Uri callsUri, Uri voicemailUri, String number) {
            this.callsUri = callsUri;
            this.voicemailUri = voicemailUri;
            this.number = number;
        }
    }

    /** Allows determining the new calls for which a notification should be generated. */
    public interface NewCallsQuery {
        /**
         * Returns the number associated with a voicemail URI, or null if the URI does not actually
         * correspond to a voicemail.
         *
         * @throws IllegalArgumentException if the given {@code uri} is not a voicemail URI.
         * Returns the new calls for which a notification should be generated.
         */
        public String query(Uri uri);
        public NewCall[] query();
    }

    /** Create a new instance of {@link VoicemailNumberQuery}. */
    public static VoicemailNumberQuery createVoicemailNumberQuery(ContentResolver contentResolver) {
        return new DefaultVoicemailNumberQuery(contentResolver);
    /** Create a new instance of {@link NewCallsQuery}. */
    public static NewCallsQuery createNewCallsQuery(ContentResolver contentResolver) {
        return new DefaultNewCallsQuery(contentResolver);
    }

    /**
     * Default implementation of {@link VoicemailNumberQuery} that looks up the number in the
     * voicemail content provider.
     * Default implementation of {@link NewCallsQuery} that looks up the list of new calls to
     * notify about in the call log.
     */
    private static final class DefaultVoicemailNumberQuery implements VoicemailNumberQuery {
        private static final String[] PROJECTION = { VoicemailContract.Voicemails.NUMBER };
        private static final int NUMBER_COLUMN_INDEX = 0;
    private static final class DefaultNewCallsQuery implements NewCallsQuery {
        private static final String[] PROJECTION = {
            Calls._ID, Calls.NUMBER, Calls.VOICEMAIL_URI
        };
        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 final ContentResolver mContentResolver;

        private DefaultVoicemailNumberQuery(ContentResolver contentResolver) {
        private DefaultNewCallsQuery(ContentResolver contentResolver) {
            mContentResolver = contentResolver;
        }

        @Override
        public String query(Uri uri) {
            validateVoicemailUri(uri);
        public NewCall[] query() {
            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(uri, PROJECTION, null, null, null);
                if (cursor.getCount() != 1) return null;
                if (!cursor.moveToFirst()) return null;
                return cursor.getString(NUMBER_COLUMN_INDEX);
            } finally {
                if (cursor != null) {
                    cursor.close();
                cursor = mContentResolver.query(Calls.CONTENT_URI_WITH_VOICEMAIL, PROJECTION,
                        selection, selectionArgs, Calls.DEFAULT_SORT_ORDER);
                NewCall[] newCalls = new NewCall[cursor.getCount()];
                while (cursor.moveToNext()) {
                    newCalls[cursor.getPosition()] = createNewCallsFromCursor(cursor);
                }
                Log.d(TAG, "DefaultNewCallsQuery: " + newCalls.length + " new calls");
                return newCalls;
            } finally {
                MoreCloseables.closeQuietly(cursor);
            }
        }

        /**
         * Makes sure that the given URI is a valid voicemail URI.
         *
         * @throws IllegalArgumentException if the URI is not valid
         */
        private void validateVoicemailUri(Uri uri) {
            // Cannot be null.
            if (uri == null) throw new IllegalArgumentException("invalid voicemail URI");
            // Must have the right schema.
            if (!VoicemailContract.Voicemails.CONTENT_URI.getScheme().equals(uri.getScheme())) {
                throw new IllegalArgumentException("invalid voicemail URI");
            }
            // Must have the right authority.
            if (!VoicemailContract.AUTHORITY.equals(uri.getAuthority())) {
                throw new IllegalArgumentException("invalid voicemail URI");
            }
            // Must have a valid path.
            if (uri.getPath() == null) {
                throw new IllegalArgumentException("invalid voicemail URI");
            }
            // Must be a path within the voicemails table.
            if (!uri.getPath().startsWith(VoicemailContract.Voicemails.CONTENT_URI.getPath())) {
                throw new IllegalArgumentException("invalid voicemail URI");
            }
        /** 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));
        }
    }

@@ -169,6 +278,10 @@ public class DefaultVoicemailNotifier implements VoicemailNotifier {
        return new DefaultNameLookupQuery(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;
@@ -195,4 +308,16 @@ public class DefaultVoicemailNotifier implements VoicemailNotifier {
            }
        }
    }

    /**
     * Create a new PhoneNumberHelper.
     * <p>
     * This will cause some Disk I/O, at least the first time it is created, so it should not be
     * called from the main thread.
     */
    public static PhoneNumberHelper createPhoneNumberHelper(Context context) {
        TelephonyManager telephonyManager =
            (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
        return new PhoneNumberHelper(context.getResources(), telephonyManager.getVoiceMailNumber());
    }
}
Loading