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

Commit f78a21f6 authored by Thomas Nguyen's avatar Thomas Nguyen Committed by Android (Google) Code Review
Browse files

Merge "Add a new module SatelliteSOSMessageRecommender" into udc-dev

parents e3968302 2a435887
Loading
Loading
Loading
Loading
+35 −54
Original line number Diff line number Diff line
@@ -40,13 +40,11 @@ import android.telephony.satellite.SatelliteCapabilities;
import android.telephony.satellite.SatelliteDatagram;
import android.telephony.satellite.SatelliteManager;
import android.util.Log;
import android.util.Pair;

import com.android.internal.telephony.CommandException;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.IIntegerConsumer;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneFactory;
import com.android.internal.telephony.RILUtils;
import com.android.internal.telephony.SubscriptionController;
import com.android.internal.telephony.subscription.SubscriptionManagerService;
import com.android.internal.util.FunctionalUtils;
@@ -149,7 +147,8 @@ public class SatelliteController extends Handler {
     *
     * @param context The Context for the SatelliteController.
     */
    private SatelliteController(@NonNull Context context) {
    @VisibleForTesting
    protected SatelliteController(@NonNull Context context) {
        super(context.getMainLooper());
        mContext = context;

@@ -1094,7 +1093,35 @@ public class SatelliteController extends Handler {
     */
    @SatelliteManager.SatelliteError public int registerForSatelliteProvisionStateChanged(int subId,
            @NonNull ISatelliteProvisionStateCallback callback) {
        return registerForSatelliteProvisionStateChangedInternal(subId, callback);
        if (!isSatelliteSupported()) {
            return SatelliteManager.SATELLITE_NOT_SUPPORTED;
        }

        final int validSubId = getValidSatelliteSubId(subId);
        Phone phone = SatelliteServiceUtils.getPhone();

        SatelliteProvisionStateChangedHandler satelliteProvisionStateChangedHandler =
                mSatelliteProvisionStateChangedHandlers.get(validSubId);
        if (satelliteProvisionStateChangedHandler == null) {
            satelliteProvisionStateChangedHandler = new SatelliteProvisionStateChangedHandler(
                    Looper.getMainLooper(), validSubId);
            if (mSatelliteModemInterface.isSatelliteServiceSupported()) {
                mSatelliteModemInterface.registerForSatelliteProvisionStateChanged(
                        satelliteProvisionStateChangedHandler,
                        SatelliteProvisionStateChangedHandler.EVENT_PROVISION_STATE_CHANGED, null);
            } else {
                phone.registerForSatelliteProvisionStateChanged(
                        satelliteProvisionStateChangedHandler,
                        SatelliteProvisionStateChangedHandler.EVENT_PROVISION_STATE_CHANGED, null);
            }
        }

        if (callback != null) {
            satelliteProvisionStateChangedHandler.addListener(callback);
        }
        mSatelliteProvisionStateChangedHandlers.put(
                validSubId, satelliteProvisionStateChangedHandler);
        return SatelliteManager.SATELLITE_ERROR_NONE;
    }

    /**
@@ -1312,12 +1339,6 @@ public class SatelliteController extends Handler {
            return;
        }

        final int validSubId = getValidSatelliteSubId(subId);
        if (!isSatelliteProvisioned(validSubId)) {
            result.send(SatelliteManager.SATELLITE_SERVICE_NOT_PROVISIONED, null);
            return;
        }

        Phone phone = SatelliteServiceUtils.getPhone();
        sendRequest(CMD_IS_SATELLITE_COMMUNICATION_ALLOWED, result, phone);
    }
@@ -1367,7 +1388,7 @@ public class SatelliteController extends Handler {
         * or SatelliteController.
         * TODO (b/267826133) we need to do this for all subscriptions on the device.
         */
        registerForSatelliteProvisionStateChangedInternal(arg.subId, null);
        registerForSatelliteProvisionStateChanged(arg.subId, null);
    }

    private void handleEventDeprovisionSatelliteServiceDone(
@@ -1389,47 +1410,6 @@ public class SatelliteController extends Handler {
        }
    }

    /**
     * Registers for the satellite provision state changed.
     *
     * @param subId The subId of the subscription associated with the satellite service.
     * @param callback The callback to handle the satellite provision state changed event.
     *
     * @return The {@link SatelliteManager.SatelliteError} result of the operation.
     */
    @SatelliteManager.SatelliteError private int registerForSatelliteProvisionStateChangedInternal(
            int subId, @Nullable ISatelliteProvisionStateCallback callback) {
        if (!isSatelliteSupported()) {
            return SatelliteManager.SATELLITE_NOT_SUPPORTED;
        }

        final int validSubId = getValidSatelliteSubId(subId);
        Phone phone = SatelliteServiceUtils.getPhone();

        SatelliteProvisionStateChangedHandler satelliteProvisionStateChangedHandler =
                mSatelliteProvisionStateChangedHandlers.get(validSubId);
        if (satelliteProvisionStateChangedHandler == null) {
            satelliteProvisionStateChangedHandler = new SatelliteProvisionStateChangedHandler(
                    Looper.getMainLooper(), validSubId);
            if (mSatelliteModemInterface.isSatelliteServiceSupported()) {
                mSatelliteModemInterface.registerForSatelliteProvisionStateChanged(
                        satelliteProvisionStateChangedHandler,
                        SatelliteProvisionStateChangedHandler.EVENT_PROVISION_STATE_CHANGED, null);
            } else {
                phone.registerForSatelliteProvisionStateChanged(
                        satelliteProvisionStateChangedHandler,
                        SatelliteProvisionStateChangedHandler.EVENT_PROVISION_STATE_CHANGED, null);
            }
        }

        if (callback != null) {
            satelliteProvisionStateChangedHandler.addListener(callback);
        }
        mSatelliteProvisionStateChangedHandlers.put(
                validSubId, satelliteProvisionStateChangedHandler);
        return SatelliteManager.SATELLITE_ERROR_NONE;
    }

    private void handleStartSatellitePositionUpdatesDone(@NonNull AsyncResult ar) {
        SatelliteControllerHandlerRequest request = (SatelliteControllerHandlerRequest) ar.userObj;
        SatellitePositionUpdateArgument arg = (SatellitePositionUpdateArgument) request.argument;
@@ -1568,7 +1548,8 @@ public class SatelliteController extends Handler {
     * @param subId The subscription id.
     * @return true if satellite is provisioned on the given subscription else return false.
     */
    public boolean isSatelliteProvisioned(int subId) {
    @VisibleForTesting
    protected boolean isSatelliteProvisioned(int subId) {
        final long identity = Binder.clearCallingIdentity();
        try {
            if (subId != SubscriptionManager.DEFAULT_SUBSCRIPTION_ID) {
+410 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.satellite;

import static android.telephony.satellite.SatelliteManager.KEY_SATELLITE_COMMUNICATION_ALLOWED;
import static android.telephony.satellite.SatelliteManager.SATELLITE_ERROR_NONE;

import android.annotation.NonNull;
import android.os.AsyncResult;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.ResultReceiver;
import android.provider.DeviceConfig;
import android.telecom.Call;
import android.telecom.Connection;
import android.telephony.Rlog;
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.ims.ImsReasonInfo;
import android.telephony.ims.ImsRegistrationAttributes;
import android.telephony.ims.RegistrationManager;
import android.telephony.satellite.ISatelliteProvisionStateCallback;
import android.util.Pair;

import com.android.ims.ImsException;
import com.android.ims.ImsManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.Phone;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * This module is responsible for monitoring the cellular service state and IMS registration state
 * during an emergency call and notify Dialer when Telephony is not able to find any network and
 * the call likely will not get connected so that Dialer will prompt the user if they would like to
 * switch to satellite messaging.
 */
public class SatelliteSOSMessageRecommender extends Handler {
    private static final String TAG = "SatelliteSOSMessageRecommender";

    /**
     * Device config for the timeout duration in milliseconds to determine whether to recommend
     * Dialer to show the SOS button to users.
     * <p>
     * The timer is started when there is an ongoing emergency call, and the IMS is not registered,
     * and cellular service is not available. When the timer expires, SatelliteSOSMessageRecommender
     * will send the event EVENT_DISPLAY_SOS_MESSAGE to Dialer, which will then prompt user to
     * switch to using satellite SOS messaging.
     */
    public static final String EMERGENCY_CALL_TO_SOS_MSG_HYSTERESIS_TIMEOUT_MILLIS =
            "emergency_call_to_sos_msg_hysteresis_timeout_millis";
    /**
     * The default value of {@link #EMERGENCY_CALL_TO_SOS_MSG_HYSTERESIS_TIMEOUT_MILLIS} when it is
     * not provided in the device config.
     */
    public static final long DEFAULT_EMERGENCY_CALL_TO_SOS_MSG_HYSTERESIS_TIMEOUT_MILLIS = 20000;

    private static final int EVENT_EMERGENCY_CALL_STARTED = 1;
    protected static final int EVENT_CELLULAR_SERVICE_STATE_CHANGED = 2;
    private static final int EVENT_IMS_REGISTRATION_STATE_CHANGED = 3;
    protected static final int EVENT_TIME_OUT = 4;
    private static final int EVENT_SATELLITE_PROVISIONED_STATE_CHANGED = 5;
    private static final int EVENT_EMERGENCY_CALL_CONNECTION_STATE_CHANGED = 6;

    @NonNull
    private final SatelliteController mSatelliteController;
    private ImsManager mImsManager;

    private Connection mEmergencyConnection = null;
    /* The phone used for emergency call */
    private Phone mPhone = null;
    private final ISatelliteProvisionStateCallback mISatelliteProvisionStateCallback;
    @ServiceState.RegState
    private AtomicInteger mCellularServiceState = new AtomicInteger();
    private AtomicBoolean mIsImsRegistered = new AtomicBoolean();
    private AtomicBoolean mIsSatelliteAllowedInCurrentLocation = new AtomicBoolean();
    private final ResultReceiver mReceiverForRequestIsSatelliteAllowedForCurrentLocation;
    private final long mTimeoutMillis;
    protected int mCountOfTimerStarted = 0;

    private RegistrationManager.RegistrationCallback mImsRegistrationCallback =
            new RegistrationManager.RegistrationCallback() {
                @Override
                public void onRegistered(ImsRegistrationAttributes attributes) {
                    sendMessage(obtainMessage(EVENT_IMS_REGISTRATION_STATE_CHANGED, true));
                }

                @Override
                public void onUnregistered(ImsReasonInfo info) {
                    sendMessage(obtainMessage(EVENT_IMS_REGISTRATION_STATE_CHANGED, false));
                }
            };

    /**
     * Create an instance of SatelliteSOSMessageRecommender.
     *
     * @param looper The looper used with the handler of this class.
     */
    public SatelliteSOSMessageRecommender(@NonNull Looper looper) {
        this(looper, SatelliteController.getInstance(), null,
                getEmergencyCallToSosMsgHysteresisTimeoutMillis());
    }

    /**
     * Create an instance of SatelliteSOSMessageRecommender. This constructor should be used in
     * only unit tests.
     *
     * @param looper The looper used with the handler of this class.
     * @param satelliteController The SatelliteController singleton instance.
     * @param imsManager The ImsManager instance associated with the phone, which is used for making
     *                   the emergency call. This argument is not null only in unit tests.
     * @param timeoutMillis The timeout duration of the timer.
     */
    @VisibleForTesting
    protected SatelliteSOSMessageRecommender(@NonNull Looper looper,
            @NonNull SatelliteController satelliteController, ImsManager imsManager,
            long timeoutMillis) {
        super(looper);
        mSatelliteController = satelliteController;
        mImsManager = imsManager;
        mTimeoutMillis = timeoutMillis;
        mISatelliteProvisionStateCallback = new ISatelliteProvisionStateCallback.Stub() {
            @Override
            public void onSatelliteProvisionStateChanged(boolean provisioned) {
                logd("onSatelliteProvisionStateChanged: provisioned=" + provisioned);
                sendMessage(obtainMessage(EVENT_SATELLITE_PROVISIONED_STATE_CHANGED, provisioned));
            }
        };
        mReceiverForRequestIsSatelliteAllowedForCurrentLocation = new ResultReceiver(this) {
            @Override
            protected void onReceiveResult(int resultCode, Bundle resultData) {
                if (resultCode == SATELLITE_ERROR_NONE) {
                    if (resultData.containsKey(KEY_SATELLITE_COMMUNICATION_ALLOWED)) {
                        boolean isSatelliteCommunicationAllowed =
                                resultData.getBoolean(KEY_SATELLITE_COMMUNICATION_ALLOWED);
                        mIsSatelliteAllowedInCurrentLocation.set(isSatelliteCommunicationAllowed);
                        if (!isSatelliteCommunicationAllowed) {
                            logd("Satellite is not allowed for current location.");
                            cleanUpResources();
                        }
                    } else {
                        loge("KEY_SATELLITE_COMMUNICATION_ALLOWED does not exist.");
                        mIsSatelliteAllowedInCurrentLocation.set(false);
                        cleanUpResources();
                    }
                } else {
                    loge("requestIsSatelliteCommunicationAllowedForCurrentLocation() resultCode="
                            + resultCode);
                    mIsSatelliteAllowedInCurrentLocation.set(false);
                    cleanUpResources();
                }
            }
        };
    }

    @Override
    public void handleMessage(@NonNull Message msg) {
        switch (msg.what) {
            case EVENT_EMERGENCY_CALL_STARTED:
                handleEmergencyCallStartedEvent((Pair<Connection, Phone>) msg.obj);
                break;
            case EVENT_TIME_OUT:
                handleTimeoutEvent();
                break;
            case EVENT_SATELLITE_PROVISIONED_STATE_CHANGED:
                handleSatelliteProvisionStateChangedEvent((boolean) msg.obj);
                break;
            case EVENT_EMERGENCY_CALL_CONNECTION_STATE_CHANGED:
                handleEmergencyCallConnectionStateChangedEvent((Pair<String, Integer>) msg.obj);
                break;
            case EVENT_IMS_REGISTRATION_STATE_CHANGED:
                handleImsRegistrationStateChangedEvent((boolean) msg.obj);
                break;
            case EVENT_CELLULAR_SERVICE_STATE_CHANGED:
                AsyncResult ar = (AsyncResult) msg.obj;
                handleCellularServiceStateChangedEvent((ServiceState) ar.result);
                break;
            default:
                logd("handleMessage: unexpected message code: " + msg.what);
                break;
        }
    }

    /**
     * Inform SatelliteSOSMessageRecommender that an emergency call has just started.
     *
     * @param connection The connection created by TelephonyConnectionService for the emergency
     *                   call.
     * @param phone The phone used for the emergency call.
     */
    public void onEmergencyCallStarted(@NonNull Connection connection, @NonNull Phone phone) {
        if (!mSatelliteController.isSatelliteSupported()) {
            logd("onEmergencyCallStarted: satellite is not supported");
            return;
        }
        Pair<Connection, Phone> argument = new Pair<>(connection, phone);
        sendMessage(obtainMessage(EVENT_EMERGENCY_CALL_STARTED, argument));
    }

    /**
     * Inform SatelliteSOSMessageRecommender that the state of the emergency call connection has
     * changed.
     *
     * @param callId The ID of the emergency call.
     * @param state The connection state of the emergency call.
     */
    public void onEmergencyCallConnectionStateChanged(
            String callId, @Connection.ConnectionState int state) {
        Pair<String, Integer> argument = new Pair<>(callId, state);
        sendMessage(obtainMessage(EVENT_EMERGENCY_CALL_CONNECTION_STATE_CHANGED, argument));
    }

    private void handleEmergencyCallStartedEvent(@NonNull Pair<Connection, Phone> arg) {
        mSatelliteController.requestIsSatelliteCommunicationAllowedForCurrentLocation(
                SubscriptionManager.DEFAULT_SUBSCRIPTION_ID,
                mReceiverForRequestIsSatelliteAllowedForCurrentLocation);
        if (mPhone != null) {
            logd("handleEmergencyCallStartedEvent: new emergency call started while there is "
                    + " an ongoing call");
            unregisterForInterestedStateChangedEvents(mPhone);
        }
        mPhone = arg.second;
        mEmergencyConnection = arg.first;
        mCellularServiceState.set(mPhone.getServiceState().getState());
        mIsImsRegistered.set(mPhone.isImsRegistered());
        handleStateChangedEventForHysteresisTimer();
        registerForInterestedStateChangedEvents(mPhone);
    }

    private void handleSatelliteProvisionStateChangedEvent(boolean provisioned) {
        if (!provisioned) {
            cleanUpResources();
        }
    }

    private void handleTimeoutEvent() {
        boolean isDialerNotified = false;
        if (!mIsImsRegistered.get() && !isCellularAvailable()
                && mIsSatelliteAllowedInCurrentLocation.get()
                && mSatelliteController.isSatelliteProvisioned(
                        SubscriptionManager.DEFAULT_SUBSCRIPTION_ID)
                && shouldTrackCall(mEmergencyConnection.getState())) {
            logd("handleTimeoutEvent: Sending EVENT_DISPLAY_SOS_MESSAGE to Dialer...");
            mEmergencyConnection.sendConnectionEvent(Call.EVENT_DISPLAY_SOS_MESSAGE, null);
            isDialerNotified = true;
        }
        reportMetrics(isDialerNotified);
        cleanUpResources();
    }

    private void handleEmergencyCallConnectionStateChangedEvent(
            @NonNull Pair<String, Integer> arg) {
        if (mEmergencyConnection == null) {
            // Either the call was not created or the timer already timed out.
            return;
        }

        String callId = arg.first;
        int state = arg.second;
        if (!mEmergencyConnection.getTelecomCallId().equals(callId)) {
            loge("handleEmergencyCallConnectionStateChangedEvent: unexpected state changed event "
                    + ", mEmergencyConnection=" + mEmergencyConnection + ", callId=" + callId
                    + ", state=" + state);
            /**
             * TelephonyConnectionService sent us a connection state changed event for a call that
             * we're not tracking. There must be some unexpected things happened in
             * TelephonyConnectionService. Thus, we need to clean up the resources.
             */
            cleanUpResources();
            return;
        }

        if (!shouldTrackCall(state)) {
            reportMetrics(false);
            cleanUpResources();
        }
    }

    private void handleImsRegistrationStateChangedEvent(boolean registered) {
        if (registered != mIsImsRegistered.get()) {
            mIsImsRegistered.set(registered);
            handleStateChangedEventForHysteresisTimer();
        }
    }

    private void handleCellularServiceStateChangedEvent(@NonNull ServiceState serviceState) {
        int state = serviceState.getState();
        if (mCellularServiceState.get() != state) {
            mCellularServiceState.set(state);
            handleStateChangedEventForHysteresisTimer();
        }
    }

    private void reportMetrics(boolean isDialerNotified) {
        /**
         * TODO: We need to report the following info
         * - Whether the Dialer is notified with the event DISPLAY_SOS_MESSAGE (isDialerNotified).
         * - Number of times the timer is started (mCountOfTimerStarted).
         * - Whether IMS is registered (mIsImsRegistered).
         * - The cellular service state (mCellularServiceState).
         */
    }

    private void cleanUpResources() {
        stopTimer();
        if (mPhone != null) {
            unregisterForInterestedStateChangedEvents(mPhone);
            mPhone = null;
        }
        mEmergencyConnection = null;
        mCountOfTimerStarted = 0;
    }

    private void registerForInterestedStateChangedEvents(@NonNull Phone phone) {
        mSatelliteController.registerForSatelliteProvisionStateChanged(
                SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, mISatelliteProvisionStateCallback);
        phone.registerForServiceStateChanged(this, EVENT_CELLULAR_SERVICE_STATE_CHANGED, null);
        registerForImsRegistrationStateChanged(phone);
    }

    private void registerForImsRegistrationStateChanged(@NonNull Phone phone) {
        ImsManager imsManager = (mImsManager != null) ? mImsManager : ImsManager.getInstance(
                phone.getContext(), phone.getPhoneId());
        try {
            imsManager.addRegistrationCallback(mImsRegistrationCallback, this::post);
        } catch (ImsException ex) {
            loge("registerForImsRegistrationStateChanged: ex=" + ex);
        }
    }

    private void unregisterForInterestedStateChangedEvents(@NonNull Phone phone) {
        mSatelliteController.unregisterForSatelliteProvisionStateChanged(
                SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, mISatelliteProvisionStateCallback);
        phone.unregisterForServiceStateChanged(this);
        unregisterForImsRegistrationStateChanged(phone);
    }

    private void unregisterForImsRegistrationStateChanged(@NonNull Phone phone) {
        ImsManager imsManager = (mImsManager != null) ? mImsManager : ImsManager.getInstance(
                phone.getContext(), phone.getPhoneId());
        imsManager.removeRegistrationListener(mImsRegistrationCallback);
    }

    private boolean isCellularAvailable() {
        return (mCellularServiceState.get() == ServiceState.STATE_IN_SERVICE
                || mCellularServiceState.get() == ServiceState.STATE_EMERGENCY_ONLY);
    }

    private void handleStateChangedEventForHysteresisTimer() {
        if (!mIsImsRegistered.get() && !isCellularAvailable()) {
            startTimer();
        } else {
            stopTimer();
        }
    }

    private void startTimer() {
        if (hasMessages(EVENT_TIME_OUT)) {
            return;
        }
        sendMessageDelayed(obtainMessage(EVENT_TIME_OUT), mTimeoutMillis);
        mCountOfTimerStarted++;
    }

    private void stopTimer() {
        removeMessages(EVENT_TIME_OUT);
    }

    private static long getEmergencyCallToSosMsgHysteresisTimeoutMillis() {
        return DeviceConfig.getLong(DeviceConfig.NAMESPACE_TELEPHONY,
                EMERGENCY_CALL_TO_SOS_MSG_HYSTERESIS_TIMEOUT_MILLIS,
                DEFAULT_EMERGENCY_CALL_TO_SOS_MSG_HYSTERESIS_TIMEOUT_MILLIS);
    }

    private boolean shouldTrackCall(int connectionState) {
        /**
         * An active connection state means both parties are connected to the call and can actively
         * communicate. A disconnected connection state means the emergency call has ended. In both
         * cases, we don't need to track the call anymore.
         */
        return (connectionState != Connection.STATE_ACTIVE
                && connectionState != Connection.STATE_DISCONNECTED);
    }

    private static void logd(@NonNull String log) {
        Rlog.d(TAG, log);
    }

    private static void loge(@NonNull String log) {
        Rlog.e(TAG, log);
    }
}
+596 −0

File added.

Preview size limit exceeded, changes collapsed.