/* * Copyright 2022 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.annotation.NonNull; import android.os.AsyncResult; import android.os.Build; import android.os.Handler; import android.os.HandlerExecutor; import android.os.Message; import android.telephony.CellBroadcastIdRange; import android.telephony.SmsCbMessage; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; import android.util.IndentingPrintWriter; import android.util.LocalLog; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo; import com.android.internal.telephony.gsm.SmsBroadcastConfigInfo; import com.android.internal.util.State; import com.android.internal.util.StateMachine; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; /** * This class is to track the state to set cell broadcast config */ public final class CellBroadcastConfigTracker extends StateMachine { private static final boolean DBG = Build.IS_DEBUGGABLE; private static final int EVENT_REQUEST = 1; private static final int EVENT_CONFIGURATION_DONE = 2; private static final int EVENT_ACTIVATION_DONE = 3; private static final int EVENT_RADIO_OFF = 4; private static final int EVENT_SUBSCRIPTION_CHANGED = 5; @VisibleForTesting public static final int EVENT_RADIO_RESET = 6; private static final int SMS_CB_CODE_SCHEME_MIN = 0; private static final int SMS_CB_CODE_SCHEME_MAX = 255; // Cache of current cell broadcast id ranges of 3gpp private List mCbRanges3gpp = new CopyOnWriteArrayList<>(); // Cache of current cell broadcast id ranges of 3gpp2 private List mCbRanges3gpp2 = new CopyOnWriteArrayList<>(); private Phone mPhone; private final LocalLog mLocalLog = new LocalLog(128); @VisibleForTesting public int mSubId; @VisibleForTesting public final SubscriptionManager.OnSubscriptionsChangedListener mSubChangedListener = new SubscriptionManager.OnSubscriptionsChangedListener() { @Override public void onSubscriptionsChanged() { sendMessage(EVENT_SUBSCRIPTION_CHANGED); } }; /** * The class is to present the request to set cell broadcast id ranges */ private static class Request { private final List mCbRangesRequest3gpp = new CopyOnWriteArrayList<>(); private final List mCbRangesRequest3gpp2 = new CopyOnWriteArrayList<>(); Consumer mCallback; Request(@NonNull List ranges, @NonNull Consumer callback) { ranges.forEach(r -> { if (r.getType() == SmsCbMessage.MESSAGE_FORMAT_3GPP) { mCbRangesRequest3gpp.add(r); } else { mCbRangesRequest3gpp2.add(r); } }); mCallback = callback; } List get3gppRanges() { return mCbRangesRequest3gpp; } List get3gpp2Ranges() { return mCbRangesRequest3gpp2; } Consumer getCallback() { return mCallback; } @Override public String toString() { return "Request[mCbRangesRequest3gpp = " + mCbRangesRequest3gpp + ", " + "mCbRangesRequest3gpp2 = " + mCbRangesRequest3gpp2 + "]"; } } /** * The default state. */ private class DefaultState extends State { @Override public void enter() { mPhone.registerForRadioOffOrNotAvailable(getHandler(), EVENT_RADIO_OFF, null); mPhone.mCi.registerForModemReset(getHandler(), EVENT_RADIO_RESET, null); mPhone.getContext().getSystemService(SubscriptionManager.class) .addOnSubscriptionsChangedListener(new HandlerExecutor(getHandler()), mSubChangedListener); } @Override public void exit() { mPhone.unregisterForRadioOffOrNotAvailable(getHandler()); mPhone.mCi.unregisterForModemReset(getHandler()); mPhone.getContext().getSystemService(SubscriptionManager.class) .removeOnSubscriptionsChangedListener(mSubChangedListener); } @Override public boolean processMessage(Message msg) { boolean retVal = HANDLED; if (DBG) { logd("DefaultState message:" + msg.what); } switch (msg.what) { case EVENT_RADIO_OFF: case EVENT_RADIO_RESET: resetConfig(); break; case EVENT_SUBSCRIPTION_CHANGED: int subId = mPhone.getSubId(); if (mSubId != subId) { log("SubId changed from " + mSubId + " to " + subId); mSubId = subId; resetConfig(); } break; default: log("unexpected message!"); break; } return retVal; } } private DefaultState mDefaultState = new DefaultState(); /* * The idle state which does not have ongoing radio request. */ private class IdleState extends State { @Override public boolean processMessage(Message msg) { boolean retVal = NOT_HANDLED; if (DBG) { logd("IdleState message:" + msg.what); } switch (msg.what) { case EVENT_REQUEST: Request request = (Request) msg.obj; if (DBG) { logd("IdleState handle EVENT_REQUEST with request:" + request); mLocalLog.log("IdleState handle EVENT_REQUEST with request:" + request + ", mCbRanges3gpp:" + mCbRanges3gpp + ", mCbRanges3gpp2:" + mCbRanges3gpp2); } if (!mCbRanges3gpp.equals(request.get3gppRanges())) { // set gsm config if the config is changed setGsmConfig(request.get3gppRanges(), request); transitionTo(mGsmConfiguringState); } else if (!mCbRanges3gpp2.equals(request.get3gpp2Ranges())) { // set cdma config directly if no gsm config change but cdma config is // changed setCdmaConfig(request.get3gpp2Ranges(), request); transitionTo(mCdmaConfiguringState); } else { logd("Do nothing as the requested ranges are same as now"); request.getCallback().accept( TelephonyManager.CELL_BROADCAST_RESULT_SUCCESS); } retVal = HANDLED; break; default: break; } return retVal; } } private IdleState mIdleState = new IdleState(); /* * The state waiting for the result to set gsm config. */ private class GsmConfiguringState extends State { @Override public boolean processMessage(Message msg) { boolean retVal = NOT_HANDLED; if (DBG) { logd("GsmConfiguringState message:" + msg.what); } switch (msg.what) { case EVENT_REQUEST: deferMessage(msg); retVal = HANDLED; break; case EVENT_CONFIGURATION_DONE: AsyncResult ar = (AsyncResult) msg.obj; Request request = (Request) ar.userObj; if (DBG) { logd("GsmConfiguringState handle EVENT_CONFIGURATION_DONE with request:" + request); } if (ar.exception == null) { // set gsm activation and transit to gsm activating state setActivation(SmsCbMessage.MESSAGE_FORMAT_3GPP, !request.get3gppRanges().isEmpty(), request); transitionTo(mGsmActivatingState); } else { logd("Failed to set gsm config"); mLocalLog.log("GsmConfiguringState Failed to set gsm config:" + request); request.getCallback().accept( TelephonyManager.CELL_BROADCAST_RESULT_FAIL_CONFIG); // transit to idle state on the failure case transitionTo(mIdleState); } retVal = HANDLED; break; default: break; } return retVal; } } private GsmConfiguringState mGsmConfiguringState = new GsmConfiguringState(); /* * The state waiting for the result to set gsm activation. */ private class GsmActivatingState extends State { @Override public boolean processMessage(Message msg) { boolean retVal = NOT_HANDLED; if (DBG) { logd("GsmActivatingState message:" + msg.what); } switch (msg.what) { case EVENT_REQUEST: deferMessage(msg); retVal = HANDLED; break; case EVENT_ACTIVATION_DONE: AsyncResult ar = (AsyncResult) msg.obj; Request request = (Request) ar.userObj; if (DBG) { logd("GsmActivatingState handle EVENT_ACTIVATION_DONE with request:" + request); mLocalLog.log("GsmActivatingState EVENT_ACTIVATION_DONE, exception:" + ar.exception + ", request:" + request); } if (ar.exception == null) { mCbRanges3gpp = request.get3gppRanges(); if (!mCbRanges3gpp2.equals(request.get3gpp2Ranges())) { // set cdma config and transit to cdma configuring state if the config // is changed. setCdmaConfig(request.get3gpp2Ranges(), request); transitionTo(mCdmaConfiguringState); } else { logd("Done as no need to update ranges for 3gpp2"); request.getCallback().accept( TelephonyManager.CELL_BROADCAST_RESULT_SUCCESS); // transit to idle state if there is no cdma config change transitionTo(mIdleState); } } else { logd("Failed to set gsm activation"); request.getCallback().accept( TelephonyManager.CELL_BROADCAST_RESULT_FAIL_ACTIVATION); // transit to idle state on the failure case transitionTo(mIdleState); } retVal = HANDLED; break; default: break; } return retVal; } } private GsmActivatingState mGsmActivatingState = new GsmActivatingState(); /* * The state waiting for the result to set cdma config. */ private class CdmaConfiguringState extends State { @Override public boolean processMessage(Message msg) { boolean retVal = NOT_HANDLED; if (DBG) { logd("CdmaConfiguringState message:" + msg.what); } switch (msg.what) { case EVENT_REQUEST: deferMessage(msg); retVal = HANDLED; break; case EVENT_CONFIGURATION_DONE: AsyncResult ar = (AsyncResult) msg.obj; Request request = (Request) ar.userObj; if (DBG) { logd("CdmaConfiguringState handle EVENT_ACTIVATION_DONE with request:" + request); } if (ar.exception == null) { // set cdma activation and transit to cdma activating state setActivation(SmsCbMessage.MESSAGE_FORMAT_3GPP2, !request.get3gpp2Ranges().isEmpty(), request); transitionTo(mCdmaActivatingState); } else { logd("Failed to set cdma config"); mLocalLog.log("CdmaConfiguringState Failed to set cdma config:" + request); request.getCallback().accept( TelephonyManager.CELL_BROADCAST_RESULT_FAIL_CONFIG); // transit to idle state on the failure case transitionTo(mIdleState); } retVal = HANDLED; break; default: break; } return retVal; } } private CdmaConfiguringState mCdmaConfiguringState = new CdmaConfiguringState(); /* * The state waiting for the result to set cdma activation. */ private class CdmaActivatingState extends State { @Override public boolean processMessage(Message msg) { boolean retVal = NOT_HANDLED; if (DBG) { logd("CdmaActivatingState message:" + msg.what); } switch (msg.what) { case EVENT_REQUEST: deferMessage(msg); retVal = HANDLED; break; case EVENT_ACTIVATION_DONE: AsyncResult ar = (AsyncResult) msg.obj; Request request = (Request) ar.userObj; if (DBG) { logd("CdmaActivatingState handle EVENT_ACTIVATION_DONE with request:" + request); mLocalLog.log("CdmaActivatingState EVENT_ACTIVATION_DONE, exception:" + ar.exception + ", request:" + request); } if (ar.exception == null) { mCbRanges3gpp2 = request.get3gpp2Ranges(); request.getCallback().accept( TelephonyManager.CELL_BROADCAST_RESULT_SUCCESS); } else { logd("Failed to set cdma activation"); request.getCallback().accept( TelephonyManager.CELL_BROADCAST_RESULT_FAIL_ACTIVATION); } // transit to idle state anyway transitionTo(mIdleState); retVal = HANDLED; break; default: break; } return retVal; } } private CdmaActivatingState mCdmaActivatingState = new CdmaActivatingState(); private CellBroadcastConfigTracker(Phone phone) { super("CellBroadcastConfigTracker-" + phone.getPhoneId()); init(phone); } private CellBroadcastConfigTracker(Phone phone, Handler handler) { super("CellBroadcastConfigTracker-" + phone.getPhoneId(), handler); init(phone); } private void init(Phone phone) { logd("init"); mPhone = phone; mSubId = mPhone.getSubId(); addState(mDefaultState); addState(mIdleState, mDefaultState); addState(mGsmConfiguringState, mDefaultState); addState(mGsmActivatingState, mDefaultState); addState(mCdmaConfiguringState, mDefaultState); addState(mCdmaActivatingState, mDefaultState); setInitialState(mIdleState); } /** * create a CellBroadcastConfigTracker instance for the phone */ public static CellBroadcastConfigTracker make(Phone phone, Handler handler, boolean shouldStart) { CellBroadcastConfigTracker tracker = handler == null ? new CellBroadcastConfigTracker(phone) : new CellBroadcastConfigTracker(phone, handler); if (shouldStart) { tracker.start(); } return tracker; } /** * Return current cell broadcast ranges. */ @NonNull public List getCellBroadcastIdRanges() { List ranges = new ArrayList<>(); ranges.addAll(mCbRanges3gpp); ranges.addAll(mCbRanges3gpp2); return ranges; } /** * Set reception of cell broadcast messages with the list of the given ranges. */ public void setCellBroadcastIdRanges( @NonNull List ranges, @NonNull Consumer callback) { if (DBG) { logd("setCellBroadcastIdRanges with ranges:" + ranges); } ranges = mergeRangesAsNeeded(ranges); sendMessage(EVENT_REQUEST, new Request(ranges, callback)); } /** * Merge the overlapped CellBroadcastIdRanges in the list as needed * @param ranges the list of CellBroadcastIdRanges * @return the list of CellBroadcastIdRanges without overlapping * * @throws IllegalArgumentException if there is conflict of the ranges. For instance, * the channel is enabled in some range, but disable in others. */ @VisibleForTesting public static @NonNull List mergeRangesAsNeeded( @NonNull List ranges) throws IllegalArgumentException { ranges.sort((r1, r2) -> r1.getType() != r2.getType() ? r1.getType() - r2.getType() : (r1.getStartId() != r2.getStartId() ? r1.getStartId() - r2.getStartId() : r2.getEndId() - r1.getEndId())); final List newRanges = new ArrayList<>(); ranges.forEach(r -> { if (newRanges.isEmpty() || newRanges.get(newRanges.size() - 1).getType() != r.getType() || newRanges.get(newRanges.size() - 1).getEndId() + 1 < r.getStartId() || (newRanges.get(newRanges.size() - 1).getEndId() + 1 == r.getStartId() && newRanges.get(newRanges.size() - 1).isEnabled() != r.isEnabled())) { newRanges.add(new CellBroadcastIdRange(r.getStartId(), r.getEndId(), r.getType(), r.isEnabled())); } else { if (newRanges.get(newRanges.size() - 1).isEnabled() != r.isEnabled()) { throw new IllegalArgumentException("range conflict " + r); } if (r.getEndId() > newRanges.get(newRanges.size() - 1).getEndId()) { CellBroadcastIdRange range = newRanges.get(newRanges.size() - 1); newRanges.set(newRanges.size() - 1, new CellBroadcastIdRange( range.getStartId(), r.getEndId(), range.getType(), range.isEnabled())); } } }); return newRanges; } private void resetConfig() { mCbRanges3gpp.clear(); mCbRanges3gpp2.clear(); } private void setGsmConfig(List ranges, Request request) { if (DBG) { logd("setGsmConfig with " + ranges); } SmsBroadcastConfigInfo[] configs = new SmsBroadcastConfigInfo[ranges.size()]; for (int i = 0; i < configs.length; i++) { CellBroadcastIdRange r = ranges.get(i); configs[i] = new SmsBroadcastConfigInfo(r.getStartId(), r.getEndId(), SMS_CB_CODE_SCHEME_MIN, SMS_CB_CODE_SCHEME_MAX, r.isEnabled()); } Message response = obtainMessage(EVENT_CONFIGURATION_DONE, request); mPhone.mCi.setGsmBroadcastConfig(configs, response); } private void setCdmaConfig(List ranges, Request request) { if (DBG) { logd("setCdmaConfig with " + ranges); } CdmaSmsBroadcastConfigInfo[] configs = new CdmaSmsBroadcastConfigInfo[ranges.size()]; for (int i = 0; i < configs.length; i++) { CellBroadcastIdRange r = ranges.get(i); configs[i] = new CdmaSmsBroadcastConfigInfo( r.getStartId(), r.getEndId(), 1, r.isEnabled()); } Message response = obtainMessage(EVENT_CONFIGURATION_DONE, request); mPhone.mCi.setCdmaBroadcastConfig(configs, response); } private void setActivation(int type, boolean activate, Request request) { if (DBG) { logd("setActivation(" + type + "." + activate + ')'); } Message response = obtainMessage(EVENT_ACTIVATION_DONE, request); if (type == SmsCbMessage.MESSAGE_FORMAT_3GPP) { mPhone.mCi.setGsmBroadcastActivation(activate, response); } else if (type == SmsCbMessage.MESSAGE_FORMAT_3GPP2) { mPhone.mCi.setCdmaBroadcastActivation(activate, response); } } /** * Dump the state of CellBroadcastConfigTracker * * @param fd File descriptor * @param printWriter Print writer * @param args Arguments */ public void dump(FileDescriptor fd, PrintWriter printWriter, String[] args) { IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, " "); pw.println(CellBroadcastConfigTracker.class.getSimpleName() + "-" + mPhone.getPhoneId() + ":"); pw.increaseIndent(); pw.println("Current mCbRanges3gpp:" + mCbRanges3gpp); pw.println("Current mCbRanges3gpp2:" + mCbRanges3gpp2); pw.decreaseIndent(); pw.println("Local logs:"); pw.increaseIndent(); mLocalLog.dump(fd, pw, args); pw.decreaseIndent(); } }