blob: 93e1e3c5e9248cc4c29f45f99e72500070808655 [file] [log] [blame]
/*
* Copyright (C) 2016 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.services.telephony;
import android.os.AsyncResult;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.telephony.ServiceState;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import com.android.internal.telephony.Phone;
/**
* Helper class that listens to a Phone's radio state and sends an onComplete callback when we
* return true for isOkToCall.
*/
public class RadioOnStateListener {
interface Callback {
/**
* Receives the result of the RadioOnStateListener's attempt to turn on the radio.
*/
void onComplete(RadioOnStateListener listener, boolean isRadioReady);
/**
* Given the Phone and the new service state of that phone, return whether or not this
* phone is ok to call. If it is, onComplete will be called shortly after.
*/
boolean isOkToCall(Phone phone, int serviceState);
}
// Number of times to retry the call, and time between retry attempts.
// not final for testing
private static int MAX_NUM_RETRIES = 5;
// not final for testing
private static long TIME_BETWEEN_RETRIES_MILLIS = 5000; // msec
// Handler message codes; see handleMessage()
private static final int MSG_START_SEQUENCE = 1;
@VisibleForTesting
public static final int MSG_SERVICE_STATE_CHANGED = 2;
private static final int MSG_RETRY_TIMEOUT = 3;
@VisibleForTesting
public static final int MSG_RADIO_ON = 4;
public static final int MSG_RADIO_OFF_OR_NOT_AVAILABLE = 5;
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_START_SEQUENCE:
SomeArgs args = (SomeArgs) msg.obj;
try {
Phone phone = (Phone) args.arg1;
RadioOnStateListener.Callback callback =
(RadioOnStateListener.Callback) args.arg2;
boolean forEmergencyCall = (boolean) args.arg3;
boolean isSelectedPhoneForEmergencyCall = (boolean) args.arg4;
startSequenceInternal(phone, callback, forEmergencyCall,
isSelectedPhoneForEmergencyCall);
} finally {
args.recycle();
}
break;
case MSG_SERVICE_STATE_CHANGED:
onServiceStateChanged((ServiceState) ((AsyncResult) msg.obj).result);
break;
case MSG_RADIO_ON:
onRadioOn();
break;
case MSG_RADIO_OFF_OR_NOT_AVAILABLE:
registerForRadioOn();
break;
case MSG_RETRY_TIMEOUT:
onRetryTimeout();
break;
default:
Log.wtf(this, "handleMessage: unexpected message: %d.", msg.what);
break;
}
}
};
private Callback mCallback; // The callback to notify upon completion.
private Phone mPhone; // The phone that will attempt to place the call.
private boolean mForEmergencyCall; // Whether radio is being turned on for emergency call.
// Whether this phone is selected to place emergency call. Can be true only if
// mForEmergencyCall is true.
private boolean mSelectedPhoneForEmergencyCall;
private int mNumRetriesSoFar;
/**
* Starts the "wait for radio" sequence. This is the (single) external API of the
* RadioOnStateListener class.
*
* This method kicks off the following sequence:
* - Listen for the service state change event telling us the radio has come up.
* - Retry if we've gone {@link #TIME_BETWEEN_RETRIES_MILLIS} without any response from the
* radio.
* - Finally, clean up any leftover state.
*
* This method is safe to call from any thread, since it simply posts a message to the
* RadioOnStateListener's handler (thus ensuring that the rest of the sequence is entirely
* serialized, and runs only on the handler thread.)
*/
public void waitForRadioOn(Phone phone, Callback callback,
boolean forEmergencyCall, boolean isSelectedPhoneForEmergencyCall) {
Log.d(this, "waitForRadioOn: Phone " + phone.getPhoneId());
if (mPhone != null) {
// If there already is an ongoing request, ignore the new one!
return;
}
SomeArgs args = SomeArgs.obtain();
args.arg1 = phone;
args.arg2 = callback;
args.arg3 = forEmergencyCall;
args.arg4 = isSelectedPhoneForEmergencyCall;
mHandler.obtainMessage(MSG_START_SEQUENCE, args).sendToTarget();
}
/**
* Actual implementation of waitForRadioOn(), guaranteed to run on the handler thread.
*
* @see #waitForRadioOn
*/
private void startSequenceInternal(Phone phone, Callback callback,
boolean forEmergencyCall, boolean isSelectedPhoneForEmergencyCall) {
Log.d(this, "startSequenceInternal: Phone " + phone.getPhoneId());
// First of all, clean up any state left over from a prior RadioOn call sequence. This
// ensures that we'll behave sanely if another startTurnOnRadioSequence() comes in while
// we're already in the middle of the sequence.
cleanup();
mPhone = phone;
mCallback = callback;
mForEmergencyCall = forEmergencyCall;
mSelectedPhoneForEmergencyCall = isSelectedPhoneForEmergencyCall;
registerForServiceStateChanged();
// Register for RADIO_OFF to handle cases where emergency call is dialed before
// we receive UNSOL_RESPONSE_RADIO_STATE_CHANGED with RADIO_OFF.
registerForRadioOff();
// Next step: when the SERVICE_STATE_CHANGED event comes in, we'll retry the call; see
// onServiceStateChanged(). But also, just in case, start a timer to make sure we'll retry
// the call even if the SERVICE_STATE_CHANGED event never comes in for some reason.
startRetryTimer();
}
/**
* Handles the SERVICE_STATE_CHANGED event. This event tells us that the radio state has changed
* and is probably coming up. We can now check to see if the conditions are met to place the
* call with {@link Callback#isOkToCall}
*/
private void onServiceStateChanged(ServiceState state) {
if (mPhone == null) return;
Log.d(this, "onServiceStateChanged(), new state = %s, Phone = %s", state,
mPhone.getPhoneId());
// Possible service states:
// - STATE_IN_SERVICE // Normal operation
// - STATE_OUT_OF_SERVICE // Still searching for an operator to register to,
// // or no radio signal
// - STATE_EMERGENCY_ONLY // Only emergency numbers are allowed; currently not used
// - STATE_POWER_OFF // Radio is explicitly powered off (airplane mode)
if (isOkToCall(state.getState())) {
// Woo hoo! It's OK to actually place the call.
Log.d(this, "onServiceStateChanged: ok to call!");
onComplete(true);
cleanup();
} else {
// The service state changed, but we're still not ready to call yet.
Log.d(this, "onServiceStateChanged: not ready to call yet, keep waiting.");
}
}
private void onRadioOn() {
if (mPhone == null) return;
ServiceState state = mPhone.getServiceState();
Log.d(this, "onRadioOn, state = %s, Phone = %s", state,
mPhone.getPhoneId());
if (isOkToCall(state.getState())) {
onComplete(true);
cleanup();
} else {
Log.d(this, "onRadioOn: not ready to call yet, keep waiting.");
}
}
/**
* Callback to see if it is okay to call yet, given the current conditions.
*/
private boolean isOkToCall(int serviceState) {
return (mCallback == null) ? false : mCallback.isOkToCall(mPhone, serviceState);
}
/**
* Handles the retry timer expiring.
*/
private void onRetryTimeout() {
if (mPhone == null) return;
int serviceState = mPhone.getServiceState().getState();
Log.d(this, "onRetryTimeout(): phone state = %s, service state = %d, retries = %d.",
mPhone.getState(), serviceState, mNumRetriesSoFar);
// - If we're actually in a call, we've succeeded.
// - Otherwise, if the radio is now on, that means we successfully got out of airplane mode
// but somehow didn't get the service state change event. In that case, try to place the
// call.
// - If the radio is still powered off, try powering it on again.
if (isOkToCall(serviceState)) {
Log.d(this, "onRetryTimeout: Radio is on. Cleaning up.");
// Woo hoo -- we successfully got out of airplane mode.
onComplete(true);
cleanup();
} else {
// Uh oh; we've waited the full TIME_BETWEEN_RETRIES_MILLIS and the radio is still not
// powered-on. Try again.
mNumRetriesSoFar++;
Log.d(this, "mNumRetriesSoFar is now " + mNumRetriesSoFar);
if (mNumRetriesSoFar > MAX_NUM_RETRIES) {
Log.w(this, "Hit MAX_NUM_RETRIES; giving up.");
cleanup();
} else {
Log.d(this, "Trying (again) to turn on the radio.");
mPhone.setRadioPower(true, mForEmergencyCall, mSelectedPhoneForEmergencyCall,
false);
startRetryTimer();
}
}
}
/**
* Clean up when done with the whole sequence: either after successfully turning on the radio,
* or after bailing out because of too many failures.
*
* The exact cleanup steps are:
* - Notify callback if we still hadn't sent it a response.
* - Double-check that we're not still registered for any telephony events
* - Clean up any extraneous handler messages (like retry timeouts) still in the queue
*
* Basically this method guarantees that there will be no more activity from the
* RadioOnStateListener until someone kicks off the whole sequence again with another call
* to {@link #waitForRadioOn}
*
* TODO: Do the work for the comment below:
* Note we don't call this method simply after a successful call to placeCall(), since it's
* still possible the call will disconnect very quickly with an OUT_OF_SERVICE error.
*/
public void cleanup() {
Log.d(this, "cleanup()");
// This will send a failure call back if callback has yet to be invoked. If the callback
// was already invoked, it's a no-op.
onComplete(false);
unregisterForServiceStateChanged();
unregisterForRadioOff();
unregisterForRadioOn();
cancelRetryTimer();
// Used for unregisterForServiceStateChanged() so we null it out here instead.
mPhone = null;
mNumRetriesSoFar = 0;
}
private void startRetryTimer() {
cancelRetryTimer();
mHandler.sendEmptyMessageDelayed(MSG_RETRY_TIMEOUT, TIME_BETWEEN_RETRIES_MILLIS);
}
private void cancelRetryTimer() {
mHandler.removeMessages(MSG_RETRY_TIMEOUT);
}
private void registerForServiceStateChanged() {
// Unregister first, just to make sure we never register ourselves twice. (We need this
// because Phone.registerForServiceStateChanged() does not prevent multiple registration of
// the same handler.)
unregisterForServiceStateChanged();
mPhone.registerForServiceStateChanged(mHandler, MSG_SERVICE_STATE_CHANGED, null);
}
private void unregisterForServiceStateChanged() {
// This method is safe to call even if we haven't set mPhone yet.
if (mPhone != null) {
mPhone.unregisterForServiceStateChanged(mHandler); // Safe even if unnecessary
}
mHandler.removeMessages(MSG_SERVICE_STATE_CHANGED); // Clean up any pending messages too
}
private void registerForRadioOff() {
mPhone.mCi.registerForOffOrNotAvailable(mHandler, MSG_RADIO_OFF_OR_NOT_AVAILABLE, null);
}
private void unregisterForRadioOff() {
// This method is safe to call even if we haven't set mPhone yet.
if (mPhone != null) {
mPhone.mCi.unregisterForOffOrNotAvailable(mHandler); // Safe even if unnecessary
}
mHandler.removeMessages(MSG_RADIO_OFF_OR_NOT_AVAILABLE); // Clean up any pending messages
}
private void registerForRadioOn() {
unregisterForRadioOff();
mPhone.mCi.registerForOn(mHandler, MSG_RADIO_ON, null);
}
private void unregisterForRadioOn() {
// This method is safe to call even if we haven't set mPhone yet.
if (mPhone != null) {
mPhone.mCi.unregisterForOn(mHandler); // Safe even if unnecessary
}
mHandler.removeMessages(MSG_RADIO_ON); // Clean up any pending messages too
}
private void onComplete(boolean isRadioReady) {
if (mCallback != null) {
Callback tempCallback = mCallback;
mCallback = null;
tempCallback.onComplete(this, isRadioReady);
}
}
@VisibleForTesting
public Handler getHandler() {
return mHandler;
}
@VisibleForTesting
public void setMaxNumRetries(int retries) {
MAX_NUM_RETRIES = retries;
}
@VisibleForTesting
public void setTimeBetweenRetriesMillis(long timeMs) {
TIME_BETWEEN_RETRIES_MILLIS = timeMs;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || !getClass().equals(o.getClass())) return false;
RadioOnStateListener that = (RadioOnStateListener) o;
if (mNumRetriesSoFar != that.mNumRetriesSoFar) {
return false;
}
if (mCallback != null ? !mCallback.equals(that.mCallback) : that.mCallback != null) {
return false;
}
return mPhone != null ? mPhone.equals(that.mPhone) : that.mPhone == null;
}
}