Merge "CameraService: Handle ActivityManager death for UidPolicy" into pi-dev
diff --git a/camera/ndk/include/camera/NdkCameraMetadataTags.h b/camera/ndk/include/camera/NdkCameraMetadataTags.h
index 940ac5f..346761c 100644
--- a/camera/ndk/include/camera/NdkCameraMetadataTags.h
+++ b/camera/ndk/include/camera/NdkCameraMetadataTags.h
@@ -2257,7 +2257,7 @@
      * from the main sensor along the +X axis (to the right from the user's perspective) will
      * report <code>(0.03, 0, 0)</code>.</p>
      * <p>To transform a pixel coordinates between two cameras facing the same direction, first
-     * the source camera ACAMERA_LENS_RADIAL_DISTORTION must be corrected for.  Then the source
+     * the source camera ACAMERA_LENS_DISTORTION must be corrected for.  Then the source
      * camera ACAMERA_LENS_INTRINSIC_CALIBRATION needs to be applied, followed by the
      * ACAMERA_LENS_POSE_ROTATION of the source camera, the translation of the source camera
      * relative to the destination camera, the ACAMERA_LENS_POSE_ROTATION of the destination
@@ -2269,10 +2269,10 @@
      * <p>When ACAMERA_LENS_POSE_REFERENCE is GYROSCOPE, then this position is relative to
      * the center of the primary gyroscope on the device.</p>
      *
+     * @see ACAMERA_LENS_DISTORTION
      * @see ACAMERA_LENS_INTRINSIC_CALIBRATION
      * @see ACAMERA_LENS_POSE_REFERENCE
      * @see ACAMERA_LENS_POSE_ROTATION
-     * @see ACAMERA_LENS_RADIAL_DISTORTION
      */
     ACAMERA_LENS_POSE_TRANSLATION =                             // float[3]
             ACAMERA_LENS_START + 7,
@@ -2382,7 +2382,7 @@
      * where <code>(0,0)</code> is the top-left of the
      * preCorrectionActiveArraySize rectangle. Once the pose and
      * intrinsic calibration transforms have been applied to a
-     * world point, then the ACAMERA_LENS_RADIAL_DISTORTION
+     * world point, then the ACAMERA_LENS_DISTORTION
      * transform needs to be applied, and the result adjusted to
      * be in the ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE coordinate
      * system (where <code>(0, 0)</code> is the top-left of the
@@ -2390,56 +2390,15 @@
      * coordinate of the world point for processed (non-RAW)
      * output buffers.</p>
      *
+     * @see ACAMERA_LENS_DISTORTION
      * @see ACAMERA_LENS_POSE_ROTATION
      * @see ACAMERA_LENS_POSE_TRANSLATION
-     * @see ACAMERA_LENS_RADIAL_DISTORTION
      * @see ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE
      * @see ACAMERA_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE
      */
     ACAMERA_LENS_INTRINSIC_CALIBRATION =                        // float[5]
             ACAMERA_LENS_START + 10,
-    /**
-     * <p>The correction coefficients to correct for this camera device's
-     * radial and tangential lens distortion.</p>
-     *
-     * <p>Type: float[6]</p>
-     *
-     * <p>This tag may appear in:
-     * <ul>
-     *   <li>ACameraMetadata from ACameraManager_getCameraCharacteristics</li>
-     *   <li>ACameraMetadata from ACameraCaptureSession_captureCallback_result callbacks</li>
-     * </ul></p>
-     *
-     * <p>Four radial distortion coefficients <code>[kappa_0, kappa_1, kappa_2,
-     * kappa_3]</code> and two tangential distortion coefficients
-     * <code>[kappa_4, kappa_5]</code> that can be used to correct the
-     * lens's geometric distortion with the mapping equations:</p>
-     * <pre><code> x_c = x_i * ( kappa_0 + kappa_1 * r^2 + kappa_2 * r^4 + kappa_3 * r^6 ) +
-     *        kappa_4 * (2 * x_i * y_i) + kappa_5 * ( r^2 + 2 * x_i^2 )
-     *  y_c = y_i * ( kappa_0 + kappa_1 * r^2 + kappa_2 * r^4 + kappa_3 * r^6 ) +
-     *        kappa_5 * (2 * x_i * y_i) + kappa_4 * ( r^2 + 2 * y_i^2 )
-     * </code></pre>
-     * <p>Here, <code>[x_c, y_c]</code> are the coordinates to sample in the
-     * input image that correspond to the pixel values in the
-     * corrected image at the coordinate <code>[x_i, y_i]</code>:</p>
-     * <pre><code> correctedImage(x_i, y_i) = sample_at(x_c, y_c, inputImage)
-     * </code></pre>
-     * <p>The pixel coordinates are defined in a normalized
-     * coordinate system related to the
-     * ACAMERA_LENS_INTRINSIC_CALIBRATION calibration fields.
-     * Both <code>[x_i, y_i]</code> and <code>[x_c, y_c]</code> have <code>(0,0)</code> at the
-     * lens optical center <code>[c_x, c_y]</code>. The maximum magnitudes
-     * of both x and y coordinates are normalized to be 1 at the
-     * edge further from the optical center, so the range
-     * for both dimensions is <code>-1 &lt;= x &lt;= 1</code>.</p>
-     * <p>Finally, <code>r</code> represents the radial distance from the
-     * optical center, <code>r^2 = x_i^2 + y_i^2</code>, and its magnitude
-     * is therefore no larger than <code>|r| &lt;= sqrt(2)</code>.</p>
-     * <p>The distortion model used is the Brown-Conrady model.</p>
-     *
-     * @see ACAMERA_LENS_INTRINSIC_CALIBRATION
-     */
-    ACAMERA_LENS_RADIAL_DISTORTION =                            // float[6]
+    ACAMERA_LENS_RADIAL_DISTORTION =                            // Deprecated! DO NOT USE
             ACAMERA_LENS_START + 11,
     /**
      * <p>The origin for ACAMERA_LENS_POSE_TRANSLATION.</p>
@@ -2458,6 +2417,51 @@
      */
     ACAMERA_LENS_POSE_REFERENCE =                               // byte (acamera_metadata_enum_android_lens_pose_reference_t)
             ACAMERA_LENS_START + 12,
+    /**
+     * <p>The correction coefficients to correct for this camera device's
+     * radial and tangential lens distortion.</p>
+     * <p>Replaces the deprecated ACAMERA_LENS_RADIAL_DISTORTION field, which was
+     * inconsistently defined.</p>
+     *
+     * @see ACAMERA_LENS_RADIAL_DISTORTION
+     *
+     * <p>Type: float[5]</p>
+     *
+     * <p>This tag may appear in:
+     * <ul>
+     *   <li>ACameraMetadata from ACameraManager_getCameraCharacteristics</li>
+     *   <li>ACameraMetadata from ACameraCaptureSession_captureCallback_result callbacks</li>
+     * </ul></p>
+     *
+     * <p>Three radial distortion coefficients <code>[kappa_1, kappa_2,
+     * kappa_3]</code> and two tangential distortion coefficients
+     * <code>[kappa_4, kappa_5]</code> that can be used to correct the
+     * lens's geometric distortion with the mapping equations:</p>
+     * <pre><code> x_c = x_i * ( 1 + kappa_1 * r^2 + kappa_2 * r^4 + kappa_3 * r^6 ) +
+     *        kappa_4 * (2 * x_i * y_i) + kappa_5 * ( r^2 + 2 * x_i^2 )
+     *  y_c = y_i * ( 1 + kappa_1 * r^2 + kappa_2 * r^4 + kappa_3 * r^6 ) +
+     *        kappa_5 * (2 * x_i * y_i) + kappa_4 * ( r^2 + 2 * y_i^2 )
+     * </code></pre>
+     * <p>Here, <code>[x_c, y_c]</code> are the coordinates to sample in the
+     * input image that correspond to the pixel values in the
+     * corrected image at the coordinate <code>[x_i, y_i]</code>:</p>
+     * <pre><code> correctedImage(x_i, y_i) = sample_at(x_c, y_c, inputImage)
+     * </code></pre>
+     * <p>The pixel coordinates are defined in a coordinate system
+     * related to the ACAMERA_LENS_INTRINSIC_CALIBRATION
+     * calibration fields; see that entry for details of the mapping stages.
+     * Both <code>[x_i, y_i]</code> and <code>[x_c, y_c]</code>
+     * have <code>(0,0)</code> at the lens optical center <code>[c_x, c_y]</code>, and
+     * the range of the coordinates depends on the focal length
+     * terms of the intrinsic calibration.</p>
+     * <p>Finally, <code>r</code> represents the radial distance from the
+     * optical center, <code>r^2 = x_i^2 + y_i^2</code>.</p>
+     * <p>The distortion model used is the Brown-Conrady model.</p>
+     *
+     * @see ACAMERA_LENS_INTRINSIC_CALIBRATION
+     */
+    ACAMERA_LENS_DISTORTION =                                   // float[5]
+            ACAMERA_LENS_START + 13,
     ACAMERA_LENS_END,
 
     /**
@@ -4212,7 +4216,7 @@
      * ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE.</p>
      * <p>The currently supported fields that correct for geometric distortion are:</p>
      * <ol>
-     * <li>ACAMERA_LENS_RADIAL_DISTORTION.</li>
+     * <li>ACAMERA_LENS_DISTORTION.</li>
      * </ol>
      * <p>If all of the geometric distortion fields are no-ops, this rectangle will be the same
      * as the post-distortion-corrected rectangle given in
@@ -4224,7 +4228,7 @@
      * full array may include black calibration pixels or other inactive regions.</p>
      * <p>The data representation is <code>int[4]</code>, which maps to <code>(left, top, width, height)</code>.</p>
      *
-     * @see ACAMERA_LENS_RADIAL_DISTORTION
+     * @see ACAMERA_LENS_DISTORTION
      * @see ACAMERA_SENSOR_INFO_ACTIVE_ARRAY_SIZE
      * @see ACAMERA_SENSOR_INFO_PIXEL_ARRAY_SIZE
      * @see ACAMERA_SENSOR_INFO_PRE_CORRECTION_ACTIVE_ARRAY_SIZE
@@ -6941,7 +6945,7 @@
      * <li>ACAMERA_LENS_POSE_TRANSLATION</li>
      * <li>ACAMERA_LENS_POSE_ROTATION</li>
      * <li>ACAMERA_LENS_INTRINSIC_CALIBRATION</li>
-     * <li>ACAMERA_LENS_RADIAL_DISTORTION</li>
+     * <li>ACAMERA_LENS_DISTORTION</li>
      * </ul>
      * </li>
      * <li>The ACAMERA_DEPTH_DEPTH_IS_EXCLUSIVE entry is listed by this device.</li>
@@ -6959,12 +6963,12 @@
      * rate, including depth stall time.</p>
      *
      * @see ACAMERA_DEPTH_DEPTH_IS_EXCLUSIVE
+     * @see ACAMERA_LENS_DISTORTION
      * @see ACAMERA_LENS_FACING
      * @see ACAMERA_LENS_INTRINSIC_CALIBRATION
      * @see ACAMERA_LENS_POSE_REFERENCE
      * @see ACAMERA_LENS_POSE_ROTATION
      * @see ACAMERA_LENS_POSE_TRANSLATION
-     * @see ACAMERA_LENS_RADIAL_DISTORTION
      */
     ACAMERA_REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT              = 8,
 
@@ -6994,7 +6998,7 @@
      * <li>ACAMERA_LENS_POSE_ROTATION</li>
      * <li>ACAMERA_LENS_POSE_TRANSLATION</li>
      * <li>ACAMERA_LENS_INTRINSIC_CALIBRATION</li>
-     * <li>ACAMERA_LENS_RADIAL_DISTORTION</li>
+     * <li>ACAMERA_LENS_DISTORTION</li>
      * </ul>
      * </li>
      * <li>The SENSOR_INFO_TIMESTAMP_SOURCE of the logical device and physical devices must be
@@ -7020,11 +7024,11 @@
      * not slow down the frame rate of the capture, as long as the minimum frame duration
      * of the physical and logical streams are the same.</p>
      *
+     * @see ACAMERA_LENS_DISTORTION
      * @see ACAMERA_LENS_INTRINSIC_CALIBRATION
      * @see ACAMERA_LENS_POSE_REFERENCE
      * @see ACAMERA_LENS_POSE_ROTATION
      * @see ACAMERA_LENS_POSE_TRANSLATION
-     * @see ACAMERA_LENS_RADIAL_DISTORTION
      * @see ACAMERA_LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE
      */
     ACAMERA_REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA      = 11,
diff --git a/drm/mediadrm/plugins/clearkey/hidl/DrmPlugin.cpp b/drm/mediadrm/plugins/clearkey/hidl/DrmPlugin.cpp
index 6b0201a..300c688 100644
--- a/drm/mediadrm/plugins/clearkey/hidl/DrmPlugin.cpp
+++ b/drm/mediadrm/plugins/clearkey/hidl/DrmPlugin.cpp
@@ -19,6 +19,7 @@
 #include <utils/Log.h>
 
 #include <stdio.h>
+#include <inttypes.h>
 
 #include "DrmPlugin.h"
 #include "ClearKeyDrmProperties.h"
@@ -26,6 +27,7 @@
 #include "TypeConvert.h"
 
 namespace {
+const int kSecureStopIdStart = 100;
 const std::string kStreaming("Streaming");
 const std::string kOffline("Offline");
 const std::string kTrue("True");
@@ -36,7 +38,18 @@
     // Value: "True" or "False"
 const std::string kQueryKeyRenewAllowed("RenewAllowed");
     // Value: "True" or "False"
-};
+
+const int kSecureStopIdSize = 10;
+
+std::vector<uint8_t> uint32ToVector(uint32_t value) {
+    // 10 bytes to display max value 4294967295 + one byte null terminator
+    char buffer[kSecureStopIdSize];
+    memset(buffer, 0, kSecureStopIdSize);
+    snprintf(buffer, kSecureStopIdSize, "%" PRIu32, value);
+    return std::vector<uint8_t>(buffer, buffer + sizeof(buffer));
+}
+
+}; // unnamed namespace
 
 namespace android {
 namespace hardware {
@@ -48,9 +61,11 @@
         : mSessionLibrary(sessionLibrary),
           mOpenSessionOkCount(0),
           mCloseSessionOkCount(0),
-          mCloseSessionNotOpenedCount(0) {
+          mCloseSessionNotOpenedCount(0),
+          mNextSecureStopId(kSecureStopIdStart) {
     mPlayPolicy.clear();
     initProperties();
+    mSecureStops.clear();
 }
 
 void DrmPlugin::initProperties() {
@@ -73,6 +88,18 @@
     mByteArrayProperties[kMetricsKey] = valueVector;
 }
 
+// The secure stop in ClearKey implementation is not installed securely.
+// This function merely creates a test environment for testing secure stops APIs.
+// The content in this secure stop is implementation dependent, the clearkey
+// secureStop does not serve as a reference implementation.
+void DrmPlugin::installSecureStop(const hidl_vec<uint8_t>& sessionId) {
+    ClearkeySecureStop clearkeySecureStop;
+    clearkeySecureStop.id = uint32ToVector(++mNextSecureStopId);
+    clearkeySecureStop.data.assign(sessionId.begin(), sessionId.end());
+
+    mSecureStops.insert(std::pair<std::vector<uint8_t>, ClearkeySecureStop>(
+            clearkeySecureStop.id, clearkeySecureStop));
+}
 
 Return<void> DrmPlugin::openSession(openSession_cb _hidl_cb) {
     sp<Session> session = mSessionLibrary->createSession();
@@ -209,6 +236,7 @@
         _hidl_cb(Status::BAD_VALUE, hidl_vec<uint8_t>());
         return Void();
     }
+
     sp<Session> session = mSessionLibrary->findSession(toVector(scope));
     if (!session.get()) {
         _hidl_cb(Status::ERROR_DRM_SESSION_NOT_OPENED, hidl_vec<uint8_t>());
@@ -224,6 +252,8 @@
         keySetId.clear();
     }
 
+    installSecureStop(scope);
+
     // Returns status and empty keySetId
     _hidl_cb(status, toHidlVec(keySetId));
     return Void();
@@ -435,7 +465,106 @@
     return Void();
 }
 
+Return<void> DrmPlugin::getSecureStops(getSecureStops_cb _hidl_cb) {
+    std::vector<SecureStop> stops;
+    for (auto itr = mSecureStops.begin(); itr != mSecureStops.end(); ++itr) {
+        ClearkeySecureStop clearkeyStop = itr->second;
+        std::vector<uint8_t> stopVec;
+        stopVec.insert(stopVec.end(), clearkeyStop.id.begin(), clearkeyStop.id.end());
+        stopVec.insert(stopVec.end(), clearkeyStop.data.begin(), clearkeyStop.data.end());
 
+        SecureStop stop;
+        stop.opaqueData = toHidlVec(stopVec);
+        stops.push_back(stop);
+    }
+    _hidl_cb(Status::OK, stops);
+    return Void();
+}
+
+Return<void> DrmPlugin::getSecureStop(const hidl_vec<uint8_t>& secureStopId,
+        getSecureStop_cb _hidl_cb) {
+    SecureStop stop;
+    auto itr = mSecureStops.find(toVector(secureStopId));
+    if (itr != mSecureStops.end()) {
+        ClearkeySecureStop clearkeyStop = itr->second;
+        std::vector<uint8_t> stopVec;
+        stopVec.insert(stopVec.end(), clearkeyStop.id.begin(), clearkeyStop.id.end());
+        stopVec.insert(stopVec.end(), clearkeyStop.data.begin(), clearkeyStop.data.end());
+
+        stop.opaqueData = toHidlVec(stopVec);
+        _hidl_cb(Status::OK, stop);
+    } else {
+        _hidl_cb(Status::BAD_VALUE, stop);
+    }
+
+    return Void();
+}
+
+Return<Status> DrmPlugin::releaseSecureStop(const hidl_vec<uint8_t>& secureStopId) {
+    return removeSecureStop(secureStopId);
+}
+
+Return<Status> DrmPlugin::releaseAllSecureStops() {
+    return removeAllSecureStops();
+}
+
+Return<void> DrmPlugin::getSecureStopIds(getSecureStopIds_cb _hidl_cb) {
+    std::vector<SecureStopId> ids;
+    for (auto itr = mSecureStops.begin(); itr != mSecureStops.end(); ++itr) {
+        ids.push_back(itr->first);
+    }
+
+    _hidl_cb(Status::OK, toHidlVec(ids));
+    return Void();
+}
+
+Return<Status> DrmPlugin::releaseSecureStops(const SecureStopRelease& ssRelease) {
+    if (ssRelease.opaqueData.size() == 0) {
+        return Status::BAD_VALUE;
+    }
+
+    Status status = Status::OK;
+    std::vector<uint8_t> input = toVector(ssRelease.opaqueData);
+
+    // The format of opaqueData is shared between the server
+    // and the drm service. The clearkey implementation consists of:
+    //    count - number of secure stops
+    //    list of fixed length secure stops
+    size_t countBufferSize = sizeof(uint32_t);
+    uint32_t count = 0;
+    sscanf(reinterpret_cast<char*>(input.data()), "%04" PRIu32, &count);
+
+    // Avoid divide by 0 below.
+    if (count == 0) {
+        return Status::BAD_VALUE;
+    }
+
+    size_t secureStopSize = (input.size() - countBufferSize) / count;
+    uint8_t buffer[secureStopSize];
+    size_t offset = countBufferSize; // skip the count
+    for (size_t i = 0; i < count; ++i, offset += secureStopSize) {
+        memcpy(buffer, input.data() + offset, secureStopSize);
+        std::vector<uint8_t> id(buffer, buffer + kSecureStopIdSize);
+
+        status = removeSecureStop(toHidlVec(id));
+        if (Status::OK != status) break;
+    }
+
+    return status;
+}
+
+Return<Status> DrmPlugin::removeSecureStop(const hidl_vec<uint8_t>& secureStopId) {
+    if (1 != mSecureStops.erase(toVector(secureStopId))) {
+        return Status::BAD_VALUE;
+    }
+    return Status::OK;
+}
+
+Return<Status> DrmPlugin::removeAllSecureStops() {
+    mSecureStops.clear();
+    mNextSecureStopId = kSecureStopIdStart;
+    return Status::OK;
+}
 
 }  // namespace clearkey
 }  // namespace V1_1
diff --git a/drm/mediadrm/plugins/clearkey/hidl/include/DrmPlugin.h b/drm/mediadrm/plugins/clearkey/hidl/include/DrmPlugin.h
index 19baf0b..fb0695a 100644
--- a/drm/mediadrm/plugins/clearkey/hidl/include/DrmPlugin.h
+++ b/drm/mediadrm/plugins/clearkey/hidl/include/DrmPlugin.h
@@ -17,9 +17,11 @@
 #ifndef CLEARKEY_DRM_PLUGIN_H_
 #define CLEARKEY_DRM_PLUGIN_H_
 
-
 #include <android/hardware/drm/1.1/IDrmPlugin.h>
 
+#include <stdio.h>
+#include <map>
+
 #include "SessionLibrary.h"
 #include "Utils.h"
 
@@ -36,6 +38,7 @@
 using ::android::hardware::drm::V1_0::KeyValue;
 using ::android::hardware::drm::V1_0::SecureStop;
 using ::android::hardware::drm::V1_0::SecureStopId;
+using ::android::hardware::drm::V1_0::SessionId;
 using ::android::hardware::drm::V1_0::Status;
 using ::android::hardware::drm::V1_1::DrmMetricGroup;
 using ::android::hardware::drm::V1_1::IDrmPlugin;
@@ -124,34 +127,6 @@
         return Void();
     }
 
-    Return<void> getSecureStops(getSecureStops_cb _hidl_cb) {
-        _hidl_cb(Status::ERROR_DRM_CANNOT_HANDLE, hidl_vec<SecureStop>());
-        return Void();
-    }
-
-    Return<void> getSecureStop(
-        const hidl_vec<uint8_t>& secureStopId,
-        getSecureStop_cb _hidl_cb) {
-
-        if (secureStopId.size() == 0) {
-            _hidl_cb(Status::BAD_VALUE, SecureStop());
-            return Void();
-        }
-        _hidl_cb(Status::ERROR_DRM_CANNOT_HANDLE, SecureStop());
-        return Void();
-    }
-
-    Return<Status> releaseSecureStop(const hidl_vec<uint8_t>& ssRelease) {
-        if (ssRelease.size() == 0) {
-            return Status::BAD_VALUE;
-        }
-        return Status::ERROR_DRM_CANNOT_HANDLE;
-    }
-
-    Return<Status> releaseAllSecureStops() {
-        return Status::ERROR_DRM_CANNOT_HANDLE;
-    }
-
     Return<void> getHdcpLevels(getHdcpLevels_cb _hidl_cb) {
         HdcpLevel connectedLevel = HdcpLevel::HDCP_NONE;
         HdcpLevel maxLevel = HdcpLevel::HDCP_NO_OUTPUT;
@@ -305,31 +280,26 @@
         return Void();
     }
 
-    Return<void> getSecureStopIds(getSecureStopIds_cb _hidl_cb) {
-        _hidl_cb(Status::ERROR_DRM_CANNOT_HANDLE, hidl_vec<SecureStopId>());
-        return Void();
-    }
+    Return<void> getSecureStops(getSecureStops_cb _hidl_cb);
 
-    Return<Status> releaseSecureStops(const SecureStopRelease& ssRelease) {
-        if (ssRelease.opaqueData.size() == 0) {
-            return Status::BAD_VALUE;
-        }
-        return Status::ERROR_DRM_CANNOT_HANDLE;
-    }
+    Return<void> getSecureStop(const hidl_vec<uint8_t>& secureStopId,
+            getSecureStop_cb _hidl_cb);
 
-    Return<Status> removeSecureStop(const hidl_vec<uint8_t>& secureStopId) {
-        if (secureStopId.size() == 0) {
-            return Status::BAD_VALUE;
-        }
-        return Status::ERROR_DRM_CANNOT_HANDLE;
-    }
+    Return<Status> releaseSecureStop(const hidl_vec<uint8_t>& ssRelease);
 
-    Return<Status> removeAllSecureStops() {
-        return Status::ERROR_DRM_CANNOT_HANDLE;
-    }
+    Return<Status> releaseAllSecureStops();
+
+    Return<void> getSecureStopIds(getSecureStopIds_cb _hidl_cb);
+
+    Return<Status> releaseSecureStops(const SecureStopRelease& ssRelease);
+
+    Return<Status> removeSecureStop(const hidl_vec<uint8_t>& secureStopId);
+
+    Return<Status> removeAllSecureStops();
 
 private:
     void initProperties();
+    void installSecureStop(const hidl_vec<uint8_t>& sessionId);
     void setPlayPolicy();
 
     Return<Status> setSecurityLevel(const hidl_vec<uint8_t>& sessionId,
@@ -344,6 +314,12 @@
             KeyRequestType *getKeyRequestType,
             std::string *defaultUrl);
 
+    struct ClearkeySecureStop {
+        std::vector<uint8_t> id;
+        std::vector<uint8_t> data;
+    };
+
+    std::map<std::vector<uint8_t>, ClearkeySecureStop> mSecureStops;
     std::vector<KeyValue> mPlayPolicy;
     std::map<std::string, std::string> mStringProperties;
     std::map<std::string, std::vector<uint8_t> > mByteArrayProperties;
@@ -353,6 +329,7 @@
     int64_t mOpenSessionOkCount;
     int64_t mCloseSessionOkCount;
     int64_t mCloseSessionNotOpenedCount;
+    uint32_t mNextSecureStopId;
 
     CLEARKEY_DISALLOW_COPY_AND_ASSIGN_AND_NEW(DrmPlugin);
 };
diff --git a/media/extractors/mp4/ItemTable.cpp b/media/extractors/mp4/ItemTable.cpp
index c082fc1..666d68a 100644
--- a/media/extractors/mp4/ItemTable.cpp
+++ b/media/extractors/mp4/ItemTable.cpp
@@ -1471,8 +1471,8 @@
 
         // point image to the first tile for grid size and HVCC
         image = &mItemIdToItemMap.editValueAt(tileItemIndex);
-        meta->setInt32(kKeyGridWidth, image->width);
-        meta->setInt32(kKeyGridHeight, image->height);
+        meta->setInt32(kKeyTileWidth, image->width);
+        meta->setInt32(kKeyTileHeight, image->height);
         meta->setInt32(kKeyMaxInputSize, image->width * image->height * 1.5);
     }
 
diff --git a/media/libmedia/NdkWrapper.cpp b/media/libmedia/NdkWrapper.cpp
index 2cdb44e..6f56d0c 100644
--- a/media/libmedia/NdkWrapper.cpp
+++ b/media/libmedia/NdkWrapper.cpp
@@ -56,10 +56,8 @@
     AMEDIAFORMAT_KEY_COLOR_TRANSFER,
     AMEDIAFORMAT_KEY_COMPLEXITY,
     AMEDIAFORMAT_KEY_FLAC_COMPRESSION_LEVEL,
-    AMEDIAFORMAT_KEY_GRID_COLS,
-    AMEDIAFORMAT_KEY_GRID_HEIGHT,
+    AMEDIAFORMAT_KEY_GRID_COLUMNS,
     AMEDIAFORMAT_KEY_GRID_ROWS,
-    AMEDIAFORMAT_KEY_GRID_WIDTH,
     AMEDIAFORMAT_KEY_HEIGHT,
     AMEDIAFORMAT_KEY_INTRA_REFRESH_PERIOD,
     AMEDIAFORMAT_KEY_IS_ADTS,
@@ -84,6 +82,8 @@
     AMEDIAFORMAT_KEY_DISPLAY_HEIGHT,
     AMEDIAFORMAT_KEY_DISPLAY_WIDTH,
     AMEDIAFORMAT_KEY_TEMPORAL_LAYER_ID,
+    AMEDIAFORMAT_KEY_TILE_HEIGHT,
+    AMEDIAFORMAT_KEY_TILE_WIDTH,
     AMEDIAFORMAT_KEY_TRACK_INDEX,
 };
 
diff --git a/media/libmedia/TypeConverter.cpp b/media/libmedia/TypeConverter.cpp
index 9323d6d..03700bf 100644
--- a/media/libmedia/TypeConverter.cpp
+++ b/media/libmedia/TypeConverter.cpp
@@ -226,8 +226,12 @@
     MAKE_STRING_FROM_ENUM(AUDIO_CHANNEL_OUT_5POINT1),
     MAKE_STRING_FROM_ENUM(AUDIO_CHANNEL_OUT_5POINT1_BACK),
     MAKE_STRING_FROM_ENUM(AUDIO_CHANNEL_OUT_5POINT1_SIDE),
+    MAKE_STRING_FROM_ENUM(AUDIO_CHANNEL_OUT_5POINT1POINT2),
+    MAKE_STRING_FROM_ENUM(AUDIO_CHANNEL_OUT_5POINT1POINT4),
     MAKE_STRING_FROM_ENUM(AUDIO_CHANNEL_OUT_6POINT1),
     MAKE_STRING_FROM_ENUM(AUDIO_CHANNEL_OUT_7POINT1),
+    MAKE_STRING_FROM_ENUM(AUDIO_CHANNEL_OUT_7POINT1POINT2),
+    MAKE_STRING_FROM_ENUM(AUDIO_CHANNEL_OUT_7POINT1POINT4),
     TERMINATOR
 };
 
diff --git a/media/libmediaextractor/include/media/stagefright/MetaDataBase.h b/media/libmediaextractor/include/media/stagefright/MetaDataBase.h
index 6010af4..18e63f2 100644
--- a/media/libmediaextractor/include/media/stagefright/MetaDataBase.h
+++ b/media/libmediaextractor/include/media/stagefright/MetaDataBase.h
@@ -211,11 +211,11 @@
     kKeyTemporalLayerId  = 'iLyr', // int32_t, temporal layer-id. 0-based (0 => base layer)
     kKeyTemporalLayerCount = 'cLyr', // int32_t, number of temporal layers encoded
 
-    kKeyGridWidth        = 'grdW', // int32_t, HEIF grid width
-    kKeyGridHeight       = 'grdH', // int32_t, HEIF grid height
+    kKeyTileWidth        = 'tilW', // int32_t, HEIF tile width
+    kKeyTileHeight       = 'tilH', // int32_t, HEIF tile height
     kKeyGridRows         = 'grdR', // int32_t, HEIF grid rows
     kKeyGridCols         = 'grdC', // int32_t, HEIF grid columns
-    kKeyIccProfile       = 'prof', // raw data, ICC prifile data
+    kKeyIccProfile       = 'prof', // raw data, ICC profile data
     kKeyIsPrimaryImage   = 'prim', // bool (int32_t), image track is the primary image
     kKeyFrameCount       = 'nfrm', // int32_t, total number of frame in video track
 };
diff --git a/media/libstagefright/ACodec.cpp b/media/libstagefright/ACodec.cpp
index 3570aaf..a8c6d15 100644
--- a/media/libstagefright/ACodec.cpp
+++ b/media/libstagefright/ACodec.cpp
@@ -4384,9 +4384,9 @@
 
 status_t ACodec::configureImageGrid(
         const sp<AMessage> &msg, sp<AMessage> &outputFormat) {
-    int32_t gridWidth, gridHeight, gridRows, gridCols;
-    if (!msg->findInt32("grid-width", &gridWidth) ||
-        !msg->findInt32("grid-height", &gridHeight) ||
+    int32_t tileWidth, tileHeight, gridRows, gridCols;
+    if (!msg->findInt32("tile-width", &tileWidth) ||
+        !msg->findInt32("tile-height", &tileHeight) ||
         !msg->findInt32("grid-rows", &gridRows) ||
         !msg->findInt32("grid-cols", &gridCols)) {
         return OK;
@@ -4396,8 +4396,8 @@
     InitOMXParams(&gridType);
     gridType.nPortIndex = kPortIndexOutput;
     gridType.bEnabled = OMX_TRUE;
-    gridType.nGridWidth = gridWidth;
-    gridType.nGridHeight = gridHeight;
+    gridType.nTileWidth = tileWidth;
+    gridType.nTileHeight = tileHeight;
     gridType.nGridRows = gridRows;
     gridType.nGridCols = gridCols;
 
@@ -4422,8 +4422,8 @@
             &gridType, sizeof(gridType));
 
     if (err == OK && gridType.bEnabled) {
-        outputFormat->setInt32("grid-width", gridType.nGridWidth);
-        outputFormat->setInt32("grid-height", gridType.nGridHeight);
+        outputFormat->setInt32("tile-width", gridType.nTileWidth);
+        outputFormat->setInt32("tile-height", gridType.nTileHeight);
         outputFormat->setInt32("grid-rows", gridType.nGridRows);
         outputFormat->setInt32("grid-cols", gridType.nGridCols);
     }
diff --git a/media/libstagefright/FrameDecoder.cpp b/media/libstagefright/FrameDecoder.cpp
index 9748a8b..3d0aad1 100644
--- a/media/libstagefright/FrameDecoder.cpp
+++ b/media/libstagefright/FrameDecoder.cpp
@@ -495,27 +495,27 @@
     mGridRows = mGridCols = 1;
     if (overrideMeta == NULL) {
         // check if we're dealing with a tiled heif
-        int32_t gridWidth, gridHeight, gridRows, gridCols;
-        if (trackMeta()->findInt32(kKeyGridWidth, &gridWidth) && gridWidth > 0
-         && trackMeta()->findInt32(kKeyGridHeight, &gridHeight) && gridHeight > 0
+        int32_t tileWidth, tileHeight, gridRows, gridCols;
+        if (trackMeta()->findInt32(kKeyTileWidth, &tileWidth) && tileWidth > 0
+         && trackMeta()->findInt32(kKeyTileHeight, &tileHeight) && tileHeight > 0
          && trackMeta()->findInt32(kKeyGridRows, &gridRows) && gridRows > 0
          && trackMeta()->findInt32(kKeyGridCols, &gridCols) && gridCols > 0) {
             int32_t width, height;
             CHECK(trackMeta()->findInt32(kKeyWidth, &width));
             CHECK(trackMeta()->findInt32(kKeyHeight, &height));
 
-            if (width <= gridWidth * gridCols && height <= gridHeight * gridRows) {
-                ALOGV("grid: %dx%d, size: %dx%d, picture size: %dx%d",
-                        gridCols, gridRows, gridWidth, gridHeight, width, height);
+            if (width <= tileWidth * gridCols && height <= tileHeight * gridRows) {
+                ALOGV("grid: %dx%d, tile size: %dx%d, picture size: %dx%d",
+                        gridCols, gridRows, tileWidth, tileHeight, width, height);
 
                 overrideMeta = new MetaData(*(trackMeta()));
-                overrideMeta->setInt32(kKeyWidth, gridWidth);
-                overrideMeta->setInt32(kKeyHeight, gridHeight);
+                overrideMeta->setInt32(kKeyWidth, tileWidth);
+                overrideMeta->setInt32(kKeyHeight, tileHeight);
                 mGridCols = gridCols;
                 mGridRows = gridRows;
             } else {
-                ALOGE("bad grid: %dx%d, size: %dx%d, picture size: %dx%d",
-                        gridCols, gridRows, gridWidth, gridHeight, width, height);
+                ALOGE("bad grid: %dx%d, tile size: %dx%d, picture size: %dx%d",
+                        gridCols, gridRows, tileWidth, tileHeight, width, height);
             }
         }
         if (overrideMeta == NULL) {
diff --git a/media/libstagefright/MPEG4Writer.cpp b/media/libstagefright/MPEG4Writer.cpp
index 768df26..a3261d7 100644
--- a/media/libstagefright/MPEG4Writer.cpp
+++ b/media/libstagefright/MPEG4Writer.cpp
@@ -358,7 +358,7 @@
     Vector<uint16_t> mDimgRefs;
     int32_t mIsPrimary;
     int32_t mWidth, mHeight;
-    int32_t mGridWidth, mGridHeight;
+    int32_t mTileWidth, mTileHeight;
     int32_t mGridRows, mGridCols;
     size_t mNumTiles, mTileIndex;
 
@@ -1770,8 +1770,8 @@
       mIsPrimary(0),
       mWidth(0),
       mHeight(0),
-      mGridWidth(0),
-      mGridHeight(0),
+      mTileWidth(0),
+      mTileHeight(0),
       mGridRows(0),
       mGridCols(0),
       mNumTiles(1),
@@ -1802,13 +1802,13 @@
         CHECK(mMeta->findInt32(kKeyWidth, &mWidth) && (mWidth > 0));
         CHECK(mMeta->findInt32(kKeyHeight, &mHeight) && (mHeight > 0));
 
-        int32_t gridWidth, gridHeight, gridRows, gridCols;
-        if (mMeta->findInt32(kKeyGridWidth, &gridWidth) && (gridWidth > 0) &&
-            mMeta->findInt32(kKeyGridHeight, &gridHeight) && (gridHeight > 0) &&
+        int32_t tileWidth, tileHeight, gridRows, gridCols;
+        if (mMeta->findInt32(kKeyTileWidth, &tileWidth) && (tileWidth > 0) &&
+            mMeta->findInt32(kKeyTileHeight, &tileHeight) && (tileHeight > 0) &&
             mMeta->findInt32(kKeyGridRows, &gridRows) && (gridRows > 0) &&
             mMeta->findInt32(kKeyGridCols, &gridCols) && (gridCols > 0)) {
-            mGridWidth = gridWidth;
-            mGridHeight = gridHeight;
+            mTileWidth = tileWidth;
+            mTileHeight = tileHeight;
             mGridRows = gridRows;
             mGridCols = gridCols;
             mNumTiles = gridRows * gridCols;
@@ -1978,8 +1978,8 @@
 
         mProperties.push_back(mOwner->addProperty_l({
             .type = FOURCC('i', 's', 'p', 'e'),
-            .width = hasGrid ? mGridWidth : mWidth,
-            .height = hasGrid ? mGridHeight : mHeight,
+            .width = hasGrid ? mTileWidth : mWidth,
+            .height = hasGrid ? mTileHeight : mHeight,
         }));
 
         if (!hasGrid && heifRotation > 0) {
diff --git a/media/libstagefright/Utils.cpp b/media/libstagefright/Utils.cpp
index 4a93051..0c6e988 100644
--- a/media/libstagefright/Utils.cpp
+++ b/media/libstagefright/Utils.cpp
@@ -665,13 +665,13 @@
         }
 
         if (!strncasecmp("image/", mime, 6)) {
-            int32_t gridWidth, gridHeight, gridRows, gridCols;
-            if (meta->findInt32(kKeyGridWidth, &gridWidth)
-                    && meta->findInt32(kKeyGridHeight, &gridHeight)
+            int32_t tileWidth, tileHeight, gridRows, gridCols;
+            if (meta->findInt32(kKeyTileWidth, &tileWidth)
+                    && meta->findInt32(kKeyTileHeight, &tileHeight)
                     && meta->findInt32(kKeyGridRows, &gridRows)
                     && meta->findInt32(kKeyGridCols, &gridCols)) {
-                msg->setInt32("grid-width", gridWidth);
-                msg->setInt32("grid-height", gridHeight);
+                msg->setInt32("tile-width", tileWidth);
+                msg->setInt32("tile-height", tileHeight);
                 msg->setInt32("grid-rows", gridRows);
                 msg->setInt32("grid-cols", gridCols);
             }
@@ -1341,12 +1341,12 @@
             if (msg->findInt32("is-default", &isPrimary) && isPrimary) {
                 meta->setInt32(kKeyTrackIsDefault, 1);
             }
-            int32_t gridWidth, gridHeight, gridRows, gridCols;
-            if (msg->findInt32("grid-width", &gridWidth)) {
-                meta->setInt32(kKeyGridWidth, gridWidth);
+            int32_t tileWidth, tileHeight, gridRows, gridCols;
+            if (msg->findInt32("tile-width", &tileWidth)) {
+                meta->setInt32(kKeyTileWidth, tileWidth);
             }
-            if (msg->findInt32("grid-height", &gridHeight)) {
-                meta->setInt32(kKeyGridHeight, gridHeight);
+            if (msg->findInt32("tile-height", &tileHeight)) {
+                meta->setInt32(kKeyTileHeight, tileHeight);
             }
             if (msg->findInt32("grid-rows", &gridRows)) {
                 meta->setInt32(kKeyGridRows, gridRows);
diff --git a/media/libstagefright/codec2/include/C2Buffer.h b/media/libstagefright/codec2/include/C2Buffer.h
index 4bdd20f..0e35f11 100644
--- a/media/libstagefright/codec2/include/C2Buffer.h
+++ b/media/libstagefright/codec2/include/C2Buffer.h
@@ -655,7 +655,8 @@
      * (Re)creates a 1D allocation from a native |handle|. If successful, the allocation is stored
      * in |allocation|. Otherwise, |allocation| is set to 'nullptr'.
      *
-     * \param handle      the handle for the existing allocation
+     * \param handle      the handle for the existing allocation. On success, the allocation will
+     *                    take ownership of |handle|.
      * \param allocation  pointer to where the allocation shall be stored on success. nullptr
      *                    will be stored here on failure
      *
@@ -712,7 +713,8 @@
      * (Re)creates a 2D allocation from a native handle.  If successful, the allocation is stored
      * in |allocation|. Otherwise, |allocation| is set to 'nullptr'.
      *
-     * \param handle      the handle for the existing allocation
+     * \param handle      the handle for the existing allocation. On success, the allocation will
+     *                    take ownership of |handle|.
      * \param allocation  pointer to where the allocation shall be stored on success. nullptr
      *                    will be stored here on failure
      *
diff --git a/media/libstagefright/codec2/vndk/C2AllocatorGralloc.cpp b/media/libstagefright/codec2/vndk/C2AllocatorGralloc.cpp
index a90e094..96b52a1 100644
--- a/media/libstagefright/codec2/vndk/C2AllocatorGralloc.cpp
+++ b/media/libstagefright/codec2/vndk/C2AllocatorGralloc.cpp
@@ -255,6 +255,7 @@
       mHidlHandle(std::move(hidlHandle)),
       mHandle(handle),
       mBuffer(nullptr),
+      mLockedHandle(nullptr),
       mLocked(false),
       mAllocatorId(allocatorId) {
 }
@@ -269,6 +270,8 @@
         unmap(addr, C2Rect(), nullptr);
     }
     mMapper->freeBuffer(const_cast<native_handle_t *>(mBuffer));
+    native_handle_delete(const_cast<native_handle_t*>(
+            reinterpret_cast<const native_handle_t*>(mHandle)));
 }
 
 c2_status_t C2AllocationGralloc::map(
@@ -608,8 +611,8 @@
         return C2_BAD_VALUE;
     }
 
-    hidl_handle hidlHandle = C2HandleGralloc::UnwrapNativeHandle(grallocHandle);
-
+    hidl_handle hidlHandle;
+    hidlHandle.setTo(C2HandleGralloc::UnwrapNativeHandle(grallocHandle), true);
     allocation->reset(new C2AllocationGralloc(info, mMapper, hidlHandle, grallocHandle, mTraits->id));
     return C2_OK;
 }
diff --git a/media/libstagefright/codec2/vndk/C2AllocatorIon.cpp b/media/libstagefright/codec2/vndk/C2AllocatorIon.cpp
index 664c09e..c5fb8d9 100644
--- a/media/libstagefright/codec2/vndk/C2AllocatorIon.cpp
+++ b/media/libstagefright/codec2/vndk/C2AllocatorIon.cpp
@@ -150,7 +150,6 @@
      * so that we can capture the error.
      *
      * \param ionFd     ion client (ownership transferred to created object)
-     * \param owned     whehter native_handle_t is owned by an allocation or not.
      * \param capacity  size of allocation
      * \param bufferFd  buffer handle (ownership transferred to created object). Must be
      *                  invalid if err is not 0.
@@ -158,9 +157,8 @@
      *                  invalid if err is not 0.
      * \param err       errno during buffer allocation or import
      */
-    Impl(int ionFd, bool owned, size_t capacity, int bufferFd, ion_user_handle_t buffer, C2Allocator::id_t id, int err)
+    Impl(int ionFd, size_t capacity, int bufferFd, ion_user_handle_t buffer, C2Allocator::id_t id, int err)
         : mIonFd(ionFd),
-          mHandleOwned(owned),
           mHandle(bufferFd, capacity),
           mBuffer(buffer),
           mId(id),
@@ -191,7 +189,7 @@
     static Impl *Import(int ionFd, size_t capacity, int bufferFd, C2Allocator::id_t id) {
         ion_user_handle_t buffer = -1;
         int ret = ion_import(ionFd, bufferFd, &buffer);
-        return new Impl(ionFd, false, capacity, bufferFd, buffer, id, ret);
+        return new Impl(ionFd, capacity, bufferFd, buffer, id, ret);
     }
 
     /**
@@ -218,7 +216,7 @@
                 buffer = -1;
             }
         }
-        return new Impl(ionFd, true, size, bufferFd, buffer, id, ret);
+        return new Impl(ionFd, size, bufferFd, buffer, id, ret);
     }
 
     c2_status_t map(size_t offset, size_t size, C2MemoryUsage usage, C2Fence *fence, void **addr) {
@@ -311,13 +309,11 @@
         }
         if (mInit == C2_OK) {
             (void)ion_free(mIonFd, mBuffer);
+            native_handle_close(&mHandle);
         }
         if (mIonFd >= 0) {
             close(mIonFd);
         }
-        if (mHandleOwned) {
-            native_handle_close(&mHandle);
-        }
     }
 
     c2_status_t status() const {
@@ -338,7 +334,6 @@
 
 private:
     int mIonFd;
-    bool mHandleOwned;
     C2HandleIon mHandle;
     ion_user_handle_t mBuffer;
     C2Allocator::id_t mId;
@@ -481,6 +476,8 @@
     c2_status_t ret = alloc->status();
     if (ret == C2_OK) {
         *allocation = alloc;
+        native_handle_delete(const_cast<native_handle_t*>(
+                reinterpret_cast<const native_handle_t*>(handle)));
     }
     return ret;
 }
diff --git a/media/libstagefright/codec2/vndk/bufferpool/BufferPoolClient.cpp b/media/libstagefright/codec2/vndk/bufferpool/BufferPoolClient.cpp
index 7cb4ba9..dda8e39 100644
--- a/media/libstagefright/codec2/vndk/bufferpool/BufferPoolClient.cpp
+++ b/media/libstagefright/codec2/vndk/bufferpool/BufferPoolClient.cpp
@@ -135,7 +135,8 @@
     bool mInvalidated; // TODO: implement
     int64_t mExpireUs;
     bool mHasCache;
-    _C2BlockPoolData mBuffer;
+    BufferId mId;
+    native_handle_t *mHandle;
     std::weak_ptr<_C2BlockPoolData> mCache;
 
     void updateExpire() {
@@ -144,11 +145,18 @@
 
 public:
     ClientBuffer(BufferId id, native_handle_t *handle)
-            : mInvalidated(false), mHasCache(false), mBuffer(id, handle) {
+            : mInvalidated(false), mHasCache(false), mId(id), mHandle(handle) {
         (void)mInvalidated;
         mExpireUs = getTimestampNow() + kCacheTtlUs;
     }
 
+    ~ClientBuffer() {
+        if (mHandle) {
+            native_handle_close(mHandle);
+            native_handle_delete(mHandle);
+        }
+    }
+
     bool expire() const {
         int64_t now = getTimestampNow();
         return now >= mExpireUs;
@@ -175,7 +183,7 @@
             // Allocates a raw ptr in order to avoid sending #postBufferRelease
             // from deleter, in case of native_handle_clone failure.
             _C2BlockPoolData *ptr = new _C2BlockPoolData(
-                    mBuffer.mId, native_handle_clone(mBuffer.mHandle));
+                    mId, native_handle_clone(mHandle));
             if (ptr && ptr->mHandle != NULL) {
                 std::shared_ptr<_C2BlockPoolData>
                         cache(ptr, BlockPoolDataDtor(impl));
diff --git a/media/libstagefright/codec2/vndk/bufferpool/include/BufferPoolTypes.h b/media/libstagefright/codec2/vndk/bufferpool/include/BufferPoolTypes.h
index 0cf023c..7f4223c 100644
--- a/media/libstagefright/codec2/vndk/bufferpool/include/BufferPoolTypes.h
+++ b/media/libstagefright/codec2/vndk/bufferpool/include/BufferPoolTypes.h
@@ -25,6 +25,7 @@
 
 struct __attribute__((visibility("hidden"))) _C2BlockPoolData {
     uint32_t mId; //BufferId
+    // Handle should be copied to somewhere else, and should be managed there.
     native_handle_t *mHandle;
 
     _C2BlockPoolData() : mId(0), mHandle(NULL) {}
@@ -33,10 +34,6 @@
             : mId(id), mHandle(handle) {}
 
     ~_C2BlockPoolData() {
-        if (mHandle != NULL) {
-            native_handle_close(mHandle);
-            native_handle_delete(mHandle);
-        }
     }
 };
 
diff --git a/media/ndk/NdkMediaFormat.cpp b/media/ndk/NdkMediaFormat.cpp
index 9bf450c..f32b83e 100644
--- a/media/ndk/NdkMediaFormat.cpp
+++ b/media/ndk/NdkMediaFormat.cpp
@@ -296,10 +296,8 @@
 EXPORT const char* AMEDIAFORMAT_KEY_DURATION = "durationUs";
 EXPORT const char* AMEDIAFORMAT_KEY_FLAC_COMPRESSION_LEVEL = "flac-compression-level";
 EXPORT const char* AMEDIAFORMAT_KEY_FRAME_RATE = "frame-rate";
-EXPORT const char* AMEDIAFORMAT_KEY_GRID_COLS = "grid-cols";
-EXPORT const char* AMEDIAFORMAT_KEY_GRID_HEIGHT = "grid-height";
+EXPORT const char* AMEDIAFORMAT_KEY_GRID_COLUMNS = "grid-cols";
 EXPORT const char* AMEDIAFORMAT_KEY_GRID_ROWS = "grid-rows";
-EXPORT const char* AMEDIAFORMAT_KEY_GRID_WIDTH = "grid-width";
 EXPORT const char* AMEDIAFORMAT_KEY_HDR_STATIC_INFO = "hdr-static-info";
 EXPORT const char* AMEDIAFORMAT_KEY_HEIGHT = "height";
 EXPORT const char* AMEDIAFORMAT_KEY_INTRA_REFRESH_PERIOD = "intra-refresh-period";
@@ -329,6 +327,8 @@
 EXPORT const char* AMEDIAFORMAT_KEY_STRIDE = "stride";
 EXPORT const char* AMEDIAFORMAT_KEY_TEMPORAL_LAYER_ID = "temporal-layer-id";
 EXPORT const char* AMEDIAFORMAT_KEY_TEMPORAL_LAYERING = "ts-schema";
+EXPORT const char* AMEDIAFORMAT_KEY_TILE_HEIGHT = "tile-height";
+EXPORT const char* AMEDIAFORMAT_KEY_TILE_WIDTH = "tile-width";
 EXPORT const char* AMEDIAFORMAT_KEY_TIME_US = "timeUs";
 EXPORT const char* AMEDIAFORMAT_KEY_TRACK_ID = "track-id";
 EXPORT const char* AMEDIAFORMAT_KEY_TRACK_INDEX = "track-index";
diff --git a/media/ndk/include/media/NdkMediaFormat.h b/media/ndk/include/media/NdkMediaFormat.h
index 1da9197..687054e 100644
--- a/media/ndk/include/media/NdkMediaFormat.h
+++ b/media/ndk/include/media/NdkMediaFormat.h
@@ -110,10 +110,8 @@
 extern const char* AMEDIAFORMAT_KEY_DURATION;
 extern const char* AMEDIAFORMAT_KEY_FLAC_COMPRESSION_LEVEL;
 extern const char* AMEDIAFORMAT_KEY_FRAME_RATE;
-extern const char* AMEDIAFORMAT_KEY_GRID_COLS;
-extern const char* AMEDIAFORMAT_KEY_GRID_HEIGHT;
+extern const char* AMEDIAFORMAT_KEY_GRID_COLUMNS;
 extern const char* AMEDIAFORMAT_KEY_GRID_ROWS;
-extern const char* AMEDIAFORMAT_KEY_GRID_WIDTH;
 extern const char* AMEDIAFORMAT_KEY_HDR_STATIC_INFO;
 extern const char* AMEDIAFORMAT_KEY_HEIGHT;
 extern const char* AMEDIAFORMAT_KEY_INTRA_REFRESH_PERIOD;
@@ -143,6 +141,8 @@
 extern const char* AMEDIAFORMAT_KEY_STRIDE;
 extern const char* AMEDIAFORMAT_KEY_TEMPORAL_LAYER_ID;
 extern const char* AMEDIAFORMAT_KEY_TEMPORAL_LAYERING;
+extern const char* AMEDIAFORMAT_KEY_TILE_HEIGHT;
+extern const char* AMEDIAFORMAT_KEY_TILE_WIDTH;
 extern const char* AMEDIAFORMAT_KEY_TIME_US;
 extern const char* AMEDIAFORMAT_KEY_TRACK_ID;
 extern const char* AMEDIAFORMAT_KEY_TRACK_INDEX;
diff --git a/packages/MediaComponents/Android.mk b/packages/MediaComponents/Android.mk
index b0d8e7d..b6480a2 100644
--- a/packages/MediaComponents/Android.mk
+++ b/packages/MediaComponents/Android.mk
@@ -40,6 +40,9 @@
 
 LOCAL_PROGUARD_FLAG_FILES := proguard.cfg
 
+# TODO: Enable proguard (b/74090741)
+LOCAL_PROGUARD_ENABLED := disabled
+
 LOCAL_MULTILIB := first
 
 LOCAL_JAVA_LIBRARIES += android-support-annotations
diff --git a/packages/MediaComponents/src/com/android/media/SessionPlaylistAgent.java b/packages/MediaComponents/src/com/android/media/SessionPlaylistAgent.java
new file mode 100644
index 0000000..6805dad
--- /dev/null
+++ b/packages/MediaComponents/src/com/android/media/SessionPlaylistAgent.java
@@ -0,0 +1,433 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
+import android.media.MediaPlayerBase;
+import android.media.MediaPlaylistAgent;
+import android.media.MediaSession2;
+import android.media.MediaSession2.OnDataSourceMissingHelper;
+import android.util.ArrayMap;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ThreadLocalRandom;
+
+public class SessionPlaylistAgent extends MediaPlaylistAgent {
+    private static final String TAG = "SessionPlaylistAgent";
+    @VisibleForTesting
+    static final int END_OF_PLAYLIST = -1;
+    @VisibleForTesting
+    static final int NO_VALID_ITEMS = -2;
+
+    private final PlayItem mEopPlayItem = new PlayItem(END_OF_PLAYLIST, null);
+
+    private final Object mLock = new Object();
+    private final MediaSession2 mSession;
+
+    // TODO: Set data sources properly into mPlayer (b/74090741)
+    @GuardedBy("mLock")
+    private MediaPlayerBase mPlayer;
+    @GuardedBy("mLock")
+    private OnDataSourceMissingHelper mDsdHelper;
+
+    // TODO: Check if having the same item is okay (b/74090741)
+    @GuardedBy("mLock")
+    private ArrayList<MediaItem2> mPlaylist = new ArrayList<>();
+    @GuardedBy("mLock")
+    private ArrayList<MediaItem2> mShuffledList = new ArrayList<>();
+    @GuardedBy("mLock")
+    private Map<MediaItem2, DataSourceDesc> mItemDsdMap = new ArrayMap<>();
+    @GuardedBy("mLock")
+    private MediaMetadata2 mMetadata;
+    @GuardedBy("mLock")
+    private int mRepeatMode;
+    @GuardedBy("mLock")
+    private int mShuffleMode;
+    @GuardedBy("mLock")
+    private PlayItem mCurrent;
+
+    private class PlayItem {
+        int shuffledIdx;
+        DataSourceDesc dsd;
+        MediaItem2 mediaItem;
+
+        PlayItem(int shuffledIdx) {
+            this(shuffledIdx, null);
+        }
+
+        PlayItem(int shuffledIdx, DataSourceDesc dsd) {
+            this.shuffledIdx = shuffledIdx;
+            if (shuffledIdx >= 0) {
+                this.mediaItem = mShuffledList.get(shuffledIdx);
+                if (dsd == null) {
+                    synchronized (mLock) {
+                        this.dsd = retrieveDataSourceDescLocked(this.mediaItem);
+                    }
+                } else {
+                    this.dsd = dsd;
+                }
+            }
+        }
+
+        boolean isValid() {
+            if (this == mEopPlayItem) {
+                return true;
+            }
+            if (mediaItem == null) {
+                return false;
+            }
+            if (dsd == null) {
+                return false;
+            }
+            if (shuffledIdx >= mShuffledList.size()) {
+                return false;
+            }
+            if (mediaItem != mShuffledList.get(shuffledIdx)) {
+                return false;
+            }
+            if (mediaItem.getDataSourceDesc() != null
+                    && !mediaItem.getDataSourceDesc().equals(dsd)) {
+                return false;
+            }
+            return true;
+        }
+    }
+
+    public SessionPlaylistAgent(@NonNull Context context, @NonNull MediaSession2 session,
+            @NonNull MediaPlayerBase player) {
+        super(context);
+        if (session == null) {
+            throw new IllegalArgumentException("session shouldn't be null");
+        }
+        if (player == null) {
+            throw new IllegalArgumentException("player shouldn't be null");
+        }
+        mSession = session;
+        mPlayer = player;
+    }
+
+    public void setPlayer(MediaPlayerBase player) {
+        if (player == null) {
+            throw new IllegalArgumentException("player shouldn't be null");
+        }
+        synchronized (mLock) {
+            mPlayer = player;
+        }
+    }
+
+    public void setOnDataSourceMissingHelper(OnDataSourceMissingHelper helper) {
+        synchronized (mLock) {
+            mDsdHelper = helper;
+        }
+    }
+
+    @Override
+    public @Nullable List<MediaItem2> getPlaylist() {
+        synchronized (mLock) {
+            return Collections.unmodifiableList(mPlaylist);
+        }
+    }
+
+    @Override
+    public void setPlaylist(@NonNull List<MediaItem2> list,
+            @Nullable MediaMetadata2 metadata) {
+        if (list == null) {
+            throw new IllegalArgumentException("list shouldn't be null");
+        }
+
+        synchronized (mLock) {
+            mItemDsdMap.clear();
+
+            mPlaylist.clear();
+            mPlaylist.addAll(list);
+            applyShuffleModeLocked();
+
+            mMetadata = metadata;
+            mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+        }
+        notifyPlaylistChanged();
+    }
+
+    @Override
+    public @Nullable MediaMetadata2 getPlaylistMetadata() {
+        return mMetadata;
+    }
+
+    @Override
+    public void updatePlaylistMetadata(@Nullable MediaMetadata2 metadata) {
+        synchronized (mLock) {
+            if (metadata == mMetadata) {
+                return;
+            }
+            mMetadata = metadata;
+        }
+        notifyPlaylistMetadataChanged();
+    }
+
+    @Override
+    public void addPlaylistItem(int index, @NonNull MediaItem2 item) {
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        synchronized (mLock) {
+            index = clamp(index, mPlaylist.size());
+            int shuffledIdx = index;
+            mPlaylist.add(index, item);
+            if (mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_NONE) {
+                mShuffledList.add(index, item);
+            } else {
+                // Add the item in random position of mShuffledList.
+                shuffledIdx = ThreadLocalRandom.current().nextInt(mShuffledList.size() + 1);
+                mShuffledList.add(shuffledIdx, item);
+            }
+            if (!hasValidItem()) {
+                mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+            } else {
+                updateCurrentIfNeededLocked();
+            }
+        }
+        notifyPlaylistChanged();
+    }
+
+    @Override
+    public void removePlaylistItem(@NonNull MediaItem2 item) {
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        synchronized (mLock) {
+            if (!mPlaylist.remove(item)) {
+                return;
+            }
+            mShuffledList.remove(item);
+            mItemDsdMap.remove(item);
+            updateCurrentIfNeededLocked();
+        }
+        notifyPlaylistChanged();
+    }
+
+    @Override
+    public void replacePlaylistItem(int index, @NonNull MediaItem2 item) {
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        synchronized (mLock) {
+            if (mPlaylist.size() <= 0) {
+                return;
+            }
+            index = clamp(index, mPlaylist.size() - 1);
+            int shuffledIdx = mShuffledList.indexOf(mPlaylist.get(index));
+            mItemDsdMap.remove(mShuffledList.get(shuffledIdx));
+            mShuffledList.set(shuffledIdx, item);
+            mPlaylist.set(index, item);
+            if (!hasValidItem()) {
+                mCurrent = getNextValidPlayItemLocked(END_OF_PLAYLIST, 1);
+            } else {
+                updateCurrentIfNeededLocked();
+            }
+        }
+        notifyPlaylistChanged();
+    }
+
+    @Override
+    public void skipToPlaylistItem(@NonNull MediaItem2 item) {
+        if (item == null) {
+            throw new IllegalArgumentException("item shouldn't be null");
+        }
+        synchronized (mLock) {
+            if (!hasValidItem() || item.equals(mCurrent.mediaItem)) {
+                return;
+            }
+            int shuffledIdx = mShuffledList.indexOf(item);
+            if (shuffledIdx < 0) {
+                return;
+            }
+            mCurrent = new PlayItem(shuffledIdx);
+            updateCurrentIfNeededLocked();
+        }
+    }
+
+    @Override
+    public void skipToPreviousItem() {
+        synchronized (mLock) {
+            if (!hasValidItem()) {
+                return;
+            }
+            PlayItem prev = getNextValidPlayItemLocked(mCurrent.shuffledIdx, -1);
+            if (prev != mEopPlayItem) {
+                mCurrent = prev;
+            }
+            updateCurrentIfNeededLocked();
+       }
+    }
+
+    @Override
+    public void skipToNextItem() {
+        synchronized (mLock) {
+            if (!hasValidItem()) {
+                return;
+            }
+            PlayItem next = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1);
+            if (next != mEopPlayItem) {
+                mCurrent = next;
+            }
+            updateCurrentIfNeededLocked();
+        }
+    }
+
+    @Override
+    public int getRepeatMode() {
+        return mRepeatMode;
+    }
+
+    @Override
+    public void setRepeatMode(int repeatMode) {
+        if (repeatMode < MediaPlaylistAgent.REPEAT_MODE_NONE
+                || repeatMode > MediaPlaylistAgent.REPEAT_MODE_GROUP) {
+            return;
+        }
+        synchronized (mLock) {
+            if (mRepeatMode == repeatMode) {
+                return;
+            }
+            mRepeatMode = repeatMode;
+        }
+        notifyRepeatModeChanged();
+    }
+
+    @Override
+    public int getShuffleMode() {
+        return mShuffleMode;
+    }
+
+    @Override
+    public void setShuffleMode(int shuffleMode) {
+        if (shuffleMode < MediaPlaylistAgent.SHUFFLE_MODE_NONE
+                || shuffleMode > MediaPlaylistAgent.SHUFFLE_MODE_GROUP) {
+            return;
+        }
+        synchronized (mLock) {
+            if (mShuffleMode == shuffleMode) {
+                return;
+            }
+            mShuffleMode = shuffleMode;
+            applyShuffleModeLocked();
+        }
+        notifyShuffleModeChanged();
+    }
+
+    @VisibleForTesting
+    int getCurShuffledIndex() {
+        return hasValidItem() ? mCurrent.shuffledIdx : NO_VALID_ITEMS;
+    }
+
+    private boolean hasValidItem() {
+        return mCurrent != null;
+    }
+
+    private DataSourceDesc retrieveDataSourceDescLocked(MediaItem2 item) {
+        DataSourceDesc dsd = item.getDataSourceDesc();
+        if (dsd != null) {
+            mItemDsdMap.put(item, dsd);
+            return dsd;
+        }
+        dsd = mItemDsdMap.get(item);
+        if (dsd != null) {
+            return dsd;
+        }
+        OnDataSourceMissingHelper helper = mDsdHelper;
+        if (helper != null) {
+            // TODO: Do not call onDataSourceMissing with the lock (b/74090741).
+            dsd = helper.onDataSourceMissing(mSession, item);
+            if (dsd != null) {
+                mItemDsdMap.put(item, dsd);
+            }
+        }
+        return dsd;
+    }
+
+    private PlayItem getNextValidPlayItemLocked(int curShuffledIdx, int direction) {
+        int size = mPlaylist.size();
+        if (curShuffledIdx == END_OF_PLAYLIST) {
+            curShuffledIdx = (direction > 0) ? -1 : size;
+        }
+        for (int i = 0; i < size; i++) {
+            curShuffledIdx += direction;
+            if (curShuffledIdx < 0 || curShuffledIdx >= mPlaylist.size()) {
+                if (mRepeatMode == REPEAT_MODE_NONE) {
+                    return (i == size - 1) ? null : mEopPlayItem;
+                } else {
+                    curShuffledIdx = curShuffledIdx < 0 ? mPlaylist.size() - 1 : 0;
+                }
+            }
+            DataSourceDesc dsd = retrieveDataSourceDescLocked(mShuffledList.get(curShuffledIdx));
+            if (dsd != null) {
+                return new PlayItem(curShuffledIdx, dsd);
+            }
+        }
+        return null;
+    }
+
+    private void updateCurrentIfNeededLocked() {
+        if (!hasValidItem() || mCurrent.isValid()) {
+            return;
+        }
+        int shuffledIdx = mShuffledList.indexOf(mCurrent.mediaItem);
+        if (shuffledIdx >= 0) {
+            // Added an item.
+            mCurrent.shuffledIdx = shuffledIdx;
+            return;
+        }
+
+        if (mCurrent.shuffledIdx >= mShuffledList.size()) {
+            mCurrent = getNextValidPlayItemLocked(mShuffledList.size() - 1, 1);
+        } else {
+            mCurrent.mediaItem = mShuffledList.get(mCurrent.shuffledIdx);
+            if (retrieveDataSourceDescLocked(mCurrent.mediaItem) == null) {
+                mCurrent = getNextValidPlayItemLocked(mCurrent.shuffledIdx, 1);
+            }
+        }
+        return;
+    }
+
+    private void applyShuffleModeLocked() {
+        mShuffledList.clear();
+        mShuffledList.addAll(mPlaylist);
+        if (mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_ALL
+                || mShuffleMode == MediaPlaylistAgent.SHUFFLE_MODE_GROUP) {
+            Collections.shuffle(mShuffledList);
+        }
+    }
+
+    // Clamps value to [0, size]
+    private static int clamp(int value, int size) {
+        if (value < 0) {
+            return 0;
+        }
+        return (value > size) ? size : value;
+    }
+}
diff --git a/packages/MediaComponents/tests/Android.mk b/packages/MediaComponents/tests/Android.mk
new file mode 100644
index 0000000..bf81efb
--- /dev/null
+++ b/packages/MediaComponents/tests/Android.mk
@@ -0,0 +1,35 @@
+# Copyright 2018 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.
+
+LOCAL_PATH:= $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE_TAGS := tests
+
+LOCAL_STATIC_JAVA_LIBRARIES := \
+    android.test.runner.stubs \
+    android.test.base.stubs \
+    junit
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := MediaComponentsTest
+
+LOCAL_INSTRUMENTATION_FOR := MediaComponents
+
+LOCAL_PRIVATE_PLATFORM_APIS := true
+
+LOCAL_CERTIFICATE := platform
+
+include $(BUILD_PACKAGE)
diff --git a/packages/MediaComponents/tests/AndroidManifest.xml b/packages/MediaComponents/tests/AndroidManifest.xml
new file mode 100644
index 0000000..7255265
--- /dev/null
+++ b/packages/MediaComponents/tests/AndroidManifest.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright 2018 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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+          package="com.android.media.tests">
+
+    <application android:label="Media API Test">
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <!--
+    To run the tests use the command:
+    "adb shell am instrument -w com.android.media.tests/android.test.InstrumentationTestRunner"
+    -->
+    <instrumentation
+        android:name="android.test.InstrumentationTestRunner"
+        android:targetPackage="com.android.media.update"
+        android:label="Media API test" />
+
+</manifest>
diff --git a/packages/MediaComponents/tests/src/com/android/media/SessionPlaylistAgentTest.java b/packages/MediaComponents/tests/src/com/android/media/SessionPlaylistAgentTest.java
new file mode 100644
index 0000000..80370ec
--- /dev/null
+++ b/packages/MediaComponents/tests/src/com/android/media/SessionPlaylistAgentTest.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.media;
+
+import android.content.Context;
+import android.media.DataSourceDesc;
+import android.media.MediaItem2;
+import android.media.MediaMetadata2;
+import android.media.MediaPlayer2;
+import android.media.MediaPlayerBase;
+import android.media.MediaPlaylistAgent;
+import android.media.MediaSession2;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.test.AndroidTestCase;
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Tests {@link SessionPlaylistAgent}.
+ */
+public class SessionPlaylistAgentTest extends AndroidTestCase {
+    private static final String TAG = "SessionPlaylistAgentTest";
+    private static final int WAIT_TIME_MS = 1000;
+    private static final int INVALID_REPEAT_MODE = -100;
+    private static final int INVALID_SHUFFLE_MODE = -100;
+
+    private Handler mHandler;
+    private Executor mHandlerExecutor;
+
+    private Object mWaitLock = new Object();
+    private Context mContext;
+    private MediaSession2 mSession;
+    private MediaPlayerBase mPlayer;
+    private SessionPlaylistAgent mAgent;
+    private MyDataSourceHelper mDataSourceHelper;
+    private MyPlaylistEventCallback mEventCallback;
+
+    public class MyPlaylistEventCallback extends MediaPlaylistAgent.PlaylistEventCallback {
+        boolean onPlaylistChangedCalled;
+        boolean onPlaylistMetadataChangedCalled;
+        boolean onRepeatModeChangedCalled;
+        boolean onShuffleModeChangedCalled;
+
+        private Object mWaitLock;
+
+        public MyPlaylistEventCallback(Object waitLock) {
+            mWaitLock = waitLock;
+        }
+
+        public void clear() {
+            onPlaylistChangedCalled = false;
+            onPlaylistMetadataChangedCalled = false;
+            onRepeatModeChangedCalled = false;
+            onShuffleModeChangedCalled = false;
+        }
+
+        public void onPlaylistChanged(MediaPlaylistAgent playlistAgent, List<MediaItem2> list,
+                MediaMetadata2 metadata) {
+            synchronized (mWaitLock) {
+                onPlaylistChangedCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        public void onPlaylistMetadataChanged(MediaPlaylistAgent playlistAgent,
+                MediaMetadata2 metadata) {
+            synchronized (mWaitLock) {
+                onPlaylistMetadataChangedCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        public void onRepeatModeChanged(MediaPlaylistAgent playlistAgent, int repeatMode) {
+            synchronized (mWaitLock) {
+                onRepeatModeChangedCalled = true;
+                mWaitLock.notify();
+            }
+        }
+
+        public void onShuffleModeChanged(MediaPlaylistAgent playlistAgent, int shuffleMode) {
+            synchronized (mWaitLock) {
+                onShuffleModeChangedCalled = true;
+                mWaitLock.notify();
+            }
+        }
+    }
+
+    public class MyDataSourceHelper implements MediaSession2.OnDataSourceMissingHelper {
+        @Override
+        public DataSourceDesc onDataSourceMissing(MediaSession2 session, MediaItem2 item) {
+            if (item.getMediaId().contains("WITHOUT_DSD")) {
+                return null;
+            }
+            return new DataSourceDesc.Builder()
+                    .setDataSource(getContext(), Uri.parse("dsd://test"))
+                    .setMediaId(item.getMediaId())
+                    .build();
+        }
+    }
+
+    @Before
+    public void setUp() throws Exception {
+        HandlerThread handlerThread = new HandlerThread("SessionPlaylistAgent");
+        handlerThread.start();
+        mHandler = new Handler(handlerThread.getLooper());
+        mHandlerExecutor = (runnable) -> {
+            mHandler.post(runnable);
+        };
+
+        mContext = getContext();
+        mPlayer = MediaPlayer2.create();
+        mSession = new MediaSession2.Builder(mContext)
+                .setPlayer(mPlayer)
+                .setSessionCallback(mHandlerExecutor,
+                        new MediaSession2.SessionCallback(mContext) {})
+                .setId(TAG).build();
+        mDataSourceHelper = new MyDataSourceHelper();
+        mAgent = new SessionPlaylistAgent(mContext, mSession, mPlayer);
+        mAgent.setOnDataSourceMissingHelper(mDataSourceHelper);
+        mEventCallback = new MyPlaylistEventCallback(mWaitLock);
+        mAgent.registerPlaylistEventCallback(mHandlerExecutor, mEventCallback);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        mSession.close();
+        mPlayer.close();
+        mHandler.getLooper().quitSafely();
+        mHandler = null;
+        mHandlerExecutor = null;
+    }
+
+    @Test
+    public void testSetAndGetShuflleMode() throws Exception {
+        int shuffleMode = mAgent.getShuffleMode();
+        if (shuffleMode != MediaPlaylistAgent.SHUFFLE_MODE_NONE) {
+            mEventCallback.clear();
+            synchronized (mWaitLock) {
+                mAgent.setShuffleMode(MediaPlaylistAgent.SHUFFLE_MODE_NONE);
+                mWaitLock.wait(WAIT_TIME_MS);
+                assertTrue(mEventCallback.onShuffleModeChangedCalled);
+            }
+            assertEquals(MediaPlaylistAgent.SHUFFLE_MODE_NONE, mAgent.getShuffleMode());
+        }
+
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            mAgent.setShuffleMode(MediaPlaylistAgent.SHUFFLE_MODE_ALL);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onShuffleModeChangedCalled);
+        }
+        assertEquals(MediaPlaylistAgent.SHUFFLE_MODE_ALL, mAgent.getShuffleMode());
+
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            mAgent.setShuffleMode(MediaPlaylistAgent.SHUFFLE_MODE_GROUP);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onShuffleModeChangedCalled);
+        }
+        assertEquals(MediaPlaylistAgent.SHUFFLE_MODE_GROUP, mAgent.getShuffleMode());
+
+        // INVALID_SHUFFLE_MODE will not change the shuffle mode.
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            mAgent.setShuffleMode(INVALID_SHUFFLE_MODE);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertFalse(mEventCallback.onShuffleModeChangedCalled);
+        }
+        assertEquals(MediaPlaylistAgent.SHUFFLE_MODE_GROUP, mAgent.getShuffleMode());
+    }
+
+    @Test
+    public void testSetAndGetRepeatMode() throws Exception {
+        int repeatMode = mAgent.getRepeatMode();
+        if (repeatMode != MediaPlaylistAgent.REPEAT_MODE_NONE) {
+            mEventCallback.clear();
+            synchronized (mWaitLock) {
+                mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_NONE);
+                mWaitLock.wait(WAIT_TIME_MS);
+                assertTrue(mEventCallback.onRepeatModeChangedCalled);
+            }
+            assertEquals(MediaPlaylistAgent.REPEAT_MODE_NONE, mAgent.getRepeatMode());
+        }
+
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_ONE);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onRepeatModeChangedCalled);
+        }
+        assertEquals(MediaPlaylistAgent.REPEAT_MODE_ONE, mAgent.getRepeatMode());
+
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_ALL);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onRepeatModeChangedCalled);
+        }
+        assertEquals(MediaPlaylistAgent.REPEAT_MODE_ALL, mAgent.getRepeatMode());
+
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_GROUP);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onRepeatModeChangedCalled);
+        }
+        assertEquals(MediaPlaylistAgent.REPEAT_MODE_GROUP, mAgent.getRepeatMode());
+
+        // INVALID_SHUFFLE_MODE will not change the shuffle mode.
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            mAgent.setRepeatMode(INVALID_REPEAT_MODE);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertFalse(mEventCallback.onRepeatModeChangedCalled);
+        }
+        assertEquals(MediaPlaylistAgent.REPEAT_MODE_GROUP, mAgent.getRepeatMode());
+    }
+
+    @Test
+    public void testSetPlaylist() throws Exception {
+        int listSize = 10;
+        createAndSetPlaylist(10);
+        assertEquals(listSize, mAgent.getPlaylist().size());
+        assertEquals(0, mAgent.getCurShuffledIndex());
+    }
+
+    @Test
+    public void testSkipItems() throws Exception {
+        int listSize = 5;
+        List<MediaItem2> playlist = createAndSetPlaylist(listSize);
+
+        mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_NONE);
+        // Test skipToPlaylistItem
+        for (int i = listSize - 1; i >= 0; --i) {
+            mAgent.skipToPlaylistItem(playlist.get(i));
+            assertEquals(i, mAgent.getCurShuffledIndex());
+        }
+
+        // Test skipToNextItem
+        // curPlayPos = 0
+        for (int curPlayPos = 0; curPlayPos < listSize - 1; ++curPlayPos) {
+            mAgent.skipToNextItem();
+            assertEquals(curPlayPos + 1, mAgent.getCurShuffledIndex());
+        }
+        mAgent.skipToNextItem();
+        assertEquals(listSize - 1, mAgent.getCurShuffledIndex());
+
+        // Test skipToPrevious
+        // curPlayPos = listSize - 1
+        for (int curPlayPos = listSize - 1; curPlayPos > 0; --curPlayPos) {
+            mAgent.skipToPreviousItem();
+            assertEquals(curPlayPos - 1, mAgent.getCurShuffledIndex());
+        }
+        mAgent.skipToPreviousItem();
+        assertEquals(0, mAgent.getCurShuffledIndex());
+
+        mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_ALL);
+        // Test skipToPrevious with repeat mode all
+        // curPlayPos = 0
+        mAgent.skipToPreviousItem();
+        assertEquals(listSize - 1, mAgent.getCurShuffledIndex());
+
+        // Test skipToNext with repeat mode all
+        // curPlayPos = listSize - 1
+        mAgent.skipToNextItem();
+        assertEquals(0, mAgent.getCurShuffledIndex());
+
+        mAgent.skipToPreviousItem();
+        // curPlayPos = listSize - 1, nextPlayPos = 0
+        // Test next play pos after setting repeat mode none.
+        mAgent.setRepeatMode(MediaPlaylistAgent.REPEAT_MODE_NONE);
+        assertEquals(listSize - 1, mAgent.getCurShuffledIndex());
+    }
+
+    @Test
+    public void testEditPlaylist() throws Exception {
+        int listSize = 5;
+        List<MediaItem2> playlist = createAndSetPlaylist(listSize);
+
+        // Test add item: [0 (cur), 1, 2, 3, 4] -> [0 (cur), 1, 5, 2, 3, 4]
+        mEventCallback.clear();
+        MediaItem2 item_5 = generateMediaItem(5);
+        synchronized (mWaitLock) {
+            playlist.add(2, item_5);
+            mAgent.addPlaylistItem(2, item_5);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+
+        mEventCallback.clear();
+        // Move current: [0 (cur), 1, 5, 2, 3, 4] -> [0, 1, 5 (cur), 2, 3, 4]
+        mAgent.skipToPlaylistItem(item_5);
+        // Remove current item: [0, 1, 5 (cur), 2, 3, 4] -> [0, 1, 2 (cur), 3, 4]
+        synchronized (mWaitLock) {
+            playlist.remove(item_5);
+            mAgent.removePlaylistItem(item_5);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        assertEquals(2, mAgent.getCurShuffledIndex());
+
+        // Remove previous item: [0, 1, 2 (cur), 3, 4] -> [0, 2 (cur), 3, 4]
+        mEventCallback.clear();
+        MediaItem2 previousItem = playlist.get(1);
+        synchronized (mWaitLock) {
+            playlist.remove(previousItem);
+            mAgent.removePlaylistItem(previousItem);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        assertEquals(1, mAgent.getCurShuffledIndex());
+
+        // Remove next item: [0, 2 (cur), 3, 4] -> [0, 2 (cur), 4]
+        mEventCallback.clear();
+        MediaItem2 nextItem = playlist.get(2);
+        synchronized (mWaitLock) {
+            playlist.remove(nextItem);
+            mAgent.removePlaylistItem(nextItem);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        assertEquals(1, mAgent.getCurShuffledIndex());
+
+        // Replace item: [0, 2 (cur), 4] -> [0, 2 (cur), 5]
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            playlist.set(2, item_5);
+            mAgent.replacePlaylistItem(2, item_5);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        assertEquals(1, mAgent.getCurShuffledIndex());
+
+        // Move last and remove the last item: [0, 2 (cur), 5] -> [0, 2, 5 (cur)] -> [0, 2 (cur)]
+        MediaItem2 lastItem = playlist.get(1);
+        mAgent.skipToPlaylistItem(lastItem);
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            playlist.remove(lastItem);
+            mAgent.removePlaylistItem(lastItem);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        assertEquals(1, mAgent.getCurShuffledIndex());
+
+        // Remove all items
+        for (int i = playlist.size() - 1; i >= 0; --i) {
+            MediaItem2 item = playlist.get(i);
+            mAgent.skipToPlaylistItem(item);
+            mEventCallback.clear();
+            synchronized (mWaitLock) {
+                playlist.remove(item);
+                mAgent.removePlaylistItem(item);
+                mWaitLock.wait(WAIT_TIME_MS);
+                assertTrue(mEventCallback.onPlaylistChangedCalled);
+            }
+            assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        }
+        assertEquals(SessionPlaylistAgent.NO_VALID_ITEMS, mAgent.getCurShuffledIndex());
+    }
+
+
+    @Test
+    public void testPlaylistWithInvalidItem() throws Exception {
+        int listSize = 2;
+        List<MediaItem2> playlist = createAndSetPlaylist(listSize);
+
+        // Add item: [0 (cur), 1] -> [0 (cur), 3 (no_dsd), 1]
+        mEventCallback.clear();
+        MediaItem2 invalidItem2 = generateMediaItemWithoutDataSourceDesc(2);
+        synchronized (mWaitLock) {
+            playlist.add(1, invalidItem2);
+            mAgent.addPlaylistItem(1, invalidItem2);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        assertEquals(0, mAgent.getCurShuffledIndex());
+
+        // Test skip to next item:  [0 (cur), 2 (no_dsd), 1] -> [0, 2 (no_dsd), 1 (cur)]
+        mAgent.skipToNextItem();
+        assertEquals(2, mAgent.getCurShuffledIndex());
+
+        // Test skip to previous item: [0, 2 (no_dsd), 1 (cur)] -> [0 (cur), 2 (no_dsd), 1]
+        mAgent.skipToPreviousItem();
+        assertEquals(0, mAgent.getCurShuffledIndex());
+
+        // Remove current item: [0 (cur), 2 (no_dsd), 1] -> [2 (no_dsd), 1 (cur)]
+        mEventCallback.clear();
+        MediaItem2 item = playlist.get(0);
+        synchronized (mWaitLock) {
+            playlist.remove(item);
+            mAgent.removePlaylistItem(item);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        assertEquals(1, mAgent.getCurShuffledIndex());
+
+        // Remove current item: [2 (no_dsd), 1 (cur)] -> [2 (no_dsd)]
+        mEventCallback.clear();
+        item = playlist.get(1);
+        synchronized (mWaitLock) {
+            playlist.remove(item);
+            mAgent.removePlaylistItem(item);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        assertEquals(SessionPlaylistAgent.NO_VALID_ITEMS, mAgent.getCurShuffledIndex());
+
+        // Add invalid item: [2 (no_dsd)] -> [0 (no_dsd), 2 (no_dsd)]
+        MediaItem2 invalidItem0 = generateMediaItemWithoutDataSourceDesc(0);
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            playlist.add(0, invalidItem0);
+            mAgent.addPlaylistItem(0, invalidItem0);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        assertEquals(SessionPlaylistAgent.NO_VALID_ITEMS, mAgent.getCurShuffledIndex());
+
+        // Add valid item: [0 (no_dsd), 2 (no_dsd)] -> [0 (no_dsd), 1, 2 (no_dsd)]
+        MediaItem2 invalidItem1 = generateMediaItem(1);
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            playlist.add(1, invalidItem1);
+            mAgent.addPlaylistItem(1, invalidItem1);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        assertEquals(1, mAgent.getCurShuffledIndex());
+
+        // Replace the valid item with an invalid item:
+        // [0 (no_dsd), 1 (cur), 2 (no_dsd)] -> [0 (no_dsd), 3 (no_dsd), 2 (no_dsd)]
+        MediaItem2 invalidItem3 = generateMediaItemWithoutDataSourceDesc(3);
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            playlist.set(1, invalidItem3);
+            mAgent.replacePlaylistItem(1, invalidItem3);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        assertPlaylistEquals(playlist, mAgent.getPlaylist());
+        assertEquals(SessionPlaylistAgent.END_OF_PLAYLIST, mAgent.getCurShuffledIndex());
+    }
+
+    private List<MediaItem2> createAndSetPlaylist(int listSize) throws Exception {
+        List<MediaItem2> items = new ArrayList<>();
+        for (int i = 0; i < listSize; ++i) {
+            items.add(generateMediaItem(i));
+        }
+        mEventCallback.clear();
+        synchronized (mWaitLock) {
+            mAgent.setPlaylist(items, null);
+            mWaitLock.wait(WAIT_TIME_MS);
+            assertTrue(mEventCallback.onPlaylistChangedCalled);
+        }
+        return items;
+    }
+
+    private void assertPlaylistEquals(List<MediaItem2> expected, List<MediaItem2> actual) {
+        if (expected == actual) {
+            return;
+        }
+        assertTrue(expected != null && actual != null);
+        assertEquals(expected.size(), actual.size());
+        for (int i = 0; i < expected.size(); ++i) {
+            assertTrue(expected.get(i).equals(actual.get(i)));
+        }
+    }
+
+    private MediaItem2 generateMediaItemWithoutDataSourceDesc(int key) {
+        return new MediaItem2.Builder(mContext, 0)
+                .setMediaId("TEST_MEDIA_ID_WITHOUT_DSD_" + key)
+                .build();
+    }
+
+    private MediaItem2 generateMediaItem(int key) {
+        return new MediaItem2.Builder(mContext, 0)
+                .setMediaId("TEST_MEDIA_ID_" + key)
+                .build();
+    }
+}
diff --git a/services/audioflinger/AudioFlinger.cpp b/services/audioflinger/AudioFlinger.cpp
index 8033382..ea06b6c 100644
--- a/services/audioflinger/AudioFlinger.cpp
+++ b/services/audioflinger/AudioFlinger.cpp
@@ -3139,16 +3139,21 @@
         bool pinned = (sessionId > AUDIO_SESSION_OUTPUT_MIX) && isSessionAcquired_l(sessionId);
         handle = thread->createEffect_l(client, effectClient, priority, sessionId,
                 &desc, enabled, &lStatus, pinned);
-        if (handle != 0 && id != NULL) {
-            *id = handle->id();
-        }
-        if (handle == 0) {
+        if (lStatus != NO_ERROR && lStatus != ALREADY_EXISTS) {
             // remove local strong reference to Client with mClientLock held
             Mutex::Autolock _cl(mClientLock);
             client.clear();
+        } else {
+            // handle must be valid here, but check again to be safe.
+            if (handle.get() != nullptr && id != nullptr) *id = handle->id();
         }
     }
 
+    if (lStatus != NO_ERROR && lStatus != ALREADY_EXISTS) {
+        // handle must be cleared outside lock.
+        handle.clear();
+    }
+
 Exit:
     *status = lStatus;
     return handle;
diff --git a/services/audioflinger/Threads.cpp b/services/audioflinger/Threads.cpp
index 20447d8..62e9fe7 100644
--- a/services/audioflinger/Threads.cpp
+++ b/services/audioflinger/Threads.cpp
@@ -1360,7 +1360,7 @@
         if (chainCreated) {
             removeEffectChain_l(chain);
         }
-        handle.clear();
+        // handle must be cleared by caller to avoid deadlock.
     }
 
     *status = lStatus;
@@ -8524,7 +8524,11 @@
         audio_devices_t outDevice, audio_devices_t inDevice, bool systemReady)
     : MmapThread(audioFlinger, id, hwDev, output->stream, outDevice, inDevice, systemReady),
       mStreamType(AUDIO_STREAM_MUSIC),
-      mStreamVolume(1.0), mStreamMute(false), mOutput(output)
+      mStreamVolume(1.0),
+      mStreamMute(false),
+      mHalVolFloat(-1.0f), // Initialize to illegal value so it always gets set properly later.
+      mNoCallbackWarningCount(0),
+      mOutput(output)
 {
     snprintf(mThreadName, kThreadNameLength, "AudioMmapOut_%X", id);
     mChannelCount = audio_channel_count_from_out_mask(mChannelMask);
@@ -8632,7 +8636,6 @@
     }
 
     if (volume != mHalVolFloat) {
-        mHalVolFloat = volume;
 
         // Convert volumes from float to 8.24
         uint32_t vol = (uint32_t)(volume * (1 << 24));
@@ -8645,7 +8648,10 @@
             volume = (float)vol / (1 << 24);
         }
         // Try to use HW volume control and fall back to SW control if not implemented
-        if (mOutput->stream->setVolume(volume, volume) != NO_ERROR) {
+        if (mOutput->stream->setVolume(volume, volume) == NO_ERROR) {
+            mHalVolFloat = volume; // HW volume control worked, so update value.
+            mNoCallbackWarningCount = 0;
+        } else {
             sp<MmapStreamCallback> callback = mCallback.promote();
             if (callback != 0) {
                 int channelCount;
@@ -8659,8 +8665,13 @@
                     values.add(volume);
                 }
                 callback->onVolumeChanged(mChannelMask, values);
+                mHalVolFloat = volume; // SW volume control worked, so update value.
+                mNoCallbackWarningCount = 0;
             } else {
-                ALOGW("Could not set MMAP stream volume: no volume callback!");
+                if (mNoCallbackWarningCount < kMaxNoCallbackWarnings) {
+                    ALOGW("Could not set MMAP stream volume: no volume callback!");
+                    mNoCallbackWarningCount++;
+                }
             }
         }
     }
diff --git a/services/audioflinger/Threads.h b/services/audioflinger/Threads.h
index ae14ac1..7cd46a7 100644
--- a/services/audioflinger/Threads.h
+++ b/services/audioflinger/Threads.h
@@ -1666,6 +1666,8 @@
                 bool                        mMasterMute;
                 bool                        mStreamMute;
                 float                       mHalVolFloat;
+                int32_t                     mNoCallbackWarningCount;
+     static     constexpr int32_t           kMaxNoCallbackWarnings = 5;
                 AudioStreamOut*             mOutput;
 };
 
diff --git a/services/audiopolicy/managerdefault/AudioPolicyManager.cpp b/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
index 74ca902..73d92ac 100644
--- a/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
+++ b/services/audiopolicy/managerdefault/AudioPolicyManager.cpp
@@ -1266,6 +1266,11 @@
         // We do not introduce additional delay here.
     }
 
+    if (stream == AUDIO_STREAM_ENFORCED_AUDIBLE &&
+            mEngine->getForceUse(AUDIO_POLICY_FORCE_FOR_SYSTEM) == AUDIO_POLICY_FORCE_SYSTEM_ENFORCED) {
+        setStrategyMute(STRATEGY_SONIFICATION, true, outputDesc);
+    }
+
     return NO_ERROR;
 }
 
@@ -1366,6 +1371,12 @@
             // update the outputs if stopping one with a stream that can affect notification routing
             handleNotificationRoutingForStream(stream);
         }
+
+        if (stream == AUDIO_STREAM_ENFORCED_AUDIBLE &&
+                mEngine->getForceUse(AUDIO_POLICY_FORCE_FOR_SYSTEM) == AUDIO_POLICY_FORCE_SYSTEM_ENFORCED) {
+            setStrategyMute(STRATEGY_SONIFICATION, false, outputDesc);
+        }
+
         if (stream == AUDIO_STREAM_MUSIC) {
             selectOutputForMusicEffects();
         }