1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package com.android.dialer.calllog;
17
18import android.app.Notification;
19import android.app.NotificationManager;
20import android.app.PendingIntent;
21import android.content.ContentValues;
22import android.content.Context;
23import android.content.Intent;
24import android.graphics.Bitmap;
25import android.os.AsyncTask;
26import android.provider.CallLog.Calls;
27import android.text.TextUtils;
28import android.util.Log;
29
30import com.android.contacts.common.ContactsUtils;
31import com.android.contacts.common.util.PhoneNumberHelper;
32import com.android.dialer.DialtactsActivity;
33import com.android.dialer.R;
34import com.android.dialer.calllog.CallLogNotificationsHelper.NewCall;
35import com.android.dialer.contactinfo.ContactPhotoLoader;
36import com.android.dialer.compat.UserManagerCompat;
37import com.android.dialer.list.ListsFragment;
38import com.android.dialer.util.DialerUtils;
39import com.android.dialer.util.IntentUtil;
40import com.android.dialer.util.IntentUtil.CallIntentBuilder;
41
42import java.util.List;
43
44/**
45 * Creates a notification for calls that the user missed (neither answered nor rejected).
46 *
47 */
48public class MissedCallNotifier {
49    public static final String TAG = "MissedCallNotifier";
50
51    /** The tag used to identify notifications from this class. */
52    private static final String NOTIFICATION_TAG = "MissedCallNotifier";
53    /** The identifier of the notification of new missed calls. */
54    private static final int NOTIFICATION_ID = 1;
55    /** Preference file key for number of missed calls. */
56    private static final String MISSED_CALL_COUNT = "missed_call_count";
57
58    private static MissedCallNotifier sInstance;
59    private Context mContext;
60
61    /** Returns the singleton instance of the {@link MissedCallNotifier}. */
62    public static MissedCallNotifier getInstance(Context context) {
63        if (sInstance == null) {
64            sInstance = new MissedCallNotifier(context);
65        }
66        return sInstance;
67    }
68
69    private MissedCallNotifier(Context context) {
70        mContext = context;
71    }
72
73    public void updateMissedCallNotification(int count, String number) {
74        final int titleResId;
75        final String expandedText;  // The text in the notification's line 1 and 2.
76
77        final List<NewCall> newCalls =
78                CallLogNotificationsHelper.getInstance(mContext).getNewMissedCalls();
79
80        if (count == CallLogNotificationsService.UNKNOWN_MISSED_CALL_COUNT) {
81            if (newCalls == null) {
82                // If the intent did not contain a count, and we are unable to get a count from the
83                // call log, then no notification can be shown.
84                return;
85            }
86            count = newCalls.size();
87        }
88
89        if (count == 0) {
90            // No voicemails to notify about: clear the notification.
91            clearMissedCalls();
92            return;
93        }
94
95        // The call log has been updated, use that information preferentially.
96        boolean useCallLog = newCalls != null && newCalls.size() == count;
97        NewCall newestCall = useCallLog ? newCalls.get(0) : null;
98        long timeMs = useCallLog ? newestCall.dateMs : System.currentTimeMillis();
99
100        Notification.Builder builder = new Notification.Builder(mContext);
101        // Display the first line of the notification:
102        // 1 missed call: <caller name || handle>
103        // More than 1 missed call: <number of calls> + "missed calls"
104        if (count == 1) {
105            //TODO: look up caller ID that is not in contacts.
106            ContactInfo contactInfo = CallLogNotificationsHelper.getInstance(mContext)
107                    .getContactInfo(useCallLog ? newestCall.number : number,
108                            useCallLog ? newestCall.numberPresentation
109                                    : Calls.PRESENTATION_ALLOWED,
110                            useCallLog ? newestCall.countryIso : null);
111
112            titleResId = contactInfo.userType == ContactsUtils.USER_TYPE_WORK
113                    ? R.string.notification_missedWorkCallTitle
114                    : R.string.notification_missedCallTitle;
115
116            expandedText = contactInfo.name;
117            ContactPhotoLoader loader = new ContactPhotoLoader(mContext, contactInfo);
118            Bitmap photoIcon = loader.loadPhotoIcon();
119            if (photoIcon != null) {
120                builder.setLargeIcon(photoIcon);
121            }
122        } else {
123            titleResId = R.string.notification_missedCallsTitle;
124            expandedText =
125                    mContext.getString(R.string.notification_missedCallsMsg, count);
126        }
127
128        // Create a public viewable version of the notification, suitable for display when sensitive
129        // notification content is hidden.
130        Notification.Builder publicBuilder = new Notification.Builder(mContext);
131        publicBuilder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
132                .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
133                // Show "Phone" for notification title.
134                .setContentTitle(mContext.getText(R.string.userCallActivityLabel))
135                // Notification details shows that there are missed call(s), but does not reveal
136                // the missed caller information.
137                .setContentText(mContext.getText(titleResId))
138                .setContentIntent(createCallLogPendingIntent())
139                .setAutoCancel(true)
140                .setWhen(timeMs)
141                .setDeleteIntent(createClearMissedCallsPendingIntent());
142
143        // Create the notification suitable for display when sensitive information is showing.
144        builder.setSmallIcon(android.R.drawable.stat_notify_missed_call)
145                .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
146                .setContentTitle(mContext.getText(titleResId))
147                .setContentText(expandedText)
148                .setContentIntent(createCallLogPendingIntent())
149                .setAutoCancel(true)
150                .setWhen(timeMs)
151                .setDeleteIntent(createClearMissedCallsPendingIntent())
152                // Include a public version of the notification to be shown when the missed call
153                // notification is shown on the user's lock screen and they have chosen to hide
154                // sensitive notification information.
155                .setPublicVersion(publicBuilder.build());
156
157        // Add additional actions when there is only 1 missed call and the user isn't locked
158        if (UserManagerCompat.isUserUnlocked(mContext) && count == 1) {
159            if (!TextUtils.isEmpty(number)
160                    && !TextUtils.equals(
161                    number, mContext.getString(R.string.handle_restricted))) {
162                builder.addAction(R.drawable.ic_phone_24dp,
163                        mContext.getString(R.string.notification_missedCall_call_back),
164                        createCallBackPendingIntent(number));
165
166                if (!PhoneNumberHelper.isUriNumber(number)) {
167                    builder.addAction(R.drawable.ic_message_24dp,
168                            mContext.getString(R.string.notification_missedCall_message),
169                            createSendSmsFromNotificationPendingIntent(number));
170                }
171            }
172        }
173
174        Notification notification = builder.build();
175        configureLedOnNotification(notification);
176
177        Log.i(TAG, "Adding missed call notification.");
178        getNotificationMgr().notify(NOTIFICATION_TAG, NOTIFICATION_ID, notification);
179    }
180
181    private void clearMissedCalls() {
182        AsyncTask.execute(new Runnable() {
183            @Override
184            public void run() {
185                // Call log is only accessible when unlocked. If that's the case, clear the list of
186                // new missed calls from the call log.
187                if (UserManagerCompat.isUserUnlocked(mContext)) {
188                    ContentValues values = new ContentValues();
189                    values.put(Calls.NEW, 0);
190                    values.put(Calls.IS_READ, 1);
191                    StringBuilder where = new StringBuilder();
192                    where.append(Calls.NEW);
193                    where.append(" = 1 AND ");
194                    where.append(Calls.TYPE);
195                    where.append(" = ?");
196                    try {
197                        mContext.getContentResolver().update(Calls.CONTENT_URI, values,
198                                where.toString(), new String[]{Integer.toString(Calls.
199                                        MISSED_TYPE)});
200                    } catch (IllegalArgumentException e) {
201                        Log.w(TAG, "ContactsProvider update command failed", e);
202                    }
203                }
204                getNotificationMgr().cancel(NOTIFICATION_TAG, NOTIFICATION_ID);
205            }
206        });
207    }
208
209    /**
210     * Trigger an intent to make a call from a missed call number.
211     */
212    public void callBackFromMissedCall(String number) {
213        closeSystemDialogs(mContext);
214        CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
215        DialerUtils.startActivityWithErrorToast(
216                mContext,
217                new CallIntentBuilder(number)
218                        .build()
219                        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
220    }
221
222    /**
223     * Trigger an intent to send an sms from a missed call number.
224     */
225    public void sendSmsFromMissedCall(String number) {
226        closeSystemDialogs(mContext);
227        CallLogNotificationsHelper.removeMissedCallNotifications(mContext);
228        DialerUtils.startActivityWithErrorToast(
229                mContext,
230                IntentUtil.getSendSmsIntent(number).setFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
231    }
232
233    /**
234     * Creates a new pending intent that sends the user to the call log.
235     *
236     * @return The pending intent.
237     */
238    private PendingIntent createCallLogPendingIntent() {
239        Intent contentIntent = new Intent(mContext, DialtactsActivity.class);
240        contentIntent.putExtra(DialtactsActivity.EXTRA_SHOW_TAB, ListsFragment.TAB_INDEX_HISTORY);
241        return PendingIntent.getActivity(
242                mContext, 0, contentIntent,PendingIntent.FLAG_UPDATE_CURRENT);
243    }
244
245    /** Creates a pending intent that marks all new missed calls as old. */
246    private PendingIntent createClearMissedCallsPendingIntent() {
247        Intent intent = new Intent(mContext, CallLogNotificationsService.class);
248        intent.setAction(CallLogNotificationsService.ACTION_MARK_NEW_MISSED_CALLS_AS_OLD);
249        return PendingIntent.getService(mContext, 0, intent, 0);
250    }
251
252    private PendingIntent createCallBackPendingIntent(String number) {
253        Intent intent = new Intent(mContext, CallLogNotificationsService.class);
254        intent.setAction(
255                CallLogNotificationsService.ACTION_CALL_BACK_FROM_MISSED_CALL_NOTIFICATION);
256        intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
257        // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
258        // extra.
259        return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
260    }
261
262    private PendingIntent createSendSmsFromNotificationPendingIntent(String number) {
263        Intent intent = new Intent(mContext, CallLogNotificationsService.class);
264        intent.setAction(
265                CallLogNotificationsService.ACTION_SEND_SMS_FROM_MISSED_CALL_NOTIFICATION);
266        intent.putExtra(CallLogNotificationsService.EXTRA_MISSED_CALL_NUMBER, number);
267        // Use FLAG_UPDATE_CURRENT to make sure any previous pending intent is updated with the new
268        // extra.
269        return PendingIntent.getService(mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
270    }
271
272    /**
273     * Configures a notification to emit the blinky notification light.
274     */
275    private void configureLedOnNotification(Notification notification) {
276        notification.flags |= Notification.FLAG_SHOW_LIGHTS;
277        notification.defaults |= Notification.DEFAULT_LIGHTS;
278    }
279
280    /**
281     * Closes open system dialogs and the notification shade.
282     */
283    private void closeSystemDialogs(Context context) {
284        context.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
285    }
286
287    private NotificationManager getNotificationMgr() {
288        return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
289    }
290}
291