Fix race condition betweeen Tuner APIs and with onReclaimResources

- Added acquire/releaseLock() in ITunerResourceManager.aidl for Tuner
  API implementations to utilize.
- Added more lock around resource related operations that have
  race condition with releaseAll() in Tuner.java
- This fix addresses race condition when Tuner object dies as well.
- Added these calls into Tuner API implementations

Also, suppress FLAG_ONEWAY warning for onReclaimResources as it is only
called against OEM/SoC packages that are part of image that goes throug
Google certification.

Bug: 206725267
Bug: 206726459
Bug: 206723996
Test: TunerTest#testResourceReclaimedDifferentProcess() and
testResourceReclaimedDifferentThread()

Change-Id: Ic9aee1e15f53b2d4984c3b4214a392c2c4d5731e
This commit is contained in:
Kensuke Miyagi
2021-12-02 10:32:06 -08:00
parent f5c3a290f6
commit 17c4a944eb
4 changed files with 763 additions and 214 deletions

View File

@@ -74,6 +74,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
/** /**
* This class is used to interact with hardware tuners devices. * This class is used to interact with hardware tuners devices.
@@ -248,6 +249,7 @@ public class Tuner implements AutoCloseable {
private static final int FILTER_CLEANUP_THRESHOLD = 256; private static final int FILTER_CLEANUP_THRESHOLD = 256;
/** @hide */ /** @hide */
@IntDef(prefix = "DVR_TYPE_", value = {DVR_TYPE_RECORD, DVR_TYPE_PLAYBACK}) @IntDef(prefix = "DVR_TYPE_", value = {DVR_TYPE_RECORD, DVR_TYPE_PLAYBACK})
@Retention(RetentionPolicy.SOURCE) @Retention(RetentionPolicy.SOURCE)
@@ -304,6 +306,11 @@ public class Tuner implements AutoCloseable {
private final Object mOnTuneEventLock = new Object(); private final Object mOnTuneEventLock = new Object();
private final Object mScanCallbackLock = new Object(); private final Object mScanCallbackLock = new Object();
private final Object mOnResourceLostListenerLock = 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 Integer mDemuxHandle; private Integer mDemuxHandle;
private Integer mFrontendCiCamHandle; private Integer mFrontendCiCamHandle;
@@ -391,7 +398,12 @@ public class Tuner implements AutoCloseable {
/** @hide */ /** @hide */
public List<Integer> getFrontendIds() { public List<Integer> getFrontendIds() {
mFrontendLock.lock();
try {
return nativeGetFrontendIds(); return nativeGetFrontendIds();
} finally {
mFrontendLock.unlock();
}
} }
/** /**
@@ -426,6 +438,9 @@ public class Tuner implements AutoCloseable {
* @param tuner the Tuner instance to share frontend resource with. * @param tuner the Tuner instance to share frontend resource with.
*/ */
public void shareFrontendFromTuner(@NonNull Tuner tuner) { public void shareFrontendFromTuner(@NonNull Tuner tuner) {
acquireTRMSLock("shareFrontendFromTuner()");
mFrontendLock.lock();
try {
mTunerResourceManager.shareFrontend(mClientId, tuner.mClientId); mTunerResourceManager.shareFrontend(mClientId, tuner.mClientId);
synchronized (mIsSharedFrontend) { synchronized (mIsSharedFrontend) {
mFrontendHandle = tuner.mFrontendHandle; mFrontendHandle = tuner.mFrontendHandle;
@@ -433,6 +448,10 @@ public class Tuner implements AutoCloseable {
mIsSharedFrontend = true; mIsSharedFrontend = true;
} }
nativeShareFrontend(mFrontend.mId); nativeShareFrontend(mFrontend.mId);
} finally {
releaseTRMSLock();
mFrontendLock.unlock();
}
} }
/** /**
@@ -498,11 +517,18 @@ public class Tuner implements AutoCloseable {
*/ */
@Override @Override
public void close() { public void close() {
acquireTRMSLock("close()");
try {
releaseAll(); releaseAll();
TunerUtils.throwExceptionForResult(nativeClose(), "failed to close tuner"); TunerUtils.throwExceptionForResult(nativeClose(), "failed to close tuner");
} finally {
releaseTRMSLock();
}
} }
private void releaseAll() { private void releaseAll() {
mFrontendLock.lock();
try {
if (mFrontendHandle != null) { if (mFrontendHandle != null) {
synchronized (mIsSharedFrontend) { synchronized (mIsSharedFrontend) {
if (!mIsSharedFrontend) { if (!mIsSharedFrontend) {
@@ -520,9 +546,21 @@ public class Tuner implements AutoCloseable {
mFrontendHandle = null; mFrontendHandle = null;
mFrontend = null; mFrontend = null;
} }
} finally {
mFrontendLock.unlock();
}
mLnbLock.lock();
try {
if (mLnb != null) { if (mLnb != null) {
mLnb.close(); mLnb.close();
} }
} finally {
mLnbLock.unlock();
}
mFrontendCiCamLock.lock();
try {
if (mFrontendCiCamHandle != null) { if (mFrontendCiCamHandle != null) {
int result = nativeUnlinkCiCam(mFrontendCiCamId); int result = nativeUnlinkCiCam(mFrontendCiCamId);
if (result == RESULT_SUCCESS) { if (result == RESULT_SUCCESS) {
@@ -531,6 +569,10 @@ public class Tuner implements AutoCloseable {
mFrontendCiCamHandle = null; mFrontendCiCamHandle = null;
} }
} }
} finally {
mFrontendCiCamLock.unlock();
}
synchronized (mDescramblers) { synchronized (mDescramblers) {
if (!mDescramblers.isEmpty()) { if (!mDescramblers.isEmpty()) {
for (Map.Entry<Integer, WeakReference<Descrambler>> d : mDescramblers.entrySet()) { for (Map.Entry<Integer, WeakReference<Descrambler>> d : mDescramblers.entrySet()) {
@@ -543,6 +585,7 @@ public class Tuner implements AutoCloseable {
mDescramblers.clear(); mDescramblers.clear();
} }
} }
synchronized (mFilters) { synchronized (mFilters) {
if (!mFilters.isEmpty()) { if (!mFilters.isEmpty()) {
for (WeakReference<Filter> weakFilter : mFilters) { for (WeakReference<Filter> weakFilter : mFilters) {
@@ -554,6 +597,9 @@ public class Tuner implements AutoCloseable {
mFilters.clear(); mFilters.clear();
} }
} }
mDemuxLock.lock();
try {
if (mDemuxHandle != null) { if (mDemuxHandle != null) {
int res = nativeCloseDemux(mDemuxHandle); int res = nativeCloseDemux(mDemuxHandle);
if (res != Tuner.RESULT_SUCCESS) { if (res != Tuner.RESULT_SUCCESS) {
@@ -562,6 +608,9 @@ public class Tuner implements AutoCloseable {
mTunerResourceManager.releaseDemux(mDemuxHandle, mClientId); mTunerResourceManager.releaseDemux(mDemuxHandle, mClientId);
mDemuxHandle = null; mDemuxHandle = null;
} }
} finally {
mDemuxLock.unlock();
}
mTunerResourceManager.unregisterClientProfile(mClientId); mTunerResourceManager.unregisterClientProfile(mClientId);
@@ -763,9 +812,12 @@ public class Tuner implements AutoCloseable {
*/ */
@Result @Result
public int tune(@NonNull FrontendSettings settings) { public int tune(@NonNull FrontendSettings settings) {
mFrontendLock.lock();
try {
final int type = settings.getType(); final int type = settings.getType();
if (mFrontendHandle != null && type != mFrontendType) { if (mFrontendHandle != null && type != mFrontendType) {
Log.e(TAG, "Frontend was opened with type " + mFrontendType + ", new type is " + type); Log.e(TAG, "Frontend was opened with type " + mFrontendType
+ ", new type is " + type);
return RESULT_INVALID_STATE; return RESULT_INVALID_STATE;
} }
Log.d(TAG, "Tune to " + settings.getFrequencyLong()); Log.d(TAG, "Tune to " + settings.getFrequencyLong());
@@ -776,16 +828,22 @@ public class Tuner implements AutoCloseable {
return RESULT_UNAVAILABLE; return RESULT_UNAVAILABLE;
} }
} }
if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND)) {
if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, mFrontendLock)) {
mFrontendInfo = null; mFrontendInfo = null;
Log.d(TAG, "Write Stats Log for tuning."); Log.d(TAG, "Write Stats Log for tuning.");
FrameworkStatsLog FrameworkStatsLog
.write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId, .write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__TUNING); FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__TUNING);
return nativeTune(settings.getType(), settings); int res = nativeTune(settings.getType(), settings);
} return res;
} else {
return RESULT_UNAVAILABLE; return RESULT_UNAVAILABLE;
} }
} finally {
mFrontendLock.unlock();
}
}
/** /**
* Stops a previous tuning. * Stops a previous tuning.
@@ -797,7 +855,12 @@ public class Tuner implements AutoCloseable {
*/ */
@Result @Result
public int cancelTuning() { public int cancelTuning() {
mFrontendLock.lock();
try {
return nativeStopTune(); return nativeStopTune();
} finally {
mFrontendLock.unlock();
}
} }
/** /**
@@ -824,11 +887,15 @@ public class Tuner implements AutoCloseable {
@Result @Result
public int scan(@NonNull FrontendSettings settings, @ScanType int scanType, public int scan(@NonNull FrontendSettings settings, @ScanType int scanType,
@NonNull @CallbackExecutor Executor executor, @NonNull ScanCallback scanCallback) { @NonNull @CallbackExecutor Executor executor, @NonNull ScanCallback scanCallback) {
mFrontendLock.lock();
try {
synchronized (mScanCallbackLock) { synchronized (mScanCallbackLock) {
// Scan can be called again for blink scan if scanCallback and executor are same as // Scan can be called again for blink scan if scanCallback and executor are same as
//before. //before.
if (((mScanCallback != null) && (mScanCallback != scanCallback)) if (((mScanCallback != null) && (mScanCallback != scanCallback))
|| ((mScanCallbackExecutor != null) && (mScanCallbackExecutor != executor))) { || ((mScanCallbackExecutor != null)
&& (mScanCallbackExecutor != executor))) {
throw new IllegalStateException( throw new IllegalStateException(
"Different Scan session already in progress. stopScan must be called " "Different Scan session already in progress. stopScan must be called "
+ "before a new scan session can be " + "started."); + "before a new scan session can be " + "started.");
@@ -841,7 +908,8 @@ public class Tuner implements AutoCloseable {
return RESULT_UNAVAILABLE; return RESULT_UNAVAILABLE;
} }
} }
if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND)) { if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND,
mFrontendLock)) {
mScanCallback = scanCallback; mScanCallback = scanCallback;
mScanCallbackExecutor = executor; mScanCallbackExecutor = executor;
mFrontendInfo = null; mFrontendInfo = null;
@@ -852,6 +920,9 @@ public class Tuner implements AutoCloseable {
} }
return RESULT_UNAVAILABLE; return RESULT_UNAVAILABLE;
} }
} finally {
mFrontendLock.unlock();
}
} }
/** /**
@@ -867,6 +938,8 @@ public class Tuner implements AutoCloseable {
*/ */
@Result @Result
public int cancelScanning() { public int cancelScanning() {
mFrontendLock.lock();
try {
synchronized (mScanCallbackLock) { synchronized (mScanCallbackLock) {
FrameworkStatsLog.write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId, FrameworkStatsLog.write(FrameworkStatsLog.TV_TUNER_STATE_CHANGED, mUserId,
FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__SCAN_STOPPED); FrameworkStatsLog.TV_TUNER_STATE_CHANGED__STATE__SCAN_STOPPED);
@@ -876,6 +949,9 @@ public class Tuner implements AutoCloseable {
mScanCallbackExecutor = null; mScanCallbackExecutor = null;
return retVal; return retVal;
} }
} finally {
mFrontendLock.unlock();
}
} }
private boolean requestFrontend() { private boolean requestFrontend() {
@@ -903,7 +979,12 @@ public class Tuner implements AutoCloseable {
*/ */
@Result @Result
private int setLnb(@NonNull Lnb lnb) { private int setLnb(@NonNull Lnb lnb) {
mLnbLock.lock();
try {
return nativeSetLnb(lnb); return nativeSetLnb(lnb);
} finally {
mLnbLock.unlock();
}
} }
/** /**
@@ -929,10 +1010,15 @@ public class Tuner implements AutoCloseable {
*/ */
@Nullable @Nullable
public FrontendStatus getFrontendStatus(@NonNull @FrontendStatusType int[] statusTypes) { public FrontendStatus getFrontendStatus(@NonNull @FrontendStatusType int[] statusTypes) {
mFrontendLock.lock();
try {
if (mFrontend == null) { if (mFrontend == null) {
throw new IllegalStateException("frontend is not initialized"); throw new IllegalStateException("frontend is not initialized");
} }
return nativeGetFrontendStatus(statusTypes); return nativeGetFrontendStatus(statusTypes);
} finally {
mFrontendLock.unlock();
}
} }
/** /**
@@ -942,11 +1028,16 @@ public class Tuner implements AutoCloseable {
* @return the id of hardware A/V sync. * @return the id of hardware A/V sync.
*/ */
public int getAvSyncHwId(@NonNull Filter filter) { public int getAvSyncHwId(@NonNull Filter filter) {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) { mDemuxLock.lock();
try {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
return INVALID_AV_SYNC_ID; return INVALID_AV_SYNC_ID;
} }
Integer id = nativeGetAvSyncHwId(filter); Integer id = nativeGetAvSyncHwId(filter);
return id == null ? INVALID_AV_SYNC_ID : id; return id == null ? INVALID_AV_SYNC_ID : id;
} finally {
mDemuxLock.unlock();
}
} }
/** /**
@@ -959,11 +1050,16 @@ public class Tuner implements AutoCloseable {
* @return the current timestamp of hardware A/V sync. * @return the current timestamp of hardware A/V sync.
*/ */
public long getAvSyncTime(int avSyncHwId) { public long getAvSyncTime(int avSyncHwId) {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) { mDemuxLock.lock();
try {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
return INVALID_TIMESTAMP; return INVALID_TIMESTAMP;
} }
Long time = nativeGetAvSyncTime(avSyncHwId); Long time = nativeGetAvSyncTime(avSyncHwId);
return time == null ? INVALID_TIMESTAMP : time; return time == null ? INVALID_TIMESTAMP : time;
} finally {
mDemuxLock.unlock();
}
} }
/** /**
@@ -980,10 +1076,15 @@ public class Tuner implements AutoCloseable {
*/ */
@Result @Result
public int connectCiCam(int ciCamId) { public int connectCiCam(int ciCamId) {
if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) { mDemuxLock.lock();
try {
if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
return nativeConnectCiCam(ciCamId); return nativeConnectCiCam(ciCamId);
} }
return RESULT_UNAVAILABLE; return RESULT_UNAVAILABLE;
} finally {
mDemuxLock.unlock();
}
} }
/** /**
@@ -1011,14 +1112,30 @@ public class Tuner implements AutoCloseable {
* {@link TunerVersionChecker#getTunerVersion()}. * {@link TunerVersionChecker#getTunerVersion()}.
*/ */
public int connectFrontendToCiCam(int ciCamId) { public int connectFrontendToCiCam(int ciCamId) {
if (TunerVersionChecker.checkHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1, // 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 (TunerVersionChecker.checkHigherOrEqualVersionTo(
TunerVersionChecker.TUNER_VERSION_1_1,
"linkFrontendToCiCam")) { "linkFrontendToCiCam")) {
if (checkCiCamResource(ciCamId) mRequestedCiCamId = ciCamId;
&& checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND)) { // 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 nativeLinkCiCam(ciCamId);
} }
} }
return INVALID_LTS_ID; return INVALID_LTS_ID;
} finally {
releaseTRMSLock();
mFrontendCiCamLock.unlock();
mFrontendLock.unlock();
}
} }
/** /**
@@ -1033,10 +1150,15 @@ public class Tuner implements AutoCloseable {
*/ */
@Result @Result
public int disconnectCiCam() { public int disconnectCiCam() {
mDemuxLock.lock();
try {
if (mDemuxHandle != null) { if (mDemuxHandle != null) {
return nativeDisconnectCiCam(); return nativeDisconnectCiCam();
} }
return RESULT_UNAVAILABLE; return RESULT_UNAVAILABLE;
} finally {
mDemuxLock.unlock();
}
} }
/** /**
@@ -1057,8 +1179,12 @@ public class Tuner implements AutoCloseable {
*/ */
@Result @Result
public int disconnectFrontendToCiCam(int ciCamId) { public int disconnectFrontendToCiCam(int ciCamId) {
if (TunerVersionChecker.checkHigherOrEqualVersionTo(TunerVersionChecker.TUNER_VERSION_1_1, acquireTRMSLock("disconnectFrontendToCiCam()");
try {
if (TunerVersionChecker.checkHigherOrEqualVersionTo(
TunerVersionChecker.TUNER_VERSION_1_1,
"unlinkFrontendToCiCam")) { "unlinkFrontendToCiCam")) {
mFrontendCiCamLock.lock();
if (mFrontendCiCamHandle != null && mFrontendCiCamId != null if (mFrontendCiCamHandle != null && mFrontendCiCamId != null
&& mFrontendCiCamId == ciCamId) { && mFrontendCiCamId == ciCamId) {
int result = nativeUnlinkCiCam(ciCamId); int result = nativeUnlinkCiCam(ciCamId);
@@ -1071,6 +1197,12 @@ public class Tuner implements AutoCloseable {
} }
} }
return RESULT_UNAVAILABLE; return RESULT_UNAVAILABLE;
} finally {
if (mFrontendCiCamLock.isLocked()) {
mFrontendCiCamLock.unlock();
}
releaseTRMSLock();
}
} }
/** /**
@@ -1082,7 +1214,9 @@ public class Tuner implements AutoCloseable {
*/ */
@Nullable @Nullable
public FrontendInfo getFrontendInfo() { public FrontendInfo getFrontendInfo() {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND)) { mFrontendLock.lock();
try {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND, mFrontendLock)) {
return null; return null;
} }
if (mFrontend == null) { if (mFrontend == null) {
@@ -1092,6 +1226,9 @@ public class Tuner implements AutoCloseable {
mFrontendInfo = getFrontendInfoById(mFrontend.mId); mFrontendInfo = getFrontendInfoById(mFrontend.mId);
} }
return mFrontendInfo; return mFrontendInfo;
} finally {
mFrontendLock.unlock();
}
} }
/** /**
@@ -1114,7 +1251,12 @@ public class Tuner implements AutoCloseable {
/** @hide */ /** @hide */
public FrontendInfo getFrontendInfoById(int id) { public FrontendInfo getFrontendInfoById(int id) {
mFrontendLock.lock();
try {
return nativeGetFrontendInfo(id); return nativeGetFrontendInfo(id);
} finally {
mFrontendLock.unlock();
}
} }
/** /**
@@ -1125,7 +1267,12 @@ public class Tuner implements AutoCloseable {
*/ */
@Nullable @Nullable
public DemuxCapabilities getDemuxCapabilities() { public DemuxCapabilities getDemuxCapabilities() {
mDemuxLock.lock();
try {
return nativeGetDemuxCapabilities(); return nativeGetDemuxCapabilities();
} finally {
mDemuxLock.unlock();
}
} }
private void onFrontendEvent(int eventType) { private void onFrontendEvent(int eventType) {
@@ -1417,7 +1564,9 @@ public class Tuner implements AutoCloseable {
public Filter openFilter(@Type int mainType, @Subtype int subType, public Filter openFilter(@Type int mainType, @Subtype int subType,
@BytesLong long bufferSize, @CallbackExecutor @Nullable Executor executor, @BytesLong long bufferSize, @CallbackExecutor @Nullable Executor executor,
@Nullable FilterCallback cb) { @Nullable FilterCallback cb) {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) { mDemuxLock.lock();
try {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
return null; return null;
} }
Filter filter = nativeOpenFilter( Filter filter = nativeOpenFilter(
@@ -1443,6 +1592,9 @@ public class Tuner implements AutoCloseable {
} }
} }
return filter; return filter;
} finally {
mDemuxLock.unlock();
}
} }
/** /**
@@ -1457,18 +1609,24 @@ public class Tuner implements AutoCloseable {
*/ */
@Nullable @Nullable
public Lnb openLnb(@CallbackExecutor @NonNull Executor executor, @NonNull LnbCallback cb) { public Lnb openLnb(@CallbackExecutor @NonNull Executor executor, @NonNull LnbCallback cb) {
mLnbLock.lock();
try {
Objects.requireNonNull(executor, "executor must not be null"); Objects.requireNonNull(executor, "executor must not be null");
Objects.requireNonNull(cb, "LnbCallback must not be null"); Objects.requireNonNull(cb, "LnbCallback must not be null");
if (mLnb != null) { if (mLnb != null) {
mLnb.setCallback(executor, cb, this); mLnb.setCallback(executor, cb, this);
return mLnb; return mLnb;
} }
if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_LNB) && mLnb != null) { if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_LNB, mLnbLock)
&& mLnb != null) {
mLnb.setCallback(executor, cb, this); mLnb.setCallback(executor, cb, this);
setLnb(mLnb); setLnb(mLnb);
return mLnb; return mLnb;
} }
return null; return null;
} finally {
mLnbLock.unlock();
}
} }
/** /**
@@ -1483,6 +1641,8 @@ public class Tuner implements AutoCloseable {
@Nullable @Nullable
public Lnb openLnbByName(@NonNull String name, @CallbackExecutor @NonNull Executor executor, public Lnb openLnbByName(@NonNull String name, @CallbackExecutor @NonNull Executor executor,
@NonNull LnbCallback cb) { @NonNull LnbCallback cb) {
mLnbLock.lock();
try {
Objects.requireNonNull(name, "LNB name must not be null"); Objects.requireNonNull(name, "LNB name must not be null");
Objects.requireNonNull(executor, "executor must not be null"); Objects.requireNonNull(executor, "executor must not be null");
Objects.requireNonNull(cb, "LnbCallback must not be null"); Objects.requireNonNull(cb, "LnbCallback must not be null");
@@ -1497,6 +1657,9 @@ public class Tuner implements AutoCloseable {
setLnb(mLnb); setLnb(mLnb);
} }
return mLnb; return mLnb;
} finally {
mLnbLock.unlock();
}
} }
private boolean requestLnb() { private boolean requestLnb() {
@@ -1518,10 +1681,15 @@ public class Tuner implements AutoCloseable {
*/ */
@Nullable @Nullable
public TimeFilter openTimeFilter() { public TimeFilter openTimeFilter() {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) { mDemuxLock.lock();
try {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
return null; return null;
} }
return nativeOpenTimeFilter(); return nativeOpenTimeFilter();
} finally {
mDemuxLock.unlock();
}
} }
/** /**
@@ -1532,10 +1700,15 @@ public class Tuner implements AutoCloseable {
@RequiresPermission(android.Manifest.permission.ACCESS_TV_DESCRAMBLER) @RequiresPermission(android.Manifest.permission.ACCESS_TV_DESCRAMBLER)
@Nullable @Nullable
public Descrambler openDescrambler() { public Descrambler openDescrambler() {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) { mDemuxLock.lock();
try {
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
return null; return null;
} }
return requestDescrambler(); return requestDescrambler();
} finally {
mDemuxLock.unlock();
}
} }
/** /**
@@ -1553,14 +1726,19 @@ public class Tuner implements AutoCloseable {
@BytesLong long bufferSize, @BytesLong long bufferSize,
@CallbackExecutor @NonNull Executor executor, @CallbackExecutor @NonNull Executor executor,
@NonNull OnRecordStatusChangedListener l) { @NonNull OnRecordStatusChangedListener l) {
mDemuxLock.lock();
try {
Objects.requireNonNull(executor, "executor must not be null"); Objects.requireNonNull(executor, "executor must not be null");
Objects.requireNonNull(l, "OnRecordStatusChangedListener must not be null"); Objects.requireNonNull(l, "OnRecordStatusChangedListener must not be null");
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) { if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
return null; return null;
} }
DvrRecorder dvr = nativeOpenDvrRecorder(bufferSize); DvrRecorder dvr = nativeOpenDvrRecorder(bufferSize);
dvr.setListener(executor, l); dvr.setListener(executor, l);
return dvr; return dvr;
} finally {
mDemuxLock.unlock();
}
} }
/** /**
@@ -1578,14 +1756,19 @@ public class Tuner implements AutoCloseable {
@BytesLong long bufferSize, @BytesLong long bufferSize,
@CallbackExecutor @NonNull Executor executor, @CallbackExecutor @NonNull Executor executor,
@NonNull OnPlaybackStatusChangedListener l) { @NonNull OnPlaybackStatusChangedListener l) {
mDemuxLock.lock();
try {
Objects.requireNonNull(executor, "executor must not be null"); Objects.requireNonNull(executor, "executor must not be null");
Objects.requireNonNull(l, "OnPlaybackStatusChangedListener must not be null"); Objects.requireNonNull(l, "OnPlaybackStatusChangedListener must not be null");
if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX)) { if (!checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX, mDemuxLock)) {
return null; return null;
} }
DvrPlayback dvr = nativeOpenDvrPlayback(bufferSize); DvrPlayback dvr = nativeOpenDvrPlayback(bufferSize);
dvr.setListener(executor, l); dvr.setListener(executor, l);
return dvr; return dvr;
} finally {
mDemuxLock.unlock();
}
} }
/** /**
@@ -1602,6 +1785,8 @@ public class Tuner implements AutoCloseable {
static public SharedFilter openSharedFilter(@NonNull Context context, static public SharedFilter openSharedFilter(@NonNull Context context,
@NonNull String sharedFilterToken, @CallbackExecutor @NonNull Executor executor, @NonNull String sharedFilterToken, @CallbackExecutor @NonNull Executor executor,
@NonNull SharedFilterCallback cb) { @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(sharedFilterToken, "sharedFilterToken must not be null");
Objects.requireNonNull(executor, "executor must not be null"); Objects.requireNonNull(executor, "executor must not be null");
Objects.requireNonNull(cb, "SharedFilterCallback must not be null"); Objects.requireNonNull(cb, "SharedFilterCallback must not be null");
@@ -1665,22 +1850,28 @@ public class Tuner implements AutoCloseable {
return granted; return granted;
} }
private boolean checkResource(int resourceType) { private boolean checkResource(int resourceType, ReentrantLock localLock) {
switch (resourceType) { switch (resourceType) {
case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND: { case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND: {
if (mFrontendHandle == null && !requestFrontend()) { if (mFrontendHandle == null && !requestResource(resourceType, localLock)) {
return false; return false;
} }
break; break;
} }
case TunerResourceManager.TUNER_RESOURCE_TYPE_LNB: { case TunerResourceManager.TUNER_RESOURCE_TYPE_LNB: {
if (mLnb == null && !requestLnb()) { if (mLnb == null && !requestResource(resourceType, localLock)) {
return false; return false;
} }
break; break;
} }
case TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX: { case TunerResourceManager.TUNER_RESOURCE_TYPE_DEMUX: {
if (mDemuxHandle == null && !requestDemux()) { if (mDemuxHandle == null && !requestResource(resourceType, localLock)) {
return false;
}
break;
}
case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND_CICAM: {
if (mFrontendCiCamHandle == null && !requestResource(resourceType, localLock)) {
return false; return false;
} }
break; break;
@@ -1691,24 +1882,91 @@ public class Tuner implements AutoCloseable {
return true; return true;
} }
private boolean checkCiCamResource(int ciCamId) { // Expected flow of how to use this function is:
if (mFrontendCiCamHandle == null && !requestFrontendCiCam(ciCamId)) { // 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; return false;
} }
return true; } finally {
if (enableLockOperations) {
releaseTRMSLock();
}
}
} }
/* package */ void releaseLnb() { /* package */ void releaseLnb() {
acquireTRMSLock("releaseLnb()");
mLnbLock.lock();
try {
if (mLnbHandle != null) { if (mLnbHandle != null) {
// LNB handle can be null if it's opened by name. // LNB handle can be null if it's opened by name.
mTunerResourceManager.releaseLnb(mLnbHandle, mClientId); mTunerResourceManager.releaseLnb(mLnbHandle, mClientId);
mLnbHandle = null; mLnbHandle = null;
} }
mLnb = null; mLnb = null;
} finally {
releaseTRMSLock();
mLnbLock.unlock();
}
} }
/** @hide */ /** @hide */
public int getClientId() { public int getClientId() {
return mClientId; return mClientId;
} }
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);
}
} }

View File

@@ -319,6 +319,48 @@ public class TunerResourceManager {
} }
} }
/**
* Grants the lock to the caller for public {@link Tuner} APIs
*
* <p>{@link Tuner} functions that call both [@link TunerResourceManager} APIs and
* grabs lock that are also used in {@link IResourcesReclaimListener#onReclaimResources()}
* must call this API before acquiring lock used in onReclaimResources().
*
* <p>This API will block until it releases the lock or fails
*
* @param clientId The ID of the caller.
*
* @return true if the lock is granted. If false is returned, calling this API again is not
* guaranteed to work and may be unrecoverrable. (This should not happen.)
*/
public boolean acquireLock(int clientId) {
try {
return mService.acquireLock(clientId, Thread.currentThread().getId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/**
* Releases the lock to the caller for public {@link Tuner} APIs
*
* <p>This API must be called in pair with {@link #acquireLock(int, int)}
*
* <p>This API will block until it releases the lock or fails
*
* @param clientId The ID of the caller.
*
* @return true if the lock is granted. If false is returned, calling this API again is not
* guaranteed to work and may be unrecoverrable. (This should not happen.)
*/
public boolean releaseLock(int clientId) {
try {
return mService.releaseLock(clientId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
/** /**
* Requests a frontend resource. * Requests a frontend resource.
* *

View File

@@ -412,4 +412,34 @@ interface ITunerResourceManager {
* @param resourceType The resource type to restore the map for. * @param resourceType The resource type to restore the map for.
*/ */
void restoreResourceMap(in int resourceType); void restoreResourceMap(in int resourceType);
/**
* Grants the lock to the caller for public {@link Tuner} APIs
*
* <p>{@link Tuner} functions that call both [@link TunerResourceManager} APIs and
* grabs lock that are also used in {@link IResourcesReclaimListener#onReclaimResources()}
* must call this API before acquiring lock used in onReclaimResources().
*
* <p>This API will block until it releases the lock or fails
*
* @param clientId The ID of the caller.
*
* @return true if the lock is granted. If false is returned, calling this API again is not
* guaranteed to work and may be unrecoverrable. (This should not happen.)
*/
boolean acquireLock(in int clientId, in long clientThreadId);
/**
* Releases the lock to the caller for public {@link Tuner} APIs
*
* <p>This API must be called in pair with {@link #acquireLock(int, int)}
*
* <p>This API will block until it releases the lock or fails
*
* @param clientId The ID of the caller.
*
* @return true if the lock is granted. If false is returned, calling this API again is not
* guaranteed to work and may be unrecoverrable. (This should not happen.)
*/
boolean releaseLock(in int clientId);
} }

View File

@@ -37,6 +37,7 @@ import android.media.tv.tunerresourcemanager.TunerResourceManager;
import android.os.Binder; import android.os.Binder;
import android.os.IBinder; import android.os.IBinder;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.SystemClock;
import android.util.IndentingPrintWriter; import android.util.IndentingPrintWriter;
import android.util.Log; import android.util.Log;
import android.util.Slog; import android.util.Slog;
@@ -52,6 +53,9 @@ import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
/** /**
* This class provides a system service that manages the TV tuner resources. * This class provides a system service that manages the TV tuner resources.
@@ -64,6 +68,8 @@ public class TunerResourceManagerService extends SystemService implements IBinde
public static final int INVALID_CLIENT_ID = -1; public static final int INVALID_CLIENT_ID = -1;
private static final int MAX_CLIENT_PRIORITY = 1000; private static final int MAX_CLIENT_PRIORITY = 1000;
private static final long INVALID_THREAD_ID = -1;
private static final long TRMS_LOCK_TIMEOUT = 500;
// Map of the registered client profiles // Map of the registered client profiles
private Map<Integer, ClientProfile> mClientProfiles = new HashMap<>(); private Map<Integer, ClientProfile> mClientProfiles = new HashMap<>();
@@ -94,6 +100,12 @@ public class TunerResourceManagerService extends SystemService implements IBinde
// Used to synchronize the access to the service. // Used to synchronize the access to the service.
private final Object mLock = new Object(); private final Object mLock = new Object();
private final ReentrantLock mLockForTRMSLock = new ReentrantLock();
private final Condition mTunerApiLockReleasedCV = mLockForTRMSLock.newCondition();
private int mTunerApiLockHolder = INVALID_CLIENT_ID;
private long mTunerApiLockHolderThreadId = INVALID_THREAD_ID;
private int mTunerApiLockNestedCount = 0;
public TunerResourceManagerService(@Nullable Context context) { public TunerResourceManagerService(@Nullable Context context) {
super(context); super(context);
} }
@@ -510,6 +522,20 @@ public class TunerResourceManagerService extends SystemService implements IBinde
} }
} }
@Override
public boolean acquireLock(int clientId, long clientThreadId) {
enforceTrmAccessPermission("acquireLock");
// this must not be locked with mLock
return acquireLockInternal(clientId, clientThreadId, TRMS_LOCK_TIMEOUT);
}
@Override
public boolean releaseLock(int clientId) {
enforceTrmAccessPermission("releaseLock");
// this must not be locked with mLock
return releaseLockInternal(clientId, TRMS_LOCK_TIMEOUT, false, false);
}
@Override @Override
protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) { protected void dump(FileDescriptor fd, final PrintWriter writer, String[] args) {
final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " "); final IndentingPrintWriter pw = new IndentingPrintWriter(writer, " ");
@@ -1194,6 +1220,187 @@ public class TunerResourceManagerService extends SystemService implements IBinde
return true; return true;
} }
// Return value is guaranteed to be positive
private long getElapsedTime(long begin) {
long now = SystemClock.uptimeMillis();
long elapsed;
if (now >= begin) {
elapsed = now - begin;
} else {
elapsed = Long.MAX_VALUE - begin + now;
if (elapsed < 0) {
elapsed = Long.MAX_VALUE;
}
}
return elapsed;
}
private boolean lockForTunerApiLock(int clientId, long timeoutMS, String callerFunction) {
try {
if (mLockForTRMSLock.tryLock(timeoutMS, TimeUnit.MILLISECONDS)) {
return true;
} else {
Slog.e(TAG, "FAILED to lock mLockForTRMSLock in " + callerFunction
+ ", clientId:" + clientId + ", timeoutMS:" + timeoutMS
+ ", mTunerApiLockHolder:" + mTunerApiLockHolder);
return false;
}
} catch (InterruptedException ie) {
Slog.e(TAG, "exception thrown in " + callerFunction + ":" + ie);
if (mLockForTRMSLock.isHeldByCurrentThread()) {
mLockForTRMSLock.unlock();
}
return false;
}
}
private boolean acquireLockInternal(int clientId, long clientThreadId, long timeoutMS) {
long begin = SystemClock.uptimeMillis();
// Grab lock
if (!lockForTunerApiLock(clientId, timeoutMS, "acquireLockInternal()")) {
return false;
}
try {
boolean available = mTunerApiLockHolder == INVALID_CLIENT_ID;
boolean nestedSelf = (clientId == mTunerApiLockHolder)
&& (clientThreadId == mTunerApiLockHolderThreadId);
boolean recovery = false;
// Allow same thread to grab the lock multiple times
while (!available && !nestedSelf) {
// calculate how much time is left before timeout
long leftOverMS = timeoutMS - getElapsedTime(begin);
if (leftOverMS <= 0) {
Slog.e(TAG, "FAILED:acquireLockInternal(" + clientId + ", " + clientThreadId
+ ", " + timeoutMS + ") - timed out, but will grant the lock to "
+ "the callee by stealing it from the current holder:"
+ mTunerApiLockHolder + "(" + mTunerApiLockHolderThreadId + "), "
+ "who likely failed to call releaseLock(), "
+ "to prevent this from becoming an unrecoverable error");
// This should not normally happen, but there sometimes are cases where
// in-flight tuner API execution gets scheduled even after binderDied(),
// which can leave the in-flight execution dissappear/stopped in between
// acquireLock and releaseLock
recovery = true;
break;
}
// Cond wait for left over time
mTunerApiLockReleasedCV.await(leftOverMS, TimeUnit.MILLISECONDS);
// Check the availability for "spurious wakeup"
// The case that was confirmed is that someone else can acquire this in between
// signal() and wakup from the above await()
available = mTunerApiLockHolder == INVALID_CLIENT_ID;
if (!available) {
Slog.w(TAG, "acquireLockInternal(" + clientId + ", " + clientThreadId + ", "
+ timeoutMS + ") - woken up from cond wait, but " + mTunerApiLockHolder
+ "(" + mTunerApiLockHolderThreadId + ") is already holding the lock. "
+ "Going to wait again if timeout hasn't reached yet");
}
}
// Will always grant unless exception is thrown (or lock is already held)
if (available || recovery) {
if (DEBUG) {
Slog.d(TAG, "SUCCESS:acquireLockInternal(" + clientId + ", " + clientThreadId
+ ", " + timeoutMS + ")");
}
if (mTunerApiLockNestedCount != 0) {
Slog.w(TAG, "Something is wrong as nestedCount(" + mTunerApiLockNestedCount
+ ") is not zero. Will overriding it to 1 anyways");
}
// set the caller to be the holder
mTunerApiLockHolder = clientId;
mTunerApiLockHolderThreadId = clientThreadId;
mTunerApiLockNestedCount = 1;
} else if (nestedSelf) {
// Increment the nested count so releaseLockInternal won't signal prematuredly
mTunerApiLockNestedCount++;
if (DEBUG) {
Slog.d(TAG, "acquireLockInternal(" + clientId + ", " + clientThreadId
+ ", " + timeoutMS + ") - nested count incremented to "
+ mTunerApiLockNestedCount);
}
} else {
Slog.e(TAG, "acquireLockInternal(" + clientId + ", " + clientThreadId
+ ", " + timeoutMS + ") - should not reach here");
}
// return true in "recovery" so callee knows that the deadlock is possible
// only when the return value is false
return (available || nestedSelf || recovery);
} catch (InterruptedException ie) {
Slog.e(TAG, "exception thrown in acquireLockInternal(" + clientId + ", "
+ clientThreadId + ", " + timeoutMS + "):" + ie);
return false;
} finally {
if (mLockForTRMSLock.isHeldByCurrentThread()) {
mLockForTRMSLock.unlock();
}
}
}
private boolean releaseLockInternal(int clientId, long timeoutMS,
boolean ignoreNestedCount, boolean suppressError) {
// Grab lock first
if (!lockForTunerApiLock(clientId, timeoutMS, "releaseLockInternal()")) {
return false;
}
try {
if (mTunerApiLockHolder == clientId) {
// Should always reach here unless called from binderDied()
mTunerApiLockNestedCount--;
if (ignoreNestedCount || mTunerApiLockNestedCount <= 0) {
if (DEBUG) {
Slog.d(TAG, "SUCCESS:releaseLockInternal(" + clientId + ", " + timeoutMS
+ ", " + ignoreNestedCount + ", " + suppressError
+ ") - signaling!");
}
// Reset the current holder and signal
mTunerApiLockHolder = INVALID_CLIENT_ID;
mTunerApiLockHolderThreadId = INVALID_THREAD_ID;
mTunerApiLockNestedCount = 0;
mTunerApiLockReleasedCV.signal();
} else {
if (DEBUG) {
Slog.d(TAG, "releaseLockInternal(" + clientId + ", " + timeoutMS
+ ", " + ignoreNestedCount + ", " + suppressError
+ ") - NOT signaling because nested count is not zero ("
+ mTunerApiLockNestedCount + ")");
}
}
return true;
} else if (mTunerApiLockHolder == INVALID_CLIENT_ID) {
if (!suppressError) {
Slog.w(TAG, "releaseLockInternal(" + clientId + ", " + timeoutMS
+ ") - called while there is no current holder");
}
// No need to do anything.
// Shouldn't reach here unless called from binderDied()
return false;
} else {
if (!suppressError) {
Slog.e(TAG, "releaseLockInternal(" + clientId + ", " + timeoutMS
+ ") - called while someone else:" + mTunerApiLockHolder
+ "is the current holder");
}
// Cannot reset the holder Id because it reaches here when called
// from binderDied()
return false;
}
} finally {
if (mLockForTRMSLock.isHeldByCurrentThread()) {
mLockForTRMSLock.unlock();
}
}
}
@VisibleForTesting @VisibleForTesting
protected class ResourcesReclaimListenerRecord implements IBinder.DeathRecipient { protected class ResourcesReclaimListenerRecord implements IBinder.DeathRecipient {
private final IResourcesReclaimListener mListener; private final IResourcesReclaimListener mListener;
@@ -1206,11 +1413,16 @@ public class TunerResourceManagerService extends SystemService implements IBinde
@Override @Override
public void binderDied() { public void binderDied() {
try {
synchronized (mLock) { synchronized (mLock) {
if (checkClientExists(mClientId)) { if (checkClientExists(mClientId)) {
removeClientProfile(mClientId); removeClientProfile(mClientId);
} }
} }
} finally {
// reset the tuner API lock
releaseLockInternal(mClientId, TRMS_LOCK_TIMEOUT, true, true);
}
} }
public int getId() { public int getId() {
@@ -1247,6 +1459,13 @@ public class TunerResourceManagerService extends SystemService implements IBinde
protected boolean reclaimResource(int reclaimingClientId, protected boolean reclaimResource(int reclaimingClientId,
@TunerResourceManager.TunerResourceType int resourceType) { @TunerResourceManager.TunerResourceType int resourceType) {
// Allowing this because:
// 1) serialization of resource reclaim is required in the current design
// 2) the outgoing transaction is handled by the system app (with
// android.Manifest.permission.TUNER_RESOURCE_ACCESS), which goes through full
// Google certification
Binder.allowBlockingForCurrentThread();
// Reclaim all the resources of the share owners of the frontend that is used by the current // Reclaim all the resources of the share owners of the frontend that is used by the current
// resource reclaimed client. // resource reclaimed client.
ClientProfile profile = getClientProfile(reclaimingClientId); ClientProfile profile = getClientProfile(reclaimingClientId);