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

Commit bdb9b8f0 authored by pkanwar's avatar pkanwar
Browse files

Show notification when VoWiFi is active, and user is out of service for

voice.

Bug: 62618428
Test: manual

Change-Id: Ib2f22ef8867cd4b999d13f81247e855b13c595af
(cherry picked from commit c5f8903840b03186b4425ab3b9869257045da340)
parent 738cc425
Loading
Loading
Loading
Loading
+283 −75
Original line number Diff line number Diff line
@@ -16,9 +16,9 @@

package com.android.internal.telephony;

import android.app.PendingIntent;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -31,8 +31,13 @@ import android.telephony.CarrierConfigManager;
import android.telephony.Rlog;
import android.telephony.ServiceState;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.util.NotificationChannelController;

import java.util.HashMap;
import java.util.Map;


/**
 * This contains Carrier specific logic based on the states/events
 * managed in ServiceStateTracker.
@@ -45,19 +50,28 @@ public class CarrierServiceStateTracker extends Handler {
    protected static final int CARRIER_EVENT_VOICE_DEREGISTRATION = CARRIER_EVENT_BASE + 2;
    protected static final int CARRIER_EVENT_DATA_REGISTRATION = CARRIER_EVENT_BASE + 3;
    protected static final int CARRIER_EVENT_DATA_DEREGISTRATION = CARRIER_EVENT_BASE + 4;
    private static final int SHOW_NOTIFICATION = 200;
    private static final int NOTIFICATION_ID = 1000;
    private static final int UNINITIALIZED_DELAY_VALUE = -1;
    private int mDelay = UNINITIALIZED_DELAY_VALUE;
    private Phone mPhone;
    private boolean mIsPhoneRegistered = false;
    private ServiceStateTracker mSST;

    public static final int NOTIFICATION_PREF_NETWORK = 1000;
    public static final int NOTIFICATION_EMERGENCY_NETWORK = 1001;

    private final Map<Integer, NotificationType> mNotificationTypeMap = new HashMap<>();

    public CarrierServiceStateTracker(Phone phone, ServiceStateTracker sst) {
        this.mPhone = phone;
        this.mSST = sst;
        phone.getContext().registerReceiver(mBroadcastReceiver, new IntentFilter(
                CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
        registerNotificationTypes();
    }

    private void registerNotificationTypes() {
        mNotificationTypeMap.put(NOTIFICATION_PREF_NETWORK,
                new PrefNetworkNotification(NOTIFICATION_PREF_NETWORK));
        mNotificationTypeMap.put(NOTIFICATION_EMERGENCY_NETWORK,
                new EmergencyNetworkNotification(NOTIFICATION_EMERGENCY_NETWORK));
    }

    @Override
@@ -65,19 +79,22 @@ public class CarrierServiceStateTracker extends Handler {
        switch (msg.what) {
            case CARRIER_EVENT_VOICE_REGISTRATION:
            case CARRIER_EVENT_DATA_REGISTRATION:
                mIsPhoneRegistered = true;
                handleConfigChanges();
                break;
            case CARRIER_EVENT_VOICE_DEREGISTRATION:
            case CARRIER_EVENT_DATA_DEREGISTRATION:
                if (isGlobalModeOrRadioOffOrAirplaneMode() || isPhoneStillRegistered()) {
                if (isRadioOffOrAirplaneMode()) {
                    break;
                }
                mIsPhoneRegistered = false;
                handleConfigChanges();
                break;
            case SHOW_NOTIFICATION:
                sendNotification();
            case NOTIFICATION_EMERGENCY_NETWORK:
            case NOTIFICATION_PREF_NETWORK:
                Rlog.d(LOG_TAG, "sending notification after delay: " + msg.what);
                NotificationType notificationType = mNotificationTypeMap.get(msg.what);
                if (notificationType != null) {
                    sendNotification(notificationType);
                }
                break;
        }
    }
@@ -90,48 +107,97 @@ public class CarrierServiceStateTracker extends Handler {
                || mSST.mSS.getDataRegState() == ServiceState.STATE_IN_SERVICE);
    }

    private boolean isPhoneVoiceRegistered() {
        if (mSST.mSS == null) {
            return true; //something has gone wrong, return true and not show the notification.
        }
        return (mSST.mSS.getVoiceRegState() == ServiceState.STATE_IN_SERVICE);
    }

    private boolean isPhoneRegisteredForWifiCalling() {
        Rlog.d(LOG_TAG, "isPhoneRegisteredForWifiCalling: " + mPhone.isWifiCallingEnabled());
        return mPhone.isWifiCallingEnabled();
    }

    /**
     * Returns true if the preferred network is set to 'Global' or the radio is off or in
     * Airplane Mode else returns false.
     * Returns true if the radio is off or in Airplane Mode else returns false.
     */
    private boolean isGlobalModeOrRadioOffOrAirplaneMode() {
    @VisibleForTesting
    public boolean isRadioOffOrAirplaneMode() {
        Context context = mPhone.getContext();
        int preferredNetworkSetting = -1;
        int airplaneMode = -1;
        int subId = mPhone.getSubId();
        try {
            preferredNetworkSetting =
                    android.provider.Settings.Global.getInt(context.getContentResolver(),
                            android.provider.Settings.Global.PREFERRED_NETWORK_MODE + subId,
                            Phone.PREFERRED_NT_MODE);
            airplaneMode = Settings.Global.getInt(context.getContentResolver(),
                    Settings.Global.AIRPLANE_MODE_ON, 0);
        } catch (Exception e) {
            Rlog.e(LOG_TAG, "Unable to get PREFERRED_NETWORK_MODE.");
            Rlog.e(LOG_TAG, "Unable to get AIRPLACE_MODE_ON.");
            return true;
        }
        return ((preferredNetworkSetting == RILConstants.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA) ||
                !mSST.isRadioOn() || (airplaneMode != 0));
        return (!mSST.isRadioOn() || (airplaneMode != 0));
    }

    /**
     * Contains logic to decide when to create/cancel notifications.
     * Returns true if the preferred network is set to 'Global'.
     */
    private void handleConfigChanges() {
        if (mDelay == UNINITIALIZED_DELAY_VALUE) {
            cancelNotification();
            return;
    private boolean isGlobalMode() {
        Context context = mPhone.getContext();
        int preferredNetworkSetting = -1;
        try {
            preferredNetworkSetting =
                    android.provider.Settings.Global.getInt(context.getContentResolver(),
                            android.provider.Settings.Global.PREFERRED_NETWORK_MODE
                                    + mPhone.getSubId(), Phone.PREFERRED_NT_MODE);
        } catch (Exception e) {
            Rlog.e(LOG_TAG, "Unable to get PREFERRED_NETWORK_MODE.");
            return true;
        }
        return (preferredNetworkSetting == RILConstants.NETWORK_MODE_LTE_CDMA_EVDO_GSM_WCDMA);
    }
        // send a notification if the device is registerd to a network.
        if (mIsPhoneRegistered) {
            cancelNotification();
            Rlog.i(LOG_TAG, "canceling all notifications. ");

    private void handleConfigChanges() {
        for (Map.Entry<Integer, NotificationType> entry : mNotificationTypeMap.entrySet()) {
            NotificationType notificationType = entry.getValue();
            if (evaluateSendingMessage(notificationType)) {
                Message notificationMsg = obtainMessage(notificationType.getTypeId(), null);
                Rlog.i(LOG_TAG, "starting timer for notifications." + notificationType.getTypeId());
                sendMessageDelayed(notificationMsg, getDelay(notificationType));
            } else {
            Message notificationMsg;
            notificationMsg = obtainMessage(SHOW_NOTIFICATION, null);
            Rlog.i(LOG_TAG, "starting timer for notifications. ");
            sendMessageDelayed(notificationMsg, mDelay);
                cancelNotification(notificationType.getTypeId());
                Rlog.i(LOG_TAG, "canceling notifications: " + notificationType.getTypeId());
            }
        }
    }

    /**
     * This method adds a level of indirection, and was created so we can unit the class.
     **/
    @VisibleForTesting
    public boolean evaluateSendingMessage(NotificationType notificationType) {
        return notificationType.sendMessage();
    }

    /**
     * This method adds a level of indirection, and was created so we can unit the class.
     **/
    @VisibleForTesting
    public int getDelay(NotificationType notificationType) {
        return notificationType.getDelay();
    }

    /**
     * This method adds a level of indirection, and was created so we can unit the class.
     **/
    @VisibleForTesting
    public Notification.Builder getNotificationBuilder(NotificationType notificationType) {
        return notificationType.getNotificationBuilder();
    }

    /**
     * This method adds a level of indirection, and was created so we can unit the class.
     **/
    @VisibleForTesting
    public NotificationManager getNotificationManager(Context context) {
        return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
    }

    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -140,8 +206,11 @@ public class CarrierServiceStateTracker extends Handler {
            CarrierConfigManager carrierConfigManager = (CarrierConfigManager)
                    context.getSystemService(Context.CARRIER_CONFIG_SERVICE);
            PersistableBundle b = carrierConfigManager.getConfigForSubId(mPhone.getSubId());
            mDelay = b.getInt(CarrierConfigManager.KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT);
            Rlog.i(LOG_TAG, "reading time to delay notification: " + mDelay);

            for (Map.Entry<Integer, NotificationType> entry : mNotificationTypeMap.entrySet()) {
                NotificationType notificationType = entry.getValue();
                notificationType.setDelay(b);
            }
            handleConfigChanges();
        }
    };
@@ -149,56 +218,195 @@ public class CarrierServiceStateTracker extends Handler {
    /**
     * Post a notification to the NotificationManager for changing network type.
     */
    private void sendNotification() {
    @VisibleForTesting
    public void sendNotification(NotificationType notificationType) {
        if (!evaluateSendingMessage(notificationType)) {
            return;
        }

        Context context = mPhone.getContext();
        Notification.Builder builder = getNotificationBuilder(notificationType);
        // set some common attributes
        builder.setWhen(System.currentTimeMillis())
                .setAutoCancel(true)
                .setSmallIcon(com.android.internal.R.drawable.stat_sys_warning)
                .setColor(context.getResources().getColor(
                       com.android.internal.R.color.system_notification_accent_color));

        Rlog.i(LOG_TAG, "w/values: " + "," + mIsPhoneRegistered + "," + mDelay
                + "," + isGlobalModeOrRadioOffOrAirplaneMode() + "," + mSST.isRadioOn());
        getNotificationManager(context).notify(notificationType.getTypeId(), builder.build());
    }

        // exit if the network preference is set to Global or if the phone is registered.
        if (isGlobalModeOrRadioOffOrAirplaneMode() || mIsPhoneRegistered) {
    /**
     * Cancel notifications if a registration is pending or has been sent.
     **/
    public void cancelNotification(int notificationId) {
        Context context = mPhone.getContext();
        removeMessages(notificationId);
        getNotificationManager(context).cancel(notificationId);
    }

    /**
     * Class that defines the different types of notifications.
     */
    public interface NotificationType {

        /**
         * decides if the message should be sent, Returns boolean
         **/
        boolean sendMessage();

        /**
         * returns the interval by which the message is delayed.
         **/
        int getDelay();

        /** sets the interval by which the message is delayed.
         * @param bundle PersistableBundle
        **/
        void setDelay(PersistableBundle bundle);

        /**
         * returns notification type id.
         **/
        int getTypeId();

        /**
         * returns the notification builder, for the notification to be displayed.
         **/
        Notification.Builder getNotificationBuilder();
    }

    /**
     * Class that defines the network notification, which is shown when the phone cannot camp on
     * a network, and has 'preferred mode' set to global.
     */
    public class PrefNetworkNotification implements NotificationType {

        private final int mTypeId;
        private int mDelay = UNINITIALIZED_DELAY_VALUE;

        PrefNetworkNotification(int typeId) {
            this.mTypeId = typeId;
        }

        /** sets the interval by which the message is delayed.
         * @param bundle PersistableBundle
         **/
        public void setDelay(PersistableBundle bundle) {
            if (bundle == null) {
                Rlog.e(LOG_TAG, "bundle is null");
                return;
            }
            this.mDelay = bundle.getInt(
                    CarrierConfigManager.KEY_PREF_NETWORK_NOTIFICATION_DELAY_INT);
            Rlog.i(LOG_TAG, "reading time to delay notification emergency: " + mDelay);
        }

        NotificationManager notificationManager = (NotificationManager)
                context.getSystemService(Context.NOTIFICATION_SERVICE);
        public int getDelay() {
            return mDelay;
        }

        public int getTypeId() {
            return mTypeId;
        }

        /**
         * Contains logic on sending notifications.
         */
        public boolean sendMessage() {
            Rlog.i(LOG_TAG, "PrefNetworkNotification: sendMessage() w/values: "
                    + "," + isPhoneStillRegistered() + "," + mDelay + "," + isGlobalMode()
                    + "," + mSST.isRadioOn());
            if (mDelay == UNINITIALIZED_DELAY_VALUE ||  isPhoneStillRegistered()
                    || isGlobalMode()) {
                return false;
            }
            return true;
        }

        /**
         * Builds a partial notificaiton builder, and returns it.
         */
        public Notification.Builder getNotificationBuilder() {
            Context context = mPhone.getContext();
            Intent notificationIntent = new Intent(Settings.ACTION_DATA_ROAMING_SETTINGS);
            PendingIntent settingsIntent = PendingIntent.getActivity(context, 0, notificationIntent,
                    PendingIntent.FLAG_ONE_SHOT);

        CharSequence title =
                context.getText(com.android.internal.R.string.NetworkPreferenceSwitchTitle);
        CharSequence details =
                context.getText(com.android.internal.R.string.NetworkPreferenceSwitchSummary);


        Notification mNotification = new Notification.Builder(context)
                .setWhen(System.currentTimeMillis())
                .setAutoCancel(true)
                .setSmallIcon(com.android.internal.R.drawable.stat_sys_warning)
            CharSequence title = context.getText(
                    com.android.internal.R.string.NetworkPreferenceSwitchTitle);
            CharSequence details = context.getText(
                    com.android.internal.R.string.NetworkPreferenceSwitchSummary);
            return new Notification.Builder(context)
                    .setContentTitle(title)
                .setColor(context.getResources().getColor(
                        com.android.internal.R.color.system_notification_accent_color))
                    .setStyle(new Notification.BigTextStyle().bigText(details))
                    .setContentText(details)
                .setContentIntent(settingsIntent)
                    .setChannel(NotificationChannelController.CHANNEL_ID_ALERT)
                .build();
                    .setContentIntent(settingsIntent);
        }
    }

    /**
     * Class that defines the emergency notification, which is shown when the user is out of cell
     * connectivity, but has wifi enabled.
     */
    public class EmergencyNetworkNotification implements NotificationType {

        private final int mTypeId;
        private int mDelay = UNINITIALIZED_DELAY_VALUE;

        notificationManager.notify(NOTIFICATION_ID, mNotification);
        EmergencyNetworkNotification(int typeId) {
            this.mTypeId = typeId;
        }

        /** sets the interval by which the message is delayed.
         * @param bundle PersistableBundle
         **/
        public void setDelay(PersistableBundle bundle) {
            if (bundle == null) {
                Rlog.e(LOG_TAG, "bundle is null");
                return;
            }
            this.mDelay = bundle.getInt(
                    CarrierConfigManager.KEY_EMERGENCY_NOTIFICATION_DELAY_INT);
            Rlog.i(LOG_TAG, "reading time to delay notification emergency: " + mDelay);
        }

        public int getDelay() {
            return mDelay;
        }

        public int getTypeId() {
            return mTypeId;
        }

        /**
     * Cancel notifications if a registration is pending or has been sent.
         * Contains logic on sending notifications,
         */
    private void cancelNotification() {
        public boolean sendMessage() {
            Rlog.i(LOG_TAG, "EmergencyNetworkNotification: sendMessage() w/values: "
                    + "," + isPhoneVoiceRegistered() + "," + mDelay + ","
                    + isPhoneRegisteredForWifiCalling() + "," + mSST.isRadioOn());
            if (mDelay == UNINITIALIZED_DELAY_VALUE || isPhoneVoiceRegistered()
                    || !isPhoneRegisteredForWifiCalling()) {
                return false;
            }
            return true;
        }

        /**
         * Builds a partial notificaiton builder, and returns it.
         */
        public Notification.Builder getNotificationBuilder() {
            Context context = mPhone.getContext();
        mIsPhoneRegistered = true;
        removeMessages(SHOW_NOTIFICATION);
        NotificationManager notificationManager = (NotificationManager)
                context.getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.cancel(NOTIFICATION_ID);
            CharSequence title = context.getText(
                    com.android.internal.R.string.EmergencyCallWarningTitle);
            CharSequence details = context.getText(
                    com.android.internal.R.string.EmergencyCallWarningSummary);
            return new Notification.Builder(context)
                    .setContentTitle(title)
                    .setStyle(new Notification.BigTextStyle().bigText(details))
                    .setContentText(details)
                    .setChannel(NotificationChannelController.CHANNEL_ID_WFC);
        }
    }
}
+128 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 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.internal.telephony;

import android.app.Notification;
import android.app.NotificationManager;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.res.Resources;
import android.os.HandlerThread;
import android.os.Message;
import android.test.suitebuilder.annotation.SmallTest;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import static org.mockito.Mockito.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.eq;
import static org.mockito.Mockito.isA;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

/**
 * Unit tests for {@link com.android.internal.telephony.CarrierServiceStateTracker}.
 */
@SmallTest
public class CarrierServiceStateTrackerTest extends TelephonyTest {
    public static final String LOG_TAG = "CSST";
    public static final int TEST_TIMEOUT = 5000;

    private CarrierServiceStateTracker mCarrierSST;
    private CarrierServiceStateTrackerTestHandler mCarrierServiceStateTrackerTestHandler;
    private  CarrierServiceStateTracker.PrefNetworkNotification mPrefNetworkNotification;
    private  CarrierServiceStateTracker.EmergencyNetworkNotification mEmergencyNetworkNotification;

    @Mock Context mContext;
    @Mock ServiceStateTracker mServiceStateTracker;
    @Mock NotificationManager mNotificationManager;
    @Mock Resources mResources;

    private class CarrierServiceStateTrackerTestHandler extends HandlerThread {

        private CarrierServiceStateTrackerTestHandler(String name) {
            super(name);
        }

        @Override
        public void onLooperPrepared() {
            mCarrierSST = spy(new CarrierServiceStateTracker(mPhone, mServiceStateTracker));
            setReady(true);
        }
    }

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
        logd(LOG_TAG + "Setup!");
        super.setUp(getClass().getSimpleName());
        mCarrierServiceStateTrackerTestHandler =
                new CarrierServiceStateTrackerTestHandler(getClass().getSimpleName());
        mCarrierServiceStateTrackerTestHandler.start();
        when(mContext.getResources()).thenReturn(mResources);
        when(mContext.getPackageManager()).thenReturn(mPackageManager);
        when(mContext.getApplicationInfo()).thenReturn(new ApplicationInfo());
        waitUntilReady();
    }

    @After
    public void tearDown() throws Exception {
        mCarrierServiceStateTrackerTestHandler.quit();
        super.tearDown();
    }

    @Test
    @SmallTest
    public void testCancelBothNotifications() {
        logd(LOG_TAG + ":testCancelBothNotifications()");
        Message notificationMsg = mCarrierSST.obtainMessage(
                CarrierServiceStateTracker.CARRIER_EVENT_DATA_REGISTRATION, null);
        doReturn(false).when(mCarrierSST).evaluateSendingMessage(any());
        doReturn(mNotificationManager).when(mCarrierSST).getNotificationManager(any());
        mCarrierSST.handleMessage(notificationMsg);
        waitForHandlerAction(mCarrierSST, TEST_TIMEOUT);
        verify(mNotificationManager).cancel(
                CarrierServiceStateTracker.NOTIFICATION_EMERGENCY_NETWORK);
        verify(mNotificationManager).cancel(
                CarrierServiceStateTracker.NOTIFICATION_PREF_NETWORK);
    }

    @Test
    @SmallTest
    public void testSendBothNotifications() {
        logd(LOG_TAG + ":testSendBothNotifications()");
        Notification.Builder mNotificationBuilder = new Notification.Builder(mContext);
        Message notificationMsg = mCarrierSST.obtainMessage(
                CarrierServiceStateTracker.CARRIER_EVENT_DATA_DEREGISTRATION, null);
        doReturn(true).when(mCarrierSST).evaluateSendingMessage(any());
        doReturn(false).when(mCarrierSST).isRadioOffOrAirplaneMode();
        doReturn(0).when(mCarrierSST).getDelay(any());
        doReturn(mNotificationBuilder).when(mCarrierSST).getNotificationBuilder(any());
        doReturn(mNotificationManager).when(mCarrierSST).getNotificationManager(any());
        mCarrierSST.handleMessage(notificationMsg);
        waitForHandlerAction(mCarrierSST, TEST_TIMEOUT);
        verify(mNotificationManager).notify(
                eq(CarrierServiceStateTracker.NOTIFICATION_PREF_NETWORK), isA(Notification.class));
        verify(mNotificationManager).notify(
                eq(CarrierServiceStateTracker.NOTIFICATION_EMERGENCY_NETWORK), any());
    }
}