1/*
2 * Copyright (C) 2006 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 */
16
17package com.android.phone;
18
19import android.app.Notification;
20import android.app.NotificationManager;
21import android.app.PendingIntent;
22import android.app.StatusBarManager;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.SharedPreferences;
27import android.content.pm.UserInfo;
28import android.net.Uri;
29import android.os.SystemProperties;
30import android.os.UserHandle;
31import android.os.UserManager;
32import android.preference.PreferenceManager;
33import android.provider.ContactsContract.PhoneLookup;
34import android.provider.Settings;
35import android.telecom.PhoneAccount;
36import android.telephony.PhoneNumberUtils;
37import android.telephony.ServiceState;
38import android.text.TextUtils;
39import android.util.Log;
40import android.widget.Toast;
41
42import com.android.internal.telephony.Phone;
43import com.android.internal.telephony.PhoneBase;
44import com.android.internal.telephony.TelephonyCapabilities;
45
46import java.util.List;
47
48/**
49 * NotificationManager-related utility code for the Phone app.
50 *
51 * This is a singleton object which acts as the interface to the
52 * framework's NotificationManager, and is used to display status bar
53 * icons and control other status bar-related behavior.
54 *
55 * @see PhoneGlobals.notificationMgr
56 */
57public class NotificationMgr {
58    private static final String LOG_TAG = "NotificationMgr";
59    private static final boolean DBG =
60            (PhoneGlobals.DBG_LEVEL >= 1) && (SystemProperties.getInt("ro.debuggable", 0) == 1);
61    // Do not check in with VDBG = true, since that may write PII to the system log.
62    private static final boolean VDBG = false;
63
64    // notification types
65    static final int MMI_NOTIFICATION = 1;
66    static final int NETWORK_SELECTION_NOTIFICATION = 2;
67    static final int VOICEMAIL_NOTIFICATION = 3;
68    static final int CALL_FORWARD_NOTIFICATION = 4;
69    static final int DATA_DISCONNECTED_ROAMING_NOTIFICATION = 5;
70    static final int SELECTED_OPERATOR_FAIL_NOTIFICATION = 6;
71
72    /** The singleton NotificationMgr instance. */
73    private static NotificationMgr sInstance;
74
75    private PhoneGlobals mApp;
76    private Phone mPhone;
77
78    private Context mContext;
79    private NotificationManager mNotificationManager;
80    private StatusBarManager mStatusBarManager;
81    private UserManager mUserManager;
82    private Toast mToast;
83
84    public StatusBarHelper statusBarHelper;
85
86    // used to track the notification of selected network unavailable
87    private boolean mSelectedUnavailableNotify = false;
88
89    // Retry params for the getVoiceMailNumber() call; see updateMwi().
90    private static final int MAX_VM_NUMBER_RETRIES = 5;
91    private static final int VM_NUMBER_RETRY_DELAY_MILLIS = 10000;
92    private int mVmNumberRetriesRemaining = MAX_VM_NUMBER_RETRIES;
93
94    /**
95     * Private constructor (this is a singleton).
96     * @see #init(PhoneGlobals)
97     */
98    private NotificationMgr(PhoneGlobals app) {
99        mApp = app;
100        mContext = app;
101        mNotificationManager =
102                (NotificationManager) app.getSystemService(Context.NOTIFICATION_SERVICE);
103        mStatusBarManager =
104                (StatusBarManager) app.getSystemService(Context.STATUS_BAR_SERVICE);
105        mUserManager = (UserManager) app.getSystemService(Context.USER_SERVICE);
106        mPhone = app.phone;  // TODO: better style to use mCM.getDefaultPhone() everywhere instead
107        statusBarHelper = new StatusBarHelper();
108    }
109
110    /**
111     * Initialize the singleton NotificationMgr instance.
112     *
113     * This is only done once, at startup, from PhoneApp.onCreate().
114     * From then on, the NotificationMgr instance is available via the
115     * PhoneApp's public "notificationMgr" field, which is why there's no
116     * getInstance() method here.
117     */
118    /* package */ static NotificationMgr init(PhoneGlobals app) {
119        synchronized (NotificationMgr.class) {
120            if (sInstance == null) {
121                sInstance = new NotificationMgr(app);
122            } else {
123                Log.wtf(LOG_TAG, "init() called multiple times!  sInstance = " + sInstance);
124            }
125            return sInstance;
126        }
127    }
128
129    /**
130     * Helper class that's a wrapper around the framework's
131     * StatusBarManager.disable() API.
132     *
133     * This class is used to control features like:
134     *
135     *   - Disabling the status bar "notification windowshade"
136     *     while the in-call UI is up
137     *
138     *   - Disabling notification alerts (audible or vibrating)
139     *     while a phone call is active
140     *
141     *   - Disabling navigation via the system bar (the "soft buttons" at
142     *     the bottom of the screen on devices with no hard buttons)
143     *
144     * We control these features through a single point of control to make
145     * sure that the various StatusBarManager.disable() calls don't
146     * interfere with each other.
147     */
148    public class StatusBarHelper {
149        // Current desired state of status bar / system bar behavior
150        private boolean mIsNotificationEnabled = true;
151        private boolean mIsExpandedViewEnabled = true;
152        private boolean mIsSystemBarNavigationEnabled = true;
153
154        private StatusBarHelper () {
155        }
156
157        /**
158         * Enables or disables auditory / vibrational alerts.
159         *
160         * (We disable these any time a voice call is active, regardless
161         * of whether or not the in-call UI is visible.)
162         */
163        public void enableNotificationAlerts(boolean enable) {
164            if (mIsNotificationEnabled != enable) {
165                mIsNotificationEnabled = enable;
166                updateStatusBar();
167            }
168        }
169
170        /**
171         * Enables or disables the expanded view of the status bar
172         * (i.e. the ability to pull down the "notification windowshade").
173         *
174         * (This feature is disabled by the InCallScreen while the in-call
175         * UI is active.)
176         */
177        public void enableExpandedView(boolean enable) {
178            if (mIsExpandedViewEnabled != enable) {
179                mIsExpandedViewEnabled = enable;
180                updateStatusBar();
181            }
182        }
183
184        /**
185         * Enables or disables the navigation via the system bar (the
186         * "soft buttons" at the bottom of the screen)
187         *
188         * (This feature is disabled while an incoming call is ringing,
189         * because it's easy to accidentally touch the system bar while
190         * pulling the phone out of your pocket.)
191         */
192        public void enableSystemBarNavigation(boolean enable) {
193            if (mIsSystemBarNavigationEnabled != enable) {
194                mIsSystemBarNavigationEnabled = enable;
195                updateStatusBar();
196            }
197        }
198
199        /**
200         * Updates the status bar to reflect the current desired state.
201         */
202        private void updateStatusBar() {
203            int state = StatusBarManager.DISABLE_NONE;
204
205            if (!mIsExpandedViewEnabled) {
206                state |= StatusBarManager.DISABLE_EXPAND;
207            }
208            if (!mIsNotificationEnabled) {
209                state |= StatusBarManager.DISABLE_NOTIFICATION_ALERTS;
210            }
211            if (!mIsSystemBarNavigationEnabled) {
212                // Disable *all* possible navigation via the system bar.
213                state |= StatusBarManager.DISABLE_HOME;
214                state |= StatusBarManager.DISABLE_RECENT;
215                state |= StatusBarManager.DISABLE_BACK;
216                state |= StatusBarManager.DISABLE_SEARCH;
217            }
218
219            if (DBG) log("updateStatusBar: state = 0x" + Integer.toHexString(state));
220            mStatusBarManager.disable(state);
221        }
222    }
223
224    /** The projection to use when querying the phones table */
225    static final String[] PHONES_PROJECTION = new String[] {
226        PhoneLookup.NUMBER,
227        PhoneLookup.DISPLAY_NAME,
228        PhoneLookup._ID
229    };
230
231    /**
232     * Updates the message waiting indicator (voicemail) notification.
233     *
234     * @param visible true if there are messages waiting
235     */
236    /* package */ void updateMwi(boolean visible) {
237        if (DBG) log("updateMwi(): " + visible);
238
239        if (visible) {
240            int resId = android.R.drawable.stat_notify_voicemail;
241
242            // This Notification can get a lot fancier once we have more
243            // information about the current voicemail messages.
244            // (For example, the current voicemail system can't tell
245            // us the caller-id or timestamp of a message, or tell us the
246            // message count.)
247
248            // But for now, the UI is ultra-simple: if the MWI indication
249            // is supposed to be visible, just show a single generic
250            // notification.
251
252            String notificationTitle = mContext.getString(R.string.notification_voicemail_title);
253            String vmNumber = mPhone.getVoiceMailNumber();
254            if (DBG) log("- got vm number: '" + vmNumber + "'");
255
256            // Watch out: vmNumber may be null, for two possible reasons:
257            //
258            //   (1) This phone really has no voicemail number
259            //
260            //   (2) This phone *does* have a voicemail number, but
261            //       the SIM isn't ready yet.
262            //
263            // Case (2) *does* happen in practice if you have voicemail
264            // messages when the device first boots: we get an MWI
265            // notification as soon as we register on the network, but the
266            // SIM hasn't finished loading yet.
267            //
268            // So handle case (2) by retrying the lookup after a short
269            // delay.
270
271            if ((vmNumber == null) && !mPhone.getIccRecordsLoaded()) {
272                if (DBG) log("- Null vm number: SIM records not loaded (yet)...");
273
274                // TODO: rather than retrying after an arbitrary delay, it
275                // would be cleaner to instead just wait for a
276                // SIM_RECORDS_LOADED notification.
277                // (Unfortunately right now there's no convenient way to
278                // get that notification in phone app code.  We'd first
279                // want to add a call like registerForSimRecordsLoaded()
280                // to Phone.java and GSMPhone.java, and *then* we could
281                // listen for that in the CallNotifier class.)
282
283                // Limit the number of retries (in case the SIM is broken
284                // or missing and can *never* load successfully.)
285                if (mVmNumberRetriesRemaining-- > 0) {
286                    if (DBG) log("  - Retrying in " + VM_NUMBER_RETRY_DELAY_MILLIS + " msec...");
287                    mApp.notifier.sendMwiChangedDelayed(VM_NUMBER_RETRY_DELAY_MILLIS);
288                    return;
289                } else {
290                    Log.w(LOG_TAG, "NotificationMgr.updateMwi: getVoiceMailNumber() failed after "
291                          + MAX_VM_NUMBER_RETRIES + " retries; giving up.");
292                    // ...and continue with vmNumber==null, just as if the
293                    // phone had no VM number set up in the first place.
294                }
295            }
296
297            if (TelephonyCapabilities.supportsVoiceMessageCount(mPhone)) {
298                int vmCount = mPhone.getVoiceMessageCount();
299                String titleFormat = mContext.getString(R.string.notification_voicemail_title_count);
300                notificationTitle = String.format(titleFormat, vmCount);
301            }
302
303            String notificationText;
304            if (TextUtils.isEmpty(vmNumber)) {
305                notificationText = mContext.getString(
306                        R.string.notification_voicemail_no_vm_number);
307            } else {
308                notificationText = String.format(
309                        mContext.getString(R.string.notification_voicemail_text_format),
310                        PhoneNumberUtils.formatNumber(vmNumber));
311            }
312
313            Intent intent = new Intent(Intent.ACTION_CALL,
314                    Uri.fromParts(PhoneAccount.SCHEME_VOICEMAIL, "", null));
315            PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
316
317            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
318            Uri ringtoneUri;
319            String uriString = prefs.getString(
320                    CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_RINGTONE_KEY, null);
321            if (!TextUtils.isEmpty(uriString)) {
322                ringtoneUri = Uri.parse(uriString);
323            } else {
324                ringtoneUri = Settings.System.DEFAULT_NOTIFICATION_URI;
325            }
326
327            Notification.Builder builder = new Notification.Builder(mContext);
328            builder.setSmallIcon(resId)
329                    .setWhen(System.currentTimeMillis())
330                    .setContentTitle(notificationTitle)
331                    .setContentText(notificationText)
332                    .setContentIntent(pendingIntent)
333                    .setSound(ringtoneUri)
334                    .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
335                    .setOngoing(true);
336
337            CallFeaturesSetting.migrateVoicemailVibrationSettingsIfNeeded(prefs);
338            final boolean vibrate = prefs.getBoolean(
339                    CallFeaturesSetting.BUTTON_VOICEMAIL_NOTIFICATION_VIBRATE_KEY, false);
340            if (vibrate) {
341                builder.setDefaults(Notification.DEFAULT_VIBRATE);
342            }
343
344            final Notification notification = builder.build();
345            List<UserInfo> users = mUserManager.getUsers(true);
346            for (int i = 0; i < users.size(); i++) {
347                final UserInfo user = users.get(i);
348                final UserHandle userHandle = user.getUserHandle();
349                if (!mUserManager.hasUserRestriction(
350                        UserManager.DISALLOW_OUTGOING_CALLS, userHandle)
351                            && !user.isManagedProfile()) {
352                    mNotificationManager.notifyAsUser(
353                            null /* tag */, VOICEMAIL_NOTIFICATION, notification, userHandle);
354                }
355            }
356        } else {
357            mNotificationManager.cancelAsUser(
358                    null /* tag */, VOICEMAIL_NOTIFICATION, UserHandle.ALL);
359        }
360    }
361
362    /**
363     * Updates the message call forwarding indicator notification.
364     *
365     * @param visible true if there are messages waiting
366     */
367    /* package */ void updateCfi(boolean visible) {
368        if (DBG) log("updateCfi(): " + visible);
369        if (visible) {
370            // If Unconditional Call Forwarding (forward all calls) for VOICE
371            // is enabled, just show a notification.  We'll default to expanded
372            // view for now, so the there is less confusion about the icon.  If
373            // it is deemed too weird to have CF indications as expanded views,
374            // then we'll flip the flag back.
375
376            // TODO: We may want to take a look to see if the notification can
377            // display the target to forward calls to.  This will require some
378            // effort though, since there are multiple layers of messages that
379            // will need to propagate that information.
380
381            Notification.Builder builder = new Notification.Builder(mContext)
382                    .setSmallIcon(R.drawable.stat_sys_phone_call_forward)
383                    .setContentTitle(mContext.getString(R.string.labelCF))
384                    .setContentText(mContext.getString(R.string.sum_cfu_enabled_indicator))
385                    .setShowWhen(false)
386                    .setOngoing(true);
387
388            Intent intent = new Intent(Intent.ACTION_MAIN);
389            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
390            intent.setClassName("com.android.phone", "com.android.phone.CallFeaturesSetting");
391            PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
392
393            List<UserInfo> users = mUserManager.getUsers(true);
394            for (int i = 0; i < users.size(); i++) {
395                UserHandle userHandle = users.get(i).getUserHandle();
396                builder.setContentIntent(userHandle.isOwner() ? contentIntent : null);
397                    mNotificationManager.notifyAsUser(
398                            null /* tag */, CALL_FORWARD_NOTIFICATION, builder.build(), userHandle);
399            }
400        } else {
401            mNotificationManager.cancelAsUser(
402                    null /* tag */, CALL_FORWARD_NOTIFICATION, UserHandle.ALL);
403        }
404    }
405
406    /**
407     * Shows the "data disconnected due to roaming" notification, which
408     * appears when you lose data connectivity because you're roaming and
409     * you have the "data roaming" feature turned off.
410     */
411    /* package */ void showDataDisconnectedRoaming() {
412        if (DBG) log("showDataDisconnectedRoaming()...");
413
414        // "Mobile network settings" screen / dialog
415        Intent intent = new Intent(mContext, com.android.phone.MobileNetworkSettings.class);
416        PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
417
418        final CharSequence contentText = mContext.getText(R.string.roaming_reenable_message);
419
420        final Notification.Builder builder = new Notification.Builder(mContext)
421                .setSmallIcon(android.R.drawable.stat_sys_warning)
422                .setContentTitle(mContext.getText(R.string.roaming))
423                .setColor(mContext.getResources().getColor(R.color.dialer_theme_color))
424                .setContentText(contentText);
425
426        List<UserInfo> users = mUserManager.getUsers(true);
427        for (int i = 0; i < users.size(); i++) {
428            UserHandle userHandle = users.get(i).getUserHandle();
429            builder.setContentIntent(userHandle.isOwner() ? contentIntent : null);
430            final Notification notif =
431                    new Notification.BigTextStyle(builder).bigText(contentText).build();
432            mNotificationManager.notifyAsUser(
433                    null /* tag */, DATA_DISCONNECTED_ROAMING_NOTIFICATION, notif, userHandle);
434        }
435    }
436
437    /**
438     * Turns off the "data disconnected due to roaming" notification.
439     */
440    /* package */ void hideDataDisconnectedRoaming() {
441        if (DBG) log("hideDataDisconnectedRoaming()...");
442        mNotificationManager.cancel(DATA_DISCONNECTED_ROAMING_NOTIFICATION);
443    }
444
445    /**
446     * Display the network selection "no service" notification
447     * @param operator is the numeric operator number
448     */
449    private void showNetworkSelection(String operator) {
450        if (DBG) log("showNetworkSelection(" + operator + ")...");
451
452        Notification.Builder builder = new Notification.Builder(mContext)
453                .setSmallIcon(android.R.drawable.stat_sys_warning)
454                .setContentTitle(mContext.getString(R.string.notification_network_selection_title))
455                .setContentText(
456                        mContext.getString(R.string.notification_network_selection_text, operator))
457                .setShowWhen(false)
458                .setOngoing(true);
459
460        // create the target network operators settings intent
461        Intent intent = new Intent(Intent.ACTION_MAIN);
462        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
463                Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
464        // Use NetworkSetting to handle the selection intent
465        intent.setComponent(new ComponentName("com.android.phone",
466                "com.android.phone.NetworkSetting"));
467        PendingIntent contentIntent = PendingIntent.getActivity(mContext, 0, intent, 0);
468
469        List<UserInfo> users = mUserManager.getUsers(true);
470        for (int i = 0; i < users.size(); i++) {
471            UserHandle userHandle = users.get(i).getUserHandle();
472            builder.setContentIntent(userHandle.isOwner() ? contentIntent : null);
473            mNotificationManager.notifyAsUser(
474                    null /* tag */,
475                    SELECTED_OPERATOR_FAIL_NOTIFICATION,
476                    builder.build(),
477                    userHandle);
478        }
479    }
480
481    /**
482     * Turn off the network selection "no service" notification
483     */
484    private void cancelNetworkSelection() {
485        if (DBG) log("cancelNetworkSelection()...");
486        mNotificationManager.cancelAsUser(
487                null /* tag */, SELECTED_OPERATOR_FAIL_NOTIFICATION, UserHandle.ALL);
488    }
489
490    /**
491     * Update notification about no service of user selected operator
492     *
493     * @param serviceState Phone service state
494     */
495    void updateNetworkSelection(int serviceState) {
496        if (TelephonyCapabilities.supportsNetworkSelection(mPhone)) {
497            // get the shared preference of network_selection.
498            // empty is auto mode, otherwise it is the operator alpha name
499            // in case there is no operator name, check the operator numeric
500            SharedPreferences sp =
501                    PreferenceManager.getDefaultSharedPreferences(mContext);
502            String networkSelection =
503                    sp.getString(PhoneBase.NETWORK_SELECTION_NAME_KEY, "");
504            if (TextUtils.isEmpty(networkSelection)) {
505                networkSelection =
506                        sp.getString(PhoneBase.NETWORK_SELECTION_KEY, "");
507            }
508
509            if (DBG) log("updateNetworkSelection()..." + "state = " +
510                    serviceState + " new network " + networkSelection);
511
512            if (serviceState == ServiceState.STATE_OUT_OF_SERVICE
513                    && !TextUtils.isEmpty(networkSelection)) {
514                if (!mSelectedUnavailableNotify) {
515                    showNetworkSelection(networkSelection);
516                    mSelectedUnavailableNotify = true;
517                }
518            } else {
519                if (mSelectedUnavailableNotify) {
520                    cancelNetworkSelection();
521                    mSelectedUnavailableNotify = false;
522                }
523            }
524        }
525    }
526
527    /* package */ void postTransientNotification(int notifyId, CharSequence msg) {
528        if (mToast != null) {
529            mToast.cancel();
530        }
531
532        mToast = Toast.makeText(mContext, msg, Toast.LENGTH_LONG);
533        mToast.show();
534    }
535
536    private void log(String msg) {
537        Log.d(LOG_TAG, msg);
538    }
539}
540