Merge changes I6902e052,I39a7f08a,I26771887

* changes:
  Extract Gesture handling code and test it
  Make assertPosition a method of FakePointerController
  Extract HardwareState conversion into a class and test it
diff --git a/libs/binder/IPCThreadState.cpp b/libs/binder/IPCThreadState.cpp
index c0f3e30..6d64e1e 100644
--- a/libs/binder/IPCThreadState.cpp
+++ b/libs/binder/IPCThreadState.cpp
@@ -132,12 +132,21 @@
     } else {
         out << "\ttarget.ptr=" << btd->target.ptr;
     }
-    out << "\t (cookie " << btd->cookie << ")"
-        << "\n"
+    out << "\t (cookie " << btd->cookie << ")\n"
         << "\tcode=" << TypeCode(btd->code) << ", flags=" << (void*)(uint64_t)btd->flags << "\n"
-        << "\tdata=" << btd->data.ptr.buffer << " (" << (void*)btd->data_size << " bytes)"
-        << "\n"
-        << "\toffsets=" << btd->data.ptr.offsets << " (" << (void*)btd->offsets_size << " bytes)";
+        << "\tdata=" << btd->data.ptr.buffer << " (" << (void*)btd->data_size << " bytes)\n"
+        << "\toffsets=" << btd->data.ptr.offsets << " (" << (void*)btd->offsets_size << " bytes)\n";
+    return btd + 1;
+}
+
+static const void* printBinderTransactionDataSecCtx(std::ostream& out, const void* data) {
+    const binder_transaction_data_secctx* btd = (const binder_transaction_data_secctx*)data;
+
+    printBinderTransactionData(out, &btd->transaction_data);
+
+    char* secctx = (char*)btd->secctx;
+    out << "\tsecctx=" << secctx << "\n";
+
     return btd+1;
 }
 
@@ -156,6 +165,11 @@
     out << "\t" << kReturnStrings[cmdIndex];
 
     switch (code) {
+        case BR_TRANSACTION_SEC_CTX: {
+            out << ": ";
+            cmd = (const int32_t*)printBinderTransactionDataSecCtx(out, cmd);
+        } break;
+
         case BR_TRANSACTION:
         case BR_REPLY: {
             out << ": ";
diff --git a/libs/binder/RpcServer.cpp b/libs/binder/RpcServer.cpp
index 0820cd1..fedc1d9 100644
--- a/libs/binder/RpcServer.cpp
+++ b/libs/binder/RpcServer.cpp
@@ -70,11 +70,8 @@
     return setupSocketServer(UnixSocketAddress(path));
 }
 
-status_t RpcServer::setupVsockServer(unsigned int port) {
-    // realizing value w/ this type at compile time to avoid ubsan abort
-    constexpr unsigned int kAnyCid = VMADDR_CID_ANY;
-
-    return setupSocketServer(VsockSocketAddress(kAnyCid, port));
+status_t RpcServer::setupVsockServer(unsigned int bindCid, unsigned int port) {
+    return setupSocketServer(VsockSocketAddress(bindCid, port));
 }
 
 status_t RpcServer::setupInetServer(const char* address, unsigned int port,
@@ -157,6 +154,12 @@
     mRootObjectFactory = std::move(makeObject);
 }
 
+void RpcServer::setConnectionFilter(std::function<bool(const void*, size_t)>&& filter) {
+    RpcMutexLockGuard _l(mLock);
+    LOG_ALWAYS_FATAL_IF(mShutdownTrigger != nullptr, "Already joined");
+    mConnectionFilter = std::move(filter);
+}
+
 sp<IBinder> RpcServer::getRootObject() {
     RpcMutexLockGuard _l(mLock);
     bool hasWeak = mRootObjectWeak.unsafe_get();
@@ -242,13 +245,19 @@
         if (mAcceptFn(*this, &clientSocket) != OK) {
             continue;
         }
+
+        LOG_RPC_DETAIL("accept on fd %d yields fd %d", mServer.fd.get(), clientSocket.fd.get());
+
         if (getpeername(clientSocket.fd.get(), reinterpret_cast<sockaddr*>(addr.data()),
                         &addrLen)) {
             ALOGE("Could not getpeername socket: %s", strerror(errno));
             continue;
         }
 
-        LOG_RPC_DETAIL("accept on fd %d yields fd %d", mServer.fd.get(), clientSocket.fd.get());
+        if (mConnectionFilter != nullptr && !mConnectionFilter(addr.data(), addrLen)) {
+            ALOGE("Dropped client connection fd %d", clientSocket.fd.get());
+            continue;
+        }
 
         {
             RpcMutexLockGuard _l(mLock);
diff --git a/libs/binder/include/binder/RpcServer.h b/libs/binder/include/binder/RpcServer.h
index 4ad0a47..25193a3 100644
--- a/libs/binder/include/binder/RpcServer.h
+++ b/libs/binder/include/binder/RpcServer.h
@@ -81,9 +81,9 @@
     [[nodiscard]] status_t setupRawSocketServer(base::unique_fd socket_fd);
 
     /**
-     * Creates an RPC server at the current port.
+     * Creates an RPC server binding to the given CID at the given port.
      */
-    [[nodiscard]] status_t setupVsockServer(unsigned int port);
+    [[nodiscard]] status_t setupVsockServer(unsigned int bindCid, unsigned int port);
 
     /**
      * Creates an RPC server at the current port using IPv4.
@@ -171,6 +171,16 @@
     sp<IBinder> getRootObject();
 
     /**
+     * Set optional filter of incoming connections based on the peer's address.
+     *
+     * Takes one argument: a callable that is invoked on each accept()-ed
+     * connection and returns false if the connection should be dropped.
+     * See the description of setPerSessionRootObject() for details about
+     * the callable's arguments.
+     */
+    void setConnectionFilter(std::function<bool(const void*, size_t)>&& filter);
+
+    /**
      * See RpcTransportCtx::getCertificate
      */
     std::vector<uint8_t> getCertificate(RpcCertificateFormat);
@@ -253,6 +263,7 @@
     sp<IBinder> mRootObject;
     wp<IBinder> mRootObjectWeak;
     std::function<sp<IBinder>(const void*, size_t)> mRootObjectFactory;
+    std::function<bool(const void*, size_t)> mConnectionFilter;
     std::map<std::vector<uint8_t>, sp<RpcSession>> mSessions;
     std::unique_ptr<FdTrigger> mShutdownTrigger;
     RpcConditionVariable mShutdownCv;
diff --git a/libs/binder/include_rpc_unstable/binder_rpc_unstable.hpp b/libs/binder/include_rpc_unstable/binder_rpc_unstable.hpp
index f08bde8..3ec049e 100644
--- a/libs/binder/include_rpc_unstable/binder_rpc_unstable.hpp
+++ b/libs/binder/include_rpc_unstable/binder_rpc_unstable.hpp
@@ -25,9 +25,13 @@
 struct ARpcServer;
 
 // Starts an RPC server on a given port and a given root IBinder object.
+// The server will only accept connections from the given CID.
+// Set `cid` to VMADDR_CID_ANY to accept connections from any client.
+// Set `cid` to VMADDR_CID_LOCAL to only bind to the local vsock interface.
 // Returns an opaque handle to the running server instance, or null if the server
 // could not be started.
-[[nodiscard]] ARpcServer* ARpcServer_newVsock(AIBinder* service, unsigned int port);
+[[nodiscard]] ARpcServer* ARpcServer_newVsock(AIBinder* service, unsigned int cid,
+                                              unsigned int port);
 
 // Starts a Unix domain RPC server with a given init-managed Unix domain `name`
 // and a given root IBinder object.
diff --git a/libs/binder/libbinder_rpc_unstable.cpp b/libs/binder/libbinder_rpc_unstable.cpp
index f55c779..88f8c94 100644
--- a/libs/binder/libbinder_rpc_unstable.cpp
+++ b/libs/binder/libbinder_rpc_unstable.cpp
@@ -51,21 +51,26 @@
     ref->decStrong(ref);
 }
 
+static unsigned int cidFromStructAddr(const void* addr, size_t addrlen) {
+    LOG_ALWAYS_FATAL_IF(addrlen < sizeof(sockaddr_vm), "sockaddr is truncated");
+    const sockaddr_vm* vaddr = reinterpret_cast<const sockaddr_vm*>(addr);
+    LOG_ALWAYS_FATAL_IF(vaddr->svm_family != AF_VSOCK, "address is not a vsock");
+    return vaddr->svm_cid;
+}
+
 extern "C" {
 
 bool RunVsockRpcServerWithFactory(AIBinder* (*factory)(unsigned int cid, void* context),
                                   void* factoryContext, unsigned int port) {
     auto server = RpcServer::make();
-    if (status_t status = server->setupVsockServer(port); status != OK) {
+    if (status_t status = server->setupVsockServer(VMADDR_CID_ANY, port); status != OK) {
         LOG(ERROR) << "Failed to set up vsock server with port " << port
                    << " error: " << statusToString(status).c_str();
         return false;
     }
     server->setPerSessionRootObject([=](const void* addr, size_t addrlen) {
-        LOG_ALWAYS_FATAL_IF(addrlen < sizeof(sockaddr_vm), "sockaddr is truncated");
-        const sockaddr_vm* vaddr = reinterpret_cast<const sockaddr_vm*>(addr);
-        LOG_ALWAYS_FATAL_IF(vaddr->svm_family != AF_VSOCK, "address is not a vsock");
-        return AIBinder_toPlatformBinder(factory(vaddr->svm_cid, factoryContext));
+        unsigned int cid = cidFromStructAddr(addr, addrlen);
+        return AIBinder_toPlatformBinder(factory(cid, factoryContext));
     });
 
     server->join();
@@ -75,13 +80,30 @@
     return true;
 }
 
-ARpcServer* ARpcServer_newVsock(AIBinder* service, unsigned int port) {
+ARpcServer* ARpcServer_newVsock(AIBinder* service, unsigned int cid, unsigned int port) {
     auto server = RpcServer::make();
-    if (status_t status = server->setupVsockServer(port); status != OK) {
+
+    unsigned int bindCid = VMADDR_CID_ANY; // bind to the remote interface
+    if (cid == VMADDR_CID_LOCAL) {
+        bindCid = VMADDR_CID_LOCAL; // bind to the local interface
+        cid = VMADDR_CID_ANY;       // no need for a connection filter
+    }
+
+    if (status_t status = server->setupVsockServer(bindCid, port); status != OK) {
         LOG(ERROR) << "Failed to set up vsock server with port " << port
                    << " error: " << statusToString(status).c_str();
         return nullptr;
     }
+    if (cid != VMADDR_CID_ANY) {
+        server->setConnectionFilter([=](const void* addr, size_t addrlen) {
+            unsigned int remoteCid = cidFromStructAddr(addr, addrlen);
+            if (cid != remoteCid) {
+                LOG(ERROR) << "Rejected vsock connection from CID " << remoteCid;
+                return false;
+            }
+            return true;
+        });
+    }
     server->setRootObject(AIBinder_toPlatformBinder(service));
     return createRpcServerHandle(server);
 }
diff --git a/libs/binder/rust/rpcbinder/src/server.rs b/libs/binder/rust/rpcbinder/src/server.rs
index 42f5567..d5f1219 100644
--- a/libs/binder/rust/rpcbinder/src/server.rs
+++ b/libs/binder/rust/rpcbinder/src/server.rs
@@ -41,14 +41,19 @@
 
 impl RpcServer {
     /// Creates a binder RPC server, serving the supplied binder service implementation on the given
-    /// vsock port.
-    pub fn new_vsock(mut service: SpIBinder, port: u32) -> Result<RpcServer, Error> {
+    /// vsock port. Only connections from the given CID are accepted.
+    ///
+    // Set `cid` to libc::VMADDR_CID_ANY to accept connections from any client.
+    // Set `cid` to libc::VMADDR_CID_LOCAL to only bind to the local vsock interface.
+    pub fn new_vsock(mut service: SpIBinder, cid: u32, port: u32) -> Result<RpcServer, Error> {
         let service = service.as_native_mut();
 
         // SAFETY: Service ownership is transferring to the server and won't be valid afterward.
         // Plus the binder objects are threadsafe.
         unsafe {
-            Self::checked_from_ptr(binder_rpc_unstable_bindgen::ARpcServer_newVsock(service, port))
+            Self::checked_from_ptr(binder_rpc_unstable_bindgen::ARpcServer_newVsock(
+                service, cid, port,
+            ))
         }
     }
 
diff --git a/libs/binder/tests/binderRpcTest.cpp b/libs/binder/tests/binderRpcTest.cpp
index 02aa45f..739c217 100644
--- a/libs/binder/tests/binderRpcTest.cpp
+++ b/libs/binder/tests/binderRpcTest.cpp
@@ -1342,7 +1342,7 @@
                 } break;
                 case SocketType::VSOCK: {
                     auto port = allocateVsockPort();
-                    auto status = rpcServer->setupVsockServer(port);
+                    auto status = rpcServer->setupVsockServer(VMADDR_CID_LOCAL, port);
                     if (status != OK) {
                         return AssertionFailure() << "setupVsockServer: " << statusToString(status);
                     }
diff --git a/libs/binder/tests/binderRpcTestService.cpp b/libs/binder/tests/binderRpcTestService.cpp
index 995e761..cc9726b 100644
--- a/libs/binder/tests/binderRpcTestService.cpp
+++ b/libs/binder/tests/binderRpcTestService.cpp
@@ -58,7 +58,7 @@
             CHECK_EQ(OK, server->setupRawSocketServer(std::move(socketFd)));
             break;
         case SocketType::VSOCK:
-            CHECK_EQ(OK, server->setupVsockServer(serverConfig.vsockPort));
+            CHECK_EQ(OK, server->setupVsockServer(VMADDR_CID_LOCAL, serverConfig.vsockPort));
             break;
         case SocketType::INET: {
             CHECK_EQ(OK, server->setupInetServer(kLocalInetAddress, 0, &outPort));
diff --git a/libs/gui/include/gui/ISurfaceComposer.h b/libs/gui/include/gui/ISurfaceComposer.h
index d70a7f0..06a246e 100644
--- a/libs/gui/include/gui/ISurfaceComposer.h
+++ b/libs/gui/include/gui/ISurfaceComposer.h
@@ -103,7 +103,7 @@
         // (sf vsync offset - debug.sf.early_phase_offset_ns). SurfaceFlinger will continue to be
         // in the early configuration until it receives eEarlyWakeupEnd. These flags are
         // expected to be used by WindowManager only and are guarded by
-        // android.permission.ACCESS_SURFACE_FLINGER
+        // android.permission.WAKEUP_SURFACE_FLINGER
         eEarlyWakeupStart = 0x08,
         eEarlyWakeupEnd = 0x10,
         eOneWay = 0x20
diff --git a/libs/jpegrecoverymap/tests/data/raw_yuv420_image.yuv420 b/libs/jpegrecoverymap/tests/data/raw_yuv420_image.yuv420
new file mode 100644
index 0000000..c043da6
--- /dev/null
+++ b/libs/jpegrecoverymap/tests/data/raw_yuv420_image.yuv420
Binary files differ
diff --git a/libs/jpegrecoverymap/tests/recoverymap_test.cpp b/libs/jpegrecoverymap/tests/recoverymap_test.cpp
index 6dea27f..c3c6fd4 100644
--- a/libs/jpegrecoverymap/tests/recoverymap_test.cpp
+++ b/libs/jpegrecoverymap/tests/recoverymap_test.cpp
@@ -22,9 +22,11 @@
 #include <utils/Log.h>
 
 #define RAW_P010_IMAGE "/sdcard/Documents/raw_p010_image.p010"
-#define RAW_P010_IMAGE_WIDTH 1280
-#define RAW_P010_IMAGE_HEIGHT 720
+#define RAW_YUV420_IMAGE "/sdcard/Documents/raw_yuv420_image.yuv420"
 #define JPEG_IMAGE "/sdcard/Documents/jpeg_image.jpg"
+#define TEST_IMAGE_WIDTH 1280
+#define TEST_IMAGE_HEIGHT 720
+#define DEFAULT_JPEG_QUALITY 90
 
 #define SAVE_ENCODING_RESULT true
 #define SAVE_DECODING_RESULT true
@@ -40,6 +42,7 @@
   virtual void TearDown();
 
   struct jpegr_uncompressed_struct mRawP010Image;
+  struct jpegr_uncompressed_struct mRawYuv420Image;
   struct jpegr_compressed_struct mJpegImage;
 };
 
@@ -49,6 +52,7 @@
 void RecoveryMapTest::SetUp() {}
 void RecoveryMapTest::TearDown() {
   free(mRawP010Image.data);
+  free(mRawYuv420Image.data);
   free(mJpegImage.data);
 }
 
@@ -108,24 +112,88 @@
   ASSERT_EQ(metadata_expected.rangeScalingFactor, metadata_read.rangeScalingFactor);
 
 }
-TEST_F(RecoveryMapTest, encodeFromP010ThenDecode) {
+
+/* Test Encode API-0 and decode */
+// TODO: enable when tonemapper is ready.
+//TEST_F(RecoveryMapTest, encodeFromP010ThenDecode) {
+//  int ret;
+//
+//  // Load input files.
+//  if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) {
+//    FAIL() << "Load file " << RAW_P010_IMAGE << " failed";
+//  }
+//  mRawP010Image.width = TEST_IMAGE_WIDTH;
+//  mRawP010Image.height = TEST_IMAGE_HEIGHT;
+//  mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100;
+//
+//  RecoveryMap recoveryMap;
+//
+//  jpegr_compressed_struct jpegR;
+//  jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t);
+//  jpegR.data = malloc(jpegR.maxLength);
+//  ret = recoveryMap.encodeJPEGR(
+//      &mRawP010Image, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR, 90, nullptr);
+//  if (ret != OK) {
+//    FAIL() << "Error code is " << ret;
+//  }
+//  if (SAVE_ENCODING_RESULT) {
+//    // Output image data to file
+//    std::string filePath = "/sdcard/Documents/encoded_from_jpeg_input.jpgr";
+//    std::ofstream imageFile(filePath.c_str(), std::ofstream::binary);
+//    if (!imageFile.is_open()) {
+//      ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str());
+//    }
+//    imageFile.write((const char*)jpegR.data, jpegR.length);
+//  }
+//
+//  jpegr_uncompressed_struct decodedJpegR;
+//  int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4;
+//  decodedJpegR.data = malloc(decodedJpegRSize);
+//  ret = recoveryMap.decodeJPEGR(&jpegR, &decodedJpegR);
+//  if (ret != OK) {
+//    FAIL() << "Error code is " << ret;
+//  }
+//  if (SAVE_DECODING_RESULT) {
+//    // Output image data to file
+//    std::string filePath = "/sdcard/Documents/decoded_from_jpeg_input.rgb10";
+//    std::ofstream imageFile(filePath.c_str(), std::ofstream::binary);
+//    if (!imageFile.is_open()) {
+//      ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str());
+//    }
+//    imageFile.write((const char*)decodedJpegR.data, decodedJpegRSize);
+//  }
+//
+//  free(jpegR.data);
+//  free(decodedJpegR.data);
+//}
+
+/* Test Encode API-1 and decode */
+TEST_F(RecoveryMapTest, encodeFromRawHdrAndSdrThenDecode) {
   int ret;
 
   // Load input files.
   if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) {
     FAIL() << "Load file " << RAW_P010_IMAGE << " failed";
   }
-  mRawP010Image.width = RAW_P010_IMAGE_WIDTH;
-  mRawP010Image.height = RAW_P010_IMAGE_HEIGHT;
+  mRawP010Image.width = TEST_IMAGE_WIDTH;
+  mRawP010Image.height = TEST_IMAGE_HEIGHT;
   mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100;
 
+  if (!loadFile(RAW_YUV420_IMAGE, mRawYuv420Image.data, nullptr)) {
+    FAIL() << "Load file " << RAW_P010_IMAGE << " failed";
+  }
+  mRawYuv420Image.width = TEST_IMAGE_WIDTH;
+  mRawYuv420Image.height = TEST_IMAGE_HEIGHT;
+  mRawYuv420Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709;
+
   RecoveryMap recoveryMap;
 
   jpegr_compressed_struct jpegR;
-  jpegR.maxLength = RAW_P010_IMAGE_WIDTH * RAW_P010_IMAGE_HEIGHT * sizeof(uint8_t);
+  jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t);
   jpegR.data = malloc(jpegR.maxLength);
   ret = recoveryMap.encodeJPEGR(
-      &mRawP010Image, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR, 90, nullptr);
+      &mRawP010Image, &mRawYuv420Image, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR,
+      DEFAULT_JPEG_QUALITY, nullptr);
   if (ret != OK) {
     FAIL() << "Error code is " << ret;
   }
@@ -140,7 +208,7 @@
   }
 
   jpegr_uncompressed_struct decodedJpegR;
-  int decodedJpegRSize = RAW_P010_IMAGE_WIDTH * RAW_P010_IMAGE_HEIGHT * 4;
+  int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4;
   decodedJpegR.data = malloc(decodedJpegRSize);
   ret = recoveryMap.decodeJPEGR(&jpegR, &decodedJpegR);
   if (ret != OK) {
@@ -160,6 +228,72 @@
   free(decodedJpegR.data);
 }
 
+/* Test Encode API-2 and decode */
+TEST_F(RecoveryMapTest, encodeFromRawHdrAndSdrAndJpegThenDecode) {
+  int ret;
+
+  // Load input files.
+  if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) {
+    FAIL() << "Load file " << RAW_P010_IMAGE << " failed";
+  }
+  mRawP010Image.width = TEST_IMAGE_WIDTH;
+  mRawP010Image.height = TEST_IMAGE_HEIGHT;
+  mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100;
+
+  if (!loadFile(RAW_YUV420_IMAGE, mRawYuv420Image.data, nullptr)) {
+    FAIL() << "Load file " << RAW_P010_IMAGE << " failed";
+  }
+  mRawYuv420Image.width = TEST_IMAGE_WIDTH;
+  mRawYuv420Image.height = TEST_IMAGE_HEIGHT;
+  mRawYuv420Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709;
+
+  if (!loadFile(JPEG_IMAGE, mJpegImage.data, &mJpegImage.length)) {
+    FAIL() << "Load file " << JPEG_IMAGE << " failed";
+  }
+  mJpegImage.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT709;
+
+  RecoveryMap recoveryMap;
+
+  jpegr_compressed_struct jpegR;
+  jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t);
+  jpegR.data = malloc(jpegR.maxLength);
+  ret = recoveryMap.encodeJPEGR(
+      &mRawP010Image, &mRawYuv420Image, &mJpegImage, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR);
+  if (ret != OK) {
+    FAIL() << "Error code is " << ret;
+  }
+  if (SAVE_ENCODING_RESULT) {
+    // Output image data to file
+    std::string filePath = "/sdcard/Documents/encoded_from_jpeg_input.jpgr";
+    std::ofstream imageFile(filePath.c_str(), std::ofstream::binary);
+    if (!imageFile.is_open()) {
+      ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str());
+    }
+    imageFile.write((const char*)jpegR.data, jpegR.length);
+  }
+
+  jpegr_uncompressed_struct decodedJpegR;
+  int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4;
+  decodedJpegR.data = malloc(decodedJpegRSize);
+  ret = recoveryMap.decodeJPEGR(&jpegR, &decodedJpegR);
+  if (ret != OK) {
+    FAIL() << "Error code is " << ret;
+  }
+  if (SAVE_DECODING_RESULT) {
+    // Output image data to file
+    std::string filePath = "/sdcard/Documents/decoded_from_jpeg_input.rgb10";
+    std::ofstream imageFile(filePath.c_str(), std::ofstream::binary);
+    if (!imageFile.is_open()) {
+      ALOGE("%s: Unable to create file %s", __FUNCTION__, filePath.c_str());
+    }
+    imageFile.write((const char*)decodedJpegR.data, decodedJpegRSize);
+  }
+
+  free(jpegR.data);
+  free(decodedJpegR.data);
+}
+
+/* Test Encode API-3 and decode */
 TEST_F(RecoveryMapTest, encodeFromJpegThenDecode) {
   int ret;
 
@@ -167,8 +301,8 @@
   if (!loadFile(RAW_P010_IMAGE, mRawP010Image.data, nullptr)) {
     FAIL() << "Load file " << RAW_P010_IMAGE << " failed";
   }
-  mRawP010Image.width = RAW_P010_IMAGE_WIDTH;
-  mRawP010Image.height = RAW_P010_IMAGE_HEIGHT;
+  mRawP010Image.width = TEST_IMAGE_WIDTH;
+  mRawP010Image.height = TEST_IMAGE_HEIGHT;
   mRawP010Image.colorGamut = jpegr_color_gamut::JPEGR_COLORGAMUT_BT2100;
 
   if (!loadFile(JPEG_IMAGE, mJpegImage.data, &mJpegImage.length)) {
@@ -179,7 +313,7 @@
   RecoveryMap recoveryMap;
 
   jpegr_compressed_struct jpegR;
-  jpegR.maxLength = RAW_P010_IMAGE_WIDTH * RAW_P010_IMAGE_HEIGHT * sizeof(uint8_t);
+  jpegR.maxLength = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * sizeof(uint8_t);
   jpegR.data = malloc(jpegR.maxLength);
   ret = recoveryMap.encodeJPEGR(
       &mRawP010Image, &mJpegImage, jpegr_transfer_function::JPEGR_TF_HLG, &jpegR);
@@ -197,7 +331,7 @@
   }
 
   jpegr_uncompressed_struct decodedJpegR;
-  int decodedJpegRSize = RAW_P010_IMAGE_WIDTH * RAW_P010_IMAGE_HEIGHT * 4;
+  int decodedJpegRSize = TEST_IMAGE_WIDTH * TEST_IMAGE_HEIGHT * 4;
   decodedJpegR.data = malloc(decodedJpegRSize);
   ret = recoveryMap.decodeJPEGR(&jpegR, &decodedJpegR);
   if (ret != OK) {
diff --git a/libs/renderengine/Android.bp b/libs/renderengine/Android.bp
index 04e24ed..8d19c45 100644
--- a/libs/renderengine/Android.bp
+++ b/libs/renderengine/Android.bp
@@ -111,9 +111,23 @@
     ],
 }
 
+// Used to consolidate and simplify pulling Skia & Skia deps into targets that depend on
+// librenderengine. This allows shared deps to be deduplicated (e.g. Perfetto), which doesn't seem
+// possible if libskia_renderengine is just pulled into librenderengine via whole_static_libs.
+cc_defaults {
+    name: "librenderengine_deps",
+    defaults: ["skia_renderengine_deps"],
+    static_libs: ["libskia_renderengine"],
+}
+
+// Note: if compilation fails when adding librenderengine as a dependency, try adding
+// librenderengine_deps to the defaults field of your dependent target.
 cc_library_static {
     name: "librenderengine",
-    defaults: ["librenderengine_defaults"],
+    defaults: [
+        "librenderengine_defaults",
+        "librenderengine_deps",
+    ],
     double_loadable: true,
     cflags: [
         "-fvisibility=hidden",
@@ -132,7 +146,6 @@
     include_dirs: [
         "external/skia/src/gpu",
     ],
-    whole_static_libs: ["libskia_renderengine"],
     lto: {
         thin: true,
     },
diff --git a/libs/renderengine/benchmark/Android.bp b/libs/renderengine/benchmark/Android.bp
index afbe6cf..55c34cd 100644
--- a/libs/renderengine/benchmark/Android.bp
+++ b/libs/renderengine/benchmark/Android.bp
@@ -25,7 +25,7 @@
     name: "librenderengine_bench",
     defaults: [
         "android.hardware.graphics.composer3-ndk_shared",
-        "skia_deps",
+        "librenderengine_deps",
         "surfaceflinger_defaults",
     ],
     srcs: [
diff --git a/libs/renderengine/tests/Android.bp b/libs/renderengine/tests/Android.bp
index 6f328d7..50e166d 100644
--- a/libs/renderengine/tests/Android.bp
+++ b/libs/renderengine/tests/Android.bp
@@ -25,7 +25,7 @@
     name: "librenderengine_test",
     defaults: [
         "android.hardware.graphics.composer3-ndk_shared",
-        "skia_deps",
+        "librenderengine_deps",
         "surfaceflinger_defaults",
     ],
     test_suites: ["device-tests"],
diff --git a/services/inputflinger/include/InputReaderBase.h b/services/inputflinger/include/InputReaderBase.h
index b8a6dad..3e4db43 100644
--- a/services/inputflinger/include/InputReaderBase.h
+++ b/services/inputflinger/include/InputReaderBase.h
@@ -191,6 +191,9 @@
         // The set of disabled input devices (disabledDevices) has changed.
         CHANGE_ENABLED_STATE = 1 << 9,
 
+        // The device type has been updated.
+        CHANGE_DEVICE_TYPE = 1 << 10,
+
         // All devices must be reopened.
         CHANGE_MUST_REOPEN = 1 << 31,
     };
@@ -212,6 +215,10 @@
     // Used to determine which DisplayViewport should be tied to which InputDevice.
     std::unordered_map<std::string, std::string> uniqueIdAssociations;
 
+    // The associations between input device ports device types.
+    // This is used to determine which device type and source should be tied to which InputDevice.
+    std::unordered_map<std::string, std::string> deviceTypeAssociations;
+
     // The suggested display ID to show the cursor.
     int32_t defaultPointerDisplayId;
 
@@ -326,7 +333,6 @@
     std::optional<DisplayViewport> getDisplayViewportById(int32_t displayId) const;
     void setDisplayViewports(const std::vector<DisplayViewport>& viewports);
 
-
     void dump(std::string& dump) const;
     void dumpViewport(std::string& dump, const DisplayViewport& viewport) const;
 
diff --git a/services/inputflinger/reader/InputDevice.cpp b/services/inputflinger/reader/InputDevice.cpp
index 11b5209..6dfe5f5 100644
--- a/services/inputflinger/reader/InputDevice.cpp
+++ b/services/inputflinger/reader/InputDevice.cpp
@@ -56,6 +56,16 @@
 
 InputDevice::~InputDevice() {}
 
+template <typename K, typename V>
+std::optional<V> getValueByKey(const std::unordered_map<K, V>& map, K key) {
+    auto it = map.find(key);
+    std::optional<V> value = std::nullopt;
+    if (it != map.end()) {
+        value = it->second;
+    }
+    return value;
+}
+
 bool InputDevice::isEnabled() {
     if (!hasEventHubDevices()) {
         return false;
@@ -291,6 +301,9 @@
                 context.getConfiguration(&configuration);
                 mConfiguration.addAll(&configuration);
             });
+
+            mAssociatedDeviceType =
+                    getValueByKey(config->deviceTypeAssociations, mIdentifier.location);
         }
 
         if (!changes || (changes & InputReaderConfiguration::CHANGE_KEYBOARD_LAYOUTS)) {
diff --git a/services/inputflinger/reader/include/InputDevice.h b/services/inputflinger/reader/include/InputDevice.h
index 6fa21e5..af59fe2 100644
--- a/services/inputflinger/reader/include/InputDevice.h
+++ b/services/inputflinger/reader/include/InputDevice.h
@@ -65,6 +65,9 @@
     inline std::optional<std::string> getAssociatedDisplayUniqueId() const {
         return mAssociatedDisplayUniqueId;
     }
+    inline std::optional<std::string> getDeviceTypeAssociation() const {
+        return mAssociatedDeviceType;
+    }
     inline std::optional<DisplayViewport> getAssociatedViewport() const {
         return mAssociatedViewport;
     }
@@ -180,6 +183,7 @@
     bool mIsExternal;
     std::optional<uint8_t> mAssociatedDisplayPort;
     std::optional<std::string> mAssociatedDisplayUniqueId;
+    std::optional<std::string> mAssociatedDeviceType;
     std::optional<DisplayViewport> mAssociatedViewport;
     bool mHasMic;
     bool mDropUntilNextSync;
@@ -408,6 +412,9 @@
     inline std::optional<std::string> getAssociatedDisplayUniqueId() const {
         return mDevice.getAssociatedDisplayUniqueId();
     }
+    inline std::optional<std::string> getDeviceTypeAssociation() const {
+        return mDevice.getDeviceTypeAssociation();
+    }
     inline std::optional<DisplayViewport> getAssociatedViewport() const {
         return mDevice.getAssociatedViewport();
     }
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.cpp b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
index cefc44e..160f9eb 100644
--- a/services/inputflinger/reader/mapper/TouchInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
@@ -392,33 +392,10 @@
         }
     }
 
-    if (getDeviceContext().hasInputProperty(INPUT_PROP_DIRECT)) {
-        // The device is a touch screen.
-        mParameters.deviceType = Parameters::DeviceType::TOUCH_SCREEN;
-    } else if (getDeviceContext().hasInputProperty(INPUT_PROP_POINTER)) {
-        // The device is a pointing device like a track pad.
-        mParameters.deviceType = Parameters::DeviceType::POINTER;
-    } else {
-        // The device is a touch pad of unknown purpose.
-        mParameters.deviceType = Parameters::DeviceType::POINTER;
-    }
+    configureDeviceType();
 
     mParameters.hasButtonUnderPad = getDeviceContext().hasInputProperty(INPUT_PROP_BUTTONPAD);
 
-    std::string deviceTypeString;
-    if (getDeviceContext().getConfiguration().tryGetProperty("touch.deviceType",
-                                                             deviceTypeString)) {
-        if (deviceTypeString == "touchScreen") {
-            mParameters.deviceType = Parameters::DeviceType::TOUCH_SCREEN;
-        } else if (deviceTypeString == "touchNavigation") {
-            mParameters.deviceType = Parameters::DeviceType::TOUCH_NAVIGATION;
-        } else if (deviceTypeString == "pointer") {
-            mParameters.deviceType = Parameters::DeviceType::POINTER;
-        } else if (deviceTypeString != "default") {
-            ALOGW("Invalid value for touch.deviceType: '%s'", deviceTypeString.c_str());
-        }
-    }
-
     mParameters.orientationAware = mParameters.deviceType == Parameters::DeviceType::TOUCH_SCREEN;
     getDeviceContext().getConfiguration().tryGetProperty("touch.orientationAware",
                                                          mParameters.orientationAware);
@@ -444,7 +421,9 @@
     mParameters.associatedDisplayIsExternal = false;
     if (mParameters.orientationAware ||
         mParameters.deviceType == Parameters::DeviceType::TOUCH_SCREEN ||
-        mParameters.deviceType == Parameters::DeviceType::POINTER) {
+        mParameters.deviceType == Parameters::DeviceType::POINTER ||
+        (mParameters.deviceType == Parameters::DeviceType::TOUCH_NAVIGATION &&
+         getDeviceContext().getAssociatedViewport())) {
         mParameters.hasAssociatedDisplay = true;
         if (mParameters.deviceType == Parameters::DeviceType::TOUCH_SCREEN) {
             mParameters.associatedDisplayIsExternal = getDeviceContext().isExternal();
@@ -473,6 +452,34 @@
                                                          mParameters.enableForInactiveViewport);
 }
 
+void TouchInputMapper::configureDeviceType() {
+    if (getDeviceContext().hasInputProperty(INPUT_PROP_DIRECT)) {
+        // The device is a touch screen.
+        mParameters.deviceType = Parameters::DeviceType::TOUCH_SCREEN;
+    } else if (getDeviceContext().hasInputProperty(INPUT_PROP_POINTER)) {
+        // The device is a pointing device like a track pad.
+        mParameters.deviceType = Parameters::DeviceType::POINTER;
+    } else {
+        // The device is a touch pad of unknown purpose.
+        mParameters.deviceType = Parameters::DeviceType::POINTER;
+    }
+
+    // Type association takes precedence over the device type found in the idc file.
+    std::string deviceTypeString = getDeviceContext().getDeviceTypeAssociation().value_or("");
+    if (deviceTypeString.empty()) {
+        getDeviceContext().getConfiguration().tryGetProperty("touch.deviceType", deviceTypeString);
+    }
+    if (deviceTypeString == "touchScreen") {
+        mParameters.deviceType = Parameters::DeviceType::TOUCH_SCREEN;
+    } else if (deviceTypeString == "touchNavigation") {
+        mParameters.deviceType = Parameters::DeviceType::TOUCH_NAVIGATION;
+    } else if (deviceTypeString == "pointer") {
+        mParameters.deviceType = Parameters::DeviceType::POINTER;
+    } else if (deviceTypeString != "default" && deviceTypeString != "") {
+        ALOGW("Invalid value for touch.deviceType: '%s'", deviceTypeString.c_str());
+    }
+}
+
 void TouchInputMapper::dumpParameters(std::string& dump) {
     dump += INDENT3 "Parameters:\n";
 
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.h b/services/inputflinger/reader/mapper/TouchInputMapper.h
index 34ba625..50a7ea3 100644
--- a/services/inputflinger/reader/mapper/TouchInputMapper.h
+++ b/services/inputflinger/reader/mapper/TouchInputMapper.h
@@ -814,6 +814,8 @@
     static void assignPointerIds(const RawState& last, RawState& current);
 
     void rotateAndScale(float& x, float& y) const;
+
+    void configureDeviceType();
 };
 
 } // namespace android
diff --git a/services/inputflinger/tests/FakeInputReaderPolicy.cpp b/services/inputflinger/tests/FakeInputReaderPolicy.cpp
index 3af4298..dc7e581 100644
--- a/services/inputflinger/tests/FakeInputReaderPolicy.cpp
+++ b/services/inputflinger/tests/FakeInputReaderPolicy.cpp
@@ -126,6 +126,11 @@
     mConfig.portAssociations.insert({inputPort, displayPort});
 }
 
+void FakeInputReaderPolicy::addDeviceTypeAssociation(const std::string& inputPort,
+                                                     const std::string& type) {
+    mConfig.deviceTypeAssociations.insert({inputPort, type});
+}
+
 void FakeInputReaderPolicy::addInputUniqueIdAssociation(const std::string& inputUniqueId,
                                                         const std::string& displayUniqueId) {
     mConfig.uniqueIdAssociations.insert({inputUniqueId, displayUniqueId});
diff --git a/services/inputflinger/tests/FakeInputReaderPolicy.h b/services/inputflinger/tests/FakeInputReaderPolicy.h
index c16cda4..faa9c01 100644
--- a/services/inputflinger/tests/FakeInputReaderPolicy.h
+++ b/services/inputflinger/tests/FakeInputReaderPolicy.h
@@ -55,6 +55,7 @@
     bool updateViewport(const DisplayViewport& viewport);
     void addExcludedDeviceName(const std::string& deviceName);
     void addInputPortAssociation(const std::string& inputPort, uint8_t displayPort);
+    void addDeviceTypeAssociation(const std::string& inputPort, const std::string& type);
     void addInputUniqueIdAssociation(const std::string& inputUniqueId,
                                      const std::string& displayUniqueId);
     void addDisabledDevice(int32_t deviceId);
diff --git a/services/inputflinger/tests/InputDispatcher_test.cpp b/services/inputflinger/tests/InputDispatcher_test.cpp
index 41c174a..d2ff097 100644
--- a/services/inputflinger/tests/InputDispatcher_test.cpp
+++ b/services/inputflinger/tests/InputDispatcher_test.cpp
@@ -29,6 +29,7 @@
 #include <sys/epoll.h>
 
 #include <cinttypes>
+#include <compare>
 #include <thread>
 #include <unordered_set>
 #include <vector>
@@ -56,6 +57,7 @@
 static constexpr int32_t DISPLAY_ID = ADISPLAY_ID_DEFAULT;
 static constexpr int32_t SECOND_DISPLAY_ID = 1;
 
+static constexpr int32_t ACTION_OUTSIDE = AMOTION_EVENT_ACTION_OUTSIDE;
 static constexpr int32_t POINTER_1_DOWN =
         AMOTION_EVENT_ACTION_POINTER_DOWN | (1 << AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT);
 static constexpr int32_t POINTER_2_DOWN =
@@ -84,6 +86,7 @@
 struct PointF {
     float x;
     float y;
+    auto operator<=>(const PointF&) const = default;
 };
 
 /**
@@ -137,6 +140,24 @@
     return arg.getSource() == source;
 }
 
+MATCHER_P2(WithCoords, x, y, "MotionEvent with specified coordinates") {
+    if (arg.getPointerCount() != 1) {
+        *result_listener << "Expected 1 pointer, got " << arg.getPointerCount();
+        return false;
+    }
+    return arg.getX(0 /*pointerIndex*/) == x && arg.getY(0 /*pointerIndex*/) == y;
+}
+
+MATCHER_P(WithPointers, pointers, "MotionEvent with specified pointers") {
+    // Build a map for the received pointers, by pointer id
+    std::map<int32_t /*pointerId*/, PointF> actualPointers;
+    for (size_t pointerIndex = 0; pointerIndex < arg.getPointerCount(); pointerIndex++) {
+        const int32_t pointerId = arg.getPointerId(pointerIndex);
+        actualPointers[pointerId] = {arg.getX(pointerIndex), arg.getY(pointerIndex)};
+    }
+    return pointers == actualPointers;
+}
+
 // --- FakeInputDispatcherPolicy ---
 
 class FakeInputDispatcherPolicy : public InputDispatcherPolicyInterface {
@@ -2536,6 +2557,39 @@
 }
 
 /**
+ * Two windows. First is a regular window. Second does not overlap with the first, and has
+ * WATCH_OUTSIDE_TOUCH.
+ * Both windows are owned by the same UID.
+ * Tap first window. Make sure that the second window receives ACTION_OUTSIDE with correct, non-zero
+ * coordinates. The coordinates are not zeroed out because both windows are owned by the same UID.
+ */
+TEST_F(InputDispatcherTest, ActionOutsideForOwnedWindowHasValidCoordinates) {
+    std::shared_ptr<FakeApplicationHandle> application = std::make_shared<FakeApplicationHandle>();
+    sp<FakeWindowHandle> window = sp<FakeWindowHandle>::make(application, mDispatcher,
+                                                             "First Window", ADISPLAY_ID_DEFAULT);
+    window->setFrame(Rect{0, 0, 100, 100});
+
+    sp<FakeWindowHandle> outsideWindow =
+            sp<FakeWindowHandle>::make(application, mDispatcher, "Second Window",
+                                       ADISPLAY_ID_DEFAULT);
+    outsideWindow->setFrame(Rect{100, 100, 200, 200});
+    outsideWindow->setWatchOutsideTouch(true);
+    // outsideWindow must be above 'window' to receive ACTION_OUTSIDE events when 'window' is tapped
+    mDispatcher->setInputWindows({{ADISPLAY_ID_DEFAULT, {outsideWindow, window}}});
+
+    // Tap on first window.
+    NotifyMotionArgs motionArgs =
+            generateMotionArgs(AMOTION_EVENT_ACTION_DOWN, AINPUT_SOURCE_TOUCHSCREEN,
+                               ADISPLAY_ID_DEFAULT, {PointF{50, 50}});
+    mDispatcher->notifyMotion(&motionArgs);
+    window->consumeMotionDown();
+    // The coordinates of the tap in 'outsideWindow' are relative to its top left corner.
+    // Therefore, we should offset them by (100, 100) relative to the screen's top left corner.
+    outsideWindow->consumeMotionEvent(
+            AllOf(WithMotionAction(ACTION_OUTSIDE), WithCoords(-50, -50)));
+}
+
+/**
  * This test documents the behavior of WATCH_OUTSIDE_TOUCH. The window will get ACTION_OUTSIDE when
  * a another pointer causes ACTION_DOWN to be sent to another window for the first time. Only one
  * ACTION_OUTSIDE event is sent per gesture.
@@ -2570,7 +2624,9 @@
     motionArgs = generateMotionArgs(POINTER_1_DOWN, AINPUT_SOURCE_TOUCHSCREEN, ADISPLAY_ID_DEFAULT,
                                     {PointF{-10, -10}, PointF{105, 105}});
     mDispatcher->notifyMotion(&motionArgs);
-    window->consumeMotionOutside();
+    const std::map<int32_t, PointF> expectedPointers{{0, PointF{-10, -10}}, {1, PointF{105, 105}}};
+    window->consumeMotionEvent(
+            AllOf(WithMotionAction(ACTION_OUTSIDE), WithPointers(expectedPointers)));
     secondWindow->consumeMotionDown();
     thirdWindow->assertNoEvents();
 
diff --git a/services/inputflinger/tests/InputReader_test.cpp b/services/inputflinger/tests/InputReader_test.cpp
index 78b70aa..db6bf45 100644
--- a/services/inputflinger/tests/InputReader_test.cpp
+++ b/services/inputflinger/tests/InputReader_test.cpp
@@ -6505,6 +6505,17 @@
                   WithCoords(toDisplayX(100), toDisplayY(200)), WithButtonState(0))));
 }
 
+TEST_F(SingleTouchInputMapperTest, WhenDeviceTypeIsSetToTouchNavigation_setsCorrectType) {
+    mFakePolicy->addDeviceTypeAssociation(DEVICE_LOCATION, "touchNavigation");
+    prepareDisplay(ui::ROTATION_0);
+    prepareButtons();
+    prepareAxes(POSITION);
+    SingleTouchInputMapper& mapper = addMapperAndConfigure<SingleTouchInputMapper>();
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyDeviceResetWasCalled());
+
+    ASSERT_EQ(AINPUT_SOURCE_TOUCH_NAVIGATION, mapper.getSources());
+}
+
 // --- TouchDisplayProjectionTest ---
 
 class TouchDisplayProjectionTest : public SingleTouchInputMapperTest {
diff --git a/services/surfaceflinger/Android.bp b/services/surfaceflinger/Android.bp
index 0a192c5..e0dcab5 100644
--- a/services/surfaceflinger/Android.bp
+++ b/services/surfaceflinger/Android.bp
@@ -26,8 +26,8 @@
     name: "libsurfaceflinger_defaults",
     defaults: [
         "android.hardware.graphics.composer3-ndk_shared",
+        "librenderengine_deps",
         "surfaceflinger_defaults",
-        "skia_renderengine_deps",
     ],
     cflags: [
         "-DLOG_TAG=\"SurfaceFlinger\"",
diff --git a/services/surfaceflinger/CompositionEngine/Android.bp b/services/surfaceflinger/CompositionEngine/Android.bp
index 30d34a5..f3a0186 100644
--- a/services/surfaceflinger/CompositionEngine/Android.bp
+++ b/services/surfaceflinger/CompositionEngine/Android.bp
@@ -11,6 +11,7 @@
     name: "libcompositionengine_defaults",
     defaults: [
         "android.hardware.graphics.composer3-ndk_shared",
+        "librenderengine_deps",
         "surfaceflinger_defaults",
     ],
     cflags: [
diff --git a/services/surfaceflinger/DisplayDevice.cpp b/services/surfaceflinger/DisplayDevice.cpp
index 46b857b..96ae77f 100644
--- a/services/surfaceflinger/DisplayDevice.cpp
+++ b/services/surfaceflinger/DisplayDevice.cpp
@@ -452,7 +452,8 @@
     }
 }
 
-auto DisplayDevice::setDesiredActiveMode(const ActiveModeInfo& info) -> DesiredActiveModeAction {
+auto DisplayDevice::setDesiredActiveMode(const ActiveModeInfo& info, bool force)
+        -> DesiredActiveModeAction {
     ATRACE_CALL();
 
     LOG_ALWAYS_FATAL_IF(!info.modeOpt, "desired mode not provided");
@@ -473,7 +474,7 @@
     const auto& desiredMode = *info.modeOpt->modePtr;
 
     // Check if we are already at the desired mode
-    if (refreshRateSelector().getActiveMode().modePtr->getId() == desiredMode.getId()) {
+    if (!force && refreshRateSelector().getActiveMode().modePtr->getId() == desiredMode.getId()) {
         if (refreshRateSelector().getActiveMode() == info.modeOpt) {
             return DesiredActiveModeAction::None;
         }
diff --git a/services/surfaceflinger/DisplayDevice.h b/services/surfaceflinger/DisplayDevice.h
index 8f9b2a1..d757673 100644
--- a/services/surfaceflinger/DisplayDevice.h
+++ b/services/surfaceflinger/DisplayDevice.h
@@ -210,7 +210,8 @@
         InitiateDisplayModeSwitch,
         InitiateRenderRateSwitch
     };
-    DesiredActiveModeAction setDesiredActiveMode(const ActiveModeInfo&) EXCLUDES(mActiveModeLock);
+    DesiredActiveModeAction setDesiredActiveMode(const ActiveModeInfo&, bool force = false)
+            EXCLUDES(mActiveModeLock);
     std::optional<ActiveModeInfo> getDesiredActiveMode() const EXCLUDES(mActiveModeLock);
     void clearDesiredActiveModeState() EXCLUDES(mActiveModeLock);
     ActiveModeInfo getUpcomingActiveMode() const REQUIRES(kMainThreadContext) {
diff --git a/services/surfaceflinger/Scheduler/Scheduler.cpp b/services/surfaceflinger/Scheduler/Scheduler.cpp
index 34f8df2..856fda0 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.cpp
+++ b/services/surfaceflinger/Scheduler/Scheduler.cpp
@@ -815,18 +815,18 @@
             .powerOnImminent = powerOnImminent};
 }
 
-ftl::Optional<FrameRateMode> Scheduler::getPreferredDisplayMode() {
+FrameRateMode Scheduler::getPreferredDisplayMode() {
     std::lock_guard<std::mutex> lock(mPolicyLock);
-    // Make sure the stored mode is up to date.
-    if (mPolicy.modeOpt) {
-        const auto ranking =
-                leaderSelectorPtr()
-                        ->getRankedFrameRates(mPolicy.contentRequirements, makeGlobalSignals())
-                        .ranking;
+    const auto frameRateMode =
+            leaderSelectorPtr()
+                    ->getRankedFrameRates(mPolicy.contentRequirements, makeGlobalSignals())
+                    .ranking.front()
+                    .frameRateMode;
 
-        mPolicy.modeOpt = ranking.front().frameRateMode;
-    }
-    return mPolicy.modeOpt;
+    // Make sure the stored mode is up to date.
+    mPolicy.modeOpt = frameRateMode;
+
+    return frameRateMode;
 }
 
 void Scheduler::onNewVsyncPeriodChangeTimeline(const hal::VsyncPeriodChangeTimeline& timeline) {
diff --git a/services/surfaceflinger/Scheduler/Scheduler.h b/services/surfaceflinger/Scheduler/Scheduler.h
index cf2ffb8..f189426 100644
--- a/services/surfaceflinger/Scheduler/Scheduler.h
+++ b/services/surfaceflinger/Scheduler/Scheduler.h
@@ -209,8 +209,8 @@
     void dump(ConnectionHandle, std::string&) const;
     void dumpVsync(std::string&) const;
 
-    // Get the appropriate refresh for current conditions.
-    ftl::Optional<FrameRateMode> getPreferredDisplayMode();
+    // Returns the preferred refresh rate and frame rate for the leader display.
+    FrameRateMode getPreferredDisplayMode();
 
     // Notifies the scheduler about a refresh rate timeline change.
     void onNewVsyncPeriodChangeTimeline(const hal::VsyncPeriodChangeTimeline& timeline);
diff --git a/services/surfaceflinger/SurfaceFlinger.cpp b/services/surfaceflinger/SurfaceFlinger.cpp
index 60000ff..b930477 100644
--- a/services/surfaceflinger/SurfaceFlinger.cpp
+++ b/services/surfaceflinger/SurfaceFlinger.cpp
@@ -299,6 +299,7 @@
 const String16 sDump("android.permission.DUMP");
 const String16 sCaptureBlackoutContent("android.permission.CAPTURE_BLACKOUT_CONTENT");
 const String16 sInternalSystemWindow("android.permission.INTERNAL_SYSTEM_WINDOW");
+const String16 sWakeupSurfaceFlinger("android.permission.WAKEUP_SURFACE_FLINGER");
 
 const char* KERNEL_IDLE_TIMER_PROP = "graphics.display.kernel_idle_timer.enabled";
 
@@ -332,20 +333,12 @@
     }
 }
 
-bool callingThreadHasRotateSurfaceFlingerAccess() {
+bool callingThreadHasPermission(const String16& permission) {
     IPCThreadState* ipc = IPCThreadState::self();
     const int pid = ipc->getCallingPid();
     const int uid = ipc->getCallingUid();
     return uid == AID_GRAPHICS || uid == AID_SYSTEM ||
-            PermissionCache::checkPermission(sRotateSurfaceFlinger, pid, uid);
-}
-
-bool callingThreadHasInternalSystemWindowAccess() {
-    IPCThreadState* ipc = IPCThreadState::self();
-    const int pid = ipc->getCallingPid();
-    const int uid = ipc->getCallingUid();
-    return uid == AID_GRAPHICS || uid == AID_SYSTEM ||
-        PermissionCache::checkPermission(sInternalSystemWindow, pid, uid);
+            PermissionCache::checkPermission(permission, pid, uid);
 }
 
 SurfaceFlinger::SurfaceFlinger(Factory& factory, SkipInitializationTag)
@@ -1139,7 +1132,7 @@
     return NO_ERROR;
 }
 
-void SurfaceFlinger::setDesiredActiveMode(display::DisplayModeRequest&& request) {
+void SurfaceFlinger::setDesiredActiveMode(display::DisplayModeRequest&& request, bool force) {
     ATRACE_CALL();
 
     auto display = getDisplayDeviceLocked(request.mode.modePtr->getPhysicalDisplayId());
@@ -1151,7 +1144,8 @@
     const auto mode = request.mode;
     const bool emitEvent = request.emitEvent;
 
-    switch (display->setDesiredActiveMode(DisplayDevice::ActiveModeInfo(std::move(request)))) {
+    switch (display->setDesiredActiveMode(DisplayDevice::ActiveModeInfo(std::move(request)),
+                                          force)) {
         case DisplayDevice::DesiredActiveModeAction::InitiateDisplayModeSwitch:
             scheduleComposite(FrameHint::kNone);
 
@@ -3464,10 +3458,21 @@
 
     for (auto& request : modeRequests) {
         const auto& modePtr = request.mode.modePtr;
-        const auto display = getDisplayDeviceLocked(modePtr->getPhysicalDisplayId());
+
+        const auto displayId = modePtr->getPhysicalDisplayId();
+        const auto display = getDisplayDeviceLocked(displayId);
 
         if (!display) continue;
 
+        const bool isInternalDisplay = mPhysicalDisplays.get(displayId)
+                                               .transform(&PhysicalDisplay::isInternal)
+                                               .value_or(false);
+
+        if (isInternalDisplay && displayId != mActiveDisplayId) {
+            ALOGV("%s(%s): Inactive display", __func__, to_string(displayId).c_str());
+            continue;
+        }
+
         if (display->refreshRateSelector().isModeAllowed(request.mode)) {
             setDesiredActiveMode(std::move(request));
         } else {
@@ -3989,18 +3994,23 @@
     // Avoid checking for rotation permissions if the caller already has ACCESS_SURFACE_FLINGER
     // permissions.
     if ((permissions & layer_state_t::Permission::ACCESS_SURFACE_FLINGER) ||
-        callingThreadHasRotateSurfaceFlingerAccess()) {
+        callingThreadHasPermission(sRotateSurfaceFlinger)) {
         permissions |= layer_state_t::Permission::ROTATE_SURFACE_FLINGER;
     }
 
-    if (callingThreadHasInternalSystemWindowAccess()) {
+    if (callingThreadHasPermission(sInternalSystemWindow)) {
         permissions |= layer_state_t::Permission::INTERNAL_SYSTEM_WINDOW;
     }
 
-    if (!(permissions & layer_state_t::Permission::ACCESS_SURFACE_FLINGER) &&
-        (flags & (eEarlyWakeupStart | eEarlyWakeupEnd))) {
-        ALOGE("Only WindowManager is allowed to use eEarlyWakeup[Start|End] flags");
-        flags &= ~(eEarlyWakeupStart | eEarlyWakeupEnd);
+    if (flags & (eEarlyWakeupStart | eEarlyWakeupEnd)) {
+        const bool hasPermission =
+                (permissions & layer_state_t::Permission::ACCESS_SURFACE_FLINGER) ||
+                callingThreadHasPermission(sWakeupSurfaceFlinger);
+        if (!hasPermission) {
+            ALOGE("Caller needs permission android.permission.WAKEUP_SURFACE_FLINGER to use "
+                  "eEarlyWakeup[Start|End] flags");
+            flags &= ~(eEarlyWakeupStart | eEarlyWakeupEnd);
+        }
     }
 
     const int64_t postTime = systemTime();
@@ -6727,7 +6737,7 @@
 ftl::Optional<scheduler::FrameRateMode> SurfaceFlinger::getPreferredDisplayMode(
         PhysicalDisplayId displayId, DisplayModeId defaultModeId) const {
     if (const auto schedulerMode = mScheduler->getPreferredDisplayMode();
-        schedulerMode && schedulerMode->modePtr->getPhysicalDisplayId() == displayId) {
+        schedulerMode.modePtr->getPhysicalDisplayId() == displayId) {
         return schedulerMode;
     }
 
@@ -6762,12 +6772,24 @@
         case SetPolicyResult::Unchanged:
             return NO_ERROR;
         case SetPolicyResult::Changed:
-            return applyRefreshRateSelectorPolicy(displayId, selector);
+            break;
     }
+
+    const bool isInternalDisplay = mPhysicalDisplays.get(displayId)
+                                           .transform(&PhysicalDisplay::isInternal)
+                                           .value_or(false);
+
+    if (isInternalDisplay && displayId != mActiveDisplayId) {
+        // The policy will be be applied when the display becomes active.
+        ALOGV("%s(%s): Inactive display", __func__, to_string(displayId).c_str());
+        return NO_ERROR;
+    }
+
+    return applyRefreshRateSelectorPolicy(displayId, selector);
 }
 
 status_t SurfaceFlinger::applyRefreshRateSelectorPolicy(
-        PhysicalDisplayId displayId, const scheduler::RefreshRateSelector& selector) {
+        PhysicalDisplayId displayId, const scheduler::RefreshRateSelector& selector, bool force) {
     const scheduler::RefreshRateSelector::Policy currentPolicy = selector.getCurrentPolicy();
     ALOGV("Setting desired display mode specs: %s", currentPolicy.toString().c_str());
 
@@ -6797,7 +6819,7 @@
         return INVALID_OPERATION;
     }
 
-    setDesiredActiveMode({std::move(preferredMode), .emitEvent = true});
+    setDesiredActiveMode({std::move(preferredMode), .emitEvent = true}, force);
     return NO_ERROR;
 }
 
@@ -7101,7 +7123,8 @@
     // case, its preferred mode has not been propagated to HWC (via setDesiredActiveMode). In either
     // case, the Scheduler's cachedModeChangedParams must be initialized to the newly active mode,
     // and the kernel idle timer of the newly active display must be toggled.
-    applyRefreshRateSelectorPolicy(mActiveDisplayId, activeDisplay->refreshRateSelector());
+    constexpr bool kForce = true;
+    applyRefreshRateSelectorPolicy(mActiveDisplayId, activeDisplay->refreshRateSelector(), kForce);
 }
 
 status_t SurfaceFlinger::addWindowInfosListener(
diff --git a/services/surfaceflinger/SurfaceFlinger.h b/services/surfaceflinger/SurfaceFlinger.h
index fa3a49f..7bb0514 100644
--- a/services/surfaceflinger/SurfaceFlinger.h
+++ b/services/surfaceflinger/SurfaceFlinger.h
@@ -649,7 +649,8 @@
     // Show render rate with refresh rate overlay
     bool mRefreshRateOverlayRenderRate = false;
 
-    void setDesiredActiveMode(display::DisplayModeRequest&&) REQUIRES(mStateLock);
+    void setDesiredActiveMode(display::DisplayModeRequest&&, bool force = false)
+            REQUIRES(mStateLock);
 
     status_t setActiveModeFromBackdoor(const sp<display::DisplayToken>&, DisplayModeId);
     // Sets the active mode and a new refresh rate in SF.
@@ -678,7 +679,8 @@
 
     // TODO(b/241285191): Look up RefreshRateSelector on Scheduler to remove redundant parameter.
     status_t applyRefreshRateSelectorPolicy(PhysicalDisplayId,
-                                            const scheduler::RefreshRateSelector&)
+                                            const scheduler::RefreshRateSelector&,
+                                            bool force = false)
             REQUIRES(mStateLock, kMainThreadContext);
 
     void commitTransactions() EXCLUDES(mStateLock) REQUIRES(kMainThreadContext);
@@ -1187,7 +1189,9 @@
     display::PhysicalDisplays mPhysicalDisplays GUARDED_BY(mStateLock);
 
     // The inner or outer display for foldables, assuming they have mutually exclusive power states.
-    PhysicalDisplayId mActiveDisplayId GUARDED_BY(mStateLock);
+    // Atomic because writes from onActiveDisplayChangedLocked are not always under mStateLock, but
+    // reads from ISchedulerCallback::requestDisplayModes may happen concurrently.
+    std::atomic<PhysicalDisplayId> mActiveDisplayId GUARDED_BY(mStateLock);
 
     struct {
         DisplayIdGenerator<GpuVirtualDisplayId> gpu;
diff --git a/services/surfaceflinger/Tracing/tools/Android.bp b/services/surfaceflinger/Tracing/tools/Android.bp
index e8fe734..b6435a8 100644
--- a/services/surfaceflinger/Tracing/tools/Android.bp
+++ b/services/surfaceflinger/Tracing/tools/Android.bp
@@ -25,8 +25,8 @@
     name: "layertracegenerator",
     defaults: [
         "libsurfaceflinger_mocks_defaults",
+        "librenderengine_deps",
         "surfaceflinger_defaults",
-        "skia_renderengine_deps",
     ],
     srcs: [
         ":libsurfaceflinger_sources",
diff --git a/services/surfaceflinger/tests/unittests/Android.bp b/services/surfaceflinger/tests/unittests/Android.bp
index bf22521..6d1b3fe 100644
--- a/services/surfaceflinger/tests/unittests/Android.bp
+++ b/services/surfaceflinger/tests/unittests/Android.bp
@@ -140,6 +140,7 @@
     defaults: [
         "android.hardware.graphics.common-ndk_static",
         "android.hardware.graphics.composer3-ndk_static",
+        "librenderengine_deps",
     ],
     static_libs: [
         "android.hardware.common-V2-ndk",
diff --git a/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp b/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp
index 074bf8c..ad3bd35 100644
--- a/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp
+++ b/services/surfaceflinger/tests/unittests/SurfaceFlinger_DisplayModeSwitching.cpp
@@ -14,12 +14,12 @@
  * limitations under the License.
  */
 
-#include "mock/MockDisplayModeSpecs.h"
-#include "mock/MockEventThread.h"
 #undef LOG_TAG
 #define LOG_TAG "LibSurfaceFlingerUnittests"
 
 #include "DisplayTransactionTestHelpers.h"
+#include "mock/DisplayHardware/MockDisplayMode.h"
+#include "mock/MockDisplayModeSpecs.h"
 
 #include <ftl/fake_guard.h>
 #include <scheduler/Fps.h>
@@ -42,8 +42,7 @@
         PrimaryDisplayVariant::setupNativeWindowSurfaceCreationCallExpectations(this);
         PrimaryDisplayVariant::setupHwcGetActiveConfigCallExpectations(this);
 
-        DisplayModes modes = makeModes(kMode60, kMode90, kMode120, kMode90_4K);
-        auto selectorPtr = std::make_shared<scheduler::RefreshRateSelector>(modes, kModeId60);
+        auto selectorPtr = std::make_shared<scheduler::RefreshRateSelector>(kModes, kModeId60);
 
         setupScheduler(selectorPtr);
 
@@ -51,7 +50,7 @@
         mFlinger.configureAndCommit();
 
         mDisplay = PrimaryDisplayVariant::makeFakeExistingDisplayInjector(this)
-                           .setDisplayModes(std::move(modes), kModeId60, std::move(selectorPtr))
+                           .setDisplayModes(kModes, kModeId60, std::move(selectorPtr))
                            .inject();
 
         // isVsyncPeriodSwitchSupported should return true, otherwise the SF's HWC proxy
@@ -78,6 +77,8 @@
     static constexpr ui::Size kResolution4K{3840, 2160};
     static inline const DisplayModePtr kMode90_4K =
             createDisplayMode(kModeId90_4K, 90_Hz, 3, kResolution4K);
+
+    static inline const DisplayModes kModes = makeModes(kMode60, kMode90, kMode120, kMode90_4K);
 };
 
 void DisplayModeSwitchingTest::setupScheduler(
@@ -283,5 +284,114 @@
     ASSERT_EQ(mDisplay->getActiveMode().modePtr->getId(), kModeId90_4K);
 }
 
+TEST_F(DisplayModeSwitchingTest, multiDisplay) {
+    ftl::FakeGuard guard(kMainThreadContext);
+
+    constexpr HWDisplayId kInnerDisplayHwcId = PrimaryDisplayVariant::HWC_DISPLAY_ID;
+    constexpr HWDisplayId kOuterDisplayHwcId = kInnerDisplayHwcId + 1;
+
+    constexpr PhysicalDisplayId kOuterDisplayId = PhysicalDisplayId::fromPort(254u);
+
+    constexpr bool kIsPrimary = false;
+    TestableSurfaceFlinger::FakeHwcDisplayInjector(kOuterDisplayId, hal::DisplayType::PHYSICAL,
+                                                   kIsPrimary)
+            .setHwcDisplayId(kOuterDisplayHwcId)
+            .inject(&mFlinger, mComposer);
+
+    const auto outerDisplay = mFakeDisplayInjector.injectInternalDisplay(
+            [&](FakeDisplayDeviceInjector& injector) {
+                injector.setDisplayModes(mock::cloneForDisplay(kOuterDisplayId, kModes),
+                                         kModeId120);
+            },
+            {.displayId = kOuterDisplayId,
+             .hwcDisplayId = kOuterDisplayHwcId,
+             .isPrimary = kIsPrimary});
+
+    const auto& innerDisplay = mDisplay;
+
+    EXPECT_FALSE(innerDisplay->getDesiredActiveMode());
+    EXPECT_FALSE(outerDisplay->getDesiredActiveMode());
+
+    EXPECT_EQ(innerDisplay->getActiveMode().modePtr->getId(), kModeId60);
+    EXPECT_EQ(outerDisplay->getActiveMode().modePtr->getId(), kModeId120);
+
+    mFlinger.onActiveDisplayChanged(innerDisplay);
+
+    EXPECT_EQ(NO_ERROR,
+              mFlinger.setDesiredDisplayModeSpecs(innerDisplay->getDisplayToken().promote(),
+                                                  mock::createDisplayModeSpecs(kModeId90.value(),
+                                                                               false, 0.f, 120.f)));
+
+    EXPECT_EQ(NO_ERROR,
+              mFlinger.setDesiredDisplayModeSpecs(outerDisplay->getDisplayToken().promote(),
+                                                  mock::createDisplayModeSpecs(kModeId60.value(),
+                                                                               false, 0.f, 120.f)));
+
+    // Transition on the inner display.
+    ASSERT_TRUE(innerDisplay->getDesiredActiveMode());
+    EXPECT_EQ(innerDisplay->getDesiredActiveMode()->modeOpt->modePtr->getId(), kModeId90);
+
+    // No transition on the outer display.
+    EXPECT_FALSE(outerDisplay->getDesiredActiveMode());
+
+    const VsyncPeriodChangeTimeline timeline{.refreshRequired = true};
+    EXPECT_CALL(*mComposer,
+                setActiveConfigWithConstraints(kInnerDisplayHwcId,
+                                               hal::HWConfigId(kModeId90.value()), _, _))
+            .WillOnce(DoAll(SetArgPointee<3>(timeline), Return(Error::NONE)));
+
+    mFlinger.commit();
+
+    // Transition on the inner display.
+    ASSERT_TRUE(innerDisplay->getDesiredActiveMode());
+    EXPECT_EQ(innerDisplay->getDesiredActiveMode()->modeOpt->modePtr->getId(), kModeId90);
+
+    // No transition on the outer display.
+    EXPECT_FALSE(outerDisplay->getDesiredActiveMode());
+
+    mFlinger.commit();
+
+    // Transition on the inner display.
+    EXPECT_FALSE(innerDisplay->getDesiredActiveMode());
+    EXPECT_EQ(innerDisplay->getActiveMode().modePtr->getId(), kModeId90);
+
+    // No transition on the outer display.
+    EXPECT_FALSE(outerDisplay->getDesiredActiveMode());
+    EXPECT_EQ(outerDisplay->getActiveMode().modePtr->getId(), kModeId120);
+
+    mFlinger.onActiveDisplayChanged(outerDisplay);
+
+    // No transition on the inner display.
+    EXPECT_FALSE(innerDisplay->getDesiredActiveMode());
+
+    // Transition on the outer display.
+    ASSERT_TRUE(outerDisplay->getDesiredActiveMode());
+    EXPECT_EQ(outerDisplay->getDesiredActiveMode()->modeOpt->modePtr->getId(), kModeId60);
+
+    EXPECT_CALL(*mComposer,
+                setActiveConfigWithConstraints(kOuterDisplayHwcId,
+                                               hal::HWConfigId(kModeId60.value()), _, _))
+            .WillOnce(DoAll(SetArgPointee<3>(timeline), Return(Error::NONE)));
+
+    mFlinger.commit();
+
+    // No transition on the inner display.
+    EXPECT_FALSE(innerDisplay->getDesiredActiveMode());
+
+    // Transition on the outer display.
+    ASSERT_TRUE(outerDisplay->getDesiredActiveMode());
+    EXPECT_EQ(outerDisplay->getDesiredActiveMode()->modeOpt->modePtr->getId(), kModeId60);
+
+    mFlinger.commit();
+
+    // No transition on the inner display.
+    EXPECT_FALSE(innerDisplay->getDesiredActiveMode());
+    EXPECT_EQ(innerDisplay->getActiveMode().modePtr->getId(), kModeId90);
+
+    // Transition on the outer display.
+    EXPECT_FALSE(outerDisplay->getDesiredActiveMode());
+    EXPECT_EQ(outerDisplay->getActiveMode().modePtr->getId(), kModeId60);
+}
+
 } // namespace
 } // namespace android
diff --git a/services/surfaceflinger/tests/unittests/mock/DisplayHardware/MockDisplayMode.h b/services/surfaceflinger/tests/unittests/mock/DisplayHardware/MockDisplayMode.h
index c78b6bd..3b36361 100644
--- a/services/surfaceflinger/tests/unittests/mock/DisplayHardware/MockDisplayMode.h
+++ b/services/surfaceflinger/tests/unittests/mock/DisplayHardware/MockDisplayMode.h
@@ -38,4 +38,24 @@
     return createDisplayMode(modeId, refreshRate, {}, {}, displayId);
 }
 
+inline DisplayModePtr cloneForDisplay(PhysicalDisplayId displayId, const DisplayModePtr& modePtr) {
+    return DisplayMode::Builder(modePtr->getHwcId())
+            .setId(modePtr->getId())
+            .setPhysicalDisplayId(displayId)
+            .setVsyncPeriod(modePtr->getVsyncPeriod())
+            .setGroup(modePtr->getGroup())
+            .setResolution(modePtr->getResolution())
+            .build();
+}
+
+inline DisplayModes cloneForDisplay(PhysicalDisplayId displayId, const DisplayModes& modes) {
+    DisplayModes clones;
+
+    for (const auto& [id, modePtr] : modes) {
+        clones.try_emplace(id, cloneForDisplay(displayId, modePtr));
+    }
+
+    return clones;
+}
+
 } // namespace android::mock