/*
 * Copyright 2019 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 android.media.tv.tuner;

import static android.media.tv.flags.Flags.FLAG_SET_RESOURCE_HOLDER_RETAIN;

import android.annotation.BytesLong;
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.tv.tuner.Constant;
import android.hardware.tv.tuner.Constant64Bit;
import android.hardware.tv.tuner.FrontendScanType;
import android.media.MediaCodec;
import android.media.tv.TvInputService;
import android.media.tv.flags.Flags;
import android.media.tv.tuner.dvr.DvrPlayback;
import android.media.tv.tuner.dvr.DvrRecorder;
import android.media.tv.tuner.dvr.OnPlaybackStatusChangedListener;
import android.media.tv.tuner.dvr.OnRecordStatusChangedListener;
import android.media.tv.tuner.filter.Filter;
import android.media.tv.tuner.filter.Filter.Subtype;
import android.media.tv.tuner.filter.Filter.Type;
import android.media.tv.tuner.filter.FilterCallback;
import android.media.tv.tuner.filter.SharedFilter;
import android.media.tv.tuner.filter.SharedFilterCallback;
import android.media.tv.tuner.filter.TimeFilter;
import android.media.tv.tuner.frontend.Atsc3PlpInfo;
import android.media.tv.tuner.frontend.FrontendInfo;
import android.media.tv.tuner.frontend.FrontendSettings;
import android.media.tv.tuner.frontend.FrontendStatus;
import android.media.tv.tuner.frontend.FrontendStatus.FrontendStatusType;
import android.media.tv.tuner.frontend.FrontendStatusReadiness;
import android.media.tv.tuner.frontend.OnTuneEventListener;
import android.media.tv.tuner.frontend.ScanCallback;
import android.media.tv.tunerresourcemanager.ResourceClientProfile;
import android.media.tv.tunerresourcemanager.TunerCiCamRequest;
import android.media.tv.tunerresourcemanager.TunerDemuxRequest;
import android.media.tv.tunerresourcemanager.TunerDescramblerRequest;
import android.media.tv.tunerresourcemanager.TunerFrontendRequest;
import android.media.tv.tunerresourcemanager.TunerLnbRequest;
import android.media.tv.tunerresourcemanager.TunerResourceManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Process;
import android.util.Log;

import com.android.internal.util.FrameworkStatsLog;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;

/**
 * This class is used to interact with hardware tuners devices.
 *
 * <p> Each TvInputService Session should create one instance of this class.
 *
 * <p> This class controls the TIS interaction with Tuner HAL.
 *
 * @hide
 */
@SystemApi
public class Tuner implements AutoCloseable  {
    /**
     * Invalid TS packet ID.
     */
    public static final int INVALID_TS_PID = Constant.INVALID_TS_PID;
    /**
     * Invalid stream ID.
     */
    public static final int INVALID_STREAM_ID = Constant.INVALID_STREAM_ID;
    /**
     * Invalid filter ID.
     */
    public static final int INVALID_FILTER_ID = Constant.INVALID_FILTER_ID;
    /**
     * Invalid AV Sync ID.
     */
    public static final int INVALID_AV_SYNC_ID = Constant.INVALID_AV_SYNC_ID;
    /**
     * Invalid timestamp.
     *
     * <p>Returned by {@link android.media.tv.tuner.filter.TimeFilter#getSourceTime()},
     * {@link android.media.tv.tuner.filter.TimeFilter#getTimeStamp()},
     * {@link Tuner#getAvSyncTime(int)} or {@link TsRecordEvent#getPts()} and
     * {@link MmtpRecordEvent#getPts()} when the requested timestamp is not available.
     *
     * @see android.media.tv.tuner.filter.TimeFilter#getSourceTime()
     * @see android.media.tv.tuner.filter.TimeFilter#getTimeStamp()
     * @see Tuner#getAvSyncTime(int)
     * @see android.media.tv.tuner.filter.TsRecordEvent#getPts()
     * @see android.media.tv.tuner.filter.MmtpRecordEvent#getPts()
     */
    public static final long INVALID_TIMESTAMP =
            Constant64Bit.INVALID_PRESENTATION_TIME_STAMP;
    /**
     * Invalid mpu sequence number in MmtpRecordEvent.
     *
     * <p>Returned by {@link MmtpRecordEvent#getMpuSequenceNumber()} when the requested sequence
     * number is not available.
     *
     * @see android.media.tv.tuner.filter.MmtpRecordEvent#getMpuSequenceNumber()
     */
    public static final int INVALID_MMTP_RECORD_EVENT_MPT_SEQUENCE_NUM =
            Constant.INVALID_MMTP_RECORD_EVENT_MPT_SEQUENCE_NUM;
    /**
     * Invalid first macroblock address in MmtpRecordEvent and TsRecordEvent.
     *
     * <p>Returned by {@link MmtpRecordEvent#getMbInSlice()} and
     * {@link TsRecordEvent#getMbInSlice()} when the requested sequence number is not available.
     *
     * @see android.media.tv.tuner.filter.MmtpRecordEvent#getMbInSlice()
     * @see android.media.tv.tuner.filter.TsRecordEvent#getMbInSlice()
     */
    public static final int INVALID_FIRST_MACROBLOCK_IN_SLICE =
            Constant.INVALID_FIRST_MACROBLOCK_IN_SLICE;
    /**
     * Invalid local transport stream id.
     *
     * <p>Returned by {@link #linkFrontendToCiCam(int)} when the requested failed
     * or the hal implementation does not support the operation.
     *
     * @see #linkFrontendToCiCam(int)
     */
    public static final int INVALID_LTS_ID = Constant.INVALID_LTS_ID;
    /**
     * Invalid 64-bit filter ID.
     */
    public static final long INVALID_FILTER_ID_LONG = Constant64Bit.INVALID_FILTER_ID_64BIT;
    /**
     * Invalid frequency that is used as the default frontend frequency setting.
     */
    public static final int INVALID_FRONTEND_SETTING_FREQUENCY =
            Constant.INVALID_FRONTEND_SETTING_FREQUENCY;
    /**
     * Invalid frontend id.
     */
    public static final int INVALID_FRONTEND_ID = Constant.INVALID_FRONTEND_ID;
    /**
     * Invalid LNB id.
     *
     * @hide
     */
    public static final int INVALID_LNB_ID = Constant.INVALID_LNB_ID;
    /**
     * A void key token. It is used to remove the current key from descrambler.
     *
     * <p>If the current keyToken comes from a MediaCas session, App is recommended to
     * to use this constant to remove current key before closing MediaCas session.
     */
    @NonNull
    public static final byte[] VOID_KEYTOKEN = {Constant.INVALID_KEYTOKEN};

    /** @hide */
    @IntDef(prefix = "SCAN_TYPE_", value = {SCAN_TYPE_UNDEFINED, SCAN_TYPE_AUTO, SCAN_TYPE_BLIND})
    @Retention(RetentionPolicy.SOURCE)
    public @interface ScanType {}
    /**
     * Scan type undefined.
     */
    public static final int SCAN_TYPE_UNDEFINED = FrontendScanType.SCAN_UNDEFINED;
    /**
     * Scan type auto.
     *
     * <p> Tuner will send {@link android.media.tv.tuner.frontend.ScanCallback#onLocked}
     */
    public static final int SCAN_TYPE_AUTO = FrontendScanType.SCAN_AUTO;
    /**
     * Blind scan.
     *
     * <p>Frequency range is not specified. The {@link android.media.tv.tuner.Tuner} will scan an
     * implementation specific range.
     */
    public static final int SCAN_TYPE_BLIND = FrontendScanType.SCAN_BLIND;


    /** @hide */
    @IntDef({RESULT_SUCCESS, RESULT_UNAVAILABLE, RESULT_NOT_INITIALIZED, RESULT_INVALID_STATE,
            RESULT_INVALID_ARGUMENT, RESULT_OUT_OF_MEMORY, RESULT_UNKNOWN_ERROR})
    @Retention(RetentionPolicy.SOURCE)
    public @interface Result {}

    /**
     * Operation succeeded.
     */
    public static final int RESULT_SUCCESS = android.hardware.tv.tuner.Result.SUCCESS;
    /**
     * Operation failed because the corresponding resources are not available.
     */
    public static final int RESULT_UNAVAILABLE = android.hardware.tv.tuner.Result.UNAVAILABLE;
    /**
     * Operation failed because the corresponding resources are not initialized.
     */
    public static final int RESULT_NOT_INITIALIZED =
            android.hardware.tv.tuner.Result.NOT_INITIALIZED;
    /**
     * Operation failed because it's not in a valid state.
     */
    public static final int RESULT_INVALID_STATE = android.hardware.tv.tuner.Result.INVALID_STATE;
    /**
     * Operation failed because there are invalid arguments.
     */
    public static final int RESULT_INVALID_ARGUMENT =
            android.hardware.tv.tuner.Result.INVALID_ARGUMENT;
    /**
     * Memory allocation failed.
     */
    public static final int RESULT_OUT_OF_MEMORY = android.hardware.tv.tuner.Result.OUT_OF_MEMORY;
    /**
     * Operation failed due to unknown errors.
     */
    public static final int RESULT_UNKNOWN_ERROR = android.hardware.tv.tuner.Result.UNKNOWN_ERROR;



    private static final String TAG = "MediaTvTuner";
    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);

    private static final int MSG_RESOURCE_LOST = 1;
    private static final int MSG_ON_FILTER_EVENT = 2;
    private static final int MSG_ON_FILTER_STATUS = 3;
    private static final int MSG_ON_LNB_EVENT = 4;

    private static final int FILTER_CLEANUP_THRESHOLD = 256;

    /** @hide */
    @IntDef(prefix = "DVR_TYPE_", value = {DVR_TYPE_RECORD, DVR_TYPE_PLAYBACK})
    @Retention(RetentionPolicy.SOURCE)
    public @interface DvrType {}

    /**
     * DVR for recording.
     * @hide
     */
    public static final int DVR_TYPE_RECORD = android.hardware.tv.tuner.DvrType.RECORD;
    /**
     * DVR for playback of recorded programs.
     * @hide
     */
    public static final int DVR_TYPE_PLAYBACK = android.hardware.tv.tuner.DvrType.PLAYBACK;

    static {
        try {
            System.loadLibrary("media_tv_tuner");
            nativeInit();
            // Load and initialize MediaCodec to avoid flaky cts test result.
            Class.forName(MediaCodec.class.getName());
        } catch (UnsatisfiedLinkError e) {
            Log.d(TAG, "tuner JNI library not found!");
        } catch (ClassNotFoundException e) {
            Log.e(TAG, "MediaCodec class not found!", e);
        }
    }

    private final Context mContext;
    private final TunerResourceManager mTunerResourceManager;
    private final int mClientId;
    private static int sTunerVersion = TunerVersionChecker.TUNER_VERSION_UNKNOWN;
    private DemuxInfo mDesiredDemuxInfo = new DemuxInfo(Filter.TYPE_UNDEFINED);

    private boolean mClosed = false;
    private Frontend mFrontend;
    private EventHandler mHandler;
    @Nullable
    private FrontendInfo mFrontendInfo;
    private Long mFrontendHandle;
    private Tuner mFeOwnerTuner = null;
    private int mFrontendType = FrontendSettings.TYPE_UNDEFINED;
    private Integer mDesiredFrontendId = null;
    private int mUserId;
    private Lnb mLnb;
    private Long mLnbHandle;
    @Nullable
    private OnTuneEventListener mOnTuneEventListener;
    @Nullable
    private Executor mOnTuneEventExecutor;
    @Nullable
    private ScanCallback mScanCallback;
    @Nullable
    private Executor mScanCallbackExecutor;
    @Nullable
    private OnResourceLostListener mOnResourceLostListener;
    @Nullable
    private Executor mOnResourceLostListenerExecutor;

    private final Object mOnTuneEventLock = new Object();
    private final Object mScanCallbackLock = new Object();
    private final Object mOnResourceLostListenerLock = new Object();
    private final ReentrantLock mFrontendLock = new ReentrantLock();
    private final ReentrantLock mLnbLock = new ReentrantLock();
    private final ReentrantLock mFrontendCiCamLock = new ReentrantLock();
    private final ReentrantLock mDemuxLock = new ReentrantLock();
    private int mRequestedCiCamId;

    private Long mDemuxHandle;
    private Long mFrontendCiCamHandle;
    private Integer mFrontendCiCamId;
    private Map<Long, WeakReference<Descrambler>> mDescramblers = new HashMap<>();
    private List<WeakReference<Filter>> mFilters = new ArrayList<WeakReference<Filter>>();

    private final TunerResourceManager.ResourcesReclaimListener mResourceListener =
            new TunerResourceManager.ResourcesReclaimListener() {
                @Override
                public void onReclaimResources() {
                    if (mFrontend != null) {
                        FrameworkStatsLog
                                .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
                                    FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__UNKNOWN);
                    }
                    releaseAll();
                    mHandler.sendMessage(mHandler.obtainMessage(MSG_RESOURCE_LOST));
                }
            };

    /**
     * Constructs a Tuner instance.
     *
     * @param context the context of the caller.
     * @param tvInputSessionId the session ID of the TV input.
     * @param useCase the use case of this Tuner instance.
     */
    @RequiresPermission(android.Manifest.permission.ACCESS_TV_TUNER)
    public Tuner(@NonNull Context context, @Nullable String tvInputSessionId,
            @TvInputService.PriorityHintUseCaseType int useCase) {
        mContext = context;
        mTunerResourceManager = mContext.getSystemService(TunerResourceManager.class);

        // The Tuner Resource Manager is only started when the device has the tuner feature.
        if (mTunerResourceManager == null) {
            throw new IllegalStateException(
                    "Tuner instance is created, but the device doesn't have tuner feature");
        }

        // This code will start tuner server if the device is running on the lazy tuner HAL.
        nativeSetup();
        sTunerVersion = nativeGetTunerVersion();
        if (sTunerVersion == TunerVersionChecker.TUNER_VERSION_UNKNOWN) {
            Log.e(TAG, "Unknown Tuner version!");
        } else {
            Log.d(TAG, "Current Tuner version is "
                    + TunerVersionChecker.getMajorVersion(sTunerVersion) + "."
                    + TunerVersionChecker.getMinorVersion(sTunerVersion) + ".");
        }
        if (mHandler == null) {
            mHandler = createEventHandler();
        }

        int[] clientId = new int[1];
        ResourceClientProfile profile = new ResourceClientProfile();
        profile.tvInputSessionId = tvInputSessionId;
        profile.useCase = useCase;
        mTunerResourceManager.registerClientProfile(
                profile, Runnable::run, mResourceListener, clientId);
        mClientId = clientId[0];

        mUserId = Process.myUid();
    }

    /**
     * Get frontend info list from native and build them into a {@link FrontendInfo} list. Any
     * {@code null} FrontendInfo element would be removed.
     */
    private FrontendInfo[] getFrontendInfoListInternal() {
        List<Integer> ids = getFrontendIds();
        if (ids == null) {
            return null;
        }
        FrontendInfo[] infos = new FrontendInfo[ids.size()];
        for (int i = 0; i < ids.size(); i++) {
            int id = ids.get(i);
            FrontendInfo frontendInfo = getFrontendInfoById(id);
            if (frontendInfo == null) {
                Log.e(TAG, "Failed to get a FrontendInfo on frontend id:" + id + "!");
                continue;
            }
            infos[i] = frontendInfo;
        }
        return Arrays.stream(infos).filter(Objects::nonNull).toArray(FrontendInfo[]::new);
    }

    /** @hide */
    public static int getTunerVersion() {
        return sTunerVersion;
    }

    /** @hide */
    public List<Integer> getFrontendIds() {
        mFrontendLock.lock();
        try {
            return nativeGetFrontendIds();
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Sets the listener for resource lost.
     *
     * @param executor the executor on which the listener should be invoked.
     * @param listener the listener that will be run.
     */
    public void setResourceLostListener(@NonNull @CallbackExecutor Executor executor,
            @NonNull OnResourceLostListener listener) {
        synchronized (mOnResourceLostListenerLock) {
            Objects.requireNonNull(executor, "OnResourceLostListener must not be null");
            Objects.requireNonNull(listener, "executor must not be null");
            mOnResourceLostListener = listener;
            mOnResourceLostListenerExecutor = executor;
        }
    }

    /**
     * Removes the listener for resource lost.
     */
    public void clearResourceLostListener() {
        synchronized (mOnResourceLostListenerLock) {
            mOnResourceLostListener = null;
            mOnResourceLostListenerExecutor = null;
        }
    }

    /**
     * Shares the frontend resource with another Tuner instance
     *
     * @param tuner the Tuner instance to share frontend resource with.
     */
    public void shareFrontendFromTuner(@NonNull Tuner tuner) {
        acquireTRMSLock("shareFrontendFromTuner()");
        mFrontendLock.lock();
        try {
            if (mFeOwnerTuner != null) {
                // unregister self from the Frontend callback
                mFeOwnerTuner.unregisterFrontendCallbackListener(this);
                mFeOwnerTuner = null;
                nativeUnshareFrontend();
            }
            mTunerResourceManager.shareFrontend(mClientId, tuner.mClientId);
            mFeOwnerTuner = tuner;
            mFeOwnerTuner.registerFrontendCallbackListener(this);
            mFrontendHandle = mFeOwnerTuner.mFrontendHandle;
            mFrontend = mFeOwnerTuner.mFrontend;
            nativeShareFrontend(mFrontend.mId);
        } finally {
            releaseTRMSLock();
            mFrontendLock.unlock();
        }
    }

    /**
     * Transfers the ownership of shared frontend and its associated resources.
     *
     * @param newOwner the Tuner instance to be the new owner.
     *
     * @return result status of tune operation.
     */
    public int transferOwner(@NonNull Tuner newOwner) {
        acquireTRMSLock("transferOwner()");
        mFrontendLock.lock();
        mFrontendCiCamLock.lock();
        mLnbLock.lock();
        try {

            if (!isFrontendOwner() || !isNewOwnerQualifiedForTransfer(newOwner)) {
                return RESULT_INVALID_STATE;
            }

            int res = transferFeOwner(newOwner);
            if (res != RESULT_SUCCESS) {
                return res;
            }

            res = transferCiCamOwner(newOwner);
            if (res != RESULT_SUCCESS) {
                return res;
            }

            res = transferLnbOwner(newOwner);
            if (res != RESULT_SUCCESS) {
                return res;
            }
        } finally {
            mFrontendLock.unlock();
            mFrontendCiCamLock.unlock();
            mLnbLock.unlock();
            releaseTRMSLock();
        }
        return RESULT_SUCCESS;
    }

    /**
     * Resets or copies Frontend related settings.
     */
    private void replicateFrontendSettings(@Nullable Tuner src) {
        mFrontendLock.lock();
        try {
            if (src == null) {
                if (DEBUG) {
                    Log.d(TAG, "resetting Frontend params for " + mClientId);
                }
                mFrontend = null;
                mFrontendHandle = null;
                mFrontendInfo = null;
                mFrontendType = FrontendSettings.TYPE_UNDEFINED;
            } else {
                if (DEBUG) {
                    Log.d(TAG, "copying Frontend params from " + src.mClientId
                            + " to " + mClientId);
                }
                mFrontend = src.mFrontend;
                mFrontendHandle = src.mFrontendHandle;
                mFrontendInfo = src.mFrontendInfo;
                mFrontendType = src.mFrontendType;
            }
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Sets the frontend owner. mFeOwnerTuner should be null for the owner Tuner instance.
     */
    private void setFrontendOwner(Tuner owner) {
        mFrontendLock.lock();
        try {
            mFeOwnerTuner = owner;
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Resets or copies the CiCam related settings.
     */
    private void replicateCiCamSettings(@Nullable Tuner src) {
        mFrontendCiCamLock.lock();
        try {
            if (src == null) {
                if (DEBUG) {
                    Log.d(TAG, "resetting CiCamParams: " + mClientId);
                }
                mFrontendCiCamHandle = null;
                mFrontendCiCamId = null;
            } else {
                if (DEBUG) {
                    Log.d(TAG, "copying CiCamParams from " + src.mClientId + " to " + mClientId);
                    Log.d(TAG, "mFrontendCiCamHandle:" + src.mFrontendCiCamHandle + ", "
                            + "mFrontendCiCamId:" + src.mFrontendCiCamId);
                }
                mFrontendCiCamHandle = src.mFrontendCiCamHandle;
                mFrontendCiCamId = src.mFrontendCiCamId;
            }
        } finally {
            mFrontendCiCamLock.unlock();
        }
    }

    /**
     * Resets or copies Lnb related settings.
     */
    private void replicateLnbSettings(@Nullable Tuner src) {
        mLnbLock.lock();
        try {
            if (src == null) {
                if (DEBUG) {
                    Log.d(TAG, "resetting Lnb params");
                }
                mLnb = null;
                mLnbHandle = null;
            } else {
                if (DEBUG) {
                    Log.d(TAG, "copying Lnb params from " + src.mClientId + " to " + mClientId);
                }
                mLnb = src.mLnb;
                mLnbHandle = src.mLnbHandle;
            }
        } finally {
            mLnbLock.unlock();
        }
    }

    /**
     * Checks if it is a frontend resource owner.
     * Proper mutex must be held prior to calling this.
     */
    private boolean isFrontendOwner() {
        boolean notAnOwner = (mFeOwnerTuner != null);
        if (notAnOwner) {
            Log.e(TAG, "transferOwner() - cannot be called on the non-owner");
            return false;
        }
        return true;
    }

    /**
     * Checks if the newOwner is qualified.
     * Proper mutex must be held prior to calling this.
     */
    private boolean isNewOwnerQualifiedForTransfer(@NonNull Tuner newOwner) {
        // new owner must be the current sharee
        boolean newOwnerIsTheCurrentSharee = (newOwner.mFeOwnerTuner == this)
                && (newOwner.mFrontendHandle.equals(mFrontendHandle));
        if (!newOwnerIsTheCurrentSharee) {
            Log.e(TAG, "transferOwner() - new owner must be the current sharee");
            return false;
        }

        // new owner must not be holding any of the to-be-shared resources
        boolean newOwnerAlreadyHoldsToBeSharedResource =
                (newOwner.mFrontendCiCamHandle != null || newOwner.mLnb != null);
        if (newOwnerAlreadyHoldsToBeSharedResource) {
            Log.e(TAG, "transferOwner() - new owner cannot be holding CiCam"
                    + " nor Lnb resource");
            return false;
        }

        return true;
    }

    /**
     * Transfers the ownership of the already held frontend resource.
     * Proper mutex must be held prior to calling this.
     */
    private int transferFeOwner(@NonNull Tuner newOwner) {
        // handle native resource first
        newOwner.nativeUpdateFrontend(getNativeContext());
        nativeUpdateFrontend(0);

        // transfer frontend related settings
        newOwner.replicateFrontendSettings(this);

        // transfer the frontend owner info
        setFrontendOwner(newOwner);
        newOwner.setFrontendOwner(null);

        // handle TRM
        if (mTunerResourceManager.transferOwner(
                TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND,
                mClientId, newOwner.mClientId)) {
            return RESULT_SUCCESS;
        } else {
            return RESULT_UNKNOWN_ERROR;
        }
    }

    /**
     * Transfers the ownership of CiCam resource.
     * This is a no-op if the CiCam resource is not held.
     * Proper mutex must be held prior to calling this.
     */
    private int transferCiCamOwner(Tuner newOwner) {
        boolean notAnOwner = (mFrontendCiCamHandle == null);
        if (notAnOwner) {
            // There is nothing to do here if there is no CiCam
            return RESULT_SUCCESS;
        }

        // no need to handle at native level

        // transfer the CiCam info at Tuner level
        newOwner.replicateCiCamSettings(this);
        replicateCiCamSettings(null);

        // handle TRM
        if (mTunerResourceManager.transferOwner(
                TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND_CICAM,
                mClientId, newOwner.mClientId)) {
            return RESULT_SUCCESS;
        } else {
            return RESULT_UNKNOWN_ERROR;
        }
    }

    /**
     * Transfers the ownership of Lnb resource.
     * This is a no-op if the Lnb resource is not held.
     * Proper mutex must be held prior to calling this.
     */
    private int transferLnbOwner(Tuner newOwner) {
        boolean notAnOwner = (mLnb == null);
        if (notAnOwner) {
            // There is nothing to do here if there is no Lnb
            return RESULT_SUCCESS;
        }

        // no need to handle at native level

        // set the new owner
        mLnb.setOwner(newOwner);

        newOwner.replicateLnbSettings(this);
        replicateLnbSettings(null);

        // handle TRM
        if (mTunerResourceManager.transferOwner(
                TunerResourceManager.TUNER_RESOURCE_TYPE_LNB,
                mClientId, newOwner.mClientId)) {
            return RESULT_SUCCESS;
        } else {
            return RESULT_UNKNOWN_ERROR;
        }
    }

    /**
     * Updates client priority with an arbitrary value along with a nice value.
     *
     * <p>Tuner resource manager (TRM) uses the client priority value to decide whether it is able
     * to reclaim insufficient resources from another client.
     *
     * <p>The nice value represents how much the client intends to give up the resource when an
     * insufficient resource situation happens.
     *
     * @param priority the new priority. Any negative value would cause no-op on priority setting
     *                 and the API would only process nice value setting in that case.
     * @param niceValue the nice value.
     */
    @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
    public void updateResourcePriority(int priority, int niceValue) {
        mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue);
    }

    /**
     * Determines whether the resource holder retains ownership of the resource during a challenge
     * scenario, when both resource holder and resource challenger have same processId and same
     * priority.
     *
     * @param enabled Set to {@code true} to allow the resource holder to retain ownership,
     *     or false to allow the resource challenger to acquire the resource.
     *     If not explicitly set, enabled is set to {@code false}.
     */
    @FlaggedApi(FLAG_SET_RESOURCE_HOLDER_RETAIN)
    @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
    public void setResourceOwnershipRetention(boolean enabled) {
        mTunerResourceManager.setResourceOwnershipRetention(mClientId, enabled);
    }

    /**
     * Checks if there is an unused frontend resource available.
     *
     * @param frontendType {@link android.media.tv.tuner.frontend.FrontendSettings.Type} for the
     * query to be done for.
     */
    @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS)
    public boolean hasUnusedFrontend(int frontendType) {
        return mTunerResourceManager.hasUnusedFrontend(frontendType);
    }

    /**
     * Checks if the calling Tuner object has the lowest priority as a client to
     * {@link TunerResourceManager}
     *
     * <p>The priority comparison is done against the current holders of the frontend resource.
     *
     * <p>The behavior of this function is independent of the availability of unused resources.
     *
     * <p>The function returns {@code true} in any of the following sceanrios:
     * <ul>
     * <li>The caller has the priority <= other clients</li>
     * <li>No one is holding the frontend resource of the specified type</li>
     * <li>The caller is the only one who is holding the resource</li>
     * <li>The frontend resource of the specified type does not exist</li>
     *
     * </ul>
     * @param frontendType {@link android.media.tv.tuner.frontend.FrontendSettings.Type} for the
     * query to be done for.
     *
     * @return {@code false} only if someone else with strictly lower priority is holding the
     *         resourece.
     *         {@code true} otherwise.
     */
    public boolean isLowestPriority(int frontendType) {
        return mTunerResourceManager.isLowestPriority(mClientId, frontendType);
    }

    private long mNativeContext; // used by native jMediaTuner

    /**
     * Registers a tuner as a listener for frontend callbacks.
     */
    private void registerFrontendCallbackListener(Tuner tuner) {
        nativeRegisterFeCbListener(tuner.getNativeContext());
    }

    /**
     * Unregisters a tuner as a listener for frontend callbacks.
     */
    private void unregisterFrontendCallbackListener(Tuner tuner) {
        nativeUnregisterFeCbListener(tuner.getNativeContext());
    }

    /**
     * Returns the pointer to the associated JTuner.
     */
    long getNativeContext() {
        return mNativeContext;
    }

    /**
     * Releases the Tuner instance.
     */
    @Override
    public void close() {
        if (mClosed) {
            return;
        }
        acquireTRMSLock("close()");
        try {
            releaseAll();
            mTunerResourceManager.unregisterClientProfile(mClientId);
            TunerUtils.throwExceptionForResult(nativeClose(), "failed to close tuner");
        } finally {
            releaseTRMSLock();
            mClosed = true;
        }
    }

    /**
     * Either unshares the frontend resource (for sharee) or release Frontend (for owner)
     */
    public void closeFrontend() {
        acquireTRMSLock("closeFrontend()");
        try {
            releaseFrontend();
        } finally {
            releaseTRMSLock();
        }
    }

    /**
     * Releases frontend resource for the owner. Unshares frontend resource for the sharee.
     */
    private void releaseFrontend() {
        if (DEBUG) {
            Log.d(TAG, "Tuner#releaseFrontend");
        }
        mFrontendLock.lock();
        try {
            if (mFrontendHandle != null) {
                if (DEBUG) {
                    Log.d(TAG, "mFrontendHandle not null");
                }
                if (mFeOwnerTuner != null) {
                    if (DEBUG) {
                        Log.d(TAG, "mFeOwnerTuner not null - sharee");
                    }
                    // unregister self from the Frontend callback
                    mFeOwnerTuner.unregisterFrontendCallbackListener(this);
                    mFeOwnerTuner = null;
                    nativeUnshareFrontend();
                } else {
                    if (DEBUG) {
                        Log.d(TAG, "mFeOwnerTuner null - owner");
                    }
                    // close resource as owner
                    int res = nativeCloseFrontend(mFrontendHandle);
                    if (res != Tuner.RESULT_SUCCESS) {
                        TunerUtils.throwExceptionForResult(res, "failed to close frontend");
                    }
                }
                if (DEBUG) {
                    Log.d(TAG, "call TRM#releaseFrontend :" + mFrontendHandle + ", " + mClientId);
                }
                mTunerResourceManager.releaseFrontend(mFrontendHandle, mClientId);
                FrameworkStatsLog
                        .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
                        FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__UNKNOWN);
                replicateFrontendSettings(null);
            }
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Releases CiCam resource if held. No-op otherwise.
     */
    private void releaseCiCam() {
        mFrontendCiCamLock.lock();
        try {
            if (mFrontendCiCamHandle != null) {
                if (DEBUG) {
                    Log.d(TAG, "releasing CiCam : " + mFrontendCiCamHandle + " for " +  mClientId);
                }
                nativeUnlinkCiCam(mFrontendCiCamId);
                mTunerResourceManager.releaseCiCam(mFrontendCiCamHandle, mClientId);
                replicateCiCamSettings(null);
            } else {
                if (DEBUG) {
                    Log.d(TAG, "NOT releasing CiCam : " + mClientId);
                }
            }
        } finally {
            mFrontendCiCamLock.unlock();
        }
    }

    /**
     * Releases Lnb resource if held. TRMS lock must be acquired prior to calling this function.
     */
    private void closeLnb() {
        mLnbLock.lock();
        try {
            // mLnb will be non-null only for owner tuner
            if (mLnb != null) {
                if (DEBUG) {
                    Log.d(TAG, "calling mLnb.close() : " + mClientId);
                }
                mLnb.closeInternal();
            } else {
                if (DEBUG) {
                    Log.d(TAG, "NOT calling mLnb.close() : " + mClientId);
                }
            }
        } finally {
            mLnbLock.unlock();
        }
    }

    private void releaseFilters() {
        synchronized (mFilters) {
            if (!mFilters.isEmpty()) {
                for (WeakReference<Filter> weakFilter : mFilters) {
                    Filter filter = weakFilter.get();
                    if (filter != null) {
                        filter.close();
                    }
                }
                mFilters.clear();
            }
        }
    }

    private void releaseDescramblers() {
        synchronized (mDescramblers) {
            if (!mDescramblers.isEmpty()) {
                for (Map.Entry<Long, WeakReference<Descrambler>> d : mDescramblers.entrySet()) {
                    Descrambler descrambler = d.getValue().get();
                    if (descrambler != null) {
                        descrambler.close();
                    }
                    mTunerResourceManager.releaseDescrambler(d.getKey(), mClientId);
                }
                mDescramblers.clear();
            }
        }
    }

    private void releaseDemux() {
        mDemuxLock.lock();
        try {
            if (mDemuxHandle != null) {
                int res = nativeCloseDemux(mDemuxHandle);
                if (res != Tuner.RESULT_SUCCESS) {
                    TunerUtils.throwExceptionForResult(res, "failed to close demux");
                }
                mTunerResourceManager.releaseDemux(mDemuxHandle, mClientId);
                mDemuxHandle = null;
            }
        } finally {
            mDemuxLock.unlock();
        }
    }

    private void releaseAll() {
        // release CiCam before frontend because frontend handle is needed to unlink CiCam
        releaseCiCam();
        releaseFrontend();
        closeLnb();
        releaseDescramblers();
        releaseFilters();
        releaseDemux();
    }

    /**
     * Native Initialization.
     */
    private static native void nativeInit();

    /**
     * Native setup.
     */
    private native void nativeSetup();

    /**
     * Native method to get all frontend IDs.
     */
    private native int nativeGetTunerVersion();

    /**
     * Native method to get all frontend IDs.
     */
    private native List<Integer> nativeGetFrontendIds();

    /**
     * Native method to open frontend of the given ID.
     */
    private native Frontend nativeOpenFrontendByHandle(long handle);
    private native int nativeShareFrontend(int id);
    private native int nativeUnshareFrontend();
    private native void nativeRegisterFeCbListener(long nativeContext);
    private native void nativeUnregisterFeCbListener(long nativeContext);
    // nativeUpdateFrontend must be called on the new owner first
    private native void nativeUpdateFrontend(long nativeContext);
    @Result
    private native int nativeTune(int type, FrontendSettings settings);
    private native int nativeStopTune();
    private native int nativeScan(int settingsType, FrontendSettings settings, int scanType);
    private native int nativeStopScan();
    private native int nativeSetLnb(Lnb lnb);
    private native boolean nativeIsLnaSupported();
    private native int nativeSetLna(boolean enable);
    private native FrontendStatus nativeGetFrontendStatus(int[] statusTypes);
    private native Integer nativeGetAvSyncHwId(Filter filter);
    private native Long nativeGetAvSyncTime(int avSyncId);
    private native int nativeConnectCiCam(int ciCamId);
    private native int nativeLinkCiCam(int ciCamId);
    private native int nativeDisconnectCiCam();
    private native int nativeUnlinkCiCam(int ciCamId);
    private native FrontendInfo nativeGetFrontendInfo(int id);
    private native Filter nativeOpenFilter(int type, int subType, long bufferSize);
    private native TimeFilter nativeOpenTimeFilter();
    private native String nativeGetFrontendHardwareInfo();
    private native int nativeSetMaxNumberOfFrontends(int frontendType, int maxNumber);
    private native int nativeGetMaxNumberOfFrontends(int frontendType);
    private native int nativeRemoveOutputPid(int pid);
    private native Lnb nativeOpenLnbByHandle(long handle);
    private native Lnb nativeOpenLnbByName(String name);
    private native FrontendStatusReadiness[] nativeGetFrontendStatusReadiness(int[] statusTypes);

    private native Descrambler nativeOpenDescramblerByHandle(long handle);
    private native int nativeOpenDemuxByhandle(long handle);

    private native DvrRecorder nativeOpenDvrRecorder(long bufferSize);
    private native DvrPlayback nativeOpenDvrPlayback(long bufferSize);

    private native DemuxCapabilities nativeGetDemuxCapabilities();
    private native DemuxInfo nativeGetDemuxInfo(long demuxHandle);

    private native int nativeCloseDemux(long handle);
    private native int nativeCloseFrontend(long handle);
    private native int nativeClose();

    private static native SharedFilter nativeOpenSharedFilter(String token);

    /**
     * Listener for resource lost.
     *
     * <p>Insufficient resources are reclaimed by higher priority clients.
     */
    public interface OnResourceLostListener {
        /**
         * Invoked when resource lost.
         *
         * @param tuner the tuner instance whose resource is being reclaimed.
         */
        void onResourceLost(@NonNull Tuner tuner);
    }

    @Nullable
    private EventHandler createEventHandler() {
        Looper looper;
        if ((looper = Looper.myLooper()) != null) {
            return new EventHandler(looper);
        } else if ((looper = Looper.getMainLooper()) != null) {
            return new EventHandler(looper);
        }
        return null;
    }

    private class EventHandler extends Handler {
        private EventHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_ON_FILTER_STATUS: {
                    Filter filter = (Filter) msg.obj;
                    if (filter.getCallback() != null) {
                        filter.getCallback().onFilterStatusChanged(filter, msg.arg1);
                    }
                    break;
                }
                case MSG_RESOURCE_LOST: {
                    synchronized (mOnResourceLostListenerLock) {
                        if (mOnResourceLostListener != null
                                && mOnResourceLostListenerExecutor != null) {
                            mOnResourceLostListenerExecutor.execute(() -> {
                                synchronized (mOnResourceLostListenerLock) {
                                    if (mOnResourceLostListener != null) {
                                        mOnResourceLostListener.onResourceLost(Tuner.this);
                                    }
                                }
                            });
                        }
                    }
                    break;
                }
                default:
                    // fall through
            }
        }
    }

    private class Frontend {
        private int mId;

        private Frontend(int id) {
            mId = id;
        }
    }

    /**
     * Listens for tune events.
     *
     * <p>
     * Tuner events are started when {@link #tune(FrontendSettings)} is called and end when {@link
     * #cancelTuning()} is called.
     *
     * @param eventListener receives tune events.
     * @throws SecurityException if the caller does not have appropriate permissions.
     * @see #tune(FrontendSettings)
     */
    public void setOnTuneEventListener(@NonNull @CallbackExecutor Executor executor,
            @NonNull OnTuneEventListener eventListener) {
        synchronized (mOnTuneEventLock) {
            mOnTuneEventListener = eventListener;
            mOnTuneEventExecutor = executor;
        }
    }

    /**
     * Clears the {@link OnTuneEventListener} and its associated {@link Executor}.
     *
     * @throws SecurityException if the caller does not have appropriate permissions.
     * @see #setOnTuneEventListener(Executor, OnTuneEventListener)
     */
    public void clearOnTuneEventListener() {
        synchronized (mOnTuneEventLock) {
            mOnTuneEventListener = null;
            mOnTuneEventExecutor = null;
        }
    }

    /**
     * Tunes the frontend to using the settings given.
     *
     * <p>Tuner resource manager (TRM) uses the client priority value to decide whether it is able
     * to get frontend resource. If the client can't get the resource, this call returns {@link
     * #RESULT_UNAVAILABLE}.
     *
     * <p>
     * This locks the frontend to a frequency by providing signal
     * delivery information. If previous tuning isn't completed, this stop the previous tuning, and
     * start a new tuning.
     *
     * <p>
     * Tune is an async call, with {@link OnTuneEventListener#SIGNAL_LOCKED} and {@link
     * OnTuneEventListener#SIGNAL_NO_SIGNAL} events sent to the {@link OnTuneEventListener}
     * specified in {@link #setOnTuneEventListener(Executor, OnTuneEventListener)}.
     *
     * <p>Tuning with {@link android.media.tv.tuner.frontend.DtmbFrontendSettings} is only
     * supported in Tuner 1.1 or higher version. Unsupported version will cause no-op. Use {@link
     * TunerVersionChecker#getTunerVersion()} to get the version information.
     *
     * <p>Tuning with {@link
     * android.media.tv.tuner.frontend.IsdbtFrontendSettings.PartialReceptionFlag} or {@link
     * android.media.tv.tuner.frontend.IsdbtFrontendSettings.IsdbtLayerSettings} is only supported
     * in Tuner 2.0 or higher version. Unsupported version will cause no-op. Use {@link
     * TunerVersionChecker#getTunerVersion()} to get the version information.
     *
     * <p>Tuning with {@link
     * android.media.tv.tuner.frontend.IptvFrontendSettings} is only supported
     * in Tuner 3.0 or higher version. Unsupported version will cause no-op. Use {@link
     * TunerVersionChecker#getTunerVersion()} to get the version information.
     *
     * @param settings Signal delivery information the frontend uses to
     *                 search and lock the signal.
     * @return result status of tune operation.
     * @throws SecurityException if the caller does not have appropriate permissions.
     * @see #setOnTuneEventListener(Executor, OnTuneEventListener)
     */
    @Result
    public int tune(@NonNull FrontendSettings settings) {
        mFrontendLock.lock();
        try {
            if (mFeOwnerTuner != null) {
                Log.d(TAG, "Operation cannot be done by sharee of tuner");
                return RESULT_INVALID_STATE;
            }
            final int type = settings.getType();
            if (mFrontendHandle != null && type != mFrontendType) {
                Log.e(TAG, "Frontend was opened with type " + mFrontendType
                        + ", new type is " + type);
                return RESULT_INVALID_STATE;
            }
            Log.d(TAG, "Tune to " + settings.getFrequencyLong());
            mFrontendType = type;
            if (mFrontendType == FrontendSettings.TYPE_DTMB) {
                if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
                        TunerVersionChecker.TUNER_VERSION_1_1, "Tuner with DTMB Frontend")) {
                    return RESULT_UNAVAILABLE;
                }
            }
            if (mFrontendType == FrontendSettings.TYPE_IPTV) {
                if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
                        TunerVersionChecker.TUNER_VERSION_3_0, "Tuner with IPTV Frontend")) {
                    return RESULT_UNAVAILABLE;
                }
            }

            if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, mFrontendLock)) {
                mFrontendInfo = null;
                Log.d(TAG, "Write Stats Log for tuning.");
                FrameworkStatsLog
                        .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
                            FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__TUNING);
                int res = nativeTune(settings.getType(), settings);
                return res;
            } else {
                return RESULT_UNAVAILABLE;
            }
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Stops a previous tuning.
     *
     * <p>If the method completes successfully, the frontend is no longer tuned and no data
     * will be sent to attached filters.
     *
     * @return result status of the operation.
     */
    @Result
    public int cancelTuning() {
        mFrontendLock.lock();
        try {
            if (mFeOwnerTuner != null) {
                Log.d(TAG, "Operation cannot be done by sharee of tuner");
                return RESULT_INVALID_STATE;
            }
            return nativeStopTune();
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Scan for channels.
     *
     * <p>Details for channels found are returned via {@link ScanCallback}.
     *
     * <p>Scanning with {@link android.media.tv.tuner.frontend.DtmbFrontendSettings} is only
     * supported in Tuner 1.1 or higher version. Unsupported version will cause no-op. Use {@link
     * TunerVersionChecker#getTunerVersion()} to get the version information.
     *
     * * <p>Scanning with {@link
     * android.media.tv.tuner.frontend.IsdbtFrontendSettings.PartialReceptionFlag} or {@link
     * android.media.tv.tuner.frontend.IsdbtFrontendSettings.IsdbtLayerSettings} is only supported
     * in Tuner 2.0 or higher version. Unsupported version will cause no-op. Use {@link
     * TunerVersionChecker#getTunerVersion()} to get the version information.
     *
     * @param settings A {@link FrontendSettings} to configure the frontend.
     * @param scanType The scan type.
     * @throws SecurityException     if the caller does not have appropriate permissions.
     * @throws IllegalStateException if {@code scan} is called again before
     *                               {@link #cancelScanning()} is called.
     */
    @Result
    public int scan(@NonNull FrontendSettings settings, @ScanType int scanType,
            @NonNull @CallbackExecutor Executor executor, @NonNull ScanCallback scanCallback) {

        mFrontendLock.lock();
        try {
            if (mFeOwnerTuner != null) {
                Log.d(TAG, "Operation cannot be done by sharee of tuner");
                return RESULT_INVALID_STATE;
            }
            synchronized (mScanCallbackLock) {
                // Scan can be called again for blink scan if scanCallback and executor are same as
                //before.
                if (((mScanCallback != null) && (mScanCallback != scanCallback))
                        || ((mScanCallbackExecutor != null)
                            && (mScanCallbackExecutor != executor))) {
                    throw new IllegalStateException(
                        "Different Scan session already in progress.  stopScan must be called "
                            + "before a new scan session can be " + "started.");
                }
                mFrontendType = settings.getType();
                if (mFrontendType == FrontendSettings.TYPE_DTMB) {
                    if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
                            TunerVersionChecker.TUNER_VERSION_1_1,
                            "Scan with DTMB Frontend")) {
                        return RESULT_UNAVAILABLE;
                    }
                }
                if (mFrontendType == FrontendSettings.TYPE_IPTV) {
                    if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
                            TunerVersionChecker.TUNER_VERSION_3_0,
                            "Tuner with IPTV Frontend")) {
                        return RESULT_UNAVAILABLE;
                    }
                }
                if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND,
                          mFrontendLock)) {
                    mScanCallback = scanCallback;
                    mScanCallbackExecutor = executor;
                    mFrontendInfo = null;
                    FrameworkStatsLog
                            .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
                            FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__SCANNING);
                    return nativeScan(settings.getType(), settings, scanType);
                }
                return RESULT_UNAVAILABLE;
            }
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Stops a previous scanning.
     *
     * <p>
     * The {@link ScanCallback} and it's {@link Executor} will be removed.
     *
     * <p>
     * If the method completes successfully, the frontend stopped previous scanning.
     *
     * @throws SecurityException if the caller does not have appropriate permissions.
     */
    @Result
    public int cancelScanning() {
        mFrontendLock.lock();
        try {
            if (mFeOwnerTuner != null) {
                Log.d(TAG, "Operation cannot be done by sharee of tuner");
                return RESULT_INVALID_STATE;
            }
            synchronized (mScanCallbackLock) {
                FrameworkStatsLog.write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
                        FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__SCAN_STOPPED);

                int retVal = nativeStopScan();
                mScanCallback = null;
                mScanCallbackExecutor = null;
                return retVal;
            }
        } finally {
            mFrontendLock.unlock();
        }
    }

    private boolean requestFrontend() {
        long[] feHandle = new long[1];
        boolean granted = false;
        try {
            TunerFrontendRequest request = new TunerFrontendRequest();
            request.clientId = mClientId;
            request.frontendType = mFrontendType;
            request.desiredId = mDesiredFrontendId == null
                    ? TunerFrontendRequest.DEFAULT_DESIRED_ID
                    : mDesiredFrontendId;
            granted = mTunerResourceManager.requestFrontend(request, feHandle);
        } finally {
            mDesiredFrontendId = null;
        }
        if (granted) {
            mFrontendHandle = feHandle[0];
            mFrontend = nativeOpenFrontendByHandle(mFrontendHandle);
        }

        // For satellite type, set Lnb if valid handle exists.
        // This is necessary as now that we support closeFrontend().
        if (mFrontendType == FrontendSettings.TYPE_DVBS
                || mFrontendType == FrontendSettings.TYPE_ISDBS
                || mFrontendType == FrontendSettings.TYPE_ISDBS3) {
            mLnbLock.lock();
            try {
                if (mLnbHandle != null && mLnb != null) {
                    nativeSetLnb(mLnb);
                }
            } finally {
                mLnbLock.unlock();
            }
        }
        return granted;
    }

    /**
     * Sets Low-Noise Block downconverter (LNB) for satellite frontend.
     *
     * <p>This assigns a hardware LNB resource to the satellite tuner. It can be
     * called multiple times to update LNB assignment.
     *
     * @param lnb the LNB instance.
     *
     * @return result status of the operation.
     */
    @Result
    private int setLnb(@NonNull Lnb lnb) {
        mLnbLock.lock();
        try {
            return nativeSetLnb(lnb);
        } finally {
            mLnbLock.unlock();
        }
    }

    /**
     * Is Low Noise Amplifier (LNA) supported by the Tuner.
     *
     * <p>This API is only supported by Tuner HAL 3.0 or higher.
     * Unsupported version would throw UnsupportedOperationException. Use
     * {@link TunerVersionChecker#getTunerVersion()} to check the version.
     *
     * @return {@code true} if supported, otherwise {@code false}.
     * @throws UnsupportedOperationException if the Tuner HAL version is lower than 3.0
     * @see android.media.tv.tuner.TunerVersionChecker#TUNER_VERSION_3_0
     */
    public boolean isLnaSupported() {
        if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
                TunerVersionChecker.TUNER_VERSION_3_0, "isLnaSupported")) {
            throw new UnsupportedOperationException("Tuner HAL version "
                    + TunerVersionChecker.getTunerVersion() + " doesn't support this method.");
        }
        return nativeIsLnaSupported();
    }

    /**
     * Enable or Disable Low Noise Amplifier (LNA).
     *
     * @param enable {@code true} to activate LNA module; {@code false} to deactivate LNA.
     *
     * @return result status of the operation. {@link #RESULT_UNAVAILABLE} if the device doesn't
     *         support LNA.
     */
    @Result
    public int setLnaEnabled(boolean enable) {
        return nativeSetLna(enable);
    }

    /**
     * Gets the statuses of the frontend.
     *
     * <p>This retrieve the statuses of the frontend for given status types.
     *
     * @param statusTypes an array of status types which the caller requests. Any types that are not
     *        in {@link FrontendInfo#getStatusCapabilities()} would be ignored.
     * @return statuses which response the caller's requests. {@code null} if the operation failed.
     */
    @Nullable
    public FrontendStatus getFrontendStatus(@NonNull @FrontendStatusType int[] statusTypes) {
        mFrontendLock.lock();
        try {
            if (mFrontend == null) {
                throw new IllegalStateException("frontend is not initialized");
            }
            if (mFeOwnerTuner != null) {
                throw new IllegalStateException("Operation cannot be done by sharee of tuner");
            }
            return nativeGetFrontendStatus(statusTypes);
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Gets hardware sync ID for audio and video.
     *
     * @param filter the filter instance for the hardware sync ID.
     * @return the id of hardware A/V sync.
     */
    public int getAvSyncHwId(@NonNull Filter filter) {
        mDemuxLock.lock();
        try {
            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
                return INVALID_AV_SYNC_ID;
            }
            Integer id = nativeGetAvSyncHwId(filter);
            return id == null ? INVALID_AV_SYNC_ID : id;
        } finally {
            mDemuxLock.unlock();
        }
    }

    /**
     * Gets the current timestamp for Audio/Video sync
     *
     * <p>The timestamp is maintained by hardware. The timestamp based on 90KHz, and it's format is
     * the same as PTS (Presentation Time Stamp).
     *
     * @param avSyncHwId the hardware id of A/V sync.
     * @return the current timestamp of hardware A/V sync.
     */
    public long getAvSyncTime(int avSyncHwId) {
        mDemuxLock.lock();
        try {
            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
                return INVALID_TIMESTAMP;
            }
            Long time = nativeGetAvSyncTime(avSyncHwId);
            return time == null ? INVALID_TIMESTAMP : time;
        } finally {
            mDemuxLock.unlock();
        }
    }

    /**
     * Connects Conditional Access Modules (CAM) through Common Interface (CI).
     *
     * <p>The demux uses the output from the frontend as the input by default, and must change to
     * use the output from CI-CAM as the input after this call.
     *
     * <p> Note that this API is used to connect the CI-CAM to the Demux module while
     * {@link #connectFrontendToCiCam(int)} is used to connect CI-CAM to the Frontend module.
     *
     * @param ciCamId specify CI-CAM Id to connect.
     * @return result status of the operation.
     */
    @Result
    public int connectCiCam(int ciCamId) {
        mDemuxLock.lock();
        try {
            if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
                return nativeConnectCiCam(ciCamId);
            }
            return RESULT_UNAVAILABLE;
        } finally {
            mDemuxLock.unlock();
        }
    }

    /**
     * Connect Conditional Access Modules (CAM) Frontend to support Common Interface (CI)
     * by-pass mode.
     *
     * <p>It is used by the client to link CI-CAM to a Frontend. CI by-pass mode requires that
     * the CICAM also receives the TS concurrently from the frontend when the Demux is receiving
     * the TS directly from the frontend.
     *
     * <p> Note that this API is used to connect the CI-CAM to the Frontend module while
     * {@link #connectCiCam(int)} is used to connect CI-CAM to the Demux module.
     *
     * <p>Use {@link #disconnectFrontendToCiCam(int)} to disconnect.
     *
     * <p>This API is only supported by Tuner HAL 1.1 or higher. Unsupported version would cause
     * no-op and return {@link #INVALID_LTS_ID}. Use {@link TunerVersionChecker#getTunerVersion()}
     * to check the version.
     *
     * @param ciCamId specify CI-CAM Id, which is the id of the Conditional Access Modules (CAM)
     *                Common Interface (CI), to link.
     * @return Local transport stream id when connection is successfully established. Failed
     *         operation returns {@link #INVALID_LTS_ID} while unsupported version also returns
     *         {@link #INVALID_LTS_ID}. Check the current HAL version using
     *         {@link TunerVersionChecker#getTunerVersion()}.
     */
    public int connectFrontendToCiCam(int ciCamId) {
        // TODO: change this so TRMS lock is held only when the resource handles for
        // CiCam/Frontend is null. Current implementation can only handle one local lock for that.
        acquireTRMSLock("connectFrontendToCiCam()");
        mFrontendCiCamLock.lock();
        mFrontendLock.lock();
        try {
            if (mFrontendHandle == null) {
                Log.d(TAG, "Operation cannot be done without frontend");
                return RESULT_INVALID_STATE;
            }
            if (mFeOwnerTuner != null) {
                Log.d(TAG, "Operation cannot be done by sharee of tuner");
                return RESULT_INVALID_STATE;
            }
            if (TunerVersionChecker.checkHigherOrEqualVersionTo(
                    TunerVersionChecker.TUNER_VERSION_1_1,
                    "linkFrontendToCiCam")) {
                mRequestedCiCamId = ciCamId;
                // No need to unlock mFrontendCiCamLock and mFrontendLock below becauase
                // TRMS lock is already acquired. Pass null to disable lock related operations
                if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND_CICAM, null)
                        && checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, null)
                    ) {
                    return nativeLinkCiCam(ciCamId);
                }
            }
            return INVALID_LTS_ID;
        } finally {
            releaseTRMSLock();
            mFrontendCiCamLock.unlock();
            mFrontendLock.unlock();
        }
    }

    /**
     * Disconnects Conditional Access Modules (CAM).
     *
     * <p>The demux will use the output from the frontend as the input after this call.
     *
     * <p> Note that this API is used to disconnect the CI-CAM to the Demux module while
     * {@link #disconnectFrontendToCiCam(int)} is used to disconnect CI-CAM to the Frontend module.
     *
     * @return result status of the operation.
     */
    @Result
    public int disconnectCiCam() {
        mDemuxLock.lock();
        try {
            if (mDemuxHandle != null) {
                return nativeDisconnectCiCam();
            }
            return RESULT_UNAVAILABLE;
        } finally {
            mDemuxLock.unlock();
        }
    }

    /**
     * Disconnect Conditional Access Modules (CAM) Frontend.
     *
     * <p>It is used by the client to unlink CI-CAM to a Frontend.
     *
     * <p> Note that this API is used to disconnect the CI-CAM to the Demux module while
     * {@link #disconnectCiCam(int)} is used to disconnect CI-CAM to the Frontend module.
     *
     * <p>This API is only supported by Tuner HAL 1.1 or higher. Unsupported version would cause
     * no-op. Use {@link TunerVersionChecker#getTunerVersion()} to check the version.
     *
     * @param ciCamId specify CI-CAM Id, which is the id of the Conditional Access Modules (CAM)
     *                Common Interface (CI), to disconnect.
     * @return result status of the operation. Unsupported version would return
     *         {@link #RESULT_UNAVAILABLE}
     */
    @Result
    public int disconnectFrontendToCiCam(int ciCamId) {
        acquireTRMSLock("disconnectFrontendToCiCam()");
        try {
            if (mFrontendHandle == null) {
                Log.d(TAG, "Operation cannot be done without frontend");
                return RESULT_INVALID_STATE;
            }
            if (mFeOwnerTuner != null) {
                Log.d(TAG, "Operation cannot be done by sharee of tuner");
                return RESULT_INVALID_STATE;
            }
            if (TunerVersionChecker.checkHigherOrEqualVersionTo(
                    TunerVersionChecker.TUNER_VERSION_1_1,
                    "unlinkFrontendToCiCam")) {
                mFrontendCiCamLock.lock();
                if (mFrontendCiCamHandle != null && mFrontendCiCamId != null
                        && mFrontendCiCamId == ciCamId) {
                    int result = nativeUnlinkCiCam(ciCamId);
                    mTunerResourceManager.releaseCiCam(mFrontendCiCamHandle, mClientId);
                    mFrontendCiCamId = null;
                    mFrontendCiCamHandle = null;
                    return result;
                }
            }
            return RESULT_UNAVAILABLE;
        } finally {
            if (mFrontendCiCamLock.isLocked()) {
                mFrontendCiCamLock.unlock();
            }
            releaseTRMSLock();
        }
    }

    /**
     * Remove PID (packet identifier) from frontend output.
     *
     * <p>It is used by the client to remove a video or audio PID of other program to reduce the
     * total amount of recorded TS.
     *
     * <p>This API is only supported by Tuner HAL 2.0 or higher. Unsupported version would cause
     * no-op. Use {@link TunerVersionChecker#getTunerVersion()} to check the version.
     *
     * @return result status of the operation. Unsupported version or if current active frontend
     *         doesn’t support PID filtering out would return {@link #RESULT_UNAVAILABLE}.
     * @throws IllegalStateException if there is no active frontend currently.
     */
    @Result
    public int removeOutputPid(@IntRange(from = 0) int pid) {
        mFrontendLock.lock();
        try {
            if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
                        TunerVersionChecker.TUNER_VERSION_2_0, "Remove output PID")) {
                return RESULT_UNAVAILABLE;
            }
            if (mFrontend == null) {
                throw new IllegalStateException("frontend is not initialized");
            }
            if (mFeOwnerTuner != null) {
                Log.d(TAG, "Operation cannot be done by sharee of tuner");
                return RESULT_INVALID_STATE;
            }
            return nativeRemoveOutputPid(pid);
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Gets Frontend Status Readiness statuses for given status types.
     *
     * <p>This API is only supported by Tuner HAL 2.0 or higher. Unsupported versions would cause
     * no-op. Use {@link TunerVersionChecker#getTunerVersion()} to check the version.
     *
     * @param statusTypes an array of status types.
     *
     * @return a list of current readiness states. It is empty if the operation fails or unsupported
     *         versions.
     * @throws IllegalStateException if there is no active frontend currently.
     */
    @NonNull
    public List<FrontendStatusReadiness> getFrontendStatusReadiness(
            @NonNull @FrontendStatusType int[] statusTypes) {
        mFrontendLock.lock();
        try {
            if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
                        TunerVersionChecker.TUNER_VERSION_2_0, "Get fronted status readiness")) {
                return Collections.EMPTY_LIST;
            }
            if (mFrontend == null) {
                throw new IllegalStateException("frontend is not initialized");
            }
            if (mFeOwnerTuner != null) {
                throw new IllegalStateException("Operation cannot be done by sharee of tuner");
            }
            FrontendStatusReadiness[] readiness = nativeGetFrontendStatusReadiness(statusTypes);
            if (readiness == null) {
                return Collections.EMPTY_LIST;
            }
            return Arrays.asList(readiness);
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Gets the currently initialized and activated frontend information. To get all the available
     * frontend info on the device, use {@link getAvailableFrontendInfos()}.
     *
     * @return The active frontend information. {@code null} if the operation failed.
     * @throws IllegalStateException if there is no active frontend currently.
     */
    @Nullable
    public FrontendInfo getFrontendInfo() {
        mFrontendLock.lock();
        try {
            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, mFrontendLock)) {
                return null;
            }
            if (mFrontend == null) {
                throw new IllegalStateException("frontend is not initialized");
            }
            if (mFrontendInfo == null) {
                mFrontendInfo = getFrontendInfoById(mFrontend.mId);
            }
            return mFrontendInfo;
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Gets a list of all the available frontend information on the device. To get the information
     * of the currently active frontend, use {@link getFrontendInfo()}. The active frontend
     * information is also included in the list of the available frontend information.
     *
     * @return The list of all the available frontend information. {@code null} if the operation
     * failed.
     */
    @Nullable
    @SuppressLint("NullableCollection")
    public List<FrontendInfo> getAvailableFrontendInfos() {
        FrontendInfo[] feInfoList = getFrontendInfoListInternal();
        if (feInfoList == null) {
            return null;
        }
        return Arrays.asList(feInfoList);
    }

    /**
     * Gets the currently initialized and activated frontend hardware information. The return values
     * would differ per device makers. E.g. RF chip version, Demod chip version, detailed status of
     * dvbs blind scan, etc
     *
     * <p>This API is only supported by Tuner HAL 2.0 or higher. Unsupported version would return
     * {@code null}. Use {@link TunerVersionChecker#getTunerVersion()} to check the version.
     *
     * @return The active frontend hardware information. {@code null} if the operation failed.
     * @throws IllegalStateException if there is no active frontend currently.
     */
    @Nullable
    public String getCurrentFrontendHardwareInfo() {
        mFrontendLock.lock();
        try {
            if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
                        TunerVersionChecker.TUNER_VERSION_2_0, "Get Frontend hardware info")) {
                return null;
            }
            if (mFrontend == null) {
                throw new IllegalStateException("frontend is not initialized");
            }
            if (mFeOwnerTuner != null) {
                throw new IllegalStateException("Operation cannot be done by sharee of tuner");
            }
            return nativeGetFrontendHardwareInfo();
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Sets the maximum usable frontends number of a given frontend type. It is used to enable or
     * disable frontends when cable connection status is changed by user.
     *
     * <p>This API is only supported by Tuner HAL 2.0 or higher. Unsupported version would return
     * {@link RESULT_UNAVAILABLE}. Use {@link TunerVersionChecker#getTunerVersion()} to check the
     * version.
     *
     * @param frontendType the {@link android.media.tv.tuner.frontend.FrontendSettings.Type} which
     *                     the maximum usable number will be set.
     * @param maxNumber the new maximum usable number.
     * @return result status of the operation.
     */
    @Result
    public int setMaxNumberOfFrontends(
            @FrontendSettings.Type int frontendType, @IntRange(from = 0) int maxNumber) {
        if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
                    TunerVersionChecker.TUNER_VERSION_2_0, "Set maximum Frontends")) {
            return RESULT_UNAVAILABLE;
        }
        if (maxNumber < 0) {
            return RESULT_INVALID_ARGUMENT;
        }
        int availableFrontendNum = getPhysicalFrontendCount(frontendType);
        if (availableFrontendNum < maxNumber) {
            Log.d(TAG, "Cannot change to maxNumber(" + maxNumber
                    + "). Exceeds available frontends(" + availableFrontendNum + ").");
            return RESULT_INVALID_ARGUMENT;
        }
        if (mFeOwnerTuner != null) {
            Log.d(TAG, "Operation cannot be done by sharee of tuner");
            return RESULT_INVALID_STATE;
        }
        int res = nativeSetMaxNumberOfFrontends(frontendType, maxNumber);
        if (res == RESULT_SUCCESS) {
            if (!mTunerResourceManager.setMaxNumberOfFrontends(frontendType, maxNumber)) {
                res = RESULT_INVALID_ARGUMENT;
            }
        }
        return res;
    }

    /**
     * Get the maximum usable frontends number of a given frontend type.
     *
     * <p>This API is only supported by Tuner HAL 2.0 or higher. Unsupported version would return
     * {@code -1}. Use {@link TunerVersionChecker#getTunerVersion()} to check the version.
     *
     * @param frontendType the {@link android.media.tv.tuner.frontend.FrontendSettings.Type} which
     *                     the maximum usable number will be queried.
     * @return the maximum usable number of the queried frontend type.
     */
    @IntRange(from = -1)
    public int getMaxNumberOfFrontends(@FrontendSettings.Type int frontendType) {
        if (!TunerVersionChecker.checkHigherOrEqualVersionTo(
                    TunerVersionChecker.TUNER_VERSION_2_0, "Set maximum Frontends")) {
            return -1;
        }
        int maxNumFromHAL = nativeGetMaxNumberOfFrontends(frontendType);
        int maxNumFromTRM = mTunerResourceManager.getMaxNumberOfFrontends(frontendType);
        int availableFrontendNum = getPhysicalFrontendCount(frontendType);
        if (maxNumFromHAL != maxNumFromTRM) {
            Log.w(TAG, "max num of usable frontend is out-of-sync b/w " + maxNumFromHAL
                    + " != " + maxNumFromTRM);
        }
        if (maxNumFromHAL > availableFrontendNum) {
            Log.d(TAG, "max num from HAL(" + maxNumFromHAL
                    + ") is greater than availableFrontends(" + availableFrontendNum + ")");
            return availableFrontendNum;
        } else {
            return maxNumFromHAL;
        }
    }

    /** @hide */
    public FrontendInfo getFrontendInfoById(int id) {
        mFrontendLock.lock();
        try {
            return nativeGetFrontendInfo(id);
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Gets Demux capabilities.
     *
     * @return A {@link DemuxCapabilities} instance that represents the demux capabilities.
     *         {@code null} if the operation failed.
     */
    @Nullable
    public DemuxCapabilities getDemuxCapabilities() {
        mDemuxLock.lock();
        try {
            return nativeGetDemuxCapabilities();
        } finally {
            mDemuxLock.unlock();
        }
    }

    /**
     * Gets DemuxInfo of the currently held demux
     *
     * @return A {@link DemuxInfo} of currently held demux resource.
     *         Returns null if no demux resource is held.
     */
    @Nullable
    public DemuxInfo getCurrentDemuxInfo() {
        mDemuxLock.lock();
        try {
            if (mDemuxHandle == null) {
                return null;
            }
            return nativeGetDemuxInfo(mDemuxHandle);
        } finally {
            mDemuxLock.unlock();
        }
    }

    /** @hide */
    public DemuxInfo getDesiredDemuxInfo() {
        return mDesiredDemuxInfo;
    }

    private void onFrontendEvent(int eventType) {
        Log.d(TAG, "Got event from tuning. Event type: " + eventType + " for " + this);
        synchronized (mOnTuneEventLock) {
            if (mOnTuneEventExecutor != null && mOnTuneEventListener != null) {
                mOnTuneEventExecutor.execute(() -> {
                    synchronized (mOnTuneEventLock) {
                        if (mOnTuneEventListener != null) {
                            mOnTuneEventListener.onTuneEvent(eventType);
                        }
                    }
                });
            }
        }

        Log.d(TAG, "Wrote Stats Log for the events from tuning.");
        if (eventType == OnTuneEventListener.SIGNAL_LOCKED) {
            FrameworkStatsLog
                    .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
                        FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__LOCKED);
        } else if (eventType == OnTuneEventListener.SIGNAL_NO_SIGNAL) {
            FrameworkStatsLog
                    .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
                        FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__NOT_LOCKED);
        } else if (eventType == OnTuneEventListener.SIGNAL_LOST_LOCK) {
            FrameworkStatsLog
                    .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
                        FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__SIGNAL_LOST);
        }
    }

    private void onLocked() {
        Log.d(TAG, "Wrote Stats Log for locked event from scanning.");
        FrameworkStatsLog.write(
                FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
                FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__LOCKED);

        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onLocked();
                        }
                    }
                });
            }
        }
    }

    private void onUnlocked() {
        Log.d(TAG, "Wrote Stats Log for unlocked event from scanning.");
        FrameworkStatsLog.write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
                FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__LOCKED);

        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onUnlocked();
                        }
                    }
                });
            }
        }
    }

    private void onScanStopped() {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onScanStopped();
                        }
                    }
                });
            }
        }
    }

    private void onProgress(int percent) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onProgress(percent);
                        }
                    }
                });
            }
        }
    }

    private void onFrequenciesReport(long[] frequencies) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onFrequenciesLongReported(frequencies);
                        }
                    }
                });
            }
        }
    }

    private void onSymbolRates(int[] rate) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onSymbolRatesReported(rate);
                        }
                    }
                });
            }
        }
    }

    private void onHierarchy(int hierarchy) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onHierarchyReported(hierarchy);
                        }
                    }
                });
            }
        }
    }

    private void onSignalType(int signalType) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onSignalTypeReported(signalType);
                        }
                    }
                });
            }
        }
    }

    private void onPlpIds(int[] plpIds) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onPlpIdsReported(plpIds);
                        }
                    }
                });
            }
        }
    }

    private void onGroupIds(int[] groupIds) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onGroupIdsReported(groupIds);
                        }
                    }
                });
            }
        }
    }

    private void onInputStreamIds(int[] inputStreamIds) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onInputStreamIdsReported(inputStreamIds);
                        }
                    }
                });
            }
        }
    }

    private void onDvbsStandard(int dvbsStandandard) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onDvbsStandardReported(dvbsStandandard);
                        }
                    }
                });
            }
        }
    }

    private void onDvbtStandard(int dvbtStandard) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onDvbtStandardReported(dvbtStandard);
                        }
                    }
                });
            }
        }
    }

    private void onAnalogSifStandard(int sif) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onAnalogSifStandardReported(sif);
                        }
                    }
                });
            }
        }
    }

    private void onAtsc3PlpInfos(Atsc3PlpInfo[] atsc3PlpInfos) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onAtsc3PlpInfosReported(atsc3PlpInfos);
                        }
                    }
                });
            }
        }
    }

    private void onModulationReported(int modulation) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onModulationReported(modulation);
                        }
                    }
                });
            }
        }
    }

    private void onPriorityReported(boolean isHighPriority) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onPriorityReported(isHighPriority);
                        }
                    }
                });
            }
        }
    }

    private void onDvbcAnnexReported(int dvbcAnnex) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onDvbcAnnexReported(dvbcAnnex);
                        }
                    }
                });
            }
        }
    }

    private void onDvbtCellIdsReported(int[] dvbtCellIds) {
        synchronized (mScanCallbackLock) {
            if (mScanCallbackExecutor != null && mScanCallback != null) {
                mScanCallbackExecutor.execute(() -> {
                    synchronized (mScanCallbackLock) {
                        if (mScanCallback != null) {
                            mScanCallback.onDvbtCellIdsReported(dvbtCellIds);
                        }
                    }
                });
            }
        }
    }

    /**
     * Opens a filter object based on the given types and buffer size.
     *
     * <p>For TUNER_VERSION_3_0 and above, configureDemuxInternal() will be called with mainType.
     * However, unlike when configureDemux() is called directly, the desired filter types will not
     * be changed when previously set desired filter types are the superset of the newly desired
     * ones.
     *
     * @param mainType the main type of the filter.
     * @param subType the subtype of the filter.
     * @param bufferSize the buffer size of the filter to be opened in bytes. The buffer holds the
     * data output from the filter.
     * @param executor the executor on which callback will be invoked. The default event handler
     * executor is used if it's {@code null}.
     * @param cb the callback to receive notifications from filter.
     * @return the opened filter. {@code null} if the operation failed.
     */
    @Nullable
    public Filter openFilter(@Type int mainType, @Subtype int subType,
            @BytesLong long bufferSize, @CallbackExecutor @Nullable Executor executor,
            @Nullable FilterCallback cb) {
        mDemuxLock.lock();
        try {
            int tunerMajorVersion = TunerVersionChecker.getMajorVersion(sTunerVersion);
            if (sTunerVersion >= TunerVersionChecker.TUNER_VERSION_3_0) {
                DemuxInfo demuxInfo = new DemuxInfo(mainType);
                int res = configureDemuxInternal(demuxInfo, false /* reduceDesiredFilterTypes */);
                if (res != RESULT_SUCCESS) {
                    Log.e(TAG, "openFilter called for unsupported mainType: " + mainType);
                    return null;
                }
            }
            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
                return null;
            }
            Filter filter = nativeOpenFilter(
                    mainType, TunerUtils.getFilterSubtype(mainType, subType), bufferSize);
            if (filter != null) {
                filter.setType(mainType, subType);
                filter.setCallback(cb, executor);
                if (mHandler == null) {
                    mHandler = createEventHandler();
                }
                synchronized (mFilters) {
                    WeakReference<Filter> weakFilter = new WeakReference<Filter>(filter);
                    mFilters.add(weakFilter);
                    if (mFilters.size() > FILTER_CLEANUP_THRESHOLD) {
                        Iterator<WeakReference<Filter>> iterator = mFilters.iterator();
                        while (iterator.hasNext()) {
                            WeakReference<Filter> wFilter = iterator.next();
                            if (wFilter.get() == null) {
                                iterator.remove();
                            }
                        }
                    }
                }
            }
            return filter;
        } finally {
            mDemuxLock.unlock();
        }
    }

    /**
     * Opens an LNB (low-noise block downconverter) object.
     *
     * <p>If there is an existing Lnb object, it will be replace by the newly opened one.
     *
     * @param executor the executor on which callback will be invoked. The default event handler
     * executor is used if it's {@code null}.
     * @param cb the callback to receive notifications from LNB.
     * @return the opened LNB object. {@code null} if the operation failed.
     */
    @Nullable
    public Lnb openLnb(@CallbackExecutor @NonNull Executor executor, @NonNull LnbCallback cb) {
        mLnbLock.lock();
        try {
            Objects.requireNonNull(executor, "executor must not be null");
            Objects.requireNonNull(cb, "LnbCallback must not be null");
            if (mLnb != null) {
                mLnb.setCallbackAndOwner(this, executor, cb);
                return mLnb;
            }
            if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_LNB, mLnbLock)
                    && mLnb != null) {
                mLnb.setCallbackAndOwner(this, executor, cb);
                if (mFrontendHandle != null && mFrontend != null) {
                    setLnb(mLnb);
                }
                return mLnb;
            }
            return null;
        } finally {
            mLnbLock.unlock();
        }
    }

    /**
     * Opens an LNB (low-noise block downconverter) object specified by the give name.
     *
     * @param name the LNB name.
     * @param executor the executor on which callback will be invoked. The default event handler
     * executor is used if it's {@code null}.
     * @param cb the callback to receive notifications from LNB.
     * @return the opened LNB object. {@code null} if the operation failed.
     */
    @Nullable
    public Lnb openLnbByName(@NonNull String name, @CallbackExecutor @NonNull Executor executor,
            @NonNull LnbCallback cb) {
        acquireTRMSLock("openLnbByName");
        mLnbLock.lock();
        try {
            Objects.requireNonNull(name, "LNB name must not be null");
            Objects.requireNonNull(executor, "executor must not be null");
            Objects.requireNonNull(cb, "LnbCallback must not be null");
            Lnb newLnb = nativeOpenLnbByName(name);
            if (newLnb != null) {
                if (mLnb != null) {
                    mLnb.closeInternal();
                    mLnbHandle = null;
                }
                mLnb = newLnb;
                mLnb.setCallbackAndOwner(this, executor, cb);
                if (mFrontendHandle != null && mFrontend != null) {
                    setLnb(mLnb);
                }
            }
            return mLnb;
        } finally {
            releaseTRMSLock();
            mLnbLock.unlock();
        }
    }

    private boolean requestLnb() {
        long[] lnbHandle = new long[1];
        TunerLnbRequest request = new TunerLnbRequest();
        request.clientId = mClientId;
        boolean granted = mTunerResourceManager.requestLnb(request, lnbHandle);
        if (granted) {
            mLnbHandle = lnbHandle[0];
            mLnb = nativeOpenLnbByHandle(mLnbHandle);
        }
        return granted;
    }

    /**
     * Open a time filter object.
     *
     * @return the opened time filter object. {@code null} if the operation failed.
     */
    @Nullable
    public TimeFilter openTimeFilter() {
        mDemuxLock.lock();
        try {
            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
                return null;
            }
            return nativeOpenTimeFilter();
        } finally {
            mDemuxLock.unlock();
        }
    }

    /**
     * Opens a Descrambler in tuner.
     *
     * @return a {@link Descrambler} object.
     */
    @RequiresPermission(android.Manifest.permission.ACCESS_TV_DESCRAMBLER)
    @Nullable
    public Descrambler openDescrambler() {
        acquireTRMSLock("openDescrambler()");
        mDemuxLock.lock();
        try {
            // no need to unlock mDemuxLock (so pass null instead) as TRMS lock is already acquired
            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, null)) {
                return null;
            }
            return requestDescrambler();
        } finally {
            releaseTRMSLock();
            mDemuxLock.unlock();
        }
    }

    /**
     * Open a DVR (Digital Video Record) recorder instance.
     *
     * @param bufferSize the buffer size of the output in bytes. It's used to hold output data of
     * the attached filters.
     * @param executor the executor on which callback will be invoked. The default event handler
     * executor is used if it's {@code null}.
     * @param l the listener to receive notifications from DVR recorder.
     * @return the opened DVR recorder object. {@code null} if the operation failed.
     */
    @Nullable
    public DvrRecorder openDvrRecorder(
            @BytesLong long bufferSize,
            @CallbackExecutor @NonNull Executor executor,
            @NonNull OnRecordStatusChangedListener l) {
        mDemuxLock.lock();
        try {
            Objects.requireNonNull(executor, "executor must not be null");
            Objects.requireNonNull(l, "OnRecordStatusChangedListener must not be null");
            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
                return null;
            }
            DvrRecorder dvr = nativeOpenDvrRecorder(bufferSize);
            dvr.setListener(executor, l);
            return dvr;
        } finally {
            mDemuxLock.unlock();
        }
    }

    /**
     * Open a DVR (Digital Video Record) playback instance.
     *
     * @param bufferSize the buffer size of the output in bytes. It's used to hold output data of
     * the attached filters.
     * @param executor the executor on which callback will be invoked. The default event handler
     * executor is used if it's {@code null}.
     * @param l the listener to receive notifications from DVR recorder.
     * @return the opened DVR playback object. {@code null} if the operation failed.
     */
    @Nullable
    public DvrPlayback openDvrPlayback(
            @BytesLong long bufferSize,
            @CallbackExecutor @NonNull Executor executor,
            @NonNull OnPlaybackStatusChangedListener l) {
        mDemuxLock.lock();
        try {
            Objects.requireNonNull(executor, "executor must not be null");
            Objects.requireNonNull(l, "OnPlaybackStatusChangedListener must not be null");
            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
                return null;
            }
            DvrPlayback dvr = nativeOpenDvrPlayback(bufferSize);
            dvr.setListener(executor, l);
            return dvr;
        } finally {
            mDemuxLock.unlock();
        }
    }

    /**
     * Request a frontend by frontend info.
     *
     * <p> This API is used if the applications want to select a desired frontend before
     * {@link tune} to use a specific satellite or sending SatCR DiSEqC command for {@link tune}.
     *
     * @param desiredFrontendInfo the FrontendInfo of the desired fronted. It can be retrieved by
     * {@link getAvailableFrontendInfos}
     *
     * @return result status of open operation.
     * @throws SecurityException if the caller does not have appropriate permissions.
     */
    @Result
    public int applyFrontend(@NonNull FrontendInfo desiredFrontendInfo) {
        Objects.requireNonNull(desiredFrontendInfo, "desiredFrontendInfo must not be null");
        mFrontendLock.lock();
        try {
            if (mFeOwnerTuner != null) {
                Log.e(TAG, "Operation connot be done by sharee of tuner");
                return RESULT_INVALID_STATE;
            }
            if (mFrontendHandle != null) {
                Log.e(TAG, "A frontend has been opened before");
                return RESULT_INVALID_STATE;
            }
            mFrontendType = desiredFrontendInfo.getType();
            mDesiredFrontendId = desiredFrontendInfo.getId();
            if (DEBUG) {
                Log.d(TAG, "Applying frontend with type " + mFrontendType + ", id "
                        + mDesiredFrontendId);
            }
            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, mFrontendLock)) {
                return RESULT_UNAVAILABLE;
            }
            return RESULT_SUCCESS;
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Request a frontend by frontend type.
     *
     * <p> This API is used (before {@link #tune(FrontendSettings)}) if the applications want to
     * select a frontend of a particular type for {@link #tune(FrontendSettings)} when there are
     * multiple frontends of the same type present, allowing the system to select which one is
     * applied. The applied frontend will be one of the not-in-use frontends. If all frontends are
     * in-use, this API will reclaim and apply the frontend owned by the lowest priority client if
     * current client has higher priority. Otherwise, this API will not apply any frontend and
     * return {@link #RESULT_UNAVAILABLE}.
     *
     * @param desiredFrontendType the Type of the desired fronted. Should be one of
     *                            {@link android.media.tv.tuner.frontend.FrontendSettings.Type}
     * @return result status of open operation.
     * @see #applyFrontend(FrontendInfo)
     * @see #tune(FrontendSettings)
     */
    @Result
    @FlaggedApi(Flags.FLAG_TUNER_W_APIS)
    @RequiresPermission(
        allOf = {"android.permission.TUNER_RESOURCE_ACCESS", "android.permission.ACCESS_TV_TUNER"})
    public int applyFrontendByType(@FrontendSettings.Type int desiredFrontendType) {
        mFrontendLock.lock();
        try {
            if (mFeOwnerTuner != null) {
                Log.e(TAG, "Operation connot be done by sharee of tuner");
                return RESULT_INVALID_STATE;
            }
            if (mFrontendHandle != null) {
                Log.e(TAG, "A frontend has been opened before");
                return RESULT_INVALID_STATE;
            }

            mDesiredFrontendId = null;
            mFrontendType = desiredFrontendType;
            if (DEBUG) {
                Log.d(TAG, "Applying frontend with type " + mFrontendType);
            }
            if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, mFrontendLock)) {
                return RESULT_UNAVAILABLE;
            }
            return RESULT_SUCCESS;
        } finally {
            mFrontendLock.unlock();
        }
    }

    /**
     * Open a shared filter instance.
     *
     * @param context the context of the caller.
     * @param sharedFilterToken the token of the shared filter being opened.
     * @param executor the executor on which callback will be invoked.
     * @param cb the listener to receive notifications from shared filter.
     * @return the opened shared filter object. {@code null} if the operation failed.
     */
    @RequiresPermission(android.Manifest.permission.ACCESS_TV_SHARED_FILTER)
    @Nullable
    static public SharedFilter openSharedFilter(@NonNull Context context,
            @NonNull String sharedFilterToken, @CallbackExecutor @NonNull Executor executor,
            @NonNull SharedFilterCallback cb) {
        // TODO: check what happenes when onReclaimResources() is called and see if
        // this needs to be protected with TRMS lock
        Objects.requireNonNull(sharedFilterToken, "sharedFilterToken must not be null");
        Objects.requireNonNull(executor, "executor must not be null");
        Objects.requireNonNull(cb, "SharedFilterCallback must not be null");

        if (context.checkCallingOrSelfPermission(
                    android.Manifest.permission.ACCESS_TV_SHARED_FILTER)
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("Caller must have ACCESS_TV_SHAREDFILTER permission.");
        }

        SharedFilter filter = nativeOpenSharedFilter(sharedFilterToken);
        if (filter != null) {
            filter.setCallback(cb, executor);
        }
        return filter;
    }

    /**
     * Configures the desired {@link DemuxInfo}
     *
     * <p>The already held demux and filters will be released when desiredDemuxInfo is null or the
     * desireDemuxInfo.getFilterTypes() is not supported by the already held demux.
     *
     * @param desiredDemuxInfo the desired {@link DemuxInfo}, which includes information such as
     *                         filterTypes ({@link DemuxFilterMainType}).
     * @return result status of configure demux operation. {@link #RESULT_UNAVAILABLE} is returned
     *                when a) the desired capabilities are not supported by the system,
     *                b) this API is called on unsupported version, or
     *                c) either getDemuxCapabilities or getFilterTypeCapabilityList()
     *                returns an empty array
     */
    @Result
    public int configureDemux(@Nullable DemuxInfo desiredDemuxInfo) {
        int tunerMajorVersion = TunerVersionChecker.getMajorVersion(sTunerVersion);
        if (sTunerVersion < TunerVersionChecker.TUNER_VERSION_3_0) {
            Log.e(TAG, "configureDemux() is not supported for tuner version:"
                    + TunerVersionChecker.getMajorVersion(sTunerVersion) + "."
                    + TunerVersionChecker.getMinorVersion(sTunerVersion) + ".");
            return RESULT_UNAVAILABLE;
        }

        synchronized (mDemuxLock) {
            return configureDemuxInternal(desiredDemuxInfo, true /* reduceDesiredFilterTypes */);
        }
    }

    private int configureDemuxInternal(@Nullable DemuxInfo desiredDemuxInfo,
            boolean reduceDesiredFilterTypes) {
        // release the currently held demux if the desired demux info is null
        if (desiredDemuxInfo == null) {
            if (mDemuxHandle != null) {
                releaseFilters();
                releaseDemux();
            }
            return RESULT_SUCCESS;
        }

        int desiredFilterTypes = desiredDemuxInfo.getFilterTypes();

        // just update and return success if the desiredFilterTypes is equal to or a subset of
        // a previously configured value
        if ((mDesiredDemuxInfo.getFilterTypes() & desiredFilterTypes)
                == desiredFilterTypes) {
            if (reduceDesiredFilterTypes) {
                mDesiredDemuxInfo.setFilterTypes(desiredFilterTypes);
            }
            return RESULT_SUCCESS;
        }

        // check if the desire capability is supported
        DemuxCapabilities caps = nativeGetDemuxCapabilities();
        if (caps == null) {
            Log.e(TAG, "configureDemuxInternal:failed to get DemuxCapabilities");
            return RESULT_UNAVAILABLE;
        }

        int[] filterCapsList = caps.getFilterTypeCapabilityList();
        if (filterCapsList.length <= 0) {
            Log.e(TAG, "configureDemuxInternal: getFilterTypeCapabilityList()"
                    + " returned an empty array");
            return RESULT_UNAVAILABLE;
        }

        boolean supported = false;
        for (int filterCaps : filterCapsList) {
            if ((desiredFilterTypes & filterCaps) == desiredFilterTypes) {
                supported = true;
                break;
            }
        }
        if (!supported) {
            Log.e(TAG, "configureDemuxInternal: requested caps:" + desiredFilterTypes
                    + " is not supported by the system");
            return RESULT_UNAVAILABLE;
        }

        // close demux if not compatible
        if (mDemuxHandle != null) {
            if (desiredFilterTypes != Filter.TYPE_UNDEFINED) {
                // Release the existing demux only if
                // the desired caps is not supported
                DemuxInfo currentDemuxInfo = nativeGetDemuxInfo(mDemuxHandle);
                if (currentDemuxInfo != null) {
                    if ((desiredFilterTypes & currentDemuxInfo.getFilterTypes())
                            != desiredFilterTypes) {
                        releaseFilters();
                        releaseDemux();
                    }
                }
            }
        }
        mDesiredDemuxInfo.setFilterTypes(desiredFilterTypes);
        return RESULT_SUCCESS;
    }

    private boolean requestDemux() {
        long[] demuxHandle = new long[1];
        TunerDemuxRequest request = new TunerDemuxRequest();
        request.clientId = mClientId;
        request.desiredFilterTypes = mDesiredDemuxInfo.getFilterTypes();
        boolean granted = mTunerResourceManager.requestDemux(request, demuxHandle);
        if (granted) {
            mDemuxHandle = demuxHandle[0];
            nativeOpenDemuxByhandle(mDemuxHandle);
        }
        return granted;
    }

    private Descrambler requestDescrambler() {
        long[] descramblerHandle = new long[1];
        TunerDescramblerRequest request = new TunerDescramblerRequest();
        request.clientId = mClientId;
        boolean granted = mTunerResourceManager.requestDescrambler(request, descramblerHandle);
        if (!granted) {
            return null;
        }
        long handle = descramblerHandle[0];
        Descrambler descrambler = nativeOpenDescramblerByHandle(handle);
        if (descrambler != null) {
            synchronized (mDescramblers) {
                WeakReference weakDescrambler = new WeakReference<Descrambler>(descrambler);
                mDescramblers.put(handle, weakDescrambler);
            }
        } else {
            mTunerResourceManager.releaseDescrambler(handle, mClientId);
        }
        return descrambler;
    }

    private boolean requestFrontendCiCam(int ciCamId) {
        long[] ciCamHandle = new long[1];
        TunerCiCamRequest request = new TunerCiCamRequest();
        request.clientId = mClientId;
        request.ciCamId = ciCamId;
        boolean granted = mTunerResourceManager.requestCiCam(request, ciCamHandle);
        if (granted) {
            mFrontendCiCamHandle = ciCamHandle[0];
            mFrontendCiCamId = ciCamId;
        }
        return granted;
    }

    private boolean checkResource(int resourceType, ReentrantLock localLock) {
        switch (resourceType) {
            case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND: {
                if (mFrontendHandle == null && !requestResource(resourceType, localLock)) {
                    return false;
                }
                break;
            }
            case TunerResourceManager.TUNER_RESOURCE_TYPE_LNB: {
                if (mLnb == null && !requestResource(resourceType, localLock)) {
                    return false;
                }
                break;
            }
            case TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX: {
                if (mDemuxHandle == null && !requestResource(resourceType, localLock)) {
                    return false;
                }
                break;
            }
            case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND_CICAM: {
                if (mFrontendCiCamHandle == null && !requestResource(resourceType, localLock)) {
                    return false;
                }
                break;
            }
            default:
                return false;
        }
        return true;
    }

    // Expected flow of how to use this function is:
    // 1) lock the localLock and check if the resource is already held
    // 2) if yes, no need to call this function and continue with the handle with the lock held
    // 3) if no, then first release the held lock and grab the TRMS lock to avoid deadlock
    // 4) grab the local lock again and release the TRMS lock
    // If localLock is null, we'll assume the caller does not want the lock related operations
    private boolean requestResource(int resourceType, ReentrantLock localLock) {
        boolean enableLockOperations = localLock != null;

        // release the local lock first to avoid deadlock
        if (enableLockOperations) {
            if (localLock.isLocked()) {
                localLock.unlock();
            } else {
                throw new IllegalStateException("local lock must be locked beforehand");
            }
        }

        // now safe to grab TRMS lock
        if (enableLockOperations) {
            acquireTRMSLock("requestResource:" + resourceType);
        }

        try {
            // lock the local lock
            if (enableLockOperations) {
                localLock.lock();
            }
            switch (resourceType) {
                case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND: {
                    return requestFrontend();
                }
                case TunerResourceManager.TUNER_RESOURCE_TYPE_LNB: {
                    return requestLnb();
                }
                case TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX: {
                    return requestDemux();
                }
                case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND_CICAM: {
                    return requestFrontendCiCam(mRequestedCiCamId);
                }
                default:
                    return false;
            }
        } finally {
            if (enableLockOperations) {
                releaseTRMSLock();
            }
        }
    }

    // Must be called while TRMS lock is being held
    /* package */ void releaseLnb() {
        mLnbLock.lock();
        try {
            if (mLnbHandle != null) {
                // LNB handle can be null if it's opened by name.
                if (DEBUG) {
                    Log.d(TAG, "releasing Lnb");
                }
                mTunerResourceManager.releaseLnb(mLnbHandle, mClientId);
                mLnbHandle = null;
            } else {
                if (DEBUG) {
                    Log.d(TAG, "NOT releasing Lnb because mLnbHandle is null");
                }
            }
            mLnb = null;
        } finally {
            mLnbLock.unlock();
        }
    }

    /** @hide */
    public int getClientId() {
        return mClientId;
    }

    /* package */ TunerResourceManager getTunerResourceManager() {
        return mTunerResourceManager;
    }

    private void acquireTRMSLock(String functionNameForLog) {
        if (DEBUG) {
            Log.d(TAG, "ATTEMPT:acquireLock() in " + functionNameForLog
                    + "for clientId:" + mClientId);
        }
        if (!mTunerResourceManager.acquireLock(mClientId)) {
            Log.e(TAG, "FAILED:acquireLock() in " + functionNameForLog
                    + " for clientId:" + mClientId + " - this can cause deadlock between"
                    + " Tuner API calls and onReclaimResources()");
        }
    }

    private void releaseTRMSLock() {
        mTunerResourceManager.releaseLock(mClientId);
    }

    private int getPhysicalFrontendCount(int frontendType) {
        List<FrontendInfo> infos = getAvailableFrontendInfos();
        int count = 0;
        if (infos != null) {
            for (FrontendInfo info : infos) {
                if (info.getType() == frontendType) {
                    count++;
                }
            }
        }
        return count;
    }
}
