/*
 * Copyright (C) 2020 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.ims.rcs.uce.presence.publish;

import static android.telephony.ims.RcsUceAdapter.PUBLISH_STATE_OK;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.PersistableBundle;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.telephony.CarrierConfigManager;
import android.telephony.ims.ImsException;
import android.telephony.ims.PublishAttributes;
import android.telephony.ims.RcsContactPresenceTuple;
import android.telephony.ims.RcsContactUceCapability;
import android.telephony.ims.RcsContactUceCapability.CapabilityMechanism;
import android.telephony.ims.RcsUceAdapter;
import android.telephony.ims.RcsUceAdapter.PublishState;
import android.telephony.ims.SipDetails;
import android.telephony.ims.aidl.IImsCapabilityCallback;
import android.telephony.ims.aidl.IRcsUcePublishStateCallback;
import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities;
import android.telephony.ims.feature.RcsFeature.RcsImsCapabilities.RcsImsCapabilityFlag;
import android.util.IndentingPrintWriter;
import android.util.LocalLog;
import android.util.Log;

import com.android.ims.RcsFeatureManager;
import com.android.ims.rcs.uce.UceController;
import com.android.ims.rcs.uce.UceController.UceControllerCallback;
import com.android.ims.rcs.uce.UceDeviceState;
import com.android.ims.rcs.uce.UceDeviceState.DeviceStateResult;
import com.android.ims.rcs.uce.UceStatsWriter;
import com.android.ims.rcs.uce.util.UceUtils;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;

import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * The implementation of PublishController.
 */
public class PublishControllerImpl implements PublishController {

    private static final String LOG_TAG = UceUtils.getLogPrefix() + "PublishController";

    /**
     * Used to inject PublishProcessor instances for testing.
     */
    @VisibleForTesting
    public interface PublishProcessorFactory {
        PublishProcessor createPublishProcessor(Context context, int subId,
                DeviceCapabilityInfo capabilityInfo, PublishControllerCallback callback);
    }

    /**
     * Used to inject DeviceCapabilityListener instances for testing.
     */
    @VisibleForTesting
    public interface DeviceCapListenerFactory {
        DeviceCapabilityListener createDeviceCapListener(Context context, int subId,
                DeviceCapabilityInfo capInfo, PublishControllerCallback callback,
                UceStatsWriter uceStatsWriter);
    }

    private final int mSubId;
    private final Context mContext;
    private final LocalLog mLocalLog = new LocalLog(UceUtils.LOG_SIZE);
    private PublishHandler mPublishHandler;
    private volatile boolean mIsDestroyedFlag;
    private volatile boolean mReceivePublishFromService;
    private volatile RcsFeatureManager mRcsFeatureManager;
    private final UceControllerCallback mUceCtrlCallback;
    private final UceStatsWriter mUceStatsWriter;

    // The capability type that the device is using.
    private @RcsImsCapabilityFlag int mCapabilityType;
    // The device publish state
    @VisibleForTesting
    public @PublishState int mLastPublishState;
    // The device publish state to support the newly added publish state
    @VisibleForTesting
    public @PublishState int mCurrentPublishState;

    // The timestamp of updating the publish state
    private Instant mPublishStateUpdatedTime = Instant.now();
    // The last PIDF XML used in the publish
    private String mPidfXml;

    // The callbacks to notify publish state changed.
    private RemoteCallbackList<IRcsUcePublishStateCallback> mPublishStateCallbacks;
    private final Object mPublishStateLock = new Object();

    // The information of the device's capabilities.
    private DeviceCapabilityInfo mDeviceCapabilityInfo;

    // The processor of publishing device's capabilities.
    private PublishProcessor mPublishProcessor;
    private PublishProcessorFactory mPublishProcessorFactory = (context, subId, capInfo, callback)
            -> new PublishProcessor(context, subId, capInfo, callback);

    // The listener to listen to the device's capabilities changed.
    private DeviceCapabilityListener mDeviceCapListener;
    private DeviceCapListenerFactory mDeviceCapListenerFactory = (context, subId, capInfo, callback,
            uceStatsWriter)
            -> new DeviceCapabilityListener(context, subId, capInfo, callback, uceStatsWriter);

    // Listen to the RCS availability status changed.
    private final IImsCapabilityCallback mRcsCapabilitiesCallback =
            new IImsCapabilityCallback.Stub() {
        @Override
        public void onQueryCapabilityConfiguration(
                int resultCapability, int resultRadioTech, boolean enabled) {
        }
        @Override
        public void onCapabilitiesStatusChanged(@RcsImsCapabilityFlag int capabilities) {
            logd("onCapabilitiesStatusChanged: " + capabilities);
            mPublishHandler.sendRcsCapabilitiesStatusChangedMsg(capabilities);
        }
        @Override
        public void onChangeCapabilityConfigurationError(int capability, int radioTech,
                int reason) {
        }
    };

    public PublishControllerImpl(Context context, int subId, UceControllerCallback callback,
            Looper looper) {
        mSubId = subId;
        mContext = context;
        mUceCtrlCallback = callback;
        mUceStatsWriter = UceStatsWriter.getInstance();
        logi("create");
        initPublishController(looper);
    }

    @VisibleForTesting
    public PublishControllerImpl(Context context, int subId, UceControllerCallback c,
            Looper looper, DeviceCapListenerFactory deviceCapFactory,
            PublishProcessorFactory processorFactory, UceStatsWriter instance) {
        mSubId = subId;
        mContext = context;
        mUceCtrlCallback = c;
        mDeviceCapListenerFactory = deviceCapFactory;
        mPublishProcessorFactory = processorFactory;
        mUceStatsWriter = instance;
        initPublishController(looper);
    }

    private void initPublishController(Looper looper) {
        mCapabilityType = PublishUtils.getCapabilityType(mContext, mSubId);
        mCurrentPublishState = getInitialPublishState(mCapabilityType);
        mLastPublishState = mCurrentPublishState;
        mPublishStateCallbacks = new RemoteCallbackList<>();
        mPublishHandler = new PublishHandler(this, looper);

        String[] serviceDescFeatureTagMap = getCarrierServiceDescriptionFeatureTagMap();
        mDeviceCapabilityInfo = new DeviceCapabilityInfo(mSubId, serviceDescFeatureTagMap);

        initPublishProcessor();
        initDeviceCapabilitiesListener();

        // Turn on the listener to listen to the device changes.
        mDeviceCapListener.initialize();

        logd("initPublishController completed: capabilityType=" + mCapabilityType +
                ", publishState=" + mCurrentPublishState);
    }

    /**
     * Get the initial publish state according to the given capability type.
     * <p>
     * The default publish state is NOT_PUBLISH when the capability type is PRESENCE.
     * The default publish state is OK when the capability type is SIP OPTIONS.
     * Otherwise, the default initial value is ERROR.
     */
    private int getInitialPublishState(@RcsImsCapabilityFlag int capabilityType) {
        if (capabilityType == RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE) {
            return RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED;
        } else if (capabilityType == RcsImsCapabilities.CAPABILITY_TYPE_OPTIONS_UCE) {
            return PUBLISH_STATE_OK;
        } else {
            return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
        }
    }

    private void initPublishProcessor() {
        mPublishProcessor = mPublishProcessorFactory.createPublishProcessor(mContext, mSubId,
                mDeviceCapabilityInfo, mPublishControllerCallback);
    }

    private void initDeviceCapabilitiesListener() {
        mDeviceCapListener = mDeviceCapListenerFactory.createDeviceCapListener(mContext, mSubId,
                mDeviceCapabilityInfo, mPublishControllerCallback, mUceStatsWriter);
    }

    @Override
    public void onRcsConnected(RcsFeatureManager manager) {
        logd("onRcsConnected");
        mPublishHandler.sendRcsConnectedMsg(manager);
    }

    @Override
    public void onRcsDisconnected() {
        logd("onRcsDisconnected");
        mPublishHandler.sendRcsDisconnectedMsg();
    }

    @Override
    public void onDestroy() {
        logi("onDestroy");
        mPublishHandler.sendDestroyedMsg();
    }

    @Override
    public void onCarrierConfigChanged() {
        logi("onCarrierConfigChanged");
        mPublishHandler.sendCarrierConfigChangedMsg();
    }

    @Override
    public int getUcePublishState(boolean isSupportPublishingState) {
        synchronized (mPublishStateLock) {
            if (mIsDestroyedFlag) {
                return RcsUceAdapter.PUBLISH_STATE_OTHER_ERROR;
            }
            if (isSupportPublishingState) {
                // in that case, the caller is support Build.VERSION_CODES.S
                // return the current state that is including newly added publishing state.
                return mCurrentPublishState;
            } else {
                if (mCurrentPublishState == RcsUceAdapter.PUBLISH_STATE_PUBLISHING) {
                    return mLastPublishState;
                }
                return mCurrentPublishState;
            }
        }
    }

    @Override
    public RcsContactUceCapability addRegistrationOverrideCapabilities(Set<String> featureTags) {
        if (mDeviceCapabilityInfo.addRegistrationOverrideCapabilities(featureTags)) {
            mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_OVERRIDE_CAPS);
        }
        return mDeviceCapabilityInfo.getDeviceCapabilities(
                RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
    }

    @Override
    public RcsContactUceCapability removeRegistrationOverrideCapabilities(Set<String> featureTags) {
        if (mDeviceCapabilityInfo.removeRegistrationOverrideCapabilities(featureTags)) {
            mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_OVERRIDE_CAPS);
        }
        return mDeviceCapabilityInfo.getDeviceCapabilities(
                RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
    }

    @Override
    public RcsContactUceCapability clearRegistrationOverrideCapabilities() {
        if (mDeviceCapabilityInfo.clearRegistrationOverrideCapabilities()) {
            mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_OVERRIDE_CAPS);
        }
        return mDeviceCapabilityInfo.getDeviceCapabilities(
                RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
    }

    @Override
    public RcsContactUceCapability getLatestRcsContactUceCapability() {
        return mDeviceCapabilityInfo.getDeviceCapabilities(
                RcsContactUceCapability.CAPABILITY_MECHANISM_PRESENCE, mContext);
    }

    @Override
    public String getLastPidfXml() {
        return mPidfXml;
    }

    /**
     * Register a {@link PublishStateCallback} to listen to the published state changed.
     */
    @Override
    public void registerPublishStateCallback(@NonNull IRcsUcePublishStateCallback c,
            boolean supportPublishingState) {
        synchronized (mPublishStateLock) {
            if (mIsDestroyedFlag) return;
            mPublishStateCallbacks.register(c, Boolean.valueOf(supportPublishingState));
            logd("registerPublishStateCallback: size="
                    + mPublishStateCallbacks.getRegisteredCallbackCount());
        }
        // Notify the current publish state
        mPublishHandler.sendNotifyCurrentPublishStateMessage(c, supportPublishingState);
    }

    /**
     * Removes an existing {@link PublishStateCallback}.
     */
    @Override
    public void unregisterPublishStateCallback(@NonNull IRcsUcePublishStateCallback c) {
        synchronized (mPublishStateLock) {
            if (mIsDestroyedFlag) return;
            mPublishStateCallbacks.unregister(c);
            logd("unregisterPublishStateCallback:mPublishStateCallbacks: size="
                    + mPublishStateCallbacks.getRegisteredCallbackCount());
        }
    }

    @Override
    public void setupResetDeviceStateTimer(long resetAfterSec) {
        logd("setupResetDeviceStateTimer: resetAfterSec=" + resetAfterSec);
        mPublishHandler.sendResetDeviceStateTimerMessage(resetAfterSec);
    }

    @Override
    public void clearResetDeviceStateTimer() {
        logd("clearResetDeviceStateTimer");
        mPublishHandler.clearResetDeviceStateTimer();
    }

    // Clear all the publish state callbacks since the publish controller instance is destroyed.
    private void clearPublishStateCallbacks() {
        synchronized (mPublishStateLock) {
            logd("clearPublishStateCallbacks");
            final int lastIndex = mPublishStateCallbacks.getRegisteredCallbackCount() - 1;
            for (int index = lastIndex; index >= 0; index--) {
                IRcsUcePublishStateCallback callback =
                        mPublishStateCallbacks.getRegisteredCallbackItem(index);
                mPublishStateCallbacks.unregister(callback);
            }
        }
    }

    /**
     * Notify that the device's capabilities has been unpublished from the network.
     */
    @Override
    public void onUnpublish() {
        logd("onUnpublish");
        if (mIsDestroyedFlag) return;
        mPublishHandler.sendUnpublishedMessage(RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED);
    }

    /**
     * Notify that the device's publish status have been changed.
     */
    @Override
    public void onPublishUpdated(@NonNull SipDetails details) {
        if (mIsDestroyedFlag) return;
        mPublishHandler.sendPublishUpdatedMessage(details);
    }

    @Override
    public RcsContactUceCapability getDeviceCapabilities(@CapabilityMechanism int mechanism) {
        return mDeviceCapabilityInfo.getDeviceCapabilities(mechanism, mContext);
    }

    // The local publish request from the sub-components which interact with PublishController.
    private final PublishControllerCallback mPublishControllerCallback =
            new PublishControllerCallback() {
                @Override
                public void requestPublishFromInternal(@PublishTriggerType int type) {
                    logd("requestPublishFromInternal: type=" + type);
                    mPublishHandler.sendPublishMessage(type);
                }

                @Override
                public void onRequestCommandError(PublishRequestResponse requestResponse) {
                    logd("onRequestCommandError: taskId=" + requestResponse.getTaskId()
                            + ", time=" + requestResponse.getResponseTimestamp());
                    mPublishHandler.sendRequestCommandErrorMessage(requestResponse);
                }

                @Override
                public void onRequestNetworkResp(PublishRequestResponse requestResponse) {
                    logd("onRequestNetworkResp: taskId=" + requestResponse.getTaskId()
                            + ", time=" + requestResponse.getResponseTimestamp());
                    mPublishHandler.sendRequestNetworkRespMessage(requestResponse);
                }

                @Override
                public void setupRequestCanceledTimer(long taskId, long delay) {
                    logd("setupRequestCanceledTimer: taskId=" + taskId + ", delay=" + delay);
                    mPublishHandler.sendRequestCanceledTimerMessage(taskId, delay);
                }

                @Override
                public void clearRequestCanceledTimer() {
                    logd("clearRequestCanceledTimer");
                    mPublishHandler.clearRequestCanceledTimer();
                }

                @Override
                public void updatePublishRequestResult(@PublishState int state,
                        Instant updatedTime, String pidfXml, SipDetails details) {
                    logd("updatePublishRequestResult: " + state + ", time=" + updatedTime);
                    mPublishHandler.sendPublishStateChangedMessage(state, updatedTime, pidfXml,
                            details);
                }

                @Override
                public void updatePublishThrottle(int value) {
                    logd("updatePublishThrottle: value=" + value);
                    mPublishProcessor.updatePublishThrottle(value);
                }

                @Override
                public void refreshDeviceState(int sipCode, String reason) {
                    mUceCtrlCallback.refreshDeviceState(sipCode, reason,
                            UceController.REQUEST_TYPE_PUBLISH);
                }

                @Override
                public void notifyPendingPublishRequest() {
                    logd("notifyPendingPublishRequest");
                    mPublishHandler.sendPublishSentMessage();
                }

                @Override
                public void updateImsUnregistered() {
                    logd("updateImsUnregistered");
                    mPublishHandler.sendImsUnregisteredMessage();
                }
            };

    /**
     * Publish the device's capabilities to the network. This method is triggered by ImsService.
     */
    @Override
    public void requestPublishCapabilitiesFromService(int triggerType) {
        logi("Receive the publish request from service: service trigger type=" + triggerType);
        mPublishHandler.sendPublishMessage(PublishController.PUBLISH_TRIGGER_SERVICE);
    }

    private static class PublishHandler extends Handler {
        private static final int MSG_RCS_CONNECTED = 1;
        private static final int MSG_RCS_DISCONNECTED = 2;
        private static final int MSG_DESTROYED = 3;
        private static final int MSG_CARRIER_CONFIG_CHANGED = 4;
        private static final int MSG_RCS_CAPABILITIES_CHANGED = 5;
        private static final int MSG_PUBLISH_STATE_CHANGED = 6;
        private static final int MSG_NOTIFY_CURRENT_PUBLISH_STATE = 7;
        private static final int MSG_REQUEST_PUBLISH = 8;
        private static final int MSG_REQUEST_CMD_ERROR = 9;
        private static final int MSG_REQUEST_NETWORK_RESPONSE = 10;
        private static final int MSG_REQUEST_CANCELED = 11;
        private static final int MSG_RESET_DEVICE_STATE = 12;
        private static final int MSG_UNPUBLISHED = 13;
        private static final int MSG_PUBLISH_SENT = 14;
        private static final int MSG_PUBLISH_UPDATED = 15;
        private static final int MSG_IMS_UNREGISTERED = 16;

        private final WeakReference<PublishControllerImpl> mPublishControllerRef;

        public PublishHandler(PublishControllerImpl publishController, Looper looper) {
            super(looper);
            mPublishControllerRef = new WeakReference<>(publishController);
        }

        @Override
        public void handleMessage(Message message) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) {
                return;
            }
            if (publishCtrl.mIsDestroyedFlag) return;
            publishCtrl.logd("handleMessage: " + EVENT_DESCRIPTION.get(message.what));
            switch (message.what) {
                case MSG_RCS_CONNECTED: {
                    SomeArgs args = (SomeArgs) message.obj;
                    RcsFeatureManager manager = (RcsFeatureManager) args.arg1;
                    args.recycle();
                    publishCtrl.handleRcsConnectedMessage(manager);
                    break;
                }
                case MSG_RCS_DISCONNECTED:
                    publishCtrl.handleRcsDisconnectedMessage();
                    break;

                case MSG_DESTROYED:
                    publishCtrl.handleDestroyedMessage();
                    break;

                case MSG_CARRIER_CONFIG_CHANGED:
                    publishCtrl.handleCarrierConfigChangedMessage();
                    break;

                case MSG_RCS_CAPABILITIES_CHANGED:
                    int RcsCapabilities = message.arg1;
                    publishCtrl.handleRcsCapabilitiesChangedMessage(RcsCapabilities);
                    break;

                case MSG_PUBLISH_STATE_CHANGED: {
                    SomeArgs args = (SomeArgs) message.obj;
                    int newPublishState = (Integer) args.arg1;
                    Instant updatedTimestamp = (Instant) args.arg2;
                    String pidfXml = (String) args.arg3;
                    SipDetails details = (SipDetails) args.arg4;
                    args.recycle();
                    publishCtrl.handlePublishStateChangedMessage(newPublishState, updatedTimestamp,
                            pidfXml, details);
                    break;
                }
                case MSG_NOTIFY_CURRENT_PUBLISH_STATE:
                    IRcsUcePublishStateCallback c = (IRcsUcePublishStateCallback) message.obj;
                    boolean supportPublishingState = false;
                    if (message.arg1 == 1) {
                        supportPublishingState = true;
                    }
                    publishCtrl.handleNotifyCurrentPublishStateMessage(c, supportPublishingState);
                    break;

                case MSG_REQUEST_PUBLISH:
                    int type = message.arg1;
                    publishCtrl.handleRequestPublishMessage(type);
                    break;

                case MSG_REQUEST_CMD_ERROR:
                    PublishRequestResponse cmdErrorResponse = (PublishRequestResponse) message.obj;
                    publishCtrl.mPublishProcessor.onCommandError(cmdErrorResponse);
                    break;

                case MSG_REQUEST_NETWORK_RESPONSE:
                    PublishRequestResponse networkResponse = (PublishRequestResponse) message.obj;
                    publishCtrl.mPublishProcessor.onNetworkResponse(networkResponse);
                    break;

                case MSG_REQUEST_CANCELED:
                    long taskId = (Long) message.obj;
                    publishCtrl.handleRequestCanceledMessage(taskId);
                    break;

                case MSG_RESET_DEVICE_STATE:
                    publishCtrl.handleResetDeviceStateMessage();
                    break;

                case MSG_UNPUBLISHED: {
                    SomeArgs args = (SomeArgs) message.obj;
                    int newPublishState = (Integer) args.arg1;
                    Instant updatedTimestamp = (Instant) args.arg2;
                    args.recycle();
                    publishCtrl.handleUnpublishedMessage(newPublishState, updatedTimestamp);
                    break;
                }

                case MSG_PUBLISH_SENT:
                    publishCtrl.handlePublishSentMessage();
                    break;

                case MSG_PUBLISH_UPDATED: {
                    SipDetails details = (SipDetails) message.obj;
                    publishCtrl.handlePublishUpdatedMessage(details);
                    break;
                }

                case MSG_IMS_UNREGISTERED:
                    publishCtrl.handleUnpublishedMessage(RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED,
                            Instant.now());
                    break;

                default:
                    publishCtrl.logd("invalid message: " + message.what);
                    break;
            }
            publishCtrl.logd("handleMessage done: " + EVENT_DESCRIPTION.get(message.what));
        }

        /**
         * Remove all the messages from the handler.
         */
        public void onDestroy() {
            removeCallbacksAndMessages(null);
        }

        public void sendRcsConnectedMsg(RcsFeatureManager manager) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;

            SomeArgs args = SomeArgs.obtain();
            args.arg1 = manager;
            Message message = obtainMessage();
            message.what = MSG_RCS_CONNECTED;
            message.obj = args;
            sendMessage(message);
        }

        public void sendRcsDisconnectedMsg() {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;

            Message message = obtainMessage();
            message.what = MSG_RCS_DISCONNECTED;
            sendMessage(message);
        }

        public void sendDestroyedMsg() {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;

            Message message = obtainMessage();
            message.what = MSG_DESTROYED;
            sendMessage(message);
        }

        public void sendCarrierConfigChangedMsg() {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;

            Message message = obtainMessage();
            message.what = MSG_CARRIER_CONFIG_CHANGED;
            sendMessage(message);
        }

        public void sendRcsCapabilitiesStatusChangedMsg(@RcsImsCapabilityFlag int capabilities) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;

            Message message = obtainMessage();
            message.what = MSG_RCS_CAPABILITIES_CHANGED;
            message.arg1 = capabilities;
            sendMessage(message);
        }

        /**
         * Send the message to notify the publish state is changed.
         */
        public void sendPublishStateChangedMessage(@PublishState int publishState,
                @NonNull Instant updatedTimestamp, String pidfXml, SipDetails details) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;

            SomeArgs args = SomeArgs.obtain();
            args.arg1 = publishState;
            args.arg2 = updatedTimestamp;
            args.arg3 = pidfXml;
            args.arg4 = details;
            Message message = obtainMessage();
            message.what = MSG_PUBLISH_STATE_CHANGED;
            message.obj = args;
            sendMessage(message);
        }

        /**
         * Send the message to notify the publish state is changed.
         */
        public void sendUnpublishedMessage(@PublishState int publishState) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;

            SomeArgs args = SomeArgs.obtain();
            args.arg1 = publishState;
            args.arg2 = Instant.now();
            Message message = obtainMessage();
            message.what = MSG_UNPUBLISHED;
            message.obj = args;
            sendMessage(message);
        }

        /**
         * Send the message to notify the publish state is changed.
         */
        public void sendPublishUpdatedMessage(@NonNull SipDetails details) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;

            Message message = obtainMessage();
            message.what = MSG_PUBLISH_UPDATED;
            message.obj = details;
            sendMessage(message);
        }

        /**
         * Send the message to notify the new added callback of the latest publish state.
         */
        public void sendNotifyCurrentPublishStateMessage(
                IRcsUcePublishStateCallback callback, boolean supportPublishingState) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;

            Message message = obtainMessage();
            message.what = MSG_NOTIFY_CURRENT_PUBLISH_STATE;
            message.arg1 = supportPublishingState ? 1 : 0;
            message.obj = callback;
            sendMessage(message);
        }

        /**
         * Send the message that the publish request has been sent to the ImsService.
         */
        public void sendPublishSentMessage() {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;
            Message message = obtainMessage();
            message.what = MSG_PUBLISH_SENT;
            sendMessage(message);
        }

        public void sendPublishMessage(@PublishTriggerType int type) {
            sendPublishMessage(type, 0L);
        }

        public void sendPublishMessage(@PublishTriggerType int type, long delay) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;

            // Disallow publish if the PRESENCE PUBLISH is not enabled and this request is not
            // triggered by the ImsService.
            if (!publishCtrl.isPresencePublishEnabled() && type != PUBLISH_TRIGGER_SERVICE) {
                publishCtrl.logd("sendPublishMessage: disallowed type=" + type);
                return;
            }

            Message message = obtainMessage();
            message.what = MSG_REQUEST_PUBLISH;
            message.arg1 = type;
            sendMessageDelayed(message, delay);
        }

        public void sendRequestCommandErrorMessage(PublishRequestResponse response) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) {
                return;
            }
            if (publishCtrl.mIsDestroyedFlag) return;
            Message message = obtainMessage();
            message.what = MSG_REQUEST_CMD_ERROR;
            message.obj = response;
            sendMessage(message);
        }

        public void sendRequestNetworkRespMessage(PublishRequestResponse response) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) {
                return;
            }
            if (publishCtrl.mIsDestroyedFlag) return;
            Message message = obtainMessage();
            message.what = MSG_REQUEST_NETWORK_RESPONSE;
            message.obj = response;
            sendMessage(message);
        }

        public void sendRequestCanceledTimerMessage(long taskId, long delay) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) {
                return;
            }
            if (publishCtrl.mIsDestroyedFlag) return;
            removeMessages(MSG_REQUEST_CANCELED, (Long) taskId);

            Message message = obtainMessage();
            message.what = MSG_REQUEST_CANCELED;
            message.obj = (Long) taskId;
            sendMessageDelayed(message, delay);
        }

        public void clearRequestCanceledTimer() {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) {
                return;
            }
            if (publishCtrl.mIsDestroyedFlag) return;
            removeMessages(MSG_REQUEST_CANCELED);
        }

        public void sendResetDeviceStateTimerMessage(long resetAfterSec) {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) {
                return;
            }
            if (publishCtrl.mIsDestroyedFlag) return;
            // Remove old timer and setup the new timer.
            removeMessages(MSG_RESET_DEVICE_STATE);
            Message message = obtainMessage();
            message.what = MSG_RESET_DEVICE_STATE;
            sendMessageDelayed(message, TimeUnit.SECONDS.toMillis(resetAfterSec));
        }

        public void clearResetDeviceStateTimer() {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) {
                return;
            }
            if (publishCtrl.mIsDestroyedFlag) return;
            removeMessages(MSG_RESET_DEVICE_STATE);
        }

        public void sendImsUnregisteredMessage() {
            PublishControllerImpl publishCtrl = mPublishControllerRef.get();
            if (publishCtrl == null) return;
            if (publishCtrl.mIsDestroyedFlag) return;
            Message message = obtainMessage();
            message.what = MSG_IMS_UNREGISTERED;
            sendMessage(message);
        }

        private static Map<Integer, String> EVENT_DESCRIPTION = new HashMap<>();
        static {
            EVENT_DESCRIPTION.put(MSG_RCS_CONNECTED, "RCS_CONNECTED");
            EVENT_DESCRIPTION.put(MSG_RCS_DISCONNECTED, "RCS_DISCONNECTED");
            EVENT_DESCRIPTION.put(MSG_DESTROYED, "DESTROYED");
            EVENT_DESCRIPTION.put(MSG_CARRIER_CONFIG_CHANGED, "CARRIER_CONFIG_CHANGED");
            EVENT_DESCRIPTION.put(MSG_RCS_CAPABILITIES_CHANGED, "RCS_CAPABILITIES_CHANGED");
            EVENT_DESCRIPTION.put(MSG_PUBLISH_STATE_CHANGED, "PUBLISH_STATE_CHANGED");
            EVENT_DESCRIPTION.put(MSG_NOTIFY_CURRENT_PUBLISH_STATE, "NOTIFY_PUBLISH_STATE");
            EVENT_DESCRIPTION.put(MSG_REQUEST_PUBLISH, "REQUEST_PUBLISH");
            EVENT_DESCRIPTION.put(MSG_REQUEST_CMD_ERROR, "REQUEST_CMD_ERROR");
            EVENT_DESCRIPTION.put(MSG_REQUEST_NETWORK_RESPONSE, "REQUEST_NETWORK_RESPONSE");
            EVENT_DESCRIPTION.put(MSG_REQUEST_CANCELED, "REQUEST_CANCELED");
            EVENT_DESCRIPTION.put(MSG_RESET_DEVICE_STATE, "RESET_DEVICE_STATE");
            EVENT_DESCRIPTION.put(MSG_UNPUBLISHED, "MSG_UNPUBLISHED");
            EVENT_DESCRIPTION.put(MSG_PUBLISH_SENT, "MSG_PUBLISH_SENT");
            EVENT_DESCRIPTION.put(MSG_PUBLISH_UPDATED, "MSG_PUBLISH_UPDATED");
            EVENT_DESCRIPTION.put(MSG_IMS_UNREGISTERED, "MSG_IMS_UNREGISTERED");
        }
    }

    /**
     * Check if the PUBLISH request is allowed.
     */
    private boolean isPublishRequestAllowed() {
        // The PUBLISH request requires that the RCS PRESENCE is capable.
        if (!mDeviceCapabilityInfo.isPresenceCapable()) {
            logd("isPublishRequestAllowed: capability presence uce is not enabled.");
            return false;
        }

        // The first PUBLISH request is required to be triggered from the service.
        if (!mReceivePublishFromService) {
            logd("isPublishRequestAllowed: "
                    + "The first PUBLISH request from the server has not been received.");
            return false;
        }

        // Check whether the device state is not allowed to execute the PUBLISH request.
        DeviceStateResult deviceState = mUceCtrlCallback.getDeviceState();
        if (deviceState.isRequestForbidden() || deviceState.isPublishRequestBlocked()) {
            logd("isPublishRequestAllowed: The device state is disallowed. "
                    + deviceState.getDeviceState());
            return false;
        }

        // Check whether there is already a publish request running or not. When the running
        // request is finished and there is a pending request, it will send a new request.
        if (mPublishProcessor.isPublishingNow()) {
            logd("isPublishRequestAllowed: There is already a publish request running now.");
            return false;
        }
        return true;
    }

    /**
     * Check whether the PRESENCE PUBLISH should be enabled or not. It should be enabled only when
     * the PRESENCE mechanism is supported.
     */
    private boolean isPresencePublishEnabled() {
        synchronized (mPublishStateLock) {
            return mCapabilityType == RcsImsCapabilities.CAPABILITY_TYPE_PRESENCE_UCE;
        }
    }

    /**
     * Handle the RCS connected message. This method is called in the handler thread.
     */
    private void handleRcsConnectedMessage(RcsFeatureManager manager) {
        if (mIsDestroyedFlag) return;
        mRcsFeatureManager = manager;
        mDeviceCapListener.onRcsConnected();
        mPublishProcessor.onRcsConnected(manager);
        registerRcsAvailabilityChanged(manager);
    }

    /**
     * Handle the RCS disconnected message. This method is called in the handler thread.
     */
    private void handleRcsDisconnectedMessage() {
        if (mIsDestroyedFlag) return;
        mRcsFeatureManager = null;
        mDeviceCapabilityInfo.updatePresenceCapable(false);
        mDeviceCapListener.onRcsDisconnected();
        mPublishProcessor.onRcsDisconnected();

        // When the RCS is disconnected, update the publish state to NOT_PUBLISH if the PRESENCE
        // PUBLISH is enabled.
        if (isPresencePublishEnabled()) {
            handlePublishStateChangedMessage(RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED,
                    Instant.now(), null /*pidfXml*/, null /*SipDetails*/);
        }
    }

    /**
     * Handle the Destroyed message. This method is called in the handler thread.
     */
    private void handleDestroyedMessage() {
        mIsDestroyedFlag = true;
        mDeviceCapabilityInfo.updatePresenceCapable(false);
        unregisterRcsAvailabilityChanged();
        mDeviceCapListener.onDestroy();   // It will turn off the listener automatically.
        mPublishHandler.onDestroy();
        mPublishProcessor.onDestroy();
        synchronized (mPublishStateLock) {
            clearPublishStateCallbacks();
        }
    }

    /*
     * Register the availability callback to receive the RCS capabilities change. This method is
     * called when the RCS is connected.
     */
    private void registerRcsAvailabilityChanged(RcsFeatureManager manager) {
        try {
            manager.registerRcsAvailabilityCallback(mSubId, mRcsCapabilitiesCallback);
        } catch (ImsException e) {
            logw("registerRcsAvailabilityChanged exception " + e);
        }
    }

    /*
     * Unregister the availability callback. This method is called when the PublishController
     * instance is destroyed.
     */
    private void unregisterRcsAvailabilityChanged() {
        RcsFeatureManager manager = mRcsFeatureManager;
        if (manager == null) return;
        try {
            manager.unregisterRcsAvailabilityCallback(mSubId, mRcsCapabilitiesCallback);
        } catch (Exception e) {
            // Do not handle the exception
        }
    }

    /**
     * Handle the carrier config changed message. This method is called in the handler thread.
     */
    private void handleCarrierConfigChangedMessage() {
        if (mIsDestroyedFlag) return;

        updateCapabilityTypeAndPublishStateIfNeeded();

        String[] newMap = getCarrierServiceDescriptionFeatureTagMap();
        if (mDeviceCapabilityInfo.updateCapabilityRegistrationTrackerMap(newMap)) {
            mPublishHandler.sendPublishMessage(
                    PublishController.PUBLISH_TRIGGER_CARRIER_CONFIG_CHANGED);
        }
    }

    /**
     * Check whether the capability type has changed or not because of the carrier config changed.
     * If the capability type has changed, the publish state also needs to be reinitialized.
     * <p>
     * This method is called in the handler thread.
     */
    private void updateCapabilityTypeAndPublishStateIfNeeded() {
        synchronized (mPublishStateLock) {
            int originalMechanism = mCapabilityType;
            mCapabilityType = PublishUtils.getCapabilityType(mContext, mSubId);

            // Return when the capability type has not changed.
            if (originalMechanism == mCapabilityType) {
                logd("updateCapTypeAndPublishStateIfNeeded: " +
                        "The capability type is not changed=" + mCapabilityType);
                return;
            }

            // Reinitialize the publish state because the capability type has changed.
            int updatedPublishState = getInitialPublishState(mCapabilityType);

            logd("updateCapTypeAndPublishStateIfNeeded from " + originalMechanism +
                    " to " + mCapabilityType + ", new publish state=" + updatedPublishState);

            // Update the publish state directly. Because this method is called in the
            // handler thread already, the process of updating publish state does not need to be
            // sent to the looper again.
            handlePublishStateChangedMessage(updatedPublishState, Instant.now(), null /*pidfxml*/,
                    null /*SipDetails*/);
        }
    }

    private String[] getCarrierServiceDescriptionFeatureTagMap() {
        CarrierConfigManager manager = mContext.getSystemService(CarrierConfigManager.class);
        PersistableBundle bundle = manager != null ? manager.getConfigForSubId(mSubId) :
                CarrierConfigManager.getDefaultConfig();
        return bundle.getStringArray(CarrierConfigManager.Ims.
                KEY_PUBLISH_SERVICE_DESC_FEATURE_TAG_MAP_OVERRIDE_STRING_ARRAY);
    }

    private void handleRcsCapabilitiesChangedMessage(int capabilities) {
        logd("handleRcsCapabilitiesChangedMessage: " + capabilities);
        if (mIsDestroyedFlag) return;
        RcsImsCapabilities RcsImsCapabilities = new RcsImsCapabilities(capabilities);
        mDeviceCapabilityInfo.updatePresenceCapable(
                RcsImsCapabilities.isCapable(RcsUceAdapter.CAPABILITY_TYPE_PRESENCE_UCE));
        // Trigger a publish request if the RCS capabilities presence is enabled.
        if (mDeviceCapabilityInfo.isPresenceCapable()) {
            mPublishProcessor.checkAndSendPendingRequest();
        }
    }

    /**
     * Update the publish state and notify the publish state callback if the new state is different
     * from original state.
     */
    private void handlePublishStateChangedMessage(@PublishState int newPublishState,
            Instant updatedTimestamp, String pidfXml, SipDetails details) {
        synchronized (mPublishStateLock) {
            if (mIsDestroyedFlag) return;
            // Check if the time of the given publish state is not earlier than existing time.
            if (updatedTimestamp == null || !updatedTimestamp.isAfter(mPublishStateUpdatedTime)) {
                logd("handlePublishStateChangedMessage: updatedTimestamp is not allowed: "
                        + mPublishStateUpdatedTime + " to " + updatedTimestamp
                        + ", publishState=" + newPublishState);
                return;
            }
            logd("publish state changes from " + mCurrentPublishState + " to " + newPublishState +
                    ", time=" + updatedTimestamp);
            mPublishStateUpdatedTime = updatedTimestamp;
            mPidfXml = pidfXml;
            // Bail early and do not update listeners if the publish state didn't change.
            if (mCurrentPublishState == newPublishState) return;
            mLastPublishState = mCurrentPublishState;
            mCurrentPublishState = newPublishState;
        }
        if (newPublishState == RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED) {
            mUceStatsWriter.setUnPublish(mSubId);
        }

        // Trigger the publish state changed in handler thread since it may take time.
        logd("Notify publish state changed: " + mCurrentPublishState);
        mPublishStateCallbacks.broadcast(c -> {
            try {
                c.onPublishUpdated(getPublishAttributes(mCurrentPublishState, details));
            } catch (RemoteException e) {
                logw("Notify publish state changed error: " + e);
            }
        });
        logd("Notify publish state changed: completed");
    }

    private PublishAttributes getPublishAttributes(@PublishState int mCurrentPublishState,
            SipDetails details) {
        List<RcsContactPresenceTuple> tuples = null;
        if (mCurrentPublishState == PUBLISH_STATE_OK) {
            tuples = mDeviceCapabilityInfo.getLastSuccessfulPresenceTuplesWithoutContactUri();
        }
        if (tuples != null && !tuples.isEmpty()) {
            return new PublishAttributes.Builder(mCurrentPublishState).setSipDetails(details)
                    .setPresenceTuples(tuples).build();
        }
        return new PublishAttributes.Builder(mCurrentPublishState).setSipDetails(details).build();
    }

    private void handleNotifyCurrentPublishStateMessage(IRcsUcePublishStateCallback callback,
            boolean supportPublishingState) {
        if (mIsDestroyedFlag || callback == null) return;
        try {
            int publishState = getUcePublishState(supportPublishingState);
            callback.onPublishUpdated(getPublishAttributes(publishState, null /*SipDetails*/));
        } catch (RemoteException e) {
            logw("handleCurrentPublishStateUpdateMessage exception: " + e);
        }
    }

    private void handleRequestPublishMessage(@PublishTriggerType int type) {
        if (mIsDestroyedFlag) return;

        logd("handleRequestPublishMessage: type=" + type);

        // Set the PUBLISH FROM SERVICE flag and reset the device state if the PUBLISH request is
        // triggered by the ImsService.
        if (type == PublishController.PUBLISH_TRIGGER_SERVICE) {
            // Set the flag
            if (!mReceivePublishFromService) {
                mReceivePublishFromService = true;
            }
            // Reset device state
            DeviceStateResult deviceState = mUceCtrlCallback.getDeviceState();
            if (deviceState.isRequestForbidden() || deviceState.isPublishRequestBlocked()) {
                mUceCtrlCallback.resetDeviceState();
            }
        }

        // Set the pending flag and return if the request is not allowed.
        if (!isPublishRequestAllowed()) {
            logd("handleRequestPublishMessage: SKIP. The request is not allowed. type=" + type);
            mPublishProcessor.setPendingRequest(type);
            return;
        }

        // Update the latest PUBLISH allowed time according to the given trigger type.
        mPublishProcessor.updatePublishingAllowedTime(type);

        // Get the publish request delay time. If the delay is not present, the first
        // PUBLISH is not allowed to be executed; If the delay time is 0, it means that
        // this request can be executed immediately.
        Optional<Long> delay = mPublishProcessor.getPublishingDelayTime();
        if (!delay.isPresent()) {
            logd("handleRequestPublishMessage: SKIP. The delay is empty. type=" + type);
            mPublishProcessor.setPendingRequest(type);
            return;
        }

        logd("handleRequestPublishMessage: " + type + ", delay=" + delay.get());
        if (delay.get() == 0L) {
            mPublishProcessor.doPublish(type);
        } else {
            mPublishHandler.sendPublishMessage(type, delay.get());
        }
    }

    private void handleRequestCanceledMessage(long taskId) {
        if (mIsDestroyedFlag) return;
        mPublishProcessor.cancelPublishRequest(taskId);
    }

    private void handleResetDeviceStateMessage() {
        if(mIsDestroyedFlag) return;
        mUceCtrlCallback.resetDeviceState();
    }

    private void handleUnpublishedMessage(@PublishState int newPublishState,
            Instant updatedTimestamp) {
        if (mIsDestroyedFlag) return;
        mPublishProcessor.resetState();
        handlePublishStateChangedMessage(newPublishState, updatedTimestamp,
                null /*pidfXml*/, null /*SipDetails*/);
    }

    private void handlePublishSentMessage() {
        synchronized (mPublishStateLock) {
            if (mIsDestroyedFlag) return;
            int lastIndex = mPublishStateCallbacks.getRegisteredCallbackCount() - 1;
            int tempPublishState = mCurrentPublishState;
            for (int index = lastIndex; index >= 0; index--) {
                IRcsUcePublishStateCallback callback =
                        mPublishStateCallbacks.getRegisteredCallbackItem(index);
                boolean isSupportPublishingState = false;
                try {
                    Object object = mPublishStateCallbacks.getRegisteredCallbackCookie(index);
                    if (object != null) {
                        isSupportPublishingState = (Boolean) object;
                    }
                } catch (Exception e) {
                    // Do not handle the exception
                }
                try {
                    mCurrentPublishState = RcsUceAdapter.PUBLISH_STATE_PUBLISHING;
                    if (isSupportPublishingState) {
                        if (callback != null) {
                            callback.onPublishUpdated(getPublishAttributes(mCurrentPublishState,
                                    null /*SipDetails*/));
                        }
                    } else {
                        // If it is currently PUBLISH_STATE_OK, the state must not be changed to
                        // PUBLISH_STATE_NOT_PUBLISHED.
                        // And in the case of the current PUBLISH_STATE_NOT_PUBLISHED, it is
                        // necessary to avoid reporting the duplicate state.
                        if (tempPublishState != PUBLISH_STATE_OK
                                && tempPublishState != RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED) {
                            // set the state to PUBLISH_STATE_NOT_PUBLISHED so that
                            // getUcePublishState is consistent with the callback
                            mLastPublishState = RcsUceAdapter.PUBLISH_STATE_NOT_PUBLISHED;
                            if (callback != null) {
                                callback.onPublishUpdated(getPublishAttributes(mLastPublishState,
                                        null /*SipDetails*/));
                            }
                        }
                    }
                } catch (RemoteException e) {
                    logw("Notify publish state changed error: " + e);
                }
            }
        }
    }

    private void handlePublishUpdatedMessage(@NonNull SipDetails details) {
        if (mIsDestroyedFlag) return;
        PublishRequestResponse updatedPublish = new PublishRequestResponse(getLastPidfXml(),
                details);
        mPublishProcessor.publishUpdated(updatedPublish);
    }

    @VisibleForTesting
    public void setCapabilityType(int type) {
        mCapabilityType = type;
        mCurrentPublishState = getInitialPublishState(mCapabilityType);
        mLastPublishState = mCurrentPublishState;
    }

    @VisibleForTesting
    public void setPublishStateCallback(RemoteCallbackList<IRcsUcePublishStateCallback> list) {
        mPublishStateCallbacks = list;
    }

    @VisibleForTesting
    public PublishHandler getPublishHandler() {
        return mPublishHandler;
    }

    @VisibleForTesting
    public IImsCapabilityCallback getRcsCapabilitiesCallback() {
        return mRcsCapabilitiesCallback;
    }

    @VisibleForTesting
    public PublishControllerCallback getPublishControllerCallback() {
        return mPublishControllerCallback;
    }

    private void logd(String log) {
        Log.d(LOG_TAG, getLogPrefix().append(log).toString());
        mLocalLog.log("[D] " + log);
    }

    private void logi(String log) {
        Log.i(LOG_TAG, getLogPrefix().append(log).toString());
        mLocalLog.log("[I] " + log);
    }

    private void logw(String log) {
        Log.w(LOG_TAG, getLogPrefix().append(log).toString());
        mLocalLog.log("[W] " + log);
    }

    private StringBuilder getLogPrefix() {
        StringBuilder builder = new StringBuilder("[");
        builder.append(mSubId);
        builder.append("] ");
        return builder;
    }

    @Override
    public void dump(PrintWriter printWriter) {
        IndentingPrintWriter pw = new IndentingPrintWriter(printWriter, "  ");
        pw.println("PublishControllerImpl" + "[subId: " + mSubId + "]:");
        pw.increaseIndent();

        pw.print("isPresenceCapable=");
        pw.println(mDeviceCapabilityInfo.isPresenceCapable());
        pw.print("mCurrentPublishState=");
        pw.print(mCurrentPublishState);
        pw.print("mLastPublishState=");
        pw.print(mLastPublishState);
        pw.print(" at time ");
        pw.println(mPublishStateUpdatedTime);
        pw.println("Last PIDF XML:");
        pw.increaseIndent();
        if (Build.IS_ENG) {
            pw.println(mPidfXml);
        } else if (Build.IS_DEBUGGABLE) {
            String pidfXml = (mPidfXml == null) ? "null" : mPidfXml;
            pw.println(PublishUtils.removeNumbersFromUris(pidfXml));
        } else {
            pw.println(mPidfXml != null ? "***" : "null");
        }
        pw.decreaseIndent();

        if (mPublishProcessor != null) {
            mPublishProcessor.dump(pw);
        } else {
            pw.println("mPublishProcessor is null");
        }

        pw.println();
        mDeviceCapListener.dump(pw);

        pw.println("Log:");
        pw.increaseIndent();
        mLocalLog.dump(pw);
        pw.decreaseIndent();
        pw.println("---");

        pw.decreaseIndent();
    }
}
