575 lines
23 KiB
Java
575 lines
23 KiB
Java
/*
|
|
* 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<CellBroadcastIdRange> mCbRanges3gpp = new CopyOnWriteArrayList<>();
|
|
// Cache of current cell broadcast id ranges of 3gpp2
|
|
private List<CellBroadcastIdRange> 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<CellBroadcastIdRange> mCbRangesRequest3gpp =
|
|
new CopyOnWriteArrayList<>();
|
|
private final List<CellBroadcastIdRange> mCbRangesRequest3gpp2 =
|
|
new CopyOnWriteArrayList<>();
|
|
Consumer<Integer> mCallback;
|
|
|
|
Request(@NonNull List<CellBroadcastIdRange> ranges, @NonNull Consumer<Integer> callback) {
|
|
ranges.forEach(r -> {
|
|
if (r.getType() == SmsCbMessage.MESSAGE_FORMAT_3GPP) {
|
|
mCbRangesRequest3gpp.add(r);
|
|
} else {
|
|
mCbRangesRequest3gpp2.add(r);
|
|
}
|
|
});
|
|
mCallback = callback;
|
|
}
|
|
|
|
List<CellBroadcastIdRange> get3gppRanges() {
|
|
return mCbRangesRequest3gpp;
|
|
}
|
|
|
|
List<CellBroadcastIdRange> get3gpp2Ranges() {
|
|
return mCbRangesRequest3gpp2;
|
|
}
|
|
|
|
Consumer<Integer> 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<CellBroadcastIdRange> getCellBroadcastIdRanges() {
|
|
List<CellBroadcastIdRange> 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<CellBroadcastIdRange> ranges, @NonNull Consumer<Integer> 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<CellBroadcastIdRange> mergeRangesAsNeeded(
|
|
@NonNull List<CellBroadcastIdRange> 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<CellBroadcastIdRange> 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<CellBroadcastIdRange> 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<CellBroadcastIdRange> 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();
|
|
}
|
|
}
|