Enhance resource sharing and enable ownership transfer

New public APIs
- Tuner.closeFrontend()
  - Enables TIS app to continue using other resource while changing
    frontend resource
  - close & release Frontend resource when called on the owner, while
    it unshares Frontend resource when called on the sharee.
- Lnb.addCallback(Callback, Executor) and Lnb.removeCallback(Callback)
  - Enables TIS app to receive callback from the sharee
  - Also helps with the ownership transfer
- Tuner.transferOwner(Tuner newOwner)
  - Transfers the ownership of Frontend, CiCam, and Lnb resource

Additionally, added the following:
- Call nativeSetLnb() in requestFrontend() in case mLnb resource
  is already held. (this use case becomes a possibility now that we
  support Tuner.closeFrontend())

Bug: 192010866
Test: cts.TunerTest#testTransferOwner, testLnbAddAndRemoveSharee

Change-Id: I4c39c3726f0dd7bd1c153975ad01393ff2773005
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 5a6ea89..ee2dfd9 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -6338,7 +6338,9 @@
   }
 
   public class Lnb implements java.lang.AutoCloseable {
+    method public void addCallback(@NonNull android.media.tv.tuner.LnbCallback, @NonNull java.util.concurrent.Executor);
     method public void close();
+    method public boolean removeCallback(@NonNull android.media.tv.tuner.LnbCallback);
     method public int sendDiseqcMessage(@NonNull byte[]);
     method public int setSatellitePosition(int);
     method public int setTone(int);
@@ -6375,6 +6377,7 @@
     method public void clearOnTuneEventListener();
     method public void clearResourceLostListener();
     method public void close();
+    method public void closeFrontend();
     method public int connectCiCam(int);
     method public int connectFrontendToCiCam(int);
     method public int disconnectCiCam();
@@ -6400,6 +6403,7 @@
     method public void setOnTuneEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.tuner.frontend.OnTuneEventListener);
     method public void setResourceLostListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.tuner.Tuner.OnResourceLostListener);
     method public void shareFrontendFromTuner(@NonNull android.media.tv.tuner.Tuner);
+    method public int transferOwner(@NonNull android.media.tv.tuner.Tuner);
     method public int tune(@NonNull android.media.tv.tuner.frontend.FrontendSettings);
     method @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public void updateResourcePriority(int, int);
     field public static final int INVALID_AV_SYNC_ID = -1; // 0xffffffff
diff --git a/media/java/android/media/tv/tuner/Lnb.java b/media/java/android/media/tv/tuner/Lnb.java
index 6a6a22c..50a2083 100644
--- a/media/java/android/media/tv/tuner/Lnb.java
+++ b/media/java/android/media/tv/tuner/Lnb.java
@@ -28,6 +28,9 @@
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
 import java.util.concurrent.Executor;
 
 /**
@@ -145,9 +148,9 @@
 
     private static final String TAG = "Lnb";
 
-    LnbCallback mCallback;
-    Executor mExecutor;
-    Tuner mTuner;
+    Map<LnbCallback, Executor> mCallbackMap =
+            new HashMap<LnbCallback, Executor>();
+    Tuner mOwner;
     private final Object mCallbackLock = new Object();
 
 
@@ -164,38 +167,82 @@
 
     private Lnb() {}
 
-    void setCallback(Executor executor, @Nullable LnbCallback callback, Tuner tuner) {
+    void setCallbackAndOwner(Executor executor, @Nullable LnbCallback callback, Tuner tuner) {
         synchronized (mCallbackLock) {
-            mCallback = callback;
-            mExecutor = executor;
-            mTuner = tuner;
+            if (callback != null && executor != null) {
+                addCallback(callback, executor);
+            }
+        }
+        setOwner(tuner);
+    }
+
+    /**
+     * Adds LnbCallback
+     *
+     * @param callback the callback to receive notifications from LNB.
+     * @param executor the executor on which callback will be invoked. Cannot be null.
+     */
+    public void addCallback(@NonNull  LnbCallback callback, @NonNull Executor executor) {
+        Objects.requireNonNull(callback, "callback must not be null");
+        Objects.requireNonNull(executor, "executor must not be null");
+        synchronized (mCallbackLock) {
+            mCallbackMap.put(callback, executor);
+        }
+    }
+
+    /**
+     * Removes LnbCallback
+     *
+     * @param callback the callback be removed for callback
+     *
+     * @return {@code true} when successful. {@code false} otherwise.
+     */
+    public boolean removeCallback(@NonNull LnbCallback callback) {
+        Objects.requireNonNull(callback, "callback must not be null");
+        synchronized (mCallbackLock) {
+            boolean result = (mCallbackMap.remove(callback) != null);
+            return result;
+        }
+    }
+
+    // allow owner transfer independent of whether callback is registered or not
+    /* package */ void setOwner(@NonNull Tuner newOwner) {
+        Objects.requireNonNull(newOwner, "newOwner must not be null");
+        synchronized (mLock) {
+            mOwner = newOwner;
         }
     }
 
     private void onEvent(int eventType) {
         synchronized (mCallbackLock) {
-            if (mExecutor != null && mCallback != null) {
-                mExecutor.execute(() -> {
-                    synchronized (mCallbackLock) {
-                        if (mCallback != null) {
-                            mCallback.onEvent(eventType);
+            for (LnbCallback callback : mCallbackMap.keySet()) {
+                Executor executor = mCallbackMap.get(callback);
+                if (callback != null && executor != null) {
+                    executor.execute(() -> {
+                        synchronized (mCallbackLock) {
+                            if (callback != null) {
+                                callback.onEvent(eventType);
+                            }
                         }
-                    }
-                });
+                    });
+                }
             }
         }
     }
 
     private void onDiseqcMessage(byte[] diseqcMessage) {
         synchronized (mCallbackLock) {
-            if (mExecutor != null && mCallback != null) {
-                mExecutor.execute(() -> {
-                    synchronized (mCallbackLock) {
-                        if (mCallback != null) {
-                            mCallback.onDiseqcMessage(diseqcMessage);
+            for (LnbCallback callback : mCallbackMap.keySet()) {
+                Executor executor = mCallbackMap.get(callback);
+                if (callback != null && executor != null) {
+                    executor.execute(() -> {
+                        synchronized (mCallbackLock) {
+                            if (callback != null) {
+                                callback.onDiseqcMessage(diseqcMessage);
+                            }
                         }
-                    }
-                });
+                    });
+                }
             }
         }
     }
@@ -279,7 +326,11 @@
                 TunerUtils.throwExceptionForResult(res, "Failed to close LNB");
             } else {
                 mIsClosed = true;
-                mTuner.releaseLnb();
+                if (mOwner != null) {
+                    mOwner.releaseLnb();
+                    mOwner = null;
+                }
+                mCallbackMap.clear();
             }
         }
     }
diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java
index 255b391b..c06d0fe 100644
--- a/media/java/android/media/tv/tuner/Tuner.java
+++ b/media/java/android/media/tv/tuner/Tuner.java
@@ -240,7 +240,7 @@
 
 
     private static final String TAG = "MediaTvTuner";
-    private static final boolean DEBUG = false;
+    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;
@@ -249,7 +249,6 @@
 
     private static final int FILTER_CLEANUP_THRESHOLD = 256;
 
-
     /** @hide */
     @IntDef(prefix = "DVR_TYPE_", value = {DVR_TYPE_RECORD, DVR_TYPE_PLAYBACK})
     @Retention(RetentionPolicy.SOURCE)
@@ -454,6 +453,260 @@
     }
 
     /**
+     * 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
@@ -546,59 +799,114 @@
         }
     }
 
+    /**
+     * 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");
                     }
-                    mTunerResourceManager.releaseFrontend(mFrontendHandle, mClientId);
                 }
+                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);
-                mFrontendHandle = null;
-                mFrontend = null;
+                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, "unlinking CiCam : " + mFrontendCiCamHandle + " for " +  mClientId);
+                }
+                int result = nativeUnlinkCiCam(mFrontendCiCamId);
+                if (result == RESULT_SUCCESS) {
+                    mTunerResourceManager.releaseCiCam(mFrontendCiCamHandle, mClientId);
+                    replicateCiCamSettings(null);
+                } else {
+                    Log.e(TAG, "nativeUnlinkCiCam(" + mFrontendCiCamHandle + ") for mClientId:"
+                            + mClientId + "failed with result:" + result);
+                }
+            } else {
+                if (DEBUG) {
+                    Log.d(TAG, "NOT unlinking CiCam : " + mClientId);
+                }
+            }
+        } finally {
+            mFrontendCiCamLock.unlock();
+        }
+    }
+
     private void releaseAll() {
+        // release CiCam before frontend because frontend handle is needed to unlink CiCam
+        releaseCiCam();
+
         releaseFrontend();
 
         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.close();
+            } else {
+                if (DEBUG) {
+                    Log.d(TAG, "NOT calling mLnb.close() : " + mClientId);
+                }
             }
         } finally {
             mLnbLock.unlock();
         }
 
-        mFrontendCiCamLock.lock();
-        try {
-            if (mFrontendCiCamHandle != null) {
-                int result = nativeUnlinkCiCam(mFrontendCiCamId);
-                if (result == RESULT_SUCCESS) {
-                    mTunerResourceManager.releaseCiCam(mFrontendCiCamHandle, mClientId);
-                    mFrontendCiCamId = null;
-                    mFrontendCiCamHandle = null;
-                }
-            }
-        } finally {
-            mFrontendCiCamLock.unlock();
-        }
 
         synchronized (mDescramblers) {
             if (!mDescramblers.isEmpty()) {
@@ -668,8 +976,11 @@
      */
     private native Frontend nativeOpenFrontendByHandle(int 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();
@@ -993,6 +1304,21 @@
             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;
     }
 
@@ -1643,12 +1969,12 @@
             Objects.requireNonNull(executor, "executor must not be null");
             Objects.requireNonNull(cb, "LnbCallback must not be null");
             if (mLnb != null) {
-                mLnb.setCallback(executor, cb, this);
+                mLnb.setCallbackAndOwner(executor, cb, this);
                 return mLnb;
             }
             if (checkResource(TunerResourceManager.TUNER_RESOURCE_TYPE_LNB, mLnbLock)
                     && mLnb != null) {
-                mLnb.setCallback(executor, cb, this);
+                mLnb.setCallbackAndOwner(executor, cb, this);
                 setLnb(mLnb);
                 return mLnb;
             }
@@ -1682,7 +2008,7 @@
                     mLnbHandle = null;
                 }
                 mLnb = newLnb;
-                mLnb.setCallback(executor, cb, this);
+                mLnb.setCallbackAndOwner(executor, cb, this);
                 setLnb(mLnb);
             }
             return mLnb;
@@ -1968,8 +2294,15 @@
         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 {
diff --git a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
index fe611c7..e978404 100644
--- a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
+++ b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java
@@ -415,6 +415,25 @@
     }
 
     /**
+     * Transfers the ownership of shared resource.
+     *
+     * <p><strong>Note:</strong> Only the existing frontend sharee can be the new owner.
+     *
+     * @param resourceType the type of the resource to transfer the ownership for.
+     * @param currentOwnerId the id of the current owner client.
+     * @param newOwnerId the id of the new owner client.
+     *
+     * @return true if successful and false otherwise.
+     */
+    public boolean transferOwner(int resourceType, int currentOwnerId, int newOwnerId) {
+        try {
+            return mService.transferOwner(resourceType, currentOwnerId, newOwnerId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Requests a Tuner Demux resource.
      *
      * <p>There are three possible scenarios:
diff --git a/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl b/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
index 5f35820..e9ad668 100644
--- a/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
+++ b/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl
@@ -177,6 +177,19 @@
     void shareFrontend(in int selfClientId, in int targetClientId);
 
     /*
+     * Transfers the ownership of the shared resource.
+     *
+     * <p><strong>Note:</strong> Only the existing frontend sharee can be the new owner.
+     *
+     * @param resourceType the type of resource to transfer the ownership for.
+     * @param currentOwnerId the id of the current owner client.
+     * @param newOwnerId the id of the new owner client.
+     *
+     * @return true if successful. false otherwise.
+     */
+    boolean transferOwner(in int resourceType, in int currentOwnerId, in int newOwnerId);
+
+    /*
      * This API is used by the Tuner framework to request an available demux from the TunerHAL.
      *
      * <p>There are three possible scenarios:
diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp
index c230df3..38a0db4 100644
--- a/media/jni/android_media_tv_Tuner.cpp
+++ b/media/jni/android_media_tv_Tuner.cpp
@@ -959,7 +959,8 @@
 void FrontendClientCallbackImpl::addCallbackListener(JTuner* jtuner, jweak listener) {
     JNIEnv *env = AndroidRuntime::getJNIEnv();
     jweak listenerRef = env->NewWeakGlobalRef(listener);
-    ALOGV("addCallbackListener() with listener:%p and ref:%p @%p", listener, listenerRef, this);
+    ALOGV("addCallbackListener() with listener:%p and ref:%p @%p",
+              listener, listenerRef, this);
     std::scoped_lock<std::mutex> lock(mMutex);
     mListenersMap[jtuner] = listenerRef;
 }
@@ -1314,18 +1315,43 @@
     return (int)Result::SUCCESS;
 }
 
+int JTuner::unshareFrontend() {
+    if (mFeClient != nullptr) {
+        ALOGE("Cannot unshare frontend because this session is already holding %d"
+              " as an owner instead of as a sharee", mFeClient->getId());
+        return (int)Result::INVALID_STATE;
+    }
+
+    mSharedFeId = (int)Constant::INVALID_FRONTEND_ID;
+    return (int)Result::SUCCESS;
+}
+
 void JTuner::registerFeCbListener(JTuner* jtuner) {
+    ALOGV("registerFeCbListener: %p", jtuner);
     if (mFeClientCb != nullptr && jtuner != nullptr) {
         mFeClientCb->addCallbackListener(jtuner, jtuner->getObject());
     }
 }
 
 void JTuner::unregisterFeCbListener(JTuner* jtuner) {
+    ALOGV("unregisterFeCbListener: %p", jtuner);
     if (mFeClientCb != nullptr && jtuner != nullptr) {
         mFeClientCb->removeCallbackListener(jtuner);
     }
 }
 
+void JTuner::updateFrontend(JTuner* jtuner) {
+    if (jtuner == nullptr) {
+        ALOGV("JTuner::updateFrontend(null) called for previous owner: %p", this);
+        mFeClient = nullptr;
+        mFeClientCb = nullptr;
+    } else {
+        ALOGV("JTuner::updateFrontend(%p) called for new owner: %p", jtuner, this);
+        mFeClient = jtuner->mFeClient;
+        mFeClientCb = jtuner->mFeClientCb;
+    }
+}
+
 jobject JTuner::getAnalogFrontendCaps(JNIEnv *env, FrontendCapabilities &caps) {
     jclass clazz = env->FindClass("android/media/tv/tuner/frontend/AnalogFrontendCapabilities");
     jmethodID capsInit = env->GetMethodID(clazz, "<init>", "(II)V");
@@ -3245,6 +3271,12 @@
     return tuner->shareFrontend(id);
 }
 
+static int android_media_tv_Tuner_unshare_frontend(
+        JNIEnv *env, jobject thiz) {
+    sp<JTuner> tuner = getTuner(env, thiz);
+    return tuner->unshareFrontend();
+}
+
 static void android_media_tv_Tuner_register_fe_cb_listener(
         JNIEnv *env, jobject thiz, jlong shareeJTuner) {
     sp<JTuner> tuner = getTuner(env, thiz);
@@ -3259,6 +3291,17 @@
     tuner->unregisterFeCbListener(jtuner);
 }
 
+static void android_media_tv_Tuner_update_frontend(JNIEnv *env, jobject thiz, jlong jtunerPtr) {
+    sp<JTuner> tuner = getTuner(env, thiz);
+    JTuner *jtuner;
+    if (jtunerPtr == 0) {
+        jtuner = nullptr;
+    } else {
+        jtuner = (JTuner *) jtunerPtr;
+    }
+    tuner->updateFrontend(jtuner);
+}
+
 static int android_media_tv_Tuner_tune(JNIEnv *env, jobject thiz, jint type, jobject settings) {
     sp<JTuner> tuner = getTuner(env, thiz);
     FrontendSettings setting = getFrontendSettings(env, type, settings);
@@ -4432,10 +4475,14 @@
             (void *)android_media_tv_Tuner_open_frontend_by_handle },
     { "nativeShareFrontend", "(I)I",
             (void *)android_media_tv_Tuner_share_frontend },
+    { "nativeUnshareFrontend", "()I",
+            (void *)android_media_tv_Tuner_unshare_frontend },
     { "nativeRegisterFeCbListener", "(J)V",
             (void*)android_media_tv_Tuner_register_fe_cb_listener },
     { "nativeUnregisterFeCbListener", "(J)V",
             (void*)android_media_tv_Tuner_unregister_fe_cb_listener },
+    { "nativeUpdateFrontend", "(J)V",
+            (void*)android_media_tv_Tuner_update_frontend },
     { "nativeTune", "(ILandroid/media/tv/tuner/frontend/FrontendSettings;)I",
             (void *)android_media_tv_Tuner_tune },
     { "nativeStopTune", "()I", (void *)android_media_tv_Tuner_stop_tune },
diff --git a/media/jni/android_media_tv_Tuner.h b/media/jni/android_media_tv_Tuner.h
index 06e2492..7195821 100644
--- a/media/jni/android_media_tv_Tuner.h
+++ b/media/jni/android_media_tv_Tuner.h
@@ -178,8 +178,10 @@
     jobject getFrontendIds();
     jobject openFrontendByHandle(int feHandle);
     int shareFrontend(int feId);
+    int unshareFrontend();
     void registerFeCbListener(JTuner* jtuner);
     void unregisterFeCbListener(JTuner* jtuner);
+    void updateFrontend(JTuner* jtuner);
     jint closeFrontendById(int id);
     jobject getFrontendInfo(int id);
     int tune(const FrontendSettings& settings);
diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
index e508260..9e4c0234 100644
--- a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
+++ b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java
@@ -288,6 +288,23 @@
         }
 
         @Override
+        public boolean transferOwner(int resourceType, int currentOwnerId, int newOwnerId) {
+            enforceTunerAccessPermission("transferOwner");
+            enforceTrmAccessPermission("transferOwner");
+            synchronized (mLock) {
+                if (!checkClientExists(currentOwnerId)) {
+                    Slog.e(TAG, "currentOwnerId:" + currentOwnerId + " does not exit");
+                    return false;
+                }
+                if (!checkClientExists(newOwnerId)) {
+                    Slog.e(TAG, "newOwnerId:" + newOwnerId + " does not exit");
+                    return false;
+                }
+                return transferOwnerInternal(resourceType, currentOwnerId, newOwnerId);
+            }
+        }
+
+        @Override
         public boolean requestDemux(@NonNull TunerDemuxRequest request,
                     @NonNull int[] demuxHandle) throws RemoteException {
             enforceTunerAccessPermission("requestDemux");
@@ -387,7 +404,11 @@
                 if (fe == null) {
                     throw new RemoteException("Releasing frontend does not exist.");
                 }
-                if (fe.getOwnerClientId() != clientId) {
+                int ownerClientId = fe.getOwnerClientId();
+                ClientProfile ownerProfile = getClientProfile(ownerClientId);
+                if (ownerClientId != clientId
+                        && (ownerProfile != null
+                              && !ownerProfile.getShareFeClientIds().contains(clientId))) {
                     throw new RemoteException(
                             "Client is not the current owner of the releasing fe.");
                 }
@@ -969,6 +990,83 @@
         getClientProfile(targetClientId).shareFrontend(selfClientId);
     }
 
+    private boolean transferFeOwner(int currentOwnerId, int newOwnerId) {
+        ClientProfile currentOwnerProfile = getClientProfile(currentOwnerId);
+        ClientProfile newOwnerProfile = getClientProfile(newOwnerId);
+        // change the owner of all the inUse frontend
+        newOwnerProfile.shareFrontend(currentOwnerId);
+        currentOwnerProfile.stopSharingFrontend(newOwnerId);
+        for (int inUseHandle : newOwnerProfile.getInUseFrontendHandles()) {
+            getFrontendResource(inUseHandle).setOwner(newOwnerId);
+        }
+        // double check there is no other resources tied to the previous owner
+        for (int inUseHandle : currentOwnerProfile.getInUseFrontendHandles()) {
+            int ownerId = getFrontendResource(inUseHandle).getOwnerClientId();
+            if (ownerId != newOwnerId) {
+                Slog.e(TAG, "something is wrong in transferFeOwner:" + inUseHandle
+                        + ", " + ownerId + ", " + newOwnerId);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean transferFeCiCamOwner(int currentOwnerId, int newOwnerId) {
+        ClientProfile currentOwnerProfile = getClientProfile(currentOwnerId);
+        ClientProfile newOwnerProfile = getClientProfile(newOwnerId);
+
+        // link ciCamId to the new profile
+        int ciCamId = currentOwnerProfile.getInUseCiCamId();
+        newOwnerProfile.useCiCam(ciCamId);
+
+        // set the new owner Id
+        CiCamResource ciCam = getCiCamResource(ciCamId);
+        ciCam.setOwner(newOwnerId);
+
+        // unlink cicam resource from the original owner profile
+        currentOwnerProfile.releaseCiCam();
+        return true;
+    }
+
+    private boolean transferLnbOwner(int currentOwnerId, int newOwnerId) {
+        ClientProfile currentOwnerProfile = getClientProfile(currentOwnerId);
+        ClientProfile newOwnerProfile = getClientProfile(newOwnerId);
+
+        Set<Integer> inUseLnbHandles = new HashSet<>();
+        for (Integer lnbHandle : currentOwnerProfile.getInUseLnbHandles()) {
+            // link lnb handle to the new profile
+            newOwnerProfile.useLnb(lnbHandle);
+
+            // set new owner Id
+            LnbResource lnb = getLnbResource(lnbHandle);
+            lnb.setOwner(newOwnerId);
+
+            inUseLnbHandles.add(lnbHandle);
+        }
+
+        // unlink lnb handles from the original owner
+        for (Integer lnbHandle : inUseLnbHandles) {
+            currentOwnerProfile.releaseLnb(lnbHandle);
+        }
+
+        return true;
+    }
+
+    @VisibleForTesting
+    protected boolean transferOwnerInternal(int resourceType, int currentOwnerId, int newOwnerId) {
+        switch (resourceType) {
+            case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND:
+                return transferFeOwner(currentOwnerId, newOwnerId);
+            case TunerResourceManager.TUNER_RESOURCE_TYPE_FRONTEND_CICAM:
+                return transferFeCiCamOwner(currentOwnerId, newOwnerId);
+            case TunerResourceManager.TUNER_RESOURCE_TYPE_LNB:
+                return transferLnbOwner(currentOwnerId, newOwnerId);
+            default:
+                Slog.e(TAG, "transferOwnerInternal. unsupported resourceType: " + resourceType);
+                return false;
+        }
+    }
+
     @VisibleForTesting
     protected boolean requestLnbInternal(TunerLnbRequest request, int[] lnbHandle) {
         if (DEBUG) {