1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.phone; 18 19 import static android.telephony.ims.ImsStateCallback.REASON_IMS_SERVICE_DISCONNECTED; 20 import static android.telephony.ims.ImsStateCallback.REASON_IMS_SERVICE_NOT_READY; 21 import static android.telephony.ims.ImsStateCallback.REASON_NO_IMS_SERVICE_CONFIGURED; 22 import static android.telephony.ims.ImsStateCallback.REASON_SUBSCRIPTION_INACTIVE; 23 import static android.telephony.ims.ImsStateCallback.REASON_UNKNOWN_PERMANENT_ERROR; 24 import static android.telephony.ims.ImsStateCallback.REASON_UNKNOWN_TEMPORARY_ERROR; 25 import static android.telephony.ims.feature.ImsFeature.FEATURE_MMTEL; 26 import static android.telephony.ims.feature.ImsFeature.FEATURE_RCS; 27 import static android.telephony.ims.feature.ImsFeature.STATE_READY; 28 import static android.telephony.ims.feature.ImsFeature.STATE_UNAVAILABLE; 29 30 import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_DISCONNECTED; 31 import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_IMS_UNSUPPORTED; 32 import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_NOT_READY; 33 import static com.android.ims.FeatureConnector.UNAVAILABLE_REASON_SERVER_UNAVAILABLE; 34 35 import android.content.BroadcastReceiver; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.content.IntentFilter; 39 import android.os.AsyncResult; 40 import android.os.Bundle; 41 import android.os.Handler; 42 import android.os.HandlerThread; 43 import android.os.IBinder; 44 import android.os.Looper; 45 import android.os.Message; 46 import android.telephony.CarrierConfigManager; 47 import android.telephony.SubscriptionManager; 48 import android.telephony.TelephonyRegistryManager; 49 import android.telephony.ims.feature.ImsFeature; 50 import android.util.LocalLog; 51 import android.util.Log; 52 import android.util.SparseArray; 53 54 import com.android.ims.FeatureConnector; 55 import com.android.ims.ImsManager; 56 import com.android.ims.RcsFeatureManager; 57 import com.android.internal.annotations.VisibleForTesting; 58 import com.android.internal.telephony.IImsStateCallback; 59 import com.android.internal.telephony.Phone; 60 import com.android.internal.telephony.PhoneConfigurationManager; 61 import com.android.internal.telephony.PhoneFactory; 62 import com.android.internal.telephony.ims.ImsResolver; 63 import com.android.internal.telephony.util.HandlerExecutor; 64 import com.android.internal.util.IndentingPrintWriter; 65 import com.android.services.telephony.rcs.RcsFeatureController; 66 import com.android.telephony.Rlog; 67 68 import java.util.ArrayList; 69 import java.util.Arrays; 70 import java.util.HashMap; 71 import java.util.concurrent.Executor; 72 73 /** 74 * Implementation of the controller managing {@link ImsStateCallback}s 75 */ 76 public class ImsStateCallbackController { 77 private static final String TAG = "ImsStateCallbackController"; 78 private static final boolean VDBG = false; 79 private static final int LOG_SIZE = 50; 80 81 /** 82 * Create a FeatureConnector for this class to use to connect to an ImsManager. 83 */ 84 @VisibleForTesting 85 public interface MmTelFeatureConnectorFactory { 86 /** 87 * Create a FeatureConnector for this class to use to connect to an ImsManager. 88 * @param listener will receive ImsManager instance. 89 * @param executor that the Listener callbacks will be called on. 90 * @return A FeatureConnector 91 */ create(Context context, int slotId, String logPrefix, FeatureConnector.Listener<ImsManager> listener, Executor executor)92 FeatureConnector<ImsManager> create(Context context, int slotId, 93 String logPrefix, FeatureConnector.Listener<ImsManager> listener, 94 Executor executor); 95 } 96 97 /** 98 * Create a FeatureConnector for this class to use to connect to an RcsFeatureManager. 99 */ 100 @VisibleForTesting 101 public interface RcsFeatureConnectorFactory { 102 /** 103 * Create a FeatureConnector for this class to use to connect to an RcsFeatureManager. 104 * @param listener will receive RcsFeatureManager instance. 105 * @param executor that the Listener callbacks will be called on. 106 * @return A FeatureConnector 107 */ create(Context context, int slotId, FeatureConnector.Listener<RcsFeatureManager> listener, Executor executor, String logPrefix)108 FeatureConnector<RcsFeatureManager> create(Context context, int slotId, 109 FeatureConnector.Listener<RcsFeatureManager> listener, 110 Executor executor, String logPrefix); 111 } 112 113 /** Indicates that the state is not valid, used in ExternalRcsFeatureState only */ 114 private static final int STATE_UNKNOWN = -1; 115 116 /** The unavailable reason of ImsFeature is not initialized */ 117 private static final int NOT_INITIALIZED = -1; 118 /** The ImsFeature is available. */ 119 private static final int AVAILABLE = 0; 120 121 private static final int EVENT_SUB_CHANGED = 1; 122 private static final int EVENT_REGISTER_CALLBACK = 2; 123 private static final int EVENT_UNREGISTER_CALLBACK = 3; 124 private static final int EVENT_CARRIER_CONFIG_CHANGED = 4; 125 private static final int EVENT_EXTERNAL_RCS_STATE_CHANGED = 5; 126 private static final int EVENT_MSIM_CONFIGURATION_CHANGE = 6; 127 128 private static ImsStateCallbackController sInstance; 129 private static final LocalLog sLocalLog = new LocalLog(LOG_SIZE); 130 131 /** 132 * get the instance 133 */ getInstance()134 public static ImsStateCallbackController getInstance() { 135 synchronized (ImsStateCallbackController.class) { 136 return sInstance; 137 } 138 } 139 140 private final PhoneGlobals mApp; 141 private final Handler mHandler; 142 private final ImsResolver mImsResolver; 143 private final SparseArray<MmTelFeatureListener> mMmTelFeatureListeners = new SparseArray<>(); 144 private final SparseArray<RcsFeatureListener> mRcsFeatureListeners = new SparseArray<>(); 145 146 private final SubscriptionManager mSubscriptionManager; 147 private final TelephonyRegistryManager mTelephonyRegistryManager; 148 private MmTelFeatureConnectorFactory mMmTelFeatureFactory; 149 private RcsFeatureConnectorFactory mRcsFeatureFactory; 150 151 private HashMap<IBinder, CallbackWrapper> mWrappers = new HashMap<>(); 152 153 private final Object mDumpLock = new Object(); 154 155 private int mNumSlots; 156 157 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 158 @Override 159 public void onReceive(Context context, Intent intent) { 160 if (intent == null) { 161 return; 162 } 163 if (CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED.equals(intent.getAction())) { 164 Bundle bundle = intent.getExtras(); 165 if (bundle == null) { 166 return; 167 } 168 int slotId = bundle.getInt(CarrierConfigManager.EXTRA_SLOT_INDEX, 169 SubscriptionManager.INVALID_PHONE_INDEX); 170 int subId = bundle.getInt(CarrierConfigManager.EXTRA_SUBSCRIPTION_INDEX, 171 SubscriptionManager.INVALID_SUBSCRIPTION_ID); 172 173 if (slotId <= SubscriptionManager.INVALID_SIM_SLOT_INDEX) { 174 loge("onReceive ACTION_CARRIER_CONFIG_CHANGED invalid slotId"); 175 return; 176 } 177 178 if (subId <= SubscriptionManager.INVALID_SUBSCRIPTION_ID) { 179 loge("onReceive ACTION_CARRIER_CONFIG_CHANGED invalid subId"); 180 //subscription changed will be notified by mSubChangedListener 181 return; 182 } 183 184 notifyCarrierConfigChanged(slotId); 185 } 186 } 187 }; 188 189 private final SubscriptionManager.OnSubscriptionsChangedListener mSubChangedListener = 190 new SubscriptionManager.OnSubscriptionsChangedListener() { 191 @Override 192 public void onSubscriptionsChanged() { 193 if (!mHandler.hasMessages(EVENT_SUB_CHANGED)) { 194 mHandler.sendEmptyMessage(EVENT_SUB_CHANGED); 195 } 196 } 197 }; 198 199 private final class MyHandler extends Handler { MyHandler(Looper looper)200 MyHandler(Looper looper) { 201 super(looper); 202 } 203 204 @Override handleMessage(Message msg)205 public void handleMessage(Message msg) { 206 if (VDBG) logv("handleMessage: " + msg); 207 synchronized (mDumpLock) { 208 switch (msg.what) { 209 case EVENT_SUB_CHANGED: 210 onSubChanged(); 211 break; 212 213 case EVENT_REGISTER_CALLBACK: 214 onRegisterCallback((ImsStateCallbackController.CallbackWrapper) msg.obj); 215 break; 216 217 case EVENT_UNREGISTER_CALLBACK: 218 onUnregisterCallback((IImsStateCallback) msg.obj); 219 break; 220 221 case EVENT_CARRIER_CONFIG_CHANGED: 222 onCarrierConfigChanged(msg.arg1); 223 break; 224 225 case EVENT_EXTERNAL_RCS_STATE_CHANGED: 226 if (msg.obj == null) break; 227 onExternalRcsStateChanged((ExternalRcsFeatureState) msg.obj); 228 break; 229 230 case EVENT_MSIM_CONFIGURATION_CHANGE: 231 AsyncResult result = (AsyncResult) msg.obj; 232 Integer numSlots = (Integer) result.result; 233 if (numSlots == null) { 234 Log.w(TAG, "msim config change with null num slots"); 235 break; 236 } 237 updateFeatureControllerSize(numSlots); 238 break; 239 240 default: 241 loge("Unhandled event " + msg.what); 242 } 243 } 244 } 245 } 246 247 private final class MmTelFeatureListener implements FeatureConnector.Listener<ImsManager> { 248 private FeatureConnector<ImsManager> mConnector; 249 private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; 250 private int mState = STATE_UNAVAILABLE; 251 private int mReason = REASON_IMS_SERVICE_DISCONNECTED; 252 253 /* 254 * Remember the last return of verifyImsMmTelConfigured(). 255 * true means ImsResolver found an IMS package for FEATURE_MMTEL. 256 * 257 * mReason is updated through connectionUnavailable triggered by ImsResolver. 258 * mHasConfig is update through notifyConfigChanged triggered by mReceiver. 259 * mHasConfig can be a redundancy of (mReason == REASON_NO_IMS_SERVICE_CONFIGURED). 260 * However, when a carrier config changes, we are not sure the order 261 * of execution of connectionUnavailable and notifyConfigChanged. 262 * So, it's safe to use a separated state to retain it. 263 * We assume mHasConfig is true, until it's determined explicitly. 264 */ 265 private boolean mHasConfig = true; 266 267 private int mSlotId = -1; 268 private String mLogPrefix = ""; 269 MmTelFeatureListener(int slotId)270 MmTelFeatureListener(int slotId) { 271 mSlotId = slotId; 272 mLogPrefix = "[" + slotId + ", MMTEL] "; 273 if (VDBG) logv(mLogPrefix + "created"); 274 275 mConnector = mMmTelFeatureFactory.create( 276 mApp, slotId, TAG, this, new HandlerExecutor(mHandler)); 277 mConnector.connect(); 278 } 279 setSubId(int subId)280 void setSubId(int subId) { 281 if (VDBG) logv(mLogPrefix + "setSubId mSubId=" + mSubId + ", subId=" + subId); 282 if (mSubId == subId) return; 283 logd(mLogPrefix + "setSubId changed subId=" + subId); 284 285 mSubId = subId; 286 } 287 destroy()288 void destroy() { 289 if (VDBG) logv(mLogPrefix + "destroy"); 290 mConnector.disconnect(); 291 mConnector = null; 292 } 293 294 @Override connectionReady(ImsManager manager, int subId)295 public void connectionReady(ImsManager manager, int subId) { 296 logd(mLogPrefix + "connectionReady " + subId); 297 298 mSubId = subId; 299 if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) return; 300 301 mState = STATE_READY; 302 mReason = AVAILABLE; 303 mHasConfig = true; 304 onFeatureStateChange(mSubId, FEATURE_MMTEL, mState, mReason); 305 } 306 307 @Override connectionUnavailable(int reason)308 public void connectionUnavailable(int reason) { 309 logd(mLogPrefix + "connectionUnavailable reason=" + connectorReasonToString(reason)); 310 311 reason = convertReasonType(reason); 312 if (mReason == reason) return; 313 314 connectionUnavailableInternal(reason); 315 } 316 connectionUnavailableInternal(int reason)317 private void connectionUnavailableInternal(int reason) { 318 mState = STATE_UNAVAILABLE; 319 mReason = reason; 320 321 /* If having no IMS package for MMTEL, 322 * dicard the reason except REASON_NO_IMS_SERVICE_CONFIGURED. */ 323 if (!mHasConfig && reason != REASON_NO_IMS_SERVICE_CONFIGURED) return; 324 325 onFeatureStateChange(mSubId, FEATURE_MMTEL, mState, mReason); 326 } 327 notifyConfigChanged(boolean hasConfig)328 void notifyConfigChanged(boolean hasConfig) { 329 if (mHasConfig == hasConfig) return; 330 331 logd(mLogPrefix + "notifyConfigChanged " + hasConfig); 332 333 mHasConfig = hasConfig; 334 if (hasConfig) { 335 // REASON_NO_IMS_SERVICE_CONFIGURED is already reported to the clients, 336 // since there is no configuration of IMS package for MMTEL. 337 // Now, a carrier configuration change is notified and 338 // the response from ImsResolver is changed from false to true. 339 if (mState != STATE_READY) { 340 if (mReason == REASON_NO_IMS_SERVICE_CONFIGURED) { 341 // In this case, notify clients the reason, REASON_DISCONNCTED, 342 // to update the state. 343 connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED); 344 } else { 345 // ImsResolver and ImsStateCallbackController run with different Looper. 346 // In this case, FeatureConnectorListener is updated ahead of this. 347 // But, connectionUnavailable didn't notify clients since mHasConfig is 348 // false. So, notify clients here. 349 connectionUnavailableInternal(mReason); 350 } 351 } 352 } else { 353 // FeatureConnector doesn't report UNAVAILABLE_REASON_IMS_UNSUPPORTED, 354 // so report the reason here. 355 connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED); 356 } 357 } 358 359 // called from onRegisterCallback notifyState(CallbackWrapper wrapper)360 boolean notifyState(CallbackWrapper wrapper) { 361 if (VDBG) logv(mLogPrefix + "notifyState subId=" + wrapper.mSubId); 362 363 return wrapper.notifyState(mSubId, FEATURE_MMTEL, mState, mReason); 364 } 365 dump(IndentingPrintWriter pw)366 void dump(IndentingPrintWriter pw) { 367 pw.println("Listener={slotId=" + mSlotId 368 + ", subId=" + mSubId 369 + ", state=" + ImsFeature.STATE_LOG_MAP.get(mState) 370 + ", reason=" + imsStateReasonToString(mReason) 371 + ", hasConfig=" + mHasConfig 372 + "}"); 373 } 374 } 375 376 private final class RcsFeatureListener implements FeatureConnector.Listener<RcsFeatureManager> { 377 private FeatureConnector<RcsFeatureManager> mConnector; 378 private int mSubId = SubscriptionManager.INVALID_SUBSCRIPTION_ID; 379 private int mState = STATE_UNAVAILABLE; 380 private int mReason = REASON_IMS_SERVICE_DISCONNECTED; 381 382 /* 383 * Remember the last return of verifyImsMmTelConfigured(). 384 * true means ImsResolver found an IMS package for FEATURE_RCS. 385 * 386 * mReason is updated through connectionUnavailable triggered by ImsResolver. 387 * mHasConfig is update through notifyConfigChanged triggered by mReceiver, 388 * and notifyExternalRcsState which triggered by TelephonyRcsService refers it. 389 * mHasConfig can be a redundancy of (mReason == REASON_NO_IMS_SERVICE_CONFIGURED). 390 * However, when a carrier config changes, we are not sure the order 391 * of execution of connectionUnavailable, notifyConfigChanged and notifyExternalRcsState. 392 * So, it's safe to use a separated state to retain it. 393 * We assume mHasConfig is true, until it's determined explicitly. 394 */ 395 private boolean mHasConfig = true; 396 397 /* 398 * TelephonyRcsService doesn’t try to connect to RcsFeature if there is no active feature 399 * for a given subscription. The active features are declared by carrier configs and 400 * configuration resources. The APIs of ImsRcsManager and SipDelegateManager are available 401 * only when the RcsFeatureController has a STATE_READY state connection. 402 * This configuration is different from the configuration of IMS package for RCS. 403 * ImsStateCallbackController's FeatureConnectorListener can be STATE_READY state, 404 * even in case there is no active RCS feature. But Manager's APIs throws exception. 405 * 406 * For RCS, in addition to mHasConfig, the sate of TelephonyRcsService and 407 * RcsFeatureConnector will be traced to determine the state to be notified to clients. 408 */ 409 private ExternalRcsFeatureState mExternalState = null; 410 411 private int mSlotId = -1; 412 private String mLogPrefix = ""; 413 RcsFeatureListener(int slotId)414 RcsFeatureListener(int slotId) { 415 mSlotId = slotId; 416 mLogPrefix = "[" + slotId + ", RCS] "; 417 if (VDBG) logv(mLogPrefix + "created"); 418 419 mConnector = mRcsFeatureFactory.create( 420 mApp, slotId, this, new HandlerExecutor(mHandler), TAG); 421 mConnector.connect(); 422 } 423 setSubId(int subId)424 void setSubId(int subId) { 425 if (VDBG) logv(mLogPrefix + "setSubId mSubId=" + mSubId + ", subId=" + subId); 426 if (mSubId == subId) return; 427 logd(mLogPrefix + "setSubId changed subId=" + subId); 428 429 mSubId = subId; 430 } 431 destroy()432 void destroy() { 433 if (VDBG) logv(mLogPrefix + "destroy"); 434 435 mConnector.disconnect(); 436 mConnector = null; 437 } 438 439 @Override connectionReady(RcsFeatureManager manager, int subId)440 public void connectionReady(RcsFeatureManager manager, int subId) { 441 logd(mLogPrefix + "connectionReady " + subId); 442 443 mSubId = subId; 444 if (subId == SubscriptionManager.INVALID_SUBSCRIPTION_ID) return; 445 446 mState = STATE_READY; 447 mReason = AVAILABLE; 448 mHasConfig = true; 449 450 if (mExternalState != null && mExternalState.isReady()) { 451 onFeatureStateChange(mSubId, FEATURE_RCS, mState, mReason); 452 } 453 } 454 455 @Override connectionUnavailable(int reason)456 public void connectionUnavailable(int reason) { 457 logd(mLogPrefix + "connectionUnavailable reason=" + connectorReasonToString(reason)); 458 459 reason = convertReasonType(reason); 460 if (mReason == reason) return; 461 462 connectionUnavailableInternal(reason); 463 } 464 connectionUnavailableInternal(int reason)465 private void connectionUnavailableInternal(int reason) { 466 mState = STATE_UNAVAILABLE; 467 mReason = reason; 468 469 /* If having no IMS package for RCS, 470 * dicard the reason except REASON_NO_IMS_SERVICE_CONFIGURED. */ 471 if (!mHasConfig && reason != REASON_NO_IMS_SERVICE_CONFIGURED) return; 472 473 if (mExternalState == null && reason != REASON_NO_IMS_SERVICE_CONFIGURED) { 474 // Wait until TelephonyRcsService notifies its state. 475 return; 476 } 477 478 if (mExternalState != null && !mExternalState.hasActiveFeatures()) { 479 // notifyExternalRcsState has notified REASON_NO_IMS_SERVICE_CONFIGURED already 480 // ignore it 481 return; 482 } 483 484 if ((mExternalState != null && mExternalState.hasActiveFeatures()) 485 || mReason == REASON_NO_IMS_SERVICE_CONFIGURED) { 486 onFeatureStateChange(mSubId, FEATURE_RCS, mState, mReason); 487 } 488 } 489 notifyConfigChanged(boolean hasConfig)490 void notifyConfigChanged(boolean hasConfig) { 491 if (mHasConfig == hasConfig) return; 492 493 logd(mLogPrefix + "notifyConfigChanged " + hasConfig); 494 495 mHasConfig = hasConfig; 496 if (hasConfig) { 497 // REASON_NO_IMS_SERVICE_CONFIGURED is already reported to the clients, 498 // since there is no configuration of IMS package for RCS. 499 // Now, a carrier configuration change is notified and 500 // the response from ImsResolver is changed from false to true. 501 if (mState != STATE_READY) { 502 if (mReason == REASON_NO_IMS_SERVICE_CONFIGURED) { 503 // In this case, notify clients the reason, REASON_DISCONNCTED, 504 // to update the state. 505 connectionUnavailable(UNAVAILABLE_REASON_DISCONNECTED); 506 } else { 507 // ImsResolver and ImsStateCallbackController run with different Looper. 508 // In this case, FeatureConnectorListener is updated ahead of this. 509 // But, connectionUnavailable didn't notify clients since mHasConfig is 510 // false. So, notify clients here. 511 connectionUnavailableInternal(mReason); 512 } 513 } 514 } else { 515 // FeatureConnector doesn't report UNAVAILABLE_REASON_IMS_UNSUPPORTED, 516 // so report the reason here. 517 connectionUnavailable(UNAVAILABLE_REASON_IMS_UNSUPPORTED); 518 } 519 } 520 notifyExternalRcsState(ExternalRcsFeatureState fs)521 void notifyExternalRcsState(ExternalRcsFeatureState fs) { 522 if (VDBG) { 523 logv(mLogPrefix + "notifyExternalRcsState" 524 + " state=" + (fs.mState == STATE_UNKNOWN 525 ? "" : ImsFeature.STATE_LOG_MAP.get(fs.mState)) 526 + ", reason=" + imsStateReasonToString(fs.mReason)); 527 } 528 529 ExternalRcsFeatureState oldFs = mExternalState; 530 // External state is from TelephonyRcsService while a feature is added or removed. 531 if (fs.mState == STATE_UNKNOWN) { 532 if (oldFs != null) fs.mState = oldFs.mState; 533 else fs.mState = STATE_UNAVAILABLE; 534 } 535 536 mExternalState = fs; 537 538 // No IMS package found. 539 // REASON_NO_IMS_SERVICE_CONFIGURED is notified to clients already. 540 if (!mHasConfig) return; 541 542 if (fs.hasActiveFeatures()) { 543 if (mState == STATE_READY) { 544 if ((oldFs == null || !oldFs.isReady()) && fs.isReady()) { 545 // it is waiting RcsFeatureConnector's notification. 546 // notify clients here. 547 onFeatureStateChange(mSubId, FEATURE_RCS, mState, mReason); 548 } else if (!fs.isReady()) { 549 // Wait RcsFeatureConnector's notification 550 } else { 551 // ignore duplicated notification 552 } 553 } 554 } else { 555 // notify only once 556 if (oldFs == null || oldFs.hasActiveFeatures()) { 557 if (mReason != REASON_NO_IMS_SERVICE_CONFIGURED) { 558 onFeatureStateChange( 559 mSubId, FEATURE_RCS, STATE_UNAVAILABLE, 560 REASON_NO_IMS_SERVICE_CONFIGURED); 561 } 562 } else { 563 // ignore duplicated notification 564 } 565 } 566 } 567 568 // called from onRegisterCallback notifyState(CallbackWrapper wrapper)569 boolean notifyState(CallbackWrapper wrapper) { 570 if (VDBG) logv(mLogPrefix + "notifyState subId=" + wrapper.mSubId); 571 572 if (mHasConfig) { 573 if (mExternalState == null) { 574 // Wait until TelephonyRcsService notifies its state. 575 return wrapper.notifyState(mSubId, FEATURE_RCS, STATE_UNAVAILABLE, 576 REASON_IMS_SERVICE_DISCONNECTED); 577 } else if (!mExternalState.hasActiveFeatures()) { 578 return wrapper.notifyState(mSubId, FEATURE_RCS, STATE_UNAVAILABLE, 579 REASON_NO_IMS_SERVICE_CONFIGURED); 580 } 581 } 582 583 return wrapper.notifyState(mSubId, FEATURE_RCS, mState, mReason); 584 } 585 dump(IndentingPrintWriter pw)586 void dump(IndentingPrintWriter pw) { 587 pw.println("Listener={slotId=" + mSlotId 588 + ", subId=" + mSubId 589 + ", state=" + ImsFeature.STATE_LOG_MAP.get(mState) 590 + ", reason=" + imsStateReasonToString(mReason) 591 + ", hasConfig=" + mHasConfig 592 + ", isReady=" + (mExternalState == null ? false : mExternalState.isReady()) 593 + ", hasFeatures=" + (mExternalState == null ? false 594 : mExternalState.hasActiveFeatures()) 595 + "}"); 596 } 597 } 598 599 /** 600 * A wrapper class for the callback registered 601 */ 602 private static class CallbackWrapper { 603 private final int mSubId; 604 private final int mRequiredFeature; 605 private final IImsStateCallback mCallback; 606 private final IBinder mBinder; 607 private final String mCallingPackage; 608 private int mLastReason = NOT_INITIALIZED; 609 CallbackWrapper(int subId, int feature, IImsStateCallback callback, String callingPackage)610 CallbackWrapper(int subId, int feature, IImsStateCallback callback, 611 String callingPackage) { 612 mSubId = subId; 613 mRequiredFeature = feature; 614 mCallback = callback; 615 mBinder = callback.asBinder(); 616 mCallingPackage = callingPackage; 617 } 618 619 /** 620 * @return false when accessing callback binder throws an Exception. 621 * That means the callback binder is not valid any longer. 622 * The death of remote process can cause this. 623 * This instance shall be removed from the list. 624 */ notifyState(int subId, int feature, int state, int reason)625 boolean notifyState(int subId, int feature, int state, int reason) { 626 if (VDBG) { 627 logv("CallbackWrapper notifyState subId=" + subId 628 + ", feature=" + ImsFeature.FEATURE_LOG_MAP.get(feature) 629 + ", state=" + ImsFeature.STATE_LOG_MAP.get(state) 630 + ", reason=" + imsStateReasonToString(reason)); 631 } 632 633 try { 634 if (state == STATE_READY) { 635 mCallback.onAvailable(); 636 } else { 637 mCallback.onUnavailable(reason); 638 } 639 mLastReason = reason; 640 } catch (Exception e) { 641 loge("CallbackWrapper notifyState e=" + e); 642 return false; 643 } 644 645 return true; 646 } 647 notifyInactive()648 void notifyInactive() { 649 logd("CallbackWrapper notifyInactive subId=" + mSubId); 650 651 try { 652 mCallback.onUnavailable(REASON_SUBSCRIPTION_INACTIVE); 653 } catch (Exception e) { 654 // ignored 655 } 656 } 657 dump(IndentingPrintWriter pw)658 void dump(IndentingPrintWriter pw) { 659 pw.println("CallbackWrapper={subId=" + mSubId 660 + ", feature=" + ImsFeature.FEATURE_LOG_MAP.get(mRequiredFeature) 661 + ", reason=" + imsStateReasonToString(mLastReason) 662 + ", pkg=" + mCallingPackage 663 + "}"); 664 } 665 } 666 667 private static class ExternalRcsFeatureState { 668 private int mSlotId; 669 private int mState = STATE_UNAVAILABLE; 670 private int mReason = NOT_INITIALIZED; 671 ExternalRcsFeatureState(int slotId, int state, int reason)672 ExternalRcsFeatureState(int slotId, int state, int reason) { 673 mSlotId = slotId; 674 mState = state; 675 mReason = reason; 676 } 677 hasActiveFeatures()678 boolean hasActiveFeatures() { 679 return mReason != REASON_NO_IMS_SERVICE_CONFIGURED; 680 } 681 isReady()682 boolean isReady() { 683 return mState == STATE_READY; 684 } 685 } 686 687 /** 688 * create an instance 689 */ make(PhoneGlobals app, int numSlots)690 public static ImsStateCallbackController make(PhoneGlobals app, int numSlots) { 691 synchronized (ImsStateCallbackController.class) { 692 if (sInstance == null) { 693 logd("ImsStateCallbackController created"); 694 695 HandlerThread handlerThread = new HandlerThread(TAG); 696 handlerThread.start(); 697 sInstance = new ImsStateCallbackController(app, handlerThread.getLooper(), numSlots, 698 ImsManager::getConnector, RcsFeatureManager::getConnector, 699 ImsResolver.getInstance()); 700 } 701 } 702 return sInstance; 703 } 704 705 @VisibleForTesting ImsStateCallbackController(PhoneGlobals app, Looper looper, int numSlots, MmTelFeatureConnectorFactory mmTelFactory, RcsFeatureConnectorFactory rcsFactory, ImsResolver imsResolver)706 public ImsStateCallbackController(PhoneGlobals app, Looper looper, int numSlots, 707 MmTelFeatureConnectorFactory mmTelFactory, RcsFeatureConnectorFactory rcsFactory, 708 ImsResolver imsResolver) { 709 mApp = app; 710 mHandler = new MyHandler(looper); 711 mImsResolver = imsResolver; 712 mSubscriptionManager = mApp.getSystemService(SubscriptionManager.class); 713 mTelephonyRegistryManager = mApp.getSystemService(TelephonyRegistryManager.class); 714 mMmTelFeatureFactory = mmTelFactory; 715 mRcsFeatureFactory = rcsFactory; 716 717 updateFeatureControllerSize(numSlots); 718 719 mTelephonyRegistryManager.addOnSubscriptionsChangedListener( 720 mSubChangedListener, mSubChangedListener.getHandlerExecutor()); 721 722 PhoneConfigurationManager.registerForMultiSimConfigChange(mHandler, 723 EVENT_MSIM_CONFIGURATION_CHANGE, null); 724 725 mApp.registerReceiver(mReceiver, new IntentFilter( 726 CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)); 727 728 onSubChanged(); 729 } 730 731 /** 732 * Update the number of {@link RcsFeatureController}s that are created based on the number of 733 * active slots on the device. 734 */ 735 @VisibleForTesting updateFeatureControllerSize(int newNumSlots)736 public void updateFeatureControllerSize(int newNumSlots) { 737 if (mNumSlots != newNumSlots) { 738 logd("updateFeatures: oldSlots=" + mNumSlots 739 + ", newNumSlots=" + newNumSlots); 740 if (mNumSlots < newNumSlots) { 741 for (int i = mNumSlots; i < newNumSlots; i++) { 742 MmTelFeatureListener m = new MmTelFeatureListener(i); 743 mMmTelFeatureListeners.put(i, m); 744 RcsFeatureListener r = new RcsFeatureListener(i); 745 mRcsFeatureListeners.put(i, r); 746 } 747 } else { 748 for (int i = (mNumSlots - 1); i > (newNumSlots - 1); i--) { 749 MmTelFeatureListener m = mMmTelFeatureListeners.get(i); 750 if (m != null) { 751 mMmTelFeatureListeners.remove(i); 752 m.destroy(); 753 } 754 RcsFeatureListener r = mRcsFeatureListeners.get(i); 755 if (r != null) { 756 mRcsFeatureListeners.remove(i); 757 r.destroy(); 758 } 759 } 760 } 761 } 762 mNumSlots = newNumSlots; 763 } 764 765 /** 766 * Dependencies for testing. 767 */ 768 @VisibleForTesting onSubChanged()769 public void onSubChanged() { 770 for (int i = 0; i < mMmTelFeatureListeners.size(); i++) { 771 MmTelFeatureListener l = mMmTelFeatureListeners.valueAt(i); 772 l.setSubId(getSubId(i)); 773 } 774 775 for (int i = 0; i < mRcsFeatureListeners.size(); i++) { 776 RcsFeatureListener l = mRcsFeatureListeners.valueAt(i); 777 l.setSubId(getSubId(i)); 778 } 779 780 if (mWrappers.size() == 0) return; 781 782 ArrayList<IBinder> inactiveCallbacks = new ArrayList<>(); 783 final int[] activeSubs = mSubscriptionManager.getActiveSubscriptionIdList(); 784 785 if (VDBG) logv("onSubChanged activeSubs=" + Arrays.toString(activeSubs)); 786 787 // Remove callbacks for inactive subscriptions 788 for (IBinder binder : mWrappers.keySet()) { 789 CallbackWrapper wrapper = mWrappers.get(binder); 790 if (wrapper != null) { 791 if (!isActive(activeSubs, wrapper.mSubId)) { 792 // inactive subscription 793 inactiveCallbacks.add(binder); 794 } 795 } else { 796 // unexpected, remove it 797 inactiveCallbacks.add(binder); 798 } 799 } 800 removeInactiveCallbacks(inactiveCallbacks, "onSubChanged"); 801 } 802 onFeatureStateChange(int subId, int feature, int state, int reason)803 private void onFeatureStateChange(int subId, int feature, int state, int reason) { 804 if (VDBG) { 805 logv("onFeatureStateChange subId=" + subId 806 + ", feature=" + ImsFeature.FEATURE_LOG_MAP.get(feature) 807 + ", state=" + ImsFeature.STATE_LOG_MAP.get(state) 808 + ", reason=" + imsStateReasonToString(reason)); 809 } 810 811 ArrayList<IBinder> inactiveCallbacks = new ArrayList<>(); 812 mWrappers.values().forEach(wrapper -> { 813 if (subId == wrapper.mSubId 814 && feature == wrapper.mRequiredFeature 815 && !wrapper.notifyState(subId, feature, state, reason)) { 816 // callback has exception, remove it 817 inactiveCallbacks.add(wrapper.mBinder); 818 } 819 }); 820 removeInactiveCallbacks(inactiveCallbacks, "onFeatureStateChange"); 821 } 822 onRegisterCallback(CallbackWrapper wrapper)823 private void onRegisterCallback(CallbackWrapper wrapper) { 824 if (wrapper == null) return; 825 826 if (VDBG) logv("onRegisterCallback before size=" + mWrappers.size()); 827 if (VDBG) { 828 logv("onRegisterCallback subId=" + wrapper.mSubId 829 + ", feature=" + wrapper.mRequiredFeature); 830 } 831 832 // Not sure the following case can happen or not: 833 // step1) Subscription changed 834 // step2) ImsStateCallbackController not processed onSubChanged yet 835 // step3) Client registers with a strange subId 836 // The validity of the subId is checked PhoneInterfaceManager#registerImsStateCallback. 837 // So, register the wrapper here before trying to notifyState. 838 // TODO: implement the recovery for this case, notifying the current reson, in onSubChanged 839 mWrappers.put(wrapper.mBinder, wrapper); 840 841 if (wrapper.mRequiredFeature == FEATURE_MMTEL) { 842 for (int i = 0; i < mMmTelFeatureListeners.size(); i++) { 843 if (wrapper.mSubId == getSubId(i)) { 844 MmTelFeatureListener l = mMmTelFeatureListeners.valueAt(i); 845 if (!l.notifyState(wrapper)) { 846 mWrappers.remove(wrapper.mBinder); 847 } 848 break; 849 } 850 } 851 } else if (wrapper.mRequiredFeature == FEATURE_RCS) { 852 for (int i = 0; i < mRcsFeatureListeners.size(); i++) { 853 if (wrapper.mSubId == getSubId(i)) { 854 RcsFeatureListener l = mRcsFeatureListeners.valueAt(i); 855 if (!l.notifyState(wrapper)) { 856 mWrappers.remove(wrapper.mBinder); 857 } 858 break; 859 } 860 } 861 } 862 863 if (VDBG) logv("onRegisterCallback after size=" + mWrappers.size()); 864 } 865 onUnregisterCallback(IImsStateCallback cb)866 private void onUnregisterCallback(IImsStateCallback cb) { 867 if (cb == null) return; 868 mWrappers.remove(cb.asBinder()); 869 } 870 onCarrierConfigChanged(int slotId)871 private void onCarrierConfigChanged(int slotId) { 872 if (slotId >= mNumSlots) { 873 logd("onCarrierConfigChanged invalid slotId " 874 + slotId + ", mNumSlots=" + mNumSlots); 875 return; 876 } 877 878 logv("onCarrierConfigChanged slotId=" + slotId); 879 880 boolean hasConfig = verifyImsMmTelConfigured(slotId); 881 if (slotId < mMmTelFeatureListeners.size()) { 882 MmTelFeatureListener listener = mMmTelFeatureListeners.valueAt(slotId); 883 listener.notifyConfigChanged(hasConfig); 884 } 885 886 hasConfig = verifyImsRcsConfigured(slotId); 887 if (slotId < mRcsFeatureListeners.size()) { 888 RcsFeatureListener listener = mRcsFeatureListeners.valueAt(slotId); 889 listener.notifyConfigChanged(hasConfig); 890 } 891 } 892 onExternalRcsStateChanged(ExternalRcsFeatureState fs)893 private void onExternalRcsStateChanged(ExternalRcsFeatureState fs) { 894 logv("onExternalRcsStateChanged slotId=" + fs.mSlotId 895 + ", state=" + (fs.mState == STATE_UNKNOWN 896 ? "" : ImsFeature.STATE_LOG_MAP.get(fs.mState)) 897 + ", reason=" + imsStateReasonToString(fs.mReason)); 898 899 RcsFeatureListener listener = mRcsFeatureListeners.get(fs.mSlotId); 900 if (listener != null) { 901 listener.notifyExternalRcsState(fs); 902 } else { 903 // unexpected state 904 loge("onExternalRcsStateChanged slotId=" + fs.mSlotId + ", no listener."); 905 } 906 } 907 908 /** 909 * Interface to be notified from TelephonyRcsSerice and RcsFeatureController 910 * 911 * @param ready true if feature's state is STATE_READY. Valid only when it is true. 912 * @param hasActiveFeatures true if the RcsFeatureController has active features. 913 */ notifyExternalRcsStateChanged( int slotId, boolean ready, boolean hasActiveFeatures)914 public void notifyExternalRcsStateChanged( 915 int slotId, boolean ready, boolean hasActiveFeatures) { 916 int state = STATE_UNKNOWN; 917 int reason = REASON_IMS_SERVICE_DISCONNECTED; 918 919 if (ready) { 920 // From RcsFeatureController 921 state = STATE_READY; 922 reason = AVAILABLE; 923 } else if (!hasActiveFeatures) { 924 // From TelephonyRcsService 925 reason = REASON_NO_IMS_SERVICE_CONFIGURED; 926 state = STATE_UNAVAILABLE; 927 } else { 928 // From TelephonyRcsService 929 // TelephonyRcsService doesn't know the exact state of FeatureConnection. 930 // Only when there is no feature, we can assume the state. 931 } 932 933 if (VDBG) { 934 logv("notifyExternalRcsStateChanged slotId=" + slotId 935 + ", ready=" + ready 936 + ", hasActiveFeatures=" + hasActiveFeatures); 937 } 938 939 ExternalRcsFeatureState fs = new ExternalRcsFeatureState(slotId, state, reason); 940 mHandler.sendMessage(mHandler.obtainMessage(EVENT_EXTERNAL_RCS_STATE_CHANGED, fs)); 941 } 942 943 /** 944 * Notifies carrier configuration has changed. 945 */ 946 @VisibleForTesting notifyCarrierConfigChanged(int slotId)947 public void notifyCarrierConfigChanged(int slotId) { 948 if (VDBG) logv("notifyCarrierConfigChanged slotId=" + slotId); 949 mHandler.sendMessage(mHandler.obtainMessage(EVENT_CARRIER_CONFIG_CHANGED, slotId, 0)); 950 } 951 /** 952 * Register IImsStateCallback 953 * 954 * @param feature for which state is changed, ImsFeature.FEATURE_* 955 */ registerImsStateCallback(int subId, int feature, IImsStateCallback cb, String callingPackage)956 public void registerImsStateCallback(int subId, int feature, IImsStateCallback cb, 957 String callingPackage) { 958 if (VDBG) { 959 logv("registerImsStateCallback subId=" + subId 960 + ", feature=" + feature + ", pkg=" + callingPackage); 961 } 962 963 CallbackWrapper wrapper = new CallbackWrapper(subId, feature, cb, callingPackage); 964 mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_CALLBACK, wrapper)); 965 } 966 967 /** 968 * Unegister previously registered callback 969 */ unregisterImsStateCallback(IImsStateCallback cb)970 public void unregisterImsStateCallback(IImsStateCallback cb) { 971 if (VDBG) logv("unregisterImsStateCallback"); 972 973 mHandler.sendMessage(mHandler.obtainMessage(EVENT_UNREGISTER_CALLBACK, cb)); 974 } 975 removeInactiveCallbacks( ArrayList<IBinder> inactiveCallbacks, String message)976 private void removeInactiveCallbacks( 977 ArrayList<IBinder> inactiveCallbacks, String message) { 978 if (inactiveCallbacks == null || inactiveCallbacks.size() == 0) return; 979 980 if (VDBG) { 981 logv("removeInactiveCallbacks size=" 982 + inactiveCallbacks.size() + " from " + message); 983 } 984 985 for (IBinder binder : inactiveCallbacks) { 986 CallbackWrapper wrapper = mWrappers.get(binder); 987 if (wrapper != null) { 988 // Send the reason REASON_SUBSCRIPTION_INACTIVE to the client 989 wrapper.notifyInactive(); 990 mWrappers.remove(binder); 991 } 992 } 993 inactiveCallbacks.clear(); 994 } 995 getSubId(int slotId)996 private int getSubId(int slotId) { 997 Phone phone = mPhoneFactoryProxy.getPhone(slotId); 998 if (phone != null) return phone.getSubId(); 999 return SubscriptionManager.INVALID_SUBSCRIPTION_ID; 1000 } 1001 isActive(final int[] activeSubs, int subId)1002 private static boolean isActive(final int[] activeSubs, int subId) { 1003 for (int i : activeSubs) { 1004 if (i == subId) return true; 1005 } 1006 return false; 1007 } 1008 convertReasonType(int reason)1009 private static int convertReasonType(int reason) { 1010 switch(reason) { 1011 case UNAVAILABLE_REASON_NOT_READY: 1012 return REASON_IMS_SERVICE_NOT_READY; 1013 case UNAVAILABLE_REASON_IMS_UNSUPPORTED: 1014 return REASON_NO_IMS_SERVICE_CONFIGURED; 1015 default: 1016 break; 1017 } 1018 1019 return REASON_IMS_SERVICE_DISCONNECTED; 1020 } 1021 verifyImsMmTelConfigured(int slotId)1022 private boolean verifyImsMmTelConfigured(int slotId) { 1023 boolean ret = false; 1024 if (mImsResolver == null) { 1025 loge("verifyImsMmTelConfigured mImsResolver is null"); 1026 } else { 1027 ret = mImsResolver.isImsServiceConfiguredForFeature(slotId, FEATURE_MMTEL); 1028 } 1029 if (VDBG) logv("verifyImsMmTelConfigured slotId=" + slotId + ", ret=" + ret); 1030 return ret; 1031 } 1032 verifyImsRcsConfigured(int slotId)1033 private boolean verifyImsRcsConfigured(int slotId) { 1034 boolean ret = false; 1035 if (mImsResolver == null) { 1036 loge("verifyImsRcsConfigured mImsResolver is null"); 1037 } else { 1038 ret = mImsResolver.isImsServiceConfiguredForFeature(slotId, FEATURE_RCS); 1039 } 1040 if (VDBG) logv("verifyImsRcsConfigured slotId=" + slotId + ", ret=" + ret); 1041 return ret; 1042 } 1043 connectorReasonToString(int reason)1044 private static String connectorReasonToString(int reason) { 1045 switch(reason) { 1046 case UNAVAILABLE_REASON_DISCONNECTED: 1047 return "DISCONNECTED"; 1048 case UNAVAILABLE_REASON_NOT_READY: 1049 return "NOT_READY"; 1050 case UNAVAILABLE_REASON_IMS_UNSUPPORTED: 1051 return "IMS_UNSUPPORTED"; 1052 case UNAVAILABLE_REASON_SERVER_UNAVAILABLE: 1053 return "SERVER_UNAVAILABLE"; 1054 default: 1055 break; 1056 } 1057 return ""; 1058 } 1059 imsStateReasonToString(int reason)1060 private static String imsStateReasonToString(int reason) { 1061 switch(reason) { 1062 case AVAILABLE: 1063 return "READY"; 1064 case REASON_UNKNOWN_TEMPORARY_ERROR: 1065 return "UNKNOWN_TEMPORARY_ERROR"; 1066 case REASON_UNKNOWN_PERMANENT_ERROR: 1067 return "UNKNOWN_PERMANENT_ERROR"; 1068 case REASON_IMS_SERVICE_DISCONNECTED: 1069 return "IMS_SERVICE_DISCONNECTED"; 1070 case REASON_NO_IMS_SERVICE_CONFIGURED: 1071 return "NO_IMS_SERVICE_CONFIGURED"; 1072 case REASON_SUBSCRIPTION_INACTIVE: 1073 return "SUBSCRIPTION_INACTIVE"; 1074 case REASON_IMS_SERVICE_NOT_READY: 1075 return "IMS_SERVICE_NOT_READY"; 1076 default: 1077 break; 1078 } 1079 return ""; 1080 } 1081 1082 /** 1083 * PhoneFactory Dependencies for testing. 1084 */ 1085 @VisibleForTesting 1086 public interface PhoneFactoryProxy { 1087 /** 1088 * Override getPhone for testing. 1089 */ getPhone(int index)1090 Phone getPhone(int index); 1091 } 1092 1093 private PhoneFactoryProxy mPhoneFactoryProxy = new PhoneFactoryProxy() { 1094 @Override 1095 public Phone getPhone(int index) { 1096 return PhoneFactory.getPhone(index); 1097 } 1098 }; 1099 release()1100 private void release() { 1101 if (VDBG) logv("release"); 1102 1103 mTelephonyRegistryManager.removeOnSubscriptionsChangedListener(mSubChangedListener); 1104 mApp.unregisterReceiver(mReceiver); 1105 1106 for (int i = 0; i < mMmTelFeatureListeners.size(); i++) { 1107 mMmTelFeatureListeners.valueAt(i).destroy(); 1108 } 1109 mMmTelFeatureListeners.clear(); 1110 1111 for (int i = 0; i < mRcsFeatureListeners.size(); i++) { 1112 mRcsFeatureListeners.valueAt(i).destroy(); 1113 } 1114 mRcsFeatureListeners.clear(); 1115 } 1116 1117 /** 1118 * destroy the instance 1119 */ 1120 @VisibleForTesting destroy()1121 public void destroy() { 1122 if (VDBG) logv("destroy it"); 1123 1124 release(); 1125 mHandler.getLooper().quit(); 1126 } 1127 1128 /** 1129 * get the handler 1130 */ 1131 @VisibleForTesting getHandler()1132 public Handler getHandler() { 1133 return mHandler; 1134 } 1135 1136 /** 1137 * Determine whether the callback is registered or not 1138 */ 1139 @VisibleForTesting isRegistered(IImsStateCallback cb)1140 public boolean isRegistered(IImsStateCallback cb) { 1141 if (cb == null) return false; 1142 return mWrappers.containsKey(cb.asBinder()); 1143 } 1144 1145 /** 1146 * Dump this instance into a readable format for dumpsys usage. 1147 */ dump(IndentingPrintWriter pw)1148 public void dump(IndentingPrintWriter pw) { 1149 pw.increaseIndent(); 1150 synchronized (mDumpLock) { 1151 pw.println("CallbackWrappers:"); 1152 pw.increaseIndent(); 1153 mWrappers.values().forEach(wrapper -> wrapper.dump(pw)); 1154 pw.decreaseIndent(); 1155 pw.println("MmTelFeatureListeners:"); 1156 pw.increaseIndent(); 1157 for (int i = 0; i < mNumSlots; i++) { 1158 MmTelFeatureListener l = mMmTelFeatureListeners.get(i); 1159 if (l == null) continue; 1160 l.dump(pw); 1161 } 1162 pw.decreaseIndent(); 1163 pw.println("RcsFeatureListeners:"); 1164 pw.increaseIndent(); 1165 for (int i = 0; i < mNumSlots; i++) { 1166 RcsFeatureListener l = mRcsFeatureListeners.get(i); 1167 if (l == null) continue; 1168 l.dump(pw); 1169 } 1170 pw.decreaseIndent(); 1171 pw.println("Most recent logs:"); 1172 pw.increaseIndent(); 1173 sLocalLog.dump(pw); 1174 pw.decreaseIndent(); 1175 } 1176 pw.decreaseIndent(); 1177 } 1178 logv(String msg)1179 private static void logv(String msg) { 1180 Rlog.d(TAG, msg); 1181 } 1182 logd(String msg)1183 private static void logd(String msg) { 1184 Rlog.d(TAG, msg); 1185 sLocalLog.log(msg); 1186 } 1187 loge(String msg)1188 private static void loge(String msg) { 1189 Rlog.e(TAG, msg); 1190 sLocalLog.log(msg); 1191 } 1192 } 1193