Merge "JPEG/R: fix a recovery map calculation bug"
diff --git a/include/input/Input.h b/include/input/Input.h
index 7573282..608519b 100644
--- a/include/input/Input.h
+++ b/include/input/Input.h
@@ -1113,6 +1113,7 @@
 enum class PointerIconStyle : int32_t {
     TYPE_CUSTOM = -1,
     TYPE_NULL = 0,
+    TYPE_NOT_SPECIFIED = 1,
     TYPE_ARROW = 1000,
     TYPE_CONTEXT_MENU = 1001,
     TYPE_HAND = 1002,
diff --git a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymap.h b/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymap.h
index 1a4b679..aee6602 100644
--- a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymap.h
+++ b/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymap.h
@@ -21,6 +21,7 @@
 
 namespace android::recoverymap {
 
+// Color gamuts for image data
 typedef enum {
   JPEGR_COLORGAMUT_UNSPECIFIED,
   JPEGR_COLORGAMUT_BT709,
@@ -28,7 +29,7 @@
   JPEGR_COLORGAMUT_BT2100,
 } jpegr_color_gamut;
 
-// Transfer functions as defined for XMP metadata
+// Transfer functions for image data
 typedef enum {
   JPEGR_TF_UNSPECIFIED = -1,
   JPEGR_TF_LINEAR = 0,
@@ -82,45 +83,11 @@
     int length;
 };
 
-struct chromaticity_coord {
-  float x;
-  float y;
-};
-
-
-struct st2086_metadata {
-  // xy chromaticity coordinate of the red primary of the mastering display
-  chromaticity_coord redPrimary;
-  // xy chromaticity coordinate of the green primary of the mastering display
-  chromaticity_coord greenPrimary;
-  // xy chromaticity coordinate of the blue primary of the mastering display
-  chromaticity_coord bluePrimary;
-  // xy chromaticity coordinate of the white point of the mastering display
-  chromaticity_coord whitePoint;
-  // Maximum luminance in nits of the mastering display
-  uint32_t maxLuminance;
-  // Minimum luminance in nits of the mastering display
-  float minLuminance;
-};
-
-struct hdr10_metadata {
-  // Mastering display color volume
-  st2086_metadata st2086Metadata;
-  // Max frame average light level in nits
-  float maxFALL;
-  // Max content light level in nits
-  float maxCLL;
-};
-
 struct jpegr_metadata {
   // JPEG/R version
   uint32_t version;
-  // Range scaling factor for the map
-  float rangeScalingFactor;
-  // The transfer function for decoding the HDR representation of the image
-  jpegr_transfer_function transferFunction;
-  // HDR10 metadata, only applicable for transferFunction of JPEGR_TF_PQ
-  hdr10_metadata hdr10Metadata;
+  // Max Content Boost for the map
+  float maxContentBoost;
 };
 
 typedef struct jpegr_uncompressed_struct* jr_uncompressed_ptr;
@@ -270,14 +237,14 @@
      *
      * @param uncompressed_yuv_420_image uncompressed SDR image in YUV_420 color format
      * @param uncompressed_p010_image uncompressed HDR image in P010 color format
+     * @param hdr_tf transfer function of the HDR image
      * @param dest recovery map; caller responsible for memory of data
-     * @param metadata metadata provides the transfer function for the HDR
-     *                 image; range_scaling_factor and hdr10 FALL and CLL will
-     *                 be updated.
+     * @param metadata max_content_boost is filled in
      * @return NO_ERROR if calculation succeeds, error code if error occurs.
      */
     status_t generateRecoveryMap(jr_uncompressed_ptr uncompressed_yuv_420_image,
                                  jr_uncompressed_ptr uncompressed_p010_image,
+                                 jpegr_transfer_function hdr_tf,
                                  jr_metadata_ptr metadata,
                                  jr_uncompressed_ptr dest);
 
@@ -285,8 +252,7 @@
      * This method is called in the decoding pipeline. It will take the uncompressed (decoded)
      * 8-bit yuv image, the uncompressed (decoded) recovery map, and extracted JPEG/R metadata as
      * input, and calculate the 10-bit recovered image. The recovered output image is the same
-     * color gamut as the SDR image, with the transfer function specified in the JPEG/R metadata,
-     * and is in RGBA1010102 data format.
+     * color gamut as the SDR image, with HLG transfer function, and is in RGBA1010102 data format.
      *
      * @param uncompressed_yuv_420_image uncompressed SDR image in YUV_420 color format
      * @param uncompressed_recovery_map uncompressed recovery map
diff --git a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymaputils.h b/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymaputils.h
index 8696851..de29a33 100644
--- a/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymaputils.h
+++ b/libs/jpegrecoverymap/include/jpegrecoverymap/recoverymaputils.h
@@ -55,7 +55,7 @@
  *
  * below is an example of the XMP metadata that this function generates where
  * secondary_image_length = 1000
- * range_scaling_factor = 1.25
+ * max_content_boost = 8.0
  *
  * <x:xmpmeta
  *   xmlns:x="adobe:ns:meta/"
@@ -63,31 +63,26 @@
  *   <rdf:RDF
  *     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
  *     <rdf:Description
- *       xmlns:GContainer="http://ns.google.com/photos/1.0/container/"
+ *       xmlns:Container="http://ns.google.com/photos/1.0/container/"
+ *       xmlns:Item="http://ns.google.com/photos/1.0/container/item/"
  *       xmlns:RecoveryMap="http://ns.google.com/photos/1.0/recoverymap/">
- *       <GContainer:Version>1</GContainer:Version>
- *       <GContainer:Directory>
+ *       <Container:Directory>
  *         <rdf:Seq>
  *           <rdf:li>
- *             <GContainer:Item
- *               GContainer:ItemSemantic="Primary"
- *               GContainer:ItemMime="image/jpeg"
- *               RecoveryMap:Version=”1”
- *               RecoveryMap:RangeScalingFactor=”1.25”
- *               RecoveryMap:TransferFunction=”2”/>
- *               <RecoveryMap:HDR10Metadata
- *                 // some attributes
- *                 // some elements
- *               </RecoveryMap:HDR10Metadata>
+ *             <Container:Item
+ *              Item:Semantic="Primary"
+ *              Item:Mime="image/jpeg"
+ *              RecoveryMap:Version="1"
+ *              RecoveryMap:MaxContentBoost="8.0"/>
  *           </rdf:li>
  *           <rdf:li>
- *             <GContainer:Item
- *               GContainer:ItemSemantic="RecoveryMap"
- *               GContainer:ItemMime="image/jpeg"
- *               GContainer:ItemLength="1000"/>
+ *             <Container:Item
+ *               Item:Semantic="RecoveryMap"
+ *               Item:Mime="image/jpeg"
+ *               Item:Length="1000"/>
  *           </rdf:li>
  *         </rdf:Seq>
- *       </GContainer:Directory>
+ *       </Container:Directory>
  *     </rdf:Description>
  *   </rdf:RDF>
  * </x:xmpmeta>
diff --git a/libs/jpegrecoverymap/recoverymap.cpp b/libs/jpegrecoverymap/recoverymap.cpp
index d2b6268..8b8c2e7 100644
--- a/libs/jpegrecoverymap/recoverymap.cpp
+++ b/libs/jpegrecoverymap/recoverymap.cpp
@@ -72,16 +72,6 @@
 // JPEG compress quality (0 ~ 100) for recovery map
 static const int kMapCompressQuality = 85;
 
-// TODO: fill in st2086 metadata
-static const st2086_metadata kSt2086Metadata = {
-  {0.0f, 0.0f},
-  {0.0f, 0.0f},
-  {0.0f, 0.0f},
-  {0.0f, 0.0f},
-  0,
-  1.0f,
-};
-
 #define CONFIG_MULTITHREAD 1
 int GetCPUCoreCount() {
   int cpuCoreCount = 1;
@@ -133,10 +123,6 @@
 
   jpegr_metadata metadata;
   metadata.version = kJpegrVersion;
-  metadata.transferFunction = hdr_tf;
-  if (hdr_tf == JPEGR_TF_PQ) {
-    metadata.hdr10Metadata.st2086Metadata = kSt2086Metadata;
-  }
 
   jpegr_uncompressed_struct uncompressed_yuv_420_image;
   unique_ptr<uint8_t[]> uncompressed_yuv_420_image_data = make_unique<uint8_t[]>(
@@ -146,7 +132,7 @@
 
   jpegr_uncompressed_struct map;
   JPEGR_CHECK(generateRecoveryMap(
-      &uncompressed_yuv_420_image, uncompressed_p010_image, &metadata, &map));
+      &uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map));
   std::unique_ptr<uint8_t[]> map_data;
   map_data.reset(reinterpret_cast<uint8_t*>(map.data));
 
@@ -207,14 +193,10 @@
 
   jpegr_metadata metadata;
   metadata.version = kJpegrVersion;
-  metadata.transferFunction = hdr_tf;
-  if (hdr_tf == JPEGR_TF_PQ) {
-    metadata.hdr10Metadata.st2086Metadata = kSt2086Metadata;
-  }
 
   jpegr_uncompressed_struct map;
   JPEGR_CHECK(generateRecoveryMap(
-      uncompressed_yuv_420_image, uncompressed_p010_image, &metadata, &map));
+      uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map));
   std::unique_ptr<uint8_t[]> map_data;
   map_data.reset(reinterpret_cast<uint8_t*>(map.data));
 
@@ -271,14 +253,10 @@
 
   jpegr_metadata metadata;
   metadata.version = kJpegrVersion;
-  metadata.transferFunction = hdr_tf;
-  if (hdr_tf == JPEGR_TF_PQ) {
-    metadata.hdr10Metadata.st2086Metadata = kSt2086Metadata;
-  }
 
   jpegr_uncompressed_struct map;
   JPEGR_CHECK(generateRecoveryMap(
-      uncompressed_yuv_420_image, uncompressed_p010_image, &metadata, &map));
+      uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map));
   std::unique_ptr<uint8_t[]> map_data;
   map_data.reset(reinterpret_cast<uint8_t*>(map.data));
 
@@ -328,14 +306,10 @@
 
   jpegr_metadata metadata;
   metadata.version = kJpegrVersion;
-  metadata.transferFunction = hdr_tf;
-  if (hdr_tf == JPEGR_TF_PQ) {
-    metadata.hdr10Metadata.st2086Metadata = kSt2086Metadata;
-  }
 
   jpegr_uncompressed_struct map;
   JPEGR_CHECK(generateRecoveryMap(
-      &uncompressed_yuv_420_image, uncompressed_p010_image, &metadata, &map));
+      &uncompressed_yuv_420_image, uncompressed_p010_image, hdr_tf, &metadata, &map));
   std::unique_ptr<uint8_t[]> map_data;
   map_data.reset(reinterpret_cast<uint8_t*>(map.data));
 
@@ -437,7 +411,6 @@
     return ERROR_JPEGR_INVALID_NULL_PTR;
   }
 
-  // TODO: should we have ICC data for the map?
   JpegEncoder jpeg_encoder;
   if (!jpeg_encoder.compressImage(uncompressed_recovery_map->data,
                                   uncompressed_recovery_map->width,
@@ -518,6 +491,7 @@
 
 status_t RecoveryMap::generateRecoveryMap(jr_uncompressed_ptr uncompressed_yuv_420_image,
                                           jr_uncompressed_ptr uncompressed_p010_image,
+                                          jpegr_transfer_function hdr_tf,
                                           jr_metadata_ptr metadata,
                                           jr_uncompressed_ptr dest) {
   if (uncompressed_yuv_420_image == nullptr
@@ -554,7 +528,7 @@
 
   ColorTransformFn hdrInvOetf = nullptr;
   float hdr_white_nits = 0.0f;
-  switch (metadata->transferFunction) {
+  switch (hdr_tf) {
     case JPEGR_TF_LINEAR:
       hdrInvOetf = identityConversion;
       break;
@@ -660,7 +634,7 @@
 
           size_t pixel_idx = x + y * dest_map_stride;
           reinterpret_cast<uint8_t*>(dest->data)[pixel_idx] =
-              encodeRecovery(sdr_y_nits, hdr_y_nits, metadata->rangeScalingFactor);
+              encodeRecovery(sdr_y_nits, hdr_y_nits, metadata->maxContentBoost);
         }
       }
     }
@@ -683,11 +657,7 @@
   workers.clear();
   hdr_y_nits_avg /= image_width * image_height;
 
-  metadata->rangeScalingFactor = hdr_y_nits_max / kSdrWhiteNits;
-  if (metadata->transferFunction == JPEGR_TF_PQ) {
-    metadata->hdr10Metadata.maxFALL = hdr_y_nits_avg;
-    metadata->hdr10Metadata.maxCLL = hdr_y_nits_max;
-  }
+  metadata->maxContentBoost = hdr_y_nits_max / kSdrWhiteNits;
 
   // generate map
   jobQueue.reset();
@@ -723,39 +693,21 @@
   dest->width = uncompressed_yuv_420_image->width;
   dest->height = uncompressed_yuv_420_image->height;
   ShepardsIDW idwTable(kMapDimensionScaleFactor);
-  RecoveryLUT recoveryLUT(metadata->rangeScalingFactor);
+  RecoveryLUT recoveryLUT(metadata->maxContentBoost);
 
   JobQueue jobQueue;
   std::function<void()> applyRecMap = [uncompressed_yuv_420_image, uncompressed_recovery_map,
                                        metadata, dest, &jobQueue, &idwTable,
                                        &recoveryLUT]() -> void {
-    const float hdr_ratio = metadata->rangeScalingFactor;
+    const float hdr_ratio = metadata->maxContentBoost;
     size_t width = uncompressed_yuv_420_image->width;
     size_t height = uncompressed_yuv_420_image->height;
 
-    ColorTransformFn hdrOetf = nullptr;
-    switch (metadata->transferFunction) {
-      case JPEGR_TF_LINEAR:
-        hdrOetf = identityConversion;
-        break;
-      case JPEGR_TF_HLG:
 #if USE_HLG_OETF_LUT
-        hdrOetf = hlgOetfLUT;
+    ColorTransformFn hdrOetf = hlgOetfLUT;
 #else
-        hdrOetf = hlgOetf;
+    ColorTransformFn hdrOetf = hlgOetf;
 #endif
-        break;
-      case JPEGR_TF_PQ:
-#if USE_PQ_OETF_LUT
-        hdrOetf = pqOetfLUT;
-#else
-        hdrOetf = pqOetf;
-#endif
-        break;
-      default:
-        // Should be impossible to hit after input validation.
-        hdrOetf = identityConversion;
-    }
 
     size_t rowStart, rowEnd;
     while (jobQueue.dequeueJob(rowStart, rowEnd)) {
@@ -785,7 +737,7 @@
 #else
           Color rgb_hdr = applyRecovery(rgb_sdr, recovery, hdr_ratio);
 #endif
-          Color rgb_gamma_hdr = hdrOetf(rgb_hdr / metadata->rangeScalingFactor);
+          Color rgb_gamma_hdr = hdrOetf(rgb_hdr / metadata->maxContentBoost);
           uint32_t rgba1010102 = colorToRgba1010102(rgb_gamma_hdr);
 
           size_t pixel_idx = x + y * width;
@@ -813,8 +765,8 @@
 }
 
 status_t RecoveryMap::extractPrimaryImageAndRecoveryMap(jr_compressed_ptr compressed_jpegr_image,
-                                               jr_compressed_ptr primary_image,
-                                               jr_compressed_ptr recovery_map) {
+                                                        jr_compressed_ptr primary_image,
+                                                        jr_compressed_ptr recovery_map) {
   if (compressed_jpegr_image == nullptr) {
     return ERROR_JPEGR_INVALID_NULL_PTR;
   }
diff --git a/libs/jpegrecoverymap/recoverymaputils.cpp b/libs/jpegrecoverymap/recoverymaputils.cpp
index 1617b8b..40956bd 100644
--- a/libs/jpegrecoverymap/recoverymaputils.cpp
+++ b/libs/jpegrecoverymap/recoverymaputils.cpp
@@ -93,10 +93,8 @@
         string val;
         if (gContainerItemState == Started) {
             if (context.BuildTokenValue(&val)) {
-                if (!val.compare(rangeScalingFactorAttrName)) {
-                    lastAttributeName = rangeScalingFactorAttrName;
-                } else if (!val.compare(transferFunctionAttrName)) {
-                    lastAttributeName = transferFunctionAttrName;
+                if (!val.compare(maxContentBoostAttrName)) {
+                    lastAttributeName = maxContentBoostAttrName;
                 } else {
                     lastAttributeName = "";
                 }
@@ -109,22 +107,20 @@
         string val;
         if (gContainerItemState == Started) {
             if (context.BuildTokenValue(&val, true)) {
-                if (!lastAttributeName.compare(rangeScalingFactorAttrName)) {
-                    rangeScalingFactorStr = val;
-                } else if (!lastAttributeName.compare(transferFunctionAttrName)) {
-                    transferFunctionStr = val;
+                if (!lastAttributeName.compare(maxContentBoostAttrName)) {
+                    maxContentBoostStr = val;
                 }
             }
         }
         return context.GetResult();
     }
 
-    bool getRangeScalingFactor(float* scaling_factor) {
+    bool getMaxContentBoost(float* max_content_boost) {
         if (gContainerItemState == Done) {
-            stringstream ss(rangeScalingFactorStr);
+            stringstream ss(maxContentBoostStr);
             float val;
             if (ss >> val) {
-                *scaling_factor = val;
+                *max_content_boost = val;
                 return true;
             } else {
                 return false;
@@ -134,84 +130,49 @@
         }
     }
 
-    bool getTransferFunction(jpegr_transfer_function* transfer_function) {
-        if (gContainerItemState == Done) {
-            stringstream ss(transferFunctionStr);
-            int val;
-            if (ss >> val) {
-                *transfer_function = static_cast<jpegr_transfer_function>(val);
-                return true;
-            } else {
-                return false;
-            }
-        } else {
-            return false;
-        }
-        return true;
-    }
-
 private:
     static const string gContainerItemName;
-    static const string rangeScalingFactorAttrName;
-    static const string transferFunctionAttrName;
-    string              rangeScalingFactorStr;
-    string              transferFunctionStr;
+    static const string maxContentBoostAttrName;
+    string              maxContentBoostStr;
     string              lastAttributeName;
     ParseState          gContainerItemState;
 };
 
 // GContainer XMP constants - URI and namespace prefix
 const string kContainerUri        = "http://ns.google.com/photos/1.0/container/";
-const string kContainerPrefix     = "GContainer";
+const string kContainerPrefix     = "Container";
 
 // GContainer XMP constants - element and attribute names
 const string kConDirectory            = Name(kContainerPrefix, "Directory");
 const string kConItem                 = Name(kContainerPrefix, "Item");
-const string kConItemLength           = Name(kContainerPrefix, "ItemLength");
-const string kConItemMime             = Name(kContainerPrefix, "ItemMime");
-const string kConItemSemantic         = Name(kContainerPrefix, "ItemSemantic");
-const string kConVersion              = Name(kContainerPrefix, "Version");
-
-// GContainer XMP constants - element and attribute values
-const string kSemanticPrimary     = "Primary";
-const string kSemanticRecoveryMap = "RecoveryMap";
-const string kMimeImageJpeg       = "image/jpeg";
-
-const int kGContainerVersion      = 1;
 
 // GContainer XMP constants - names for XMP handlers
 const string XMPXmlHandler::gContainerItemName = kConItem;
 
+// Item XMP constants - URI and namespace prefix
+const string kItemUri        = "http://ns.google.com/photos/1.0/container/item/";
+const string kItemPrefix     = "Item";
+
+// Item XMP constants - element and attribute names
+const string kItemLength           = Name(kItemPrefix, "Length");
+const string kItemMime             = Name(kItemPrefix, "Mime");
+const string kItemSemantic         = Name(kItemPrefix, "Semantic");
+
+// Item XMP constants - element and attribute values
+const string kSemanticPrimary     = "Primary";
+const string kSemanticRecoveryMap = "RecoveryMap";
+const string kMimeImageJpeg       = "image/jpeg";
+
 // RecoveryMap XMP constants - URI and namespace prefix
 const string kRecoveryMapUri      = "http://ns.google.com/photos/1.0/recoverymap/";
 const string kRecoveryMapPrefix   = "RecoveryMap";
 
 // RecoveryMap XMP constants - element and attribute names
-const string kMapRangeScalingFactor = Name(kRecoveryMapPrefix, "RangeScalingFactor");
-const string kMapTransferFunction   = Name(kRecoveryMapPrefix, "TransferFunction");
-const string kMapVersion            = Name(kRecoveryMapPrefix, "Version");
-
-const string kMapHdr10Metadata      = Name(kRecoveryMapPrefix, "HDR10Metadata");
-const string kMapHdr10MaxFall       = Name(kRecoveryMapPrefix, "HDR10MaxFALL");
-const string kMapHdr10MaxCll        = Name(kRecoveryMapPrefix, "HDR10MaxCLL");
-
-const string kMapSt2086Metadata     = Name(kRecoveryMapPrefix, "ST2086Metadata");
-const string kMapSt2086MaxLum       = Name(kRecoveryMapPrefix, "ST2086MaxLuminance");
-const string kMapSt2086MinLum       = Name(kRecoveryMapPrefix, "ST2086MinLuminance");
-const string kMapSt2086Primary      = Name(kRecoveryMapPrefix, "ST2086Primary");
-const string kMapSt2086Coordinate   = Name(kRecoveryMapPrefix, "ST2086Coordinate");
-const string kMapSt2086CoordinateX  = Name(kRecoveryMapPrefix, "ST2086CoordinateX");
-const string kMapSt2086CoordinateY  = Name(kRecoveryMapPrefix, "ST2086CoordinateY");
-
-// RecoveryMap XMP constants - element and attribute values
-const int kSt2086PrimaryRed       = 0;
-const int kSt2086PrimaryGreen     = 1;
-const int kSt2086PrimaryBlue      = 2;
-const int kSt2086PrimaryWhite     = 3;
+const string kMapMaxContentBoost  = Name(kRecoveryMapPrefix, "MaxContentBoost");
+const string kMapVersion          = Name(kRecoveryMapPrefix, "Version");
 
 // RecoveryMap XMP constants - names for XMP handlers
-const string XMPXmlHandler::rangeScalingFactorAttrName = kMapRangeScalingFactor;
-const string XMPXmlHandler::transferFunctionAttrName = kMapTransferFunction;
+const string XMPXmlHandler::maxContentBoostAttrName = kMapMaxContentBoost;
 
 bool getMetadataFromXMP(uint8_t* xmp_data, size_t xmp_size, jpegr_metadata* metadata) {
     string nameSpace = "http://ns.adobe.com/xap/1.0/\0";
@@ -248,13 +209,10 @@
         return false;
     }
 
-    if (!handler.getRangeScalingFactor(&metadata->rangeScalingFactor)) {
+    if (!handler.getMaxContentBoost(&metadata->maxContentBoost)) {
         return false;
     }
 
-    if (!handler.getTransferFunction(&metadata->transferFunction)) {
-        return false;
-    }
     return true;
 }
 
@@ -271,66 +229,19 @@
   writer.WriteXmlns("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#");
   writer.StartWritingElement("rdf:Description");
   writer.WriteXmlns(kContainerPrefix, kContainerUri);
+  writer.WriteXmlns(kItemPrefix, kItemUri);
   writer.WriteXmlns(kRecoveryMapPrefix, kRecoveryMapUri);
-  writer.WriteElementAndContent(kConVersion, kGContainerVersion);
   writer.StartWritingElements(kConDirSeq);
   size_t item_depth = writer.StartWritingElements(kLiItem);
-  writer.WriteAttributeNameAndValue(kConItemSemantic, kSemanticPrimary);
-  writer.WriteAttributeNameAndValue(kConItemMime, kMimeImageJpeg);
+  writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticPrimary);
+  writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg);
   writer.WriteAttributeNameAndValue(kMapVersion, metadata.version);
-  writer.WriteAttributeNameAndValue(kMapRangeScalingFactor, metadata.rangeScalingFactor);
-  writer.WriteAttributeNameAndValue(kMapTransferFunction, metadata.transferFunction);
-  if (metadata.transferFunction == JPEGR_TF_PQ) {
-    writer.StartWritingElement(kMapHdr10Metadata);
-    writer.WriteAttributeNameAndValue(kMapHdr10MaxFall, metadata.hdr10Metadata.maxFALL);
-    writer.WriteAttributeNameAndValue(kMapHdr10MaxCll, metadata.hdr10Metadata.maxCLL);
-    writer.StartWritingElement(kMapSt2086Metadata);
-    writer.WriteAttributeNameAndValue(
-        kMapSt2086MaxLum, metadata.hdr10Metadata.st2086Metadata.maxLuminance);
-    writer.WriteAttributeNameAndValue(
-        kMapSt2086MinLum, metadata.hdr10Metadata.st2086Metadata.minLuminance);
-
-    // red
-    writer.StartWritingElement(kMapSt2086Coordinate);
-    writer.WriteAttributeNameAndValue(kMapSt2086Primary, kSt2086PrimaryRed);
-    writer.WriteAttributeNameAndValue(
-        kMapSt2086CoordinateX, metadata.hdr10Metadata.st2086Metadata.redPrimary.x);
-    writer.WriteAttributeNameAndValue(
-        kMapSt2086CoordinateY, metadata.hdr10Metadata.st2086Metadata.redPrimary.y);
-    writer.FinishWritingElement();
-
-    // green
-    writer.StartWritingElement(kMapSt2086Coordinate);
-    writer.WriteAttributeNameAndValue(kMapSt2086Primary, kSt2086PrimaryGreen);
-    writer.WriteAttributeNameAndValue(
-        kMapSt2086CoordinateX, metadata.hdr10Metadata.st2086Metadata.greenPrimary.x);
-    writer.WriteAttributeNameAndValue(
-        kMapSt2086CoordinateY, metadata.hdr10Metadata.st2086Metadata.greenPrimary.y);
-    writer.FinishWritingElement();
-
-    // blue
-    writer.StartWritingElement(kMapSt2086Coordinate);
-    writer.WriteAttributeNameAndValue(kMapSt2086Primary, kSt2086PrimaryBlue);
-    writer.WriteAttributeNameAndValue(
-        kMapSt2086CoordinateX, metadata.hdr10Metadata.st2086Metadata.bluePrimary.x);
-    writer.WriteAttributeNameAndValue(
-        kMapSt2086CoordinateY, metadata.hdr10Metadata.st2086Metadata.bluePrimary.y);
-    writer.FinishWritingElement();
-
-    // white
-    writer.StartWritingElement(kMapSt2086Coordinate);
-    writer.WriteAttributeNameAndValue(kMapSt2086Primary, kSt2086PrimaryWhite);
-    writer.WriteAttributeNameAndValue(
-        kMapSt2086CoordinateX, metadata.hdr10Metadata.st2086Metadata.whitePoint.x);
-    writer.WriteAttributeNameAndValue(
-        kMapSt2086CoordinateY, metadata.hdr10Metadata.st2086Metadata.whitePoint.y);
-    writer.FinishWritingElement();
-  }
+  writer.WriteAttributeNameAndValue(kMapMaxContentBoost, metadata.maxContentBoost);
   writer.FinishWritingElementsToDepth(item_depth);
   writer.StartWritingElements(kLiItem);
-  writer.WriteAttributeNameAndValue(kConItemSemantic, kSemanticRecoveryMap);
-  writer.WriteAttributeNameAndValue(kConItemMime, kMimeImageJpeg);
-  writer.WriteAttributeNameAndValue(kConItemLength, secondary_image_length);
+  writer.WriteAttributeNameAndValue(kItemSemantic, kSemanticRecoveryMap);
+  writer.WriteAttributeNameAndValue(kItemMime, kMimeImageJpeg);
+  writer.WriteAttributeNameAndValue(kItemLength, secondary_image_length);
   writer.FinishWriting();
 
   return ss.str();
diff --git a/libs/jpegrecoverymap/tests/Android.bp b/libs/jpegrecoverymap/tests/Android.bp
index cad273e..e381caf 100644
--- a/libs/jpegrecoverymap/tests/Android.bp
+++ b/libs/jpegrecoverymap/tests/Android.bp
@@ -29,14 +29,17 @@
         "recoverymapmath_test.cpp",
     ],
     shared_libs: [
-        "libjpeg",
-        "libjpegrecoverymap",
         "libimage_io",
+        "libjpeg",
         "liblog",
     ],
     static_libs: [
         "libgmock",
         "libgtest",
+        "libjpegdecoder",
+        "libjpegencoder",
+        "libjpegrecoverymap",
+        "libskia",
     ],
 }
 
@@ -48,11 +51,11 @@
     ],
     shared_libs: [
         "libjpeg",
-        "libjpegencoder",
         "liblog",
     ],
     static_libs: [
         "libgtest",
+        "libjpegencoder",
     ],
 }
 
@@ -64,10 +67,10 @@
     ],
     shared_libs: [
         "libjpeg",
-        "libjpegdecoder",
         "liblog",
     ],
     static_libs: [
         "libgtest",
+        "libjpegdecoder",
     ],
 }
diff --git a/libs/jpegrecoverymap/tests/recoverymap_test.cpp b/libs/jpegrecoverymap/tests/recoverymap_test.cpp
index dfab76a..3e9a76d 100644
--- a/libs/jpegrecoverymap/tests/recoverymap_test.cpp
+++ b/libs/jpegrecoverymap/tests/recoverymap_test.cpp
@@ -103,8 +103,7 @@
 
 TEST_F(RecoveryMapTest, writeXmpThenRead) {
   jpegr_metadata metadata_expected;
-  metadata_expected.transferFunction = JPEGR_TF_HLG;
-  metadata_expected.rangeScalingFactor = 1.25;
+  metadata_expected.maxContentBoost = 1.25;
   int length_expected = 1000;
   const std::string nameSpace = "http://ns.adobe.com/xap/1.0/\0";
   const int nameSpaceLength = nameSpace.size() + 1;  // need to count the null terminator
@@ -120,8 +119,7 @@
 
   jpegr_metadata metadata_read;
   EXPECT_TRUE(getMetadataFromXMP(xmpData.data(), xmpData.size(), &metadata_read));
-  ASSERT_EQ(metadata_expected.transferFunction, metadata_read.transferFunction);
-  ASSERT_EQ(metadata_expected.rangeScalingFactor, metadata_read.rangeScalingFactor);
+  ASSERT_EQ(metadata_expected.maxContentBoost, metadata_read.maxContentBoost);
 }
 
 /* Test Encode API-0 and decode */
diff --git a/libs/jpegrecoverymap/tests/recoverymapmath_test.cpp b/libs/jpegrecoverymap/tests/recoverymapmath_test.cpp
index 1d522d1..2eec95f 100644
--- a/libs/jpegrecoverymap/tests/recoverymapmath_test.cpp
+++ b/libs/jpegrecoverymap/tests/recoverymapmath_test.cpp
@@ -88,10 +88,10 @@
     return luminance_scaled * scale_factor;
   }
 
-  Color Recover(Color yuv_gamma, float recovery, float range_scaling_factor) {
+  Color Recover(Color yuv_gamma, float recovery, float max_content_boost) {
     Color rgb_gamma = srgbYuvToRgb(yuv_gamma);
     Color rgb = srgbInvOetf(rgb_gamma);
-    return applyRecovery(rgb, recovery, range_scaling_factor);
+    return applyRecovery(rgb, recovery, max_content_boost);
   }
 
   jpegr_uncompressed_struct Yuv420Image() {
diff --git a/opengl/libs/Android.bp b/opengl/libs/Android.bp
index 750338b..49e1cba 100644
--- a/opengl/libs/Android.bp
+++ b/opengl/libs/Android.bp
@@ -144,6 +144,7 @@
     srcs: [
         "EGL/BlobCache.cpp",
         "EGL/FileBlobCache.cpp",
+        "EGL/MultifileBlobCache.cpp",
     ],
     export_include_dirs: ["EGL"],
 }
@@ -160,7 +161,6 @@
     srcs: [
         "EGL/egl_tls.cpp",
         "EGL/egl_cache.cpp",
-        "EGL/egl_cache_multifile.cpp",
         "EGL/egl_display.cpp",
         "EGL/egl_object.cpp",
         "EGL/egl_layers.cpp",
@@ -205,6 +205,11 @@
     srcs: [
         "EGL/BlobCache.cpp",
         "EGL/BlobCache_test.cpp",
+        "EGL/MultifileBlobCache.cpp",
+        "EGL/MultifileBlobCache_test.cpp",
+    ],
+    shared_libs: [
+        "libutils",
     ],
 }
 
diff --git a/opengl/libs/EGL/MultifileBlobCache.cpp b/opengl/libs/EGL/MultifileBlobCache.cpp
new file mode 100644
index 0000000..48b184b
--- /dev/null
+++ b/opengl/libs/EGL/MultifileBlobCache.cpp
@@ -0,0 +1,668 @@
+/*
+ ** Copyright 2022, 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.
+ */
+
+// #define LOG_NDEBUG 0
+
+#include "MultifileBlobCache.h"
+
+#include <android-base/properties.h>
+#include <dirent.h>
+#include <fcntl.h>
+#include <inttypes.h>
+#include <log/log.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <time.h>
+#include <unistd.h>
+#include <utime.h>
+
+#include <algorithm>
+#include <chrono>
+#include <limits>
+#include <locale>
+
+#include <utils/JenkinsHash.h>
+
+using namespace std::literals;
+
+namespace {
+
+// Open the file and determine the size of the value it contains
+size_t getValueSizeFromFile(int fd, const std::string& entryPath) {
+    // Read the beginning of the file to get header
+    android::MultifileHeader header;
+    size_t result = read(fd, static_cast<void*>(&header), sizeof(android::MultifileHeader));
+    if (result != sizeof(android::MultifileHeader)) {
+        ALOGE("Error reading MultifileHeader from cache entry (%s): %s", entryPath.c_str(),
+              std::strerror(errno));
+        return 0;
+    }
+
+    return header.valueSize;
+}
+
+// Helper function to close entries or free them
+void freeHotCacheEntry(android::MultifileHotCache& entry) {
+    if (entry.entryFd != -1) {
+        // If we have an fd, then this entry was added to hot cache via INIT or GET
+        // We need to unmap and close the entry
+        munmap(entry.entryBuffer, entry.entrySize);
+        close(entry.entryFd);
+    } else {
+        // Otherwise, this was added to hot cache during SET, so it was never mapped
+        // and fd was only on the deferred thread.
+        delete[] entry.entryBuffer;
+    }
+}
+
+} // namespace
+
+namespace android {
+
+MultifileBlobCache::MultifileBlobCache(size_t maxTotalSize, size_t maxHotCacheSize,
+                                       const std::string& baseDir)
+      : mInitialized(false),
+        mMaxTotalSize(maxTotalSize),
+        mTotalCacheSize(0),
+        mHotCacheLimit(maxHotCacheSize),
+        mHotCacheSize(0),
+        mWorkerThreadIdle(true) {
+    if (baseDir.empty()) {
+        return;
+    }
+
+    // Establish the name of our multifile directory
+    mMultifileDirName = baseDir + ".multifile";
+
+    // Set a limit for max key and value, ensuring at least one entry can always fit in hot cache
+    mMaxKeySize = mHotCacheLimit / 4;
+    mMaxValueSize = mHotCacheLimit / 2;
+
+    // Initialize our cache with the contents of the directory
+    mTotalCacheSize = 0;
+
+    // See if the dir exists, and initialize using its contents
+    struct stat st;
+    if (stat(mMultifileDirName.c_str(), &st) == 0) {
+        // Read all the files and gather details, then preload their contents
+        DIR* dir;
+        struct dirent* entry;
+        if ((dir = opendir(mMultifileDirName.c_str())) != nullptr) {
+            while ((entry = readdir(dir)) != nullptr) {
+                if (entry->d_name == "."s || entry->d_name == ".."s) {
+                    continue;
+                }
+
+                std::string entryName = entry->d_name;
+                std::string fullPath = mMultifileDirName + "/" + entryName;
+
+                // The filename is the same as the entryHash
+                uint32_t entryHash = static_cast<uint32_t>(strtoul(entry->d_name, nullptr, 10));
+
+                // Look up the details of the file
+                struct stat st;
+                if (stat(fullPath.c_str(), &st) != 0) {
+                    ALOGE("Failed to stat %s", fullPath.c_str());
+                    return;
+                }
+
+                // Open the file so we can read its header
+                int fd = open(fullPath.c_str(), O_RDONLY);
+                if (fd == -1) {
+                    ALOGE("Cache error - failed to open fullPath: %s, error: %s", fullPath.c_str(),
+                          std::strerror(errno));
+                    return;
+                }
+
+                // Look up the details we track about each file
+                size_t valueSize = getValueSizeFromFile(fd, fullPath);
+                size_t fileSize = st.st_size;
+                time_t accessTime = st.st_atime;
+
+                // If the cache entry is damaged or no good, remove it
+                // TODO: Perform any other checks
+                if (valueSize <= 0 || fileSize <= 0 || accessTime <= 0) {
+                    if (remove(fullPath.c_str()) != 0) {
+                        ALOGE("Error removing %s: %s", fullPath.c_str(), std::strerror(errno));
+                    }
+                    continue;
+                }
+
+                // Track details for rapid lookup later
+                trackEntry(entryHash, valueSize, fileSize, accessTime);
+
+                // Track the total size
+                increaseTotalCacheSize(fileSize);
+
+                // Preload the entry for fast retrieval
+                if ((mHotCacheSize + fileSize) < mHotCacheLimit) {
+                    // Memory map the file
+                    uint8_t* mappedEntry = reinterpret_cast<uint8_t*>(
+                            mmap(nullptr, fileSize, PROT_READ, MAP_PRIVATE, fd, 0));
+                    if (mappedEntry == MAP_FAILED) {
+                        ALOGE("Failed to mmap cacheEntry, error: %s", std::strerror(errno));
+                    }
+
+                    ALOGV("INIT: Populating hot cache with fd = %i, cacheEntry = %p for "
+                          "entryHash %u",
+                          fd, mappedEntry, entryHash);
+
+                    // Track the details of the preload so they can be retrieved later
+                    if (!addToHotCache(entryHash, fd, mappedEntry, fileSize)) {
+                        ALOGE("INIT Failed to add %u to hot cache", entryHash);
+                        munmap(mappedEntry, fileSize);
+                        close(fd);
+                        return;
+                    }
+                } else {
+                    close(fd);
+                }
+            }
+            closedir(dir);
+        } else {
+            ALOGE("Unable to open filename: %s", mMultifileDirName.c_str());
+        }
+    } else {
+        // If the multifile directory does not exist, create it and start from scratch
+        if (mkdir(mMultifileDirName.c_str(), 0755) != 0 && (errno != EEXIST)) {
+            ALOGE("Unable to create directory (%s), errno (%i)", mMultifileDirName.c_str(), errno);
+        }
+    }
+
+    mTaskThread = std::thread(&MultifileBlobCache::processTasks, this);
+
+    mInitialized = true;
+}
+
+MultifileBlobCache::~MultifileBlobCache() {
+    // Inform the worker thread we're done
+    ALOGV("DESCTRUCTOR: Shutting down worker thread");
+    DeferredTask task(TaskCommand::Exit);
+    queueTask(std::move(task));
+
+    // Wait for it to complete
+    ALOGV("DESCTRUCTOR: Waiting for worker thread to complete");
+    waitForWorkComplete();
+    mTaskThread.join();
+}
+
+// Set will add the entry to hot cache and start a deferred process to write it to disk
+void MultifileBlobCache::set(const void* key, EGLsizeiANDROID keySize, const void* value,
+                             EGLsizeiANDROID valueSize) {
+    if (!mInitialized) {
+        return;
+    }
+
+    // Ensure key and value are under their limits
+    if (keySize > mMaxKeySize || valueSize > mMaxValueSize) {
+        ALOGV("SET: keySize (%lu vs %zu) or valueSize (%lu vs %zu) too large", keySize, mMaxKeySize,
+              valueSize, mMaxValueSize);
+        return;
+    }
+
+    // Generate a hash of the key and use it to track this entry
+    uint32_t entryHash = android::JenkinsHashMixBytes(0, static_cast<const uint8_t*>(key), keySize);
+
+    size_t fileSize = sizeof(MultifileHeader) + keySize + valueSize;
+
+    // If we're going to be over the cache limit, kick off a trim to clear space
+    if (getTotalSize() + fileSize > mMaxTotalSize) {
+        ALOGV("SET: Cache is full, calling trimCache to clear space");
+        trimCache(mMaxTotalSize);
+    }
+
+    ALOGV("SET: Add %u to cache", entryHash);
+
+    uint8_t* buffer = new uint8_t[fileSize];
+
+    // Write the key and value after the header
+    android::MultifileHeader header = {keySize, valueSize};
+    memcpy(static_cast<void*>(buffer), static_cast<const void*>(&header),
+           sizeof(android::MultifileHeader));
+    memcpy(static_cast<void*>(buffer + sizeof(MultifileHeader)), static_cast<const void*>(key),
+           keySize);
+    memcpy(static_cast<void*>(buffer + sizeof(MultifileHeader) + keySize),
+           static_cast<const void*>(value), valueSize);
+
+    std::string fullPath = mMultifileDirName + "/" + std::to_string(entryHash);
+
+    // Track the size and access time for quick recall
+    trackEntry(entryHash, valueSize, fileSize, time(0));
+
+    // Update the overall cache size
+    increaseTotalCacheSize(fileSize);
+
+    // Keep the entry in hot cache for quick retrieval
+    ALOGV("SET: Adding %u to hot cache.", entryHash);
+
+    // Sending -1 as the fd indicates we don't have an fd for this
+    if (!addToHotCache(entryHash, -1, buffer, fileSize)) {
+        ALOGE("GET: Failed to add %u to hot cache", entryHash);
+        return;
+    }
+
+    // Track that we're creating a pending write for this entry
+    // Include the buffer to handle the case when multiple writes are pending for an entry
+    mDeferredWrites.insert(std::make_pair(entryHash, buffer));
+
+    // Create deferred task to write to storage
+    ALOGV("SET: Adding task to queue.");
+    DeferredTask task(TaskCommand::WriteToDisk);
+    task.initWriteToDisk(fullPath, buffer, fileSize);
+    queueTask(std::move(task));
+}
+
+// Get will check the hot cache, then load it from disk if needed
+EGLsizeiANDROID MultifileBlobCache::get(const void* key, EGLsizeiANDROID keySize, void* value,
+                                        EGLsizeiANDROID valueSize) {
+    if (!mInitialized) {
+        return 0;
+    }
+
+    // Ensure key and value are under their limits
+    if (keySize > mMaxKeySize || valueSize > mMaxValueSize) {
+        ALOGV("GET: keySize (%lu vs %zu) or valueSize (%lu vs %zu) too large", keySize, mMaxKeySize,
+              valueSize, mMaxValueSize);
+        return 0;
+    }
+
+    // Generate a hash of the key and use it to track this entry
+    uint32_t entryHash = android::JenkinsHashMixBytes(0, static_cast<const uint8_t*>(key), keySize);
+
+    // See if we have this file
+    if (!contains(entryHash)) {
+        ALOGV("GET: Cache MISS - cache does not contain entry: %u", entryHash);
+        return 0;
+    }
+
+    // Look up the data for this entry
+    MultifileEntryStats entryStats = getEntryStats(entryHash);
+
+    size_t cachedValueSize = entryStats.valueSize;
+    if (cachedValueSize > valueSize) {
+        ALOGV("GET: Cache MISS - valueSize not large enough (%lu) for entry %u, returning required"
+              "size (%zu)",
+              valueSize, entryHash, cachedValueSize);
+        return cachedValueSize;
+    }
+
+    // We have the file and have enough room to write it out, return the entry
+    ALOGV("GET: Cache HIT - cache contains entry: %u", entryHash);
+
+    // Look up the size of the file
+    size_t fileSize = entryStats.fileSize;
+    if (keySize > fileSize) {
+        ALOGW("keySize (%lu) is larger than entrySize (%zu). This is a hash collision or modified "
+              "file",
+              keySize, fileSize);
+        return 0;
+    }
+
+    std::string fullPath = mMultifileDirName + "/" + std::to_string(entryHash);
+
+    // Open the hashed filename path
+    uint8_t* cacheEntry = 0;
+
+    // Check hot cache
+    if (mHotCache.find(entryHash) != mHotCache.end()) {
+        ALOGV("GET: HotCache HIT for entry %u", entryHash);
+        cacheEntry = mHotCache[entryHash].entryBuffer;
+    } else {
+        ALOGV("GET: HotCache MISS for entry: %u", entryHash);
+
+        if (mDeferredWrites.find(entryHash) != mDeferredWrites.end()) {
+            // Wait for writes to complete if there is an outstanding write for this entry
+            ALOGV("GET: Waiting for write to complete for %u", entryHash);
+            waitForWorkComplete();
+        }
+
+        // Open the entry file
+        int fd = open(fullPath.c_str(), O_RDONLY);
+        if (fd == -1) {
+            ALOGE("Cache error - failed to open fullPath: %s, error: %s", fullPath.c_str(),
+                  std::strerror(errno));
+            return 0;
+        }
+
+        // Memory map the file
+        cacheEntry =
+                reinterpret_cast<uint8_t*>(mmap(nullptr, fileSize, PROT_READ, MAP_PRIVATE, fd, 0));
+        if (cacheEntry == MAP_FAILED) {
+            ALOGE("Failed to mmap cacheEntry, error: %s", std::strerror(errno));
+            close(fd);
+            return 0;
+        }
+
+        ALOGV("GET: Adding %u to hot cache", entryHash);
+        if (!addToHotCache(entryHash, fd, cacheEntry, fileSize)) {
+            ALOGE("GET: Failed to add %u to hot cache", entryHash);
+            return 0;
+        }
+
+        cacheEntry = mHotCache[entryHash].entryBuffer;
+    }
+
+    // Ensure the header matches
+    MultifileHeader* header = reinterpret_cast<MultifileHeader*>(cacheEntry);
+    if (header->keySize != keySize || header->valueSize != valueSize) {
+        ALOGW("Mismatch on keySize(%ld vs. cached %ld) or valueSize(%ld vs. cached %ld) compared "
+              "to cache header values for fullPath: %s",
+              keySize, header->keySize, valueSize, header->valueSize, fullPath.c_str());
+        removeFromHotCache(entryHash);
+        return 0;
+    }
+
+    // Compare the incoming key with our stored version (the beginning of the entry)
+    uint8_t* cachedKey = cacheEntry + sizeof(MultifileHeader);
+    int compare = memcmp(cachedKey, key, keySize);
+    if (compare != 0) {
+        ALOGW("Cached key and new key do not match! This is a hash collision or modified file");
+        removeFromHotCache(entryHash);
+        return 0;
+    }
+
+    // Remaining entry following the key is the value
+    uint8_t* cachedValue = cacheEntry + (keySize + sizeof(MultifileHeader));
+    memcpy(value, cachedValue, cachedValueSize);
+
+    return cachedValueSize;
+}
+
+void MultifileBlobCache::finish() {
+    // Wait for all deferred writes to complete
+    ALOGV("FINISH: Waiting for work to complete.");
+    waitForWorkComplete();
+
+    // Close all entries in the hot cache
+    for (auto hotCacheIter = mHotCache.begin(); hotCacheIter != mHotCache.end();) {
+        uint32_t entryHash = hotCacheIter->first;
+        MultifileHotCache entry = hotCacheIter->second;
+
+        ALOGV("FINISH: Closing hot cache entry for %u", entryHash);
+        freeHotCacheEntry(entry);
+
+        mHotCache.erase(hotCacheIter++);
+    }
+}
+
+void MultifileBlobCache::trackEntry(uint32_t entryHash, EGLsizeiANDROID valueSize, size_t fileSize,
+                                    time_t accessTime) {
+    mEntries.insert(entryHash);
+    mEntryStats[entryHash] = {valueSize, fileSize, accessTime};
+}
+
+bool MultifileBlobCache::contains(uint32_t hashEntry) const {
+    return mEntries.find(hashEntry) != mEntries.end();
+}
+
+MultifileEntryStats MultifileBlobCache::getEntryStats(uint32_t entryHash) {
+    return mEntryStats[entryHash];
+}
+
+void MultifileBlobCache::increaseTotalCacheSize(size_t fileSize) {
+    mTotalCacheSize += fileSize;
+}
+
+void MultifileBlobCache::decreaseTotalCacheSize(size_t fileSize) {
+    mTotalCacheSize -= fileSize;
+}
+
+bool MultifileBlobCache::addToHotCache(uint32_t newEntryHash, int newFd, uint8_t* newEntryBuffer,
+                                       size_t newEntrySize) {
+    ALOGV("HOTCACHE(ADD): Adding %u to hot cache", newEntryHash);
+
+    // Clear space if we need to
+    if ((mHotCacheSize + newEntrySize) > mHotCacheLimit) {
+        ALOGV("HOTCACHE(ADD): mHotCacheSize (%zu) + newEntrySize (%zu) is to big for "
+              "mHotCacheLimit "
+              "(%zu), freeing up space for %u",
+              mHotCacheSize, newEntrySize, mHotCacheLimit, newEntryHash);
+
+        // Wait for all the files to complete writing so our hot cache is accurate
+        waitForWorkComplete();
+
+        // Free up old entries until under the limit
+        for (auto hotCacheIter = mHotCache.begin(); hotCacheIter != mHotCache.end();) {
+            uint32_t oldEntryHash = hotCacheIter->first;
+            MultifileHotCache oldEntry = hotCacheIter->second;
+
+            // Move our iterator before deleting the entry
+            hotCacheIter++;
+            if (!removeFromHotCache(oldEntryHash)) {
+                ALOGE("HOTCACHE(ADD): Unable to remove entry %u", oldEntryHash);
+                return false;
+            }
+
+            // Clear at least half the hot cache
+            if ((mHotCacheSize + newEntrySize) <= mHotCacheLimit / 2) {
+                ALOGV("HOTCACHE(ADD): Freed enough space for %zu", mHotCacheSize);
+                break;
+            }
+        }
+    }
+
+    // Track it
+    mHotCache[newEntryHash] = {newFd, newEntryBuffer, newEntrySize};
+    mHotCacheSize += newEntrySize;
+
+    ALOGV("HOTCACHE(ADD): New hot cache size: %zu", mHotCacheSize);
+
+    return true;
+}
+
+bool MultifileBlobCache::removeFromHotCache(uint32_t entryHash) {
+    if (mHotCache.find(entryHash) != mHotCache.end()) {
+        ALOGV("HOTCACHE(REMOVE): Removing %u from hot cache", entryHash);
+
+        // Wait for all the files to complete writing so our hot cache is accurate
+        waitForWorkComplete();
+
+        ALOGV("HOTCACHE(REMOVE): Closing hot cache entry for %u", entryHash);
+        MultifileHotCache entry = mHotCache[entryHash];
+        freeHotCacheEntry(entry);
+
+        // Delete the entry from our tracking
+        mHotCacheSize -= entry.entrySize;
+        size_t count = mHotCache.erase(entryHash);
+
+        return true;
+    }
+
+    return false;
+}
+
+bool MultifileBlobCache::applyLRU(size_t cacheLimit) {
+    // Walk through our map of sorted last access times and remove files until under the limit
+    for (auto cacheEntryIter = mEntryStats.begin(); cacheEntryIter != mEntryStats.end();) {
+        uint32_t entryHash = cacheEntryIter->first;
+
+        ALOGV("LRU: Removing entryHash %u", entryHash);
+
+        // Track the overall size
+        MultifileEntryStats entryStats = getEntryStats(entryHash);
+        decreaseTotalCacheSize(entryStats.fileSize);
+
+        // Remove it from hot cache if present
+        removeFromHotCache(entryHash);
+
+        // Remove it from the system
+        std::string entryPath = mMultifileDirName + "/" + std::to_string(entryHash);
+        if (remove(entryPath.c_str()) != 0) {
+            ALOGE("LRU: Error removing %s: %s", entryPath.c_str(), std::strerror(errno));
+            return false;
+        }
+
+        // Increment the iterator before clearing the entry
+        cacheEntryIter++;
+
+        // Delete the entry from our tracking
+        size_t count = mEntryStats.erase(entryHash);
+        if (count != 1) {
+            ALOGE("LRU: Failed to remove entryHash (%u) from mEntryStats", entryHash);
+            return false;
+        }
+
+        // See if it has been reduced enough
+        size_t totalCacheSize = getTotalSize();
+        if (totalCacheSize <= cacheLimit) {
+            // Success
+            ALOGV("LRU: Reduced cache to %zu", totalCacheSize);
+            return true;
+        }
+    }
+
+    ALOGV("LRU: Cache is emptry");
+    return false;
+}
+
+// When removing files, what fraction of the overall limit should be reached when removing files
+// A divisor of two will decrease the cache to 50%, four to 25% and so on
+constexpr uint32_t kCacheLimitDivisor = 2;
+
+// Calculate the cache size and remove old entries until under the limit
+void MultifileBlobCache::trimCache(size_t cacheByteLimit) {
+    // Start with the value provided by egl_cache
+    size_t limit = cacheByteLimit;
+
+    // Wait for all deferred writes to complete
+    waitForWorkComplete();
+
+    size_t size = getTotalSize();
+
+    // If size is larger than the threshold, remove files using LRU
+    if (size > limit) {
+        ALOGV("TRIM: Multifile cache size is larger than %zu, removing old entries",
+              cacheByteLimit);
+        if (!applyLRU(limit / kCacheLimitDivisor)) {
+            ALOGE("Error when clearing multifile shader cache");
+            return;
+        }
+    }
+}
+
+// This function performs a task.  It only knows how to write files to disk,
+// but it could be expanded if needed.
+void MultifileBlobCache::processTask(DeferredTask& task) {
+    switch (task.getTaskCommand()) {
+        case TaskCommand::Exit: {
+            ALOGV("DEFERRED: Shutting down");
+            return;
+        }
+        case TaskCommand::WriteToDisk: {
+            uint32_t entryHash = task.getEntryHash();
+            std::string& fullPath = task.getFullPath();
+            uint8_t* buffer = task.getBuffer();
+            size_t bufferSize = task.getBufferSize();
+
+            // Create the file or reset it if already present, read+write for user only
+            int fd = open(fullPath.c_str(), O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR);
+            if (fd == -1) {
+                ALOGE("Cache error in SET - failed to open fullPath: %s, error: %s",
+                      fullPath.c_str(), std::strerror(errno));
+                return;
+            }
+
+            ALOGV("DEFERRED: Opened fd %i from %s", fd, fullPath.c_str());
+
+            ssize_t result = write(fd, buffer, bufferSize);
+            if (result != bufferSize) {
+                ALOGE("Error writing fileSize to cache entry (%s): %s", fullPath.c_str(),
+                      std::strerror(errno));
+                return;
+            }
+
+            ALOGV("DEFERRED: Completed write for: %s", fullPath.c_str());
+            close(fd);
+
+            // Erase the entry from mDeferredWrites
+            // Since there could be multiple outstanding writes for an entry, find the matching one
+            typedef std::multimap<uint32_t, uint8_t*>::iterator entryIter;
+            std::pair<entryIter, entryIter> iterPair = mDeferredWrites.equal_range(entryHash);
+            for (entryIter it = iterPair.first; it != iterPair.second; ++it) {
+                if (it->second == buffer) {
+                    ALOGV("DEFERRED: Marking write complete for %u at %p", it->first, it->second);
+                    mDeferredWrites.erase(it);
+                    break;
+                }
+            }
+
+            return;
+        }
+        default: {
+            ALOGE("DEFERRED: Unhandled task type");
+            return;
+        }
+    }
+}
+
+// This function will wait until tasks arrive, then execute them
+// If the exit command is submitted, the loop will terminate
+void MultifileBlobCache::processTasksImpl(bool* exitThread) {
+    while (true) {
+        std::unique_lock<std::mutex> lock(mWorkerMutex);
+        if (mTasks.empty()) {
+            ALOGV("WORKER: No tasks available, waiting");
+            mWorkerThreadIdle = true;
+            mWorkerIdleCondition.notify_all();
+            // Only wake if notified and command queue is not empty
+            mWorkAvailableCondition.wait(lock, [this] { return !mTasks.empty(); });
+        }
+
+        ALOGV("WORKER: Task available, waking up.");
+        mWorkerThreadIdle = false;
+        DeferredTask task = std::move(mTasks.front());
+        mTasks.pop();
+
+        if (task.getTaskCommand() == TaskCommand::Exit) {
+            ALOGV("WORKER: Exiting work loop.");
+            *exitThread = true;
+            mWorkerThreadIdle = true;
+            mWorkerIdleCondition.notify_one();
+            return;
+        }
+
+        lock.unlock();
+        processTask(task);
+    }
+}
+
+// Process tasks until the exit task is submitted
+void MultifileBlobCache::processTasks() {
+    while (true) {
+        bool exitThread = false;
+        processTasksImpl(&exitThread);
+        if (exitThread) {
+            break;
+        }
+    }
+}
+
+// Add a task to the queue to be processed by the worker thread
+void MultifileBlobCache::queueTask(DeferredTask&& task) {
+    std::lock_guard<std::mutex> queueLock(mWorkerMutex);
+    mTasks.emplace(std::move(task));
+    mWorkAvailableCondition.notify_one();
+}
+
+// Wait until all tasks have been completed
+void MultifileBlobCache::waitForWorkComplete() {
+    std::unique_lock<std::mutex> lock(mWorkerMutex);
+    mWorkerIdleCondition.wait(lock, [this] { return (mTasks.empty() && mWorkerThreadIdle); });
+}
+
+}; // namespace android
\ No newline at end of file
diff --git a/opengl/libs/EGL/MultifileBlobCache.h b/opengl/libs/EGL/MultifileBlobCache.h
new file mode 100644
index 0000000..dcdfe47
--- /dev/null
+++ b/opengl/libs/EGL/MultifileBlobCache.h
@@ -0,0 +1,164 @@
+/*
+ ** Copyright 2022, 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.
+ */
+
+#ifndef ANDROID_MULTIFILE_BLOB_CACHE_H
+#define ANDROID_MULTIFILE_BLOB_CACHE_H
+
+#include <EGL/egl.h>
+#include <EGL/eglext.h>
+
+#include <future>
+#include <map>
+#include <queue>
+#include <string>
+#include <thread>
+#include <unordered_map>
+#include <unordered_set>
+
+namespace android {
+
+struct MultifileHeader {
+    EGLsizeiANDROID keySize;
+    EGLsizeiANDROID valueSize;
+};
+
+struct MultifileEntryStats {
+    EGLsizeiANDROID valueSize;
+    size_t fileSize;
+    time_t accessTime;
+};
+
+struct MultifileHotCache {
+    int entryFd;
+    uint8_t* entryBuffer;
+    size_t entrySize;
+};
+
+enum class TaskCommand {
+    Invalid = 0,
+    WriteToDisk,
+    Exit,
+};
+
+class DeferredTask {
+public:
+    DeferredTask(TaskCommand command) : mCommand(command) {}
+
+    TaskCommand getTaskCommand() { return mCommand; }
+
+    void initWriteToDisk(std::string fullPath, uint8_t* buffer, size_t bufferSize) {
+        mCommand = TaskCommand::WriteToDisk;
+        mFullPath = fullPath;
+        mBuffer = buffer;
+        mBufferSize = bufferSize;
+    }
+
+    uint32_t getEntryHash() { return mEntryHash; }
+    std::string& getFullPath() { return mFullPath; }
+    uint8_t* getBuffer() { return mBuffer; }
+    size_t getBufferSize() { return mBufferSize; };
+
+private:
+    TaskCommand mCommand;
+
+    // Parameters for WriteToDisk
+    uint32_t mEntryHash;
+    std::string mFullPath;
+    uint8_t* mBuffer;
+    size_t mBufferSize;
+};
+
+class MultifileBlobCache {
+public:
+    MultifileBlobCache(size_t maxTotalSize, size_t maxHotCacheSize, const std::string& baseDir);
+    ~MultifileBlobCache();
+
+    void set(const void* key, EGLsizeiANDROID keySize, const void* value,
+             EGLsizeiANDROID valueSize);
+    EGLsizeiANDROID get(const void* key, EGLsizeiANDROID keySize, void* value,
+                        EGLsizeiANDROID valueSize);
+
+    void finish();
+
+    size_t getTotalSize() const { return mTotalCacheSize; }
+    void trimCache(size_t cacheByteLimit);
+
+private:
+    void trackEntry(uint32_t entryHash, EGLsizeiANDROID valueSize, size_t fileSize,
+                    time_t accessTime);
+    bool contains(uint32_t entryHash) const;
+    bool removeEntry(uint32_t entryHash);
+    MultifileEntryStats getEntryStats(uint32_t entryHash);
+
+    size_t getFileSize(uint32_t entryHash);
+    size_t getValueSize(uint32_t entryHash);
+
+    void increaseTotalCacheSize(size_t fileSize);
+    void decreaseTotalCacheSize(size_t fileSize);
+
+    bool addToHotCache(uint32_t entryHash, int fd, uint8_t* entryBufer, size_t entrySize);
+    bool removeFromHotCache(uint32_t entryHash);
+
+    bool applyLRU(size_t cacheLimit);
+
+    bool mInitialized;
+    std::string mMultifileDirName;
+
+    std::unordered_set<uint32_t> mEntries;
+    std::unordered_map<uint32_t, MultifileEntryStats> mEntryStats;
+    std::unordered_map<uint32_t, MultifileHotCache> mHotCache;
+
+    size_t mMaxKeySize;
+    size_t mMaxValueSize;
+    size_t mMaxTotalSize;
+    size_t mTotalCacheSize;
+    size_t mHotCacheLimit;
+    size_t mHotCacheEntryLimit;
+    size_t mHotCacheSize;
+
+    // Below are the components used to allow a deferred write
+
+    // Track whether we have pending writes for an entry
+    std::multimap<uint32_t, uint8_t*> mDeferredWrites;
+
+    // Functions to work through tasks in the queue
+    void processTasks();
+    void processTasksImpl(bool* exitThread);
+    void processTask(DeferredTask& task);
+
+    // Used by main thread to create work for the worker thread
+    void queueTask(DeferredTask&& task);
+
+    // Used by main thread to wait for worker thread to complete all outstanding work.
+    void waitForWorkComplete();
+
+    std::thread mTaskThread;
+    std::queue<DeferredTask> mTasks;
+    std::mutex mWorkerMutex;
+
+    // This condition will block the worker thread until a task is queued
+    std::condition_variable mWorkAvailableCondition;
+
+    // This condition will block the main thread while the worker thread still has tasks
+    std::condition_variable mWorkerIdleCondition;
+
+    // This bool will track whether all tasks have been completed
+    bool mWorkerThreadIdle;
+};
+
+}; // namespace android
+
+#endif // ANDROID_MULTIFILE_BLOB_CACHE_H
diff --git a/opengl/libs/EGL/MultifileBlobCache_test.cpp b/opengl/libs/EGL/MultifileBlobCache_test.cpp
new file mode 100644
index 0000000..1a55a4f
--- /dev/null
+++ b/opengl/libs/EGL/MultifileBlobCache_test.cpp
@@ -0,0 +1,200 @@
+/*
+ ** Copyright 2023, 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.
+ */
+
+#include "MultifileBlobCache.h"
+
+#include <android-base/test_utils.h>
+#include <fcntl.h>
+#include <gtest/gtest.h>
+#include <stdio.h>
+
+#include <memory>
+
+namespace android {
+
+template <typename T>
+using sp = std::shared_ptr<T>;
+
+constexpr size_t kMaxTotalSize = 32 * 1024;
+constexpr size_t kMaxPreloadSize = 8 * 1024;
+
+constexpr size_t kMaxKeySize = kMaxPreloadSize / 4;
+constexpr size_t kMaxValueSize = kMaxPreloadSize / 2;
+
+class MultifileBlobCacheTest : public ::testing::Test {
+protected:
+    virtual void SetUp() {
+        mTempFile.reset(new TemporaryFile());
+        mMBC.reset(new MultifileBlobCache(kMaxTotalSize, kMaxPreloadSize, &mTempFile->path[0]));
+    }
+
+    virtual void TearDown() { mMBC.reset(); }
+
+    std::unique_ptr<TemporaryFile> mTempFile;
+    std::unique_ptr<MultifileBlobCache> mMBC;
+};
+
+TEST_F(MultifileBlobCacheTest, CacheSingleValueSucceeds) {
+    unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
+    mMBC->set("abcd", 4, "efgh", 4);
+    ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 4));
+    ASSERT_EQ('e', buf[0]);
+    ASSERT_EQ('f', buf[1]);
+    ASSERT_EQ('g', buf[2]);
+    ASSERT_EQ('h', buf[3]);
+}
+
+TEST_F(MultifileBlobCacheTest, CacheTwoValuesSucceeds) {
+    unsigned char buf[2] = {0xee, 0xee};
+    mMBC->set("ab", 2, "cd", 2);
+    mMBC->set("ef", 2, "gh", 2);
+    ASSERT_EQ(size_t(2), mMBC->get("ab", 2, buf, 2));
+    ASSERT_EQ('c', buf[0]);
+    ASSERT_EQ('d', buf[1]);
+    ASSERT_EQ(size_t(2), mMBC->get("ef", 2, buf, 2));
+    ASSERT_EQ('g', buf[0]);
+    ASSERT_EQ('h', buf[1]);
+}
+
+TEST_F(MultifileBlobCacheTest, GetSetTwiceSucceeds) {
+    unsigned char buf[2] = {0xee, 0xee};
+    mMBC->set("ab", 2, "cd", 2);
+    ASSERT_EQ(size_t(2), mMBC->get("ab", 2, buf, 2));
+    ASSERT_EQ('c', buf[0]);
+    ASSERT_EQ('d', buf[1]);
+    // Use the same key, but different value
+    mMBC->set("ab", 2, "ef", 2);
+    ASSERT_EQ(size_t(2), mMBC->get("ab", 2, buf, 2));
+    ASSERT_EQ('e', buf[0]);
+    ASSERT_EQ('f', buf[1]);
+}
+
+TEST_F(MultifileBlobCacheTest, GetOnlyWritesInsideBounds) {
+    unsigned char buf[6] = {0xee, 0xee, 0xee, 0xee, 0xee, 0xee};
+    mMBC->set("abcd", 4, "efgh", 4);
+    ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf + 1, 4));
+    ASSERT_EQ(0xee, buf[0]);
+    ASSERT_EQ('e', buf[1]);
+    ASSERT_EQ('f', buf[2]);
+    ASSERT_EQ('g', buf[3]);
+    ASSERT_EQ('h', buf[4]);
+    ASSERT_EQ(0xee, buf[5]);
+}
+
+TEST_F(MultifileBlobCacheTest, GetOnlyWritesIfBufferIsLargeEnough) {
+    unsigned char buf[3] = {0xee, 0xee, 0xee};
+    mMBC->set("abcd", 4, "efgh", 4);
+    ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 3));
+    ASSERT_EQ(0xee, buf[0]);
+    ASSERT_EQ(0xee, buf[1]);
+    ASSERT_EQ(0xee, buf[2]);
+}
+
+TEST_F(MultifileBlobCacheTest, GetDoesntAccessNullBuffer) {
+    mMBC->set("abcd", 4, "efgh", 4);
+    ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, nullptr, 0));
+}
+
+TEST_F(MultifileBlobCacheTest, MultipleSetsCacheLatestValue) {
+    unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
+    mMBC->set("abcd", 4, "efgh", 4);
+    mMBC->set("abcd", 4, "ijkl", 4);
+    ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 4));
+    ASSERT_EQ('i', buf[0]);
+    ASSERT_EQ('j', buf[1]);
+    ASSERT_EQ('k', buf[2]);
+    ASSERT_EQ('l', buf[3]);
+}
+
+TEST_F(MultifileBlobCacheTest, SecondSetKeepsFirstValueIfTooLarge) {
+    unsigned char buf[kMaxValueSize + 1] = {0xee, 0xee, 0xee, 0xee};
+    mMBC->set("abcd", 4, "efgh", 4);
+    mMBC->set("abcd", 4, buf, kMaxValueSize + 1);
+    ASSERT_EQ(size_t(4), mMBC->get("abcd", 4, buf, 4));
+    ASSERT_EQ('e', buf[0]);
+    ASSERT_EQ('f', buf[1]);
+    ASSERT_EQ('g', buf[2]);
+    ASSERT_EQ('h', buf[3]);
+}
+
+TEST_F(MultifileBlobCacheTest, DoesntCacheIfKeyIsTooBig) {
+    char key[kMaxKeySize + 1];
+    unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
+    for (int i = 0; i < kMaxKeySize + 1; i++) {
+        key[i] = 'a';
+    }
+    mMBC->set(key, kMaxKeySize + 1, "bbbb", 4);
+    ASSERT_EQ(size_t(0), mMBC->get(key, kMaxKeySize + 1, buf, 4));
+    ASSERT_EQ(0xee, buf[0]);
+    ASSERT_EQ(0xee, buf[1]);
+    ASSERT_EQ(0xee, buf[2]);
+    ASSERT_EQ(0xee, buf[3]);
+}
+
+TEST_F(MultifileBlobCacheTest, DoesntCacheIfValueIsTooBig) {
+    char buf[kMaxValueSize + 1];
+    for (int i = 0; i < kMaxValueSize + 1; i++) {
+        buf[i] = 'b';
+    }
+    mMBC->set("abcd", 4, buf, kMaxValueSize + 1);
+    for (int i = 0; i < kMaxValueSize + 1; i++) {
+        buf[i] = 0xee;
+    }
+    ASSERT_EQ(size_t(0), mMBC->get("abcd", 4, buf, kMaxValueSize + 1));
+    for (int i = 0; i < kMaxValueSize + 1; i++) {
+        SCOPED_TRACE(i);
+        ASSERT_EQ(0xee, buf[i]);
+    }
+}
+
+TEST_F(MultifileBlobCacheTest, CacheMaxKeySizeSucceeds) {
+    char key[kMaxKeySize];
+    unsigned char buf[4] = {0xee, 0xee, 0xee, 0xee};
+    for (int i = 0; i < kMaxKeySize; i++) {
+        key[i] = 'a';
+    }
+    mMBC->set(key, kMaxKeySize, "wxyz", 4);
+    ASSERT_EQ(size_t(4), mMBC->get(key, kMaxKeySize, buf, 4));
+    ASSERT_EQ('w', buf[0]);
+    ASSERT_EQ('x', buf[1]);
+    ASSERT_EQ('y', buf[2]);
+    ASSERT_EQ('z', buf[3]);
+}
+
+TEST_F(MultifileBlobCacheTest, CacheMaxValueSizeSucceeds) {
+    char buf[kMaxValueSize];
+    for (int i = 0; i < kMaxValueSize; i++) {
+        buf[i] = 'b';
+    }
+    mMBC->set("abcd", 4, buf, kMaxValueSize);
+    for (int i = 0; i < kMaxValueSize; i++) {
+        buf[i] = 0xee;
+    }
+    mMBC->get("abcd", 4, buf, kMaxValueSize);
+    for (int i = 0; i < kMaxValueSize; i++) {
+        SCOPED_TRACE(i);
+        ASSERT_EQ('b', buf[i]);
+    }
+}
+
+TEST_F(MultifileBlobCacheTest, CacheMinKeyAndValueSizeSucceeds) {
+    unsigned char buf[1] = {0xee};
+    mMBC->set("x", 1, "y", 1);
+    ASSERT_EQ(size_t(1), mMBC->get("x", 1, buf, 1));
+    ASSERT_EQ('y', buf[0]);
+}
+
+} // namespace android
diff --git a/opengl/libs/EGL/egl_cache.cpp b/opengl/libs/EGL/egl_cache.cpp
index 1e8a348..b00ee33 100644
--- a/opengl/libs/EGL/egl_cache.cpp
+++ b/opengl/libs/EGL/egl_cache.cpp
@@ -14,6 +14,8 @@
  ** limitations under the License.
  */
 
+// #define LOG_NDEBUG 0
+
 #include "egl_cache.h"
 
 #include <android-base/properties.h>
@@ -25,22 +27,19 @@
 #include <thread>
 
 #include "../egl_impl.h"
-#include "egl_cache_multifile.h"
 #include "egl_display.h"
 
 // Monolithic cache size limits.
-static const size_t maxKeySize = 12 * 1024;
-static const size_t maxValueSize = 64 * 1024;
-static const size_t maxTotalSize = 32 * 1024 * 1024;
+static const size_t kMaxMonolithicKeySize = 12 * 1024;
+static const size_t kMaxMonolithicValueSize = 64 * 1024;
+static const size_t kMaxMonolithicTotalSize = 2 * 1024 * 1024;
 
 // The time in seconds to wait before saving newly inserted monolithic cache entries.
-static const unsigned int deferredSaveDelay = 4;
+static const unsigned int kDeferredMonolithicSaveDelay = 4;
 
-// Multifile cache size limit
-constexpr size_t kMultifileCacheByteLimit = 64 * 1024 * 1024;
-
-// Delay before cleaning up multifile cache entries
-static const unsigned int deferredMultifileCleanupDelaySeconds = 1;
+// Multifile cache size limits
+constexpr uint32_t kMultifileHotCacheLimit = 8 * 1024 * 1024;
+constexpr uint32_t kMultifileCacheByteLimit = 32 * 1024 * 1024;
 
 namespace android {
 
@@ -68,10 +67,7 @@
 // egl_cache_t definition
 //
 egl_cache_t::egl_cache_t()
-      : mInitialized(false),
-        mMultifileMode(false),
-        mCacheByteLimit(maxTotalSize),
-        mMultifileCleanupPending(false) {}
+      : mInitialized(false), mMultifileMode(false), mCacheByteLimit(kMaxMonolithicTotalSize) {}
 
 egl_cache_t::~egl_cache_t() {}
 
@@ -85,7 +81,7 @@
     std::lock_guard<std::mutex> lock(mMutex);
 
     egl_connection_t* const cnx = &gEGLImpl;
-    if (cnx->dso && cnx->major >= 0 && cnx->minor >= 0) {
+    if (display && cnx->dso && cnx->major >= 0 && cnx->minor >= 0) {
         const char* exts = display->disp.queryString.extensions;
         size_t bcExtLen = strlen(BC_EXT_STR);
         size_t extsLen = strlen(exts);
@@ -114,14 +110,36 @@
         }
     }
 
-    // Allow forcing monolithic cache for debug purposes
-    if (base::GetProperty("debug.egl.blobcache.multifilemode", "") == "false") {
-        ALOGD("Forcing monolithic cache due to debug.egl.blobcache.multifilemode == \"false\"");
+    // Check the device config to decide whether multifile should be used
+    if (base::GetBoolProperty("ro.egl.blobcache.multifile", false)) {
+        mMultifileMode = true;
+        ALOGV("Using multifile EGL blobcache");
+    }
+
+    // Allow forcing the mode for debug purposes
+    std::string mode = base::GetProperty("debug.egl.blobcache.multifile", "");
+    if (mode == "true") {
+        ALOGV("Forcing multifile cache due to debug.egl.blobcache.multifile == %s", mode.c_str());
+        mMultifileMode = true;
+    } else if (mode == "false") {
+        ALOGV("Forcing monolithic cache due to debug.egl.blobcache.multifile == %s", mode.c_str());
         mMultifileMode = false;
     }
 
     if (mMultifileMode) {
-        mCacheByteLimit = kMultifileCacheByteLimit;
+        mCacheByteLimit = static_cast<size_t>(
+                base::GetUintProperty<uint32_t>("ro.egl.blobcache.multifile_limit",
+                                                kMultifileCacheByteLimit));
+
+        // Check for a debug value
+        int debugCacheSize = base::GetIntProperty("debug.egl.blobcache.multifile_limit", -1);
+        if (debugCacheSize >= 0) {
+            ALOGV("Overriding cache limit %zu with %i from debug.egl.blobcache.multifile_limit",
+                  mCacheByteLimit, debugCacheSize);
+            mCacheByteLimit = debugCacheSize;
+        }
+
+        ALOGV("Using multifile EGL blobcache limit of %zu bytes", mCacheByteLimit);
     }
 
     mInitialized = true;
@@ -133,10 +151,10 @@
         mBlobCache->writeToFile();
     }
     mBlobCache = nullptr;
-    if (mMultifileMode) {
-        checkMultifileCacheSize(mCacheByteLimit);
+    if (mMultifileBlobCache) {
+        mMultifileBlobCache->finish();
     }
-    mMultifileMode = false;
+    mMultifileBlobCache = nullptr;
     mInitialized = false;
 }
 
@@ -151,20 +169,8 @@
 
     if (mInitialized) {
         if (mMultifileMode) {
-            setBlobMultifile(key, keySize, value, valueSize, mFilename);
-
-            if (!mMultifileCleanupPending) {
-                mMultifileCleanupPending = true;
-                // Kick off a thread to cull cache files below limit
-                std::thread deferredMultifileCleanupThread([this]() {
-                    sleep(deferredMultifileCleanupDelaySeconds);
-                    std::lock_guard<std::mutex> lock(mMutex);
-                    // Check the size of cache and remove entries to stay under limit
-                    checkMultifileCacheSize(mCacheByteLimit);
-                    mMultifileCleanupPending = false;
-                });
-                deferredMultifileCleanupThread.detach();
-            }
+            MultifileBlobCache* mbc = getMultifileBlobCacheLocked();
+            mbc->set(key, keySize, value, valueSize);
         } else {
             BlobCache* bc = getBlobCacheLocked();
             bc->set(key, keySize, value, valueSize);
@@ -172,7 +178,7 @@
             if (!mSavePending) {
                 mSavePending = true;
                 std::thread deferredSaveThread([this]() {
-                    sleep(deferredSaveDelay);
+                    sleep(kDeferredMonolithicSaveDelay);
                     std::lock_guard<std::mutex> lock(mMutex);
                     if (mInitialized && mBlobCache) {
                         mBlobCache->writeToFile();
@@ -196,15 +202,21 @@
 
     if (mInitialized) {
         if (mMultifileMode) {
-            return getBlobMultifile(key, keySize, value, valueSize, mFilename);
+            MultifileBlobCache* mbc = getMultifileBlobCacheLocked();
+            return mbc->get(key, keySize, value, valueSize);
         } else {
             BlobCache* bc = getBlobCacheLocked();
             return bc->get(key, keySize, value, valueSize);
         }
     }
+
     return 0;
 }
 
+void egl_cache_t::setCacheMode(EGLCacheMode cacheMode) {
+    mMultifileMode = (cacheMode == EGLCacheMode::Multifile);
+}
+
 void egl_cache_t::setCacheFilename(const char* filename) {
     std::lock_guard<std::mutex> lock(mMutex);
     mFilename = filename;
@@ -216,7 +228,7 @@
     if (!mMultifileMode) {
         // If we're not in multifile mode, ensure the cache limit is only being lowered,
         // not increasing above the hard coded platform limit
-        if (cacheByteLimit > maxTotalSize) {
+        if (cacheByteLimit > kMaxMonolithicTotalSize) {
             return;
         }
     }
@@ -226,8 +238,8 @@
 
 size_t egl_cache_t::getCacheSize() {
     std::lock_guard<std::mutex> lock(mMutex);
-    if (mMultifileMode) {
-        return getMultifileCacheSize();
+    if (mMultifileBlobCache) {
+        return mMultifileBlobCache->getTotalSize();
     }
     if (mBlobCache) {
         return mBlobCache->getSize();
@@ -237,9 +249,18 @@
 
 BlobCache* egl_cache_t::getBlobCacheLocked() {
     if (mBlobCache == nullptr) {
-        mBlobCache.reset(new FileBlobCache(maxKeySize, maxValueSize, mCacheByteLimit, mFilename));
+        mBlobCache.reset(new FileBlobCache(kMaxMonolithicKeySize, kMaxMonolithicValueSize,
+                                           mCacheByteLimit, mFilename));
     }
     return mBlobCache.get();
 }
 
+MultifileBlobCache* egl_cache_t::getMultifileBlobCacheLocked() {
+    if (mMultifileBlobCache == nullptr) {
+        mMultifileBlobCache.reset(
+                new MultifileBlobCache(mCacheByteLimit, kMultifileHotCacheLimit, mFilename));
+    }
+    return mMultifileBlobCache.get();
+}
+
 }; // namespace android
diff --git a/opengl/libs/EGL/egl_cache.h b/opengl/libs/EGL/egl_cache.h
index 2dcd803..1399368 100644
--- a/opengl/libs/EGL/egl_cache.h
+++ b/opengl/libs/EGL/egl_cache.h
@@ -25,6 +25,7 @@
 #include <string>
 
 #include "FileBlobCache.h"
+#include "MultifileBlobCache.h"
 
 namespace android {
 
@@ -32,6 +33,11 @@
 
 class EGLAPI egl_cache_t {
 public:
+    enum class EGLCacheMode {
+        Monolithic,
+        Multifile,
+    };
+
     // get returns a pointer to the singleton egl_cache_t object.  This
     // singleton object will never be destroyed.
     static egl_cache_t* get();
@@ -64,6 +70,9 @@
     // cache contents from one program invocation to another.
     void setCacheFilename(const char* filename);
 
+    // Allow setting monolithic or multifile modes
+    void setCacheMode(EGLCacheMode cacheMode);
+
     // Allow the fixed cache limit to be overridden
     void setCacheLimit(int64_t cacheByteLimit);
 
@@ -85,6 +94,9 @@
     // possible.
     BlobCache* getBlobCacheLocked();
 
+    // Get or create the multifile blobcache
+    MultifileBlobCache* getMultifileBlobCacheLocked();
+
     // mInitialized indicates whether the egl_cache_t is in the initialized
     // state.  It is initialized to false at construction time, and gets set to
     // true when initialize is called.  It is set back to false when terminate
@@ -98,6 +110,9 @@
     // first time it's needed.
     std::unique_ptr<FileBlobCache> mBlobCache;
 
+    // The multifile version of blobcache allowing larger contents to be stored
+    std::unique_ptr<MultifileBlobCache> mMultifileBlobCache;
+
     // mFilename is the name of the file for storing cache contents in between
     // program invocations.  It is initialized to an empty string at
     // construction time, and can be set with the setCacheFilename method.  An
@@ -123,11 +138,7 @@
     bool mMultifileMode;
 
     // Cache limit
-    int64_t mCacheByteLimit;
-
-    // Whether we've kicked off a side thread that will check the multifile
-    // cache size and remove entries if needed.
-    bool mMultifileCleanupPending;
+    size_t mCacheByteLimit;
 };
 
 }; // namespace android
diff --git a/opengl/libs/EGL/egl_cache_multifile.cpp b/opengl/libs/EGL/egl_cache_multifile.cpp
deleted file mode 100644
index 48e557f..0000000
--- a/opengl/libs/EGL/egl_cache_multifile.cpp
+++ /dev/null
@@ -1,343 +0,0 @@
-/*
- ** Copyright 2022, 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.
- */
-
-// #define LOG_NDEBUG 0
-
-#include "egl_cache_multifile.h"
-
-#include <android-base/properties.h>
-#include <dirent.h>
-#include <fcntl.h>
-#include <inttypes.h>
-#include <log/log.h>
-#include <stdio.h>
-#include <sys/mman.h>
-#include <sys/stat.h>
-#include <utime.h>
-
-#include <algorithm>
-#include <chrono>
-#include <fstream>
-#include <limits>
-#include <locale>
-#include <map>
-#include <sstream>
-#include <unordered_map>
-
-#include <utils/JenkinsHash.h>
-
-static std::string multifileDirName = "";
-
-using namespace std::literals;
-
-namespace {
-
-// Create a directory for tracking multiple files
-void setupMultifile(const std::string& baseDir) {
-    // If we've already set up the multifile dir in this base directory, we're done
-    if (!multifileDirName.empty() && multifileDirName.find(baseDir) != std::string::npos) {
-        return;
-    }
-
-    // Otherwise, create it
-    multifileDirName = baseDir + ".multifile";
-    if (mkdir(multifileDirName.c_str(), 0755) != 0 && (errno != EEXIST)) {
-        ALOGW("Unable to create directory (%s), errno (%i)", multifileDirName.c_str(), errno);
-    }
-}
-
-// Create a filename that is based on the hash of the key
-std::string getCacheEntryFilename(const void* key, EGLsizeiANDROID keySize,
-                                  const std::string& baseDir) {
-    // Hash the key into a string
-    std::stringstream keyName;
-    keyName << android::JenkinsHashMixBytes(0, static_cast<const uint8_t*>(key), keySize);
-
-    // Build a filename using dir and hash
-    return baseDir + "/" + keyName.str();
-}
-
-// Determine file age based on stat modification time
-// Newer files have a higher age (time since epoch)
-time_t getFileAge(const std::string& filePath) {
-    struct stat st;
-    if (stat(filePath.c_str(), &st) == 0) {
-        ALOGD("getFileAge returning %" PRId64 " for file age", static_cast<uint64_t>(st.st_mtime));
-        return st.st_mtime;
-    } else {
-        ALOGW("Failed to stat %s", filePath.c_str());
-        return 0;
-    }
-}
-
-size_t getFileSize(const std::string& filePath) {
-    struct stat st;
-    if (stat(filePath.c_str(), &st) != 0) {
-        ALOGE("Unable to stat %s", filePath.c_str());
-        return 0;
-    }
-    return st.st_size;
-}
-
-// Walk through directory entries and track age and size
-// Then iterate through the entries, oldest first, and remove them until under the limit.
-// This will need to be updated if we move to a multilevel cache dir.
-bool applyLRU(size_t cacheLimit) {
-    // Build a multimap of files indexed by age.
-    // They will be automatically sorted smallest (oldest) to largest (newest)
-    std::multimap<time_t, std::string> agesToFiles;
-
-    // Map files to sizes
-    std::unordered_map<std::string, size_t> filesToSizes;
-
-    size_t totalCacheSize = 0;
-
-    DIR* dir;
-    struct dirent* entry;
-    if ((dir = opendir(multifileDirName.c_str())) != nullptr) {
-        while ((entry = readdir(dir)) != nullptr) {
-            if (entry->d_name == "."s || entry->d_name == ".."s) {
-                continue;
-            }
-
-            // Look up each file age
-            std::string fullPath = multifileDirName + "/" + entry->d_name;
-            time_t fileAge = getFileAge(fullPath);
-
-            // Track the files, sorted by age
-            agesToFiles.insert(std::make_pair(fileAge, fullPath));
-
-            // Also track the size so we know how much room we have freed
-            size_t fileSize = getFileSize(fullPath);
-            filesToSizes[fullPath] = fileSize;
-            totalCacheSize += fileSize;
-        }
-        closedir(dir);
-    } else {
-        ALOGE("Unable to open filename: %s", multifileDirName.c_str());
-        return false;
-    }
-
-    if (totalCacheSize <= cacheLimit) {
-        // If LRU was called on a sufficiently small cache, no need to remove anything
-        return true;
-    }
-
-    // Walk through the map of files until we're under the cache size
-    for (const auto& cacheEntryIter : agesToFiles) {
-        time_t entryAge = cacheEntryIter.first;
-        const std::string entryPath = cacheEntryIter.second;
-
-        ALOGD("Removing %s with age %ld", entryPath.c_str(), entryAge);
-        if (std::remove(entryPath.c_str()) != 0) {
-            ALOGE("Error removing %s: %s", entryPath.c_str(), std::strerror(errno));
-            return false;
-        }
-
-        totalCacheSize -= filesToSizes[entryPath];
-        if (totalCacheSize <= cacheLimit) {
-            // Success
-            ALOGV("Reduced cache to %zu", totalCacheSize);
-            return true;
-        } else {
-            ALOGD("Cache size is still too large (%zu), removing more files", totalCacheSize);
-        }
-    }
-
-    // Should never reach this return
-    return false;
-}
-
-} // namespace
-
-namespace android {
-
-void setBlobMultifile(const void* key, EGLsizeiANDROID keySize, const void* value,
-                      EGLsizeiANDROID valueSize, const std::string& baseDir) {
-    if (baseDir.empty()) {
-        return;
-    }
-
-    setupMultifile(baseDir);
-    std::string filename = getCacheEntryFilename(key, keySize, multifileDirName);
-
-    ALOGD("Attempting to open filename for set: %s", filename.c_str());
-    std::ofstream outfile(filename, std::ofstream::binary);
-    if (outfile.fail()) {
-        ALOGW("Unable to open filename: %s", filename.c_str());
-        return;
-    }
-
-    // First write the key
-    outfile.write(static_cast<const char*>(key), keySize);
-    if (outfile.bad()) {
-        ALOGW("Unable to write key to filename: %s", filename.c_str());
-        outfile.close();
-        return;
-    }
-    ALOGD("Wrote %i bytes to out file for key", static_cast<int>(outfile.tellp()));
-
-    // Then write the value
-    outfile.write(static_cast<const char*>(value), valueSize);
-    if (outfile.bad()) {
-        ALOGW("Unable to write value to filename: %s", filename.c_str());
-        outfile.close();
-        return;
-    }
-    ALOGD("Wrote %i bytes to out file for full entry", static_cast<int>(outfile.tellp()));
-
-    outfile.close();
-}
-
-EGLsizeiANDROID getBlobMultifile(const void* key, EGLsizeiANDROID keySize, void* value,
-                                 EGLsizeiANDROID valueSize, const std::string& baseDir) {
-    if (baseDir.empty()) {
-        return 0;
-    }
-
-    setupMultifile(baseDir);
-    std::string filename = getCacheEntryFilename(key, keySize, multifileDirName);
-
-    // Open the hashed filename path
-    ALOGD("Attempting to open filename for get: %s", filename.c_str());
-    int fd = open(filename.c_str(), O_RDONLY);
-
-    // File doesn't exist, this is a MISS, return zero bytes read
-    if (fd == -1) {
-        ALOGD("Cache MISS - failed to open filename: %s, error: %s", filename.c_str(),
-              std::strerror(errno));
-        return 0;
-    }
-
-    ALOGD("Cache HIT - opened filename: %s", filename.c_str());
-
-    // Get the size of the file
-    size_t entrySize = getFileSize(filename);
-    if (keySize > entrySize) {
-        ALOGW("keySize (%lu) is larger than entrySize (%zu). This is a hash collision or modified "
-              "file",
-              keySize, entrySize);
-        close(fd);
-        return 0;
-    }
-
-    // Memory map the file
-    uint8_t* cacheEntry =
-            reinterpret_cast<uint8_t*>(mmap(nullptr, entrySize, PROT_READ, MAP_PRIVATE, fd, 0));
-    if (cacheEntry == MAP_FAILED) {
-        ALOGE("Failed to mmap cacheEntry, error: %s", std::strerror(errno));
-        close(fd);
-        return 0;
-    }
-
-    // Compare the incoming key with our stored version (the beginning of the entry)
-    int compare = memcmp(cacheEntry, key, keySize);
-    if (compare != 0) {
-        ALOGW("Cached key and new key do not match! This is a hash collision or modified file");
-        munmap(cacheEntry, entrySize);
-        close(fd);
-        return 0;
-    }
-
-    // Keys matched, so remaining cache is value size
-    size_t cachedValueSize = entrySize - keySize;
-
-    // Return actual value size if valueSize is not large enough
-    if (cachedValueSize > valueSize) {
-        ALOGD("Skipping file read, not enough room provided (valueSize): %lu, "
-              "returning required space as %zu",
-              valueSize, cachedValueSize);
-        munmap(cacheEntry, entrySize);
-        close(fd);
-        return cachedValueSize;
-    }
-
-    // Remaining entry following the key is the value
-    uint8_t* cachedValue = cacheEntry + keySize;
-    memcpy(value, cachedValue, cachedValueSize);
-    munmap(cacheEntry, entrySize);
-    close(fd);
-
-    ALOGD("Read %zu bytes from %s", cachedValueSize, filename.c_str());
-    return cachedValueSize;
-}
-
-// Walk through the files in our flat directory, checking the size of each one.
-// Return the total size of normal files in the directory.
-// This will need to be updated if we move to a multilevel cache dir.
-size_t getMultifileCacheSize() {
-    if (multifileDirName.empty()) {
-        return 0;
-    }
-
-    DIR* dir;
-    struct dirent* entry;
-    size_t size = 0;
-
-    ALOGD("Using %s as the multifile cache dir ", multifileDirName.c_str());
-
-    if ((dir = opendir(multifileDirName.c_str())) != nullptr) {
-        while ((entry = readdir(dir)) != nullptr) {
-            if (entry->d_name == "."s || entry->d_name == ".."s) {
-                continue;
-            }
-
-            // Add up the size of all files in the dir
-            std::string fullPath = multifileDirName + "/" + entry->d_name;
-            size += getFileSize(fullPath);
-        }
-        closedir(dir);
-    } else {
-        ALOGW("Unable to open filename: %s", multifileDirName.c_str());
-        return 0;
-    }
-
-    return size;
-}
-
-// When removing files, what fraction of the overall limit should be reached when removing files
-// A divisor of two will decrease the cache to 50%, four to 25% and so on
-constexpr uint32_t kCacheLimitDivisor = 2;
-
-// Calculate the cache size and remove old entries until under the limit
-void checkMultifileCacheSize(size_t cacheByteLimit) {
-    // Start with the value provided by egl_cache
-    size_t limit = cacheByteLimit;
-
-    // Check for a debug value
-    int debugCacheSize = base::GetIntProperty("debug.egl.blobcache.bytelimit", -1);
-    if (debugCacheSize >= 0) {
-        ALOGV("Overriding cache limit %zu with %i from debug.egl.blobcache.bytelimit", limit,
-              debugCacheSize);
-        limit = debugCacheSize;
-    }
-
-    // Tally up the initial amount of cache in use
-    size_t size = getMultifileCacheSize();
-    ALOGD("Multifile cache dir size: %zu", size);
-
-    // If size is larger than the threshold, remove files using LRU
-    if (size > limit) {
-        ALOGV("Multifile cache size is larger than %zu, removing old entries", cacheByteLimit);
-        if (!applyLRU(limit / kCacheLimitDivisor)) {
-            ALOGE("Error when clearing multifile shader cache");
-            return;
-        }
-    }
-    ALOGD("Multifile cache size after reduction: %zu", getMultifileCacheSize());
-}
-
-}; // namespace android
\ No newline at end of file
diff --git a/opengl/libs/EGL/egl_cache_multifile.h b/opengl/libs/EGL/egl_cache_multifile.h
deleted file mode 100644
index ee5fe81..0000000
--- a/opengl/libs/EGL/egl_cache_multifile.h
+++ /dev/null
@@ -1,36 +0,0 @@
-/*
- ** Copyright 2022, 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.
- */
-
-#ifndef ANDROID_EGL_CACHE_MULTIFILE_H
-#define ANDROID_EGL_CACHE_MULTIFILE_H
-
-#include <EGL/egl.h>
-#include <EGL/eglext.h>
-
-#include <string>
-
-namespace android {
-
-void setBlobMultifile(const void* key, EGLsizeiANDROID keySize, const void* value,
-                      EGLsizeiANDROID valueSize, const std::string& baseDir);
-EGLsizeiANDROID getBlobMultifile(const void* key, EGLsizeiANDROID keySize, void* value,
-                                 EGLsizeiANDROID valueSize, const std::string& baseDir);
-size_t getMultifileCacheSize();
-void checkMultifileCacheSize(size_t cacheByteLimit);
-
-}; // namespace android
-
-#endif // ANDROID_EGL_CACHE_MULTIFILE_H
diff --git a/opengl/tests/EGLTest/egl_cache_test.cpp b/opengl/tests/EGLTest/egl_cache_test.cpp
index 265bec4..32e408c 100644
--- a/opengl/tests/EGLTest/egl_cache_test.cpp
+++ b/opengl/tests/EGLTest/egl_cache_test.cpp
@@ -24,7 +24,7 @@
 #include <android-base/test_utils.h>
 
 #include "egl_cache.h"
-#include "egl_cache_multifile.h"
+#include "MultifileBlobCache.h"
 #include "egl_display.h"
 
 #include <memory>
@@ -33,12 +33,16 @@
 
 namespace android {
 
-class EGLCacheTest : public ::testing::Test {
+class EGLCacheTest : public ::testing::TestWithParam<egl_cache_t::EGLCacheMode> {
 protected:
     virtual void SetUp() {
-        mCache = egl_cache_t::get();
+        // Terminate to clean up any previous cache in this process
+        mCache->terminate();
+
         mTempFile.reset(new TemporaryFile());
         mCache->setCacheFilename(&mTempFile->path[0]);
+        mCache->setCacheLimit(1024);
+        mCache->setCacheMode(mCacheMode);
     }
 
     virtual void TearDown() {
@@ -49,11 +53,12 @@
 
     std::string getCachefileName();
 
-    egl_cache_t* mCache;
+    egl_cache_t* mCache = egl_cache_t::get();
     std::unique_ptr<TemporaryFile> mTempFile;
+    egl_cache_t::EGLCacheMode mCacheMode = GetParam();
 };
 
-TEST_F(EGLCacheTest, UninitializedCacheAlwaysMisses) {
+TEST_P(EGLCacheTest, UninitializedCacheAlwaysMisses) {
     uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee };
     mCache->setBlob("abcd", 4, "efgh", 4);
     ASSERT_EQ(0, mCache->getBlob("abcd", 4, buf, 4));
@@ -63,7 +68,7 @@
     ASSERT_EQ(0xee, buf[3]);
 }
 
-TEST_F(EGLCacheTest, InitializedCacheAlwaysHits) {
+TEST_P(EGLCacheTest, InitializedCacheAlwaysHits) {
     uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee };
     mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY));
     mCache->setBlob("abcd", 4, "efgh", 4);
@@ -74,7 +79,7 @@
     ASSERT_EQ('h', buf[3]);
 }
 
-TEST_F(EGLCacheTest, TerminatedCacheAlwaysMisses) {
+TEST_P(EGLCacheTest, TerminatedCacheAlwaysMisses) {
     uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee };
     mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY));
     mCache->setBlob("abcd", 4, "efgh", 4);
@@ -86,7 +91,7 @@
     ASSERT_EQ(0xee, buf[3]);
 }
 
-TEST_F(EGLCacheTest, ReinitializedCacheContainsValues) {
+TEST_P(EGLCacheTest, ReinitializedCacheContainsValues) {
     uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee };
     mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY));
     mCache->setBlob("abcd", 4, "efgh", 4);
@@ -101,12 +106,12 @@
 
 std::string EGLCacheTest::getCachefileName() {
     // Return the monolithic filename unless we find the multifile dir
-    std::string cachefileName = &mTempFile->path[0];
-    std::string multifileDirName = cachefileName + ".multifile";
+    std::string cachePath = &mTempFile->path[0];
+    std::string multifileDirName = cachePath + ".multifile";
+    std::string cachefileName = "";
 
     struct stat info;
     if (stat(multifileDirName.c_str(), &info) == 0) {
-
         // Ensure we only have one file to manage
         int realFileCount = 0;
 
@@ -121,6 +126,8 @@
                 cachefileName = multifileDirName + "/" + entry->d_name;
                 realFileCount++;
             }
+        } else {
+            printf("Unable to open %s, error: %s\n", multifileDirName.c_str(), std::strerror(errno));
         }
 
         if (realFileCount != 1) {
@@ -128,14 +135,18 @@
             // violates test assumptions
             cachefileName = "";
         }
+    } else {
+        printf("Unable to stat %s, error: %s\n", multifileDirName.c_str(), std::strerror(errno));
     }
 
     return cachefileName;
 }
 
-TEST_F(EGLCacheTest, ModifiedCacheMisses) {
-    // Turn this back on if multifile becomes the default
-    GTEST_SKIP() << "Skipping test designed for multifile, see b/263574392 and b/246966894";
+TEST_P(EGLCacheTest, ModifiedCacheMisses) {
+    // Skip if not in multifile mode
+    if (mCacheMode == egl_cache_t::EGLCacheMode::Monolithic) {
+        GTEST_SKIP() << "Skipping test designed for multifile";
+    }
 
     uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee };
     mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY));
@@ -147,13 +158,13 @@
     ASSERT_EQ('g', buf[2]);
     ASSERT_EQ('h', buf[3]);
 
+    // Ensure the cache file is written to disk
+    mCache->terminate();
+
     // Depending on the cache mode, the file will be in different locations
     std::string cachefileName = getCachefileName();
     ASSERT_TRUE(cachefileName.length() > 0);
 
-    // Ensure the cache file is written to disk
-    mCache->terminate();
-
     // Stomp on the beginning of the cache file, breaking the key match
     const long stomp = 0xbadf00d;
     FILE *file = fopen(cachefileName.c_str(), "w");
@@ -164,14 +175,15 @@
     // Ensure no cache hit
     mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY));
     uint8_t buf2[4] = { 0xee, 0xee, 0xee, 0xee };
-    ASSERT_EQ(0, mCache->getBlob("abcd", 4, buf2, 4));
+    // getBlob may return junk for required size, but should not return a cache hit
+    mCache->getBlob("abcd", 4, buf2, 4);
     ASSERT_EQ(0xee, buf2[0]);
     ASSERT_EQ(0xee, buf2[1]);
     ASSERT_EQ(0xee, buf2[2]);
     ASSERT_EQ(0xee, buf2[3]);
 }
 
-TEST_F(EGLCacheTest, TerminatedCacheBelowCacheLimit) {
+TEST_P(EGLCacheTest, TerminatedCacheBelowCacheLimit) {
     uint8_t buf[4] = { 0xee, 0xee, 0xee, 0xee };
     mCache->initialize(egl_display_t::get(EGL_DEFAULT_DISPLAY));
 
@@ -204,4 +216,6 @@
     ASSERT_LE(mCache->getCacheSize(), 4);
 }
 
+INSTANTIATE_TEST_CASE_P(MonolithicCacheTests, EGLCacheTest, ::testing::Values(egl_cache_t::EGLCacheMode::Monolithic));
+INSTANTIATE_TEST_CASE_P(MultifileCacheTests, EGLCacheTest, ::testing::Values(egl_cache_t::EGLCacheMode::Multifile));
 }
diff --git a/services/inputflinger/include/InputReaderBase.h b/services/inputflinger/include/InputReaderBase.h
index 2173117..841c914 100644
--- a/services/inputflinger/include/InputReaderBase.h
+++ b/services/inputflinger/include/InputReaderBase.h
@@ -333,6 +333,9 @@
     // stylus button state changes are reported through motion events.
     bool stylusButtonMotionEventsEnabled;
 
+    // True if a pointer icon should be shown for direct stylus pointers.
+    bool stylusPointerIconEnabled;
+
     InputReaderConfiguration()
           : virtualKeyQuietTime(0),
             pointerVelocityControlParameters(1.0f, 500.0f, 3000.0f,
@@ -358,7 +361,8 @@
             touchpadNaturalScrollingEnabled(true),
             touchpadTapToClickEnabled(true),
             touchpadRightClickZoneEnabled(false),
-            stylusButtonMotionEventsEnabled(true) {}
+            stylusButtonMotionEventsEnabled(true),
+            stylusPointerIconEnabled(false) {}
 
     static std::string changesToString(uint32_t changes);
 
diff --git a/services/inputflinger/include/PointerControllerInterface.h b/services/inputflinger/include/PointerControllerInterface.h
index 7e0c1c7..9dbdd5a 100644
--- a/services/inputflinger/include/PointerControllerInterface.h
+++ b/services/inputflinger/include/PointerControllerInterface.h
@@ -79,8 +79,10 @@
         POINTER,
         // Show spots and a spot anchor in place of the mouse pointer.
         SPOT,
+        // Show the stylus hover pointer.
+        STYLUS_HOVER,
 
-        ftl_last = SPOT,
+        ftl_last = STYLUS_HOVER,
     };
 
     /* Sets the mode of the pointer controller. */
diff --git a/services/inputflinger/reader/mapper/TouchInputMapper.cpp b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
index d415854..b789156 100644
--- a/services/inputflinger/reader/mapper/TouchInputMapper.cpp
+++ b/services/inputflinger/reader/mapper/TouchInputMapper.cpp
@@ -977,12 +977,18 @@
         mOrientedRanges.clear();
     }
 
-    // Create pointer controller if needed, and keep it around if Pointer Capture is enabled to
-    // preserve the cursor position.
-    if (mDeviceMode == DeviceMode::POINTER ||
-        (mDeviceMode == DeviceMode::DIRECT && mConfig.showTouches) ||
-        (mParameters.deviceType == Parameters::DeviceType::POINTER &&
-         mConfig.pointerCaptureRequest.enable)) {
+    // Create and preserve the pointer controller in the following cases:
+    const bool isPointerControllerNeeded =
+            // - when the device is in pointer mode, to show the mouse cursor;
+            (mDeviceMode == DeviceMode::POINTER) ||
+            // - when pointer capture is enabled, to preserve the mouse cursor position;
+            (mParameters.deviceType == Parameters::DeviceType::POINTER &&
+             mConfig.pointerCaptureRequest.enable) ||
+            // - when we should be showing touches;
+            (mDeviceMode == DeviceMode::DIRECT && mConfig.showTouches) ||
+            // - when we should be showing a pointer icon for direct styluses.
+            (mDeviceMode == DeviceMode::DIRECT && mConfig.stylusPointerIconEnabled && hasStylus());
+    if (isPointerControllerNeeded) {
         if (mPointerController == nullptr) {
             mPointerController = getContext()->getPointerController(getDeviceId());
         }
@@ -3650,6 +3656,14 @@
     return out;
 }
 
+static bool isStylusEvent(uint32_t source, int32_t action, const PointerProperties* properties) {
+    if (!isFromSource(source, AINPUT_SOURCE_STYLUS)) {
+        return false;
+    }
+    const auto actionIndex = action >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT;
+    return isStylusToolType(properties[actionIndex].toolType);
+}
+
 NotifyMotionArgs TouchInputMapper::dispatchMotion(
         nsecs_t when, nsecs_t readTime, uint32_t policyFlags, uint32_t source, int32_t action,
         int32_t actionButton, int32_t flags, int32_t metaState, int32_t buttonState,
@@ -3691,12 +3705,35 @@
             ALOG_ASSERT(false);
         }
     }
+
+    const int32_t displayId = getAssociatedDisplayId().value_or(ADISPLAY_ID_NONE);
+    const bool showDirectStylusPointer = mConfig.stylusPointerIconEnabled &&
+            mDeviceMode == DeviceMode::DIRECT && isStylusEvent(source, action, pointerProperties) &&
+            mPointerController && displayId != ADISPLAY_ID_NONE &&
+            displayId == mPointerController->getDisplayId();
+    if (showDirectStylusPointer) {
+        switch (action & AMOTION_EVENT_ACTION_MASK) {
+            case AMOTION_EVENT_ACTION_HOVER_ENTER:
+            case AMOTION_EVENT_ACTION_HOVER_MOVE:
+                mPointerController->setPresentation(
+                        PointerControllerInterface::Presentation::STYLUS_HOVER);
+                mPointerController
+                        ->setPosition(mCurrentCookedState.cookedPointerData.pointerCoords[0].getX(),
+                                      mCurrentCookedState.cookedPointerData.pointerCoords[0]
+                                              .getY());
+                mPointerController->unfade(PointerControllerInterface::Transition::IMMEDIATE);
+                break;
+            case AMOTION_EVENT_ACTION_HOVER_EXIT:
+                mPointerController->fade(PointerControllerInterface::Transition::IMMEDIATE);
+                break;
+        }
+    }
+
     float xCursorPosition = AMOTION_EVENT_INVALID_CURSOR_POSITION;
     float yCursorPosition = AMOTION_EVENT_INVALID_CURSOR_POSITION;
     if (mDeviceMode == DeviceMode::POINTER) {
         mPointerController->getPosition(&xCursorPosition, &yCursorPosition);
     }
-    const int32_t displayId = getAssociatedDisplayId().value_or(ADISPLAY_ID_NONE);
     const int32_t deviceId = getDeviceId();
     std::vector<TouchVideoFrame> frames = getDeviceContext().getVideoFrames();
     std::for_each(frames.begin(), frames.end(),
diff --git a/services/inputflinger/tests/FakeInputReaderPolicy.cpp b/services/inputflinger/tests/FakeInputReaderPolicy.cpp
index bb8a30e..30c1719 100644
--- a/services/inputflinger/tests/FakeInputReaderPolicy.cpp
+++ b/services/inputflinger/tests/FakeInputReaderPolicy.cpp
@@ -205,6 +205,10 @@
     mConfig.stylusButtonMotionEventsEnabled = enabled;
 }
 
+void FakeInputReaderPolicy::setStylusPointerIconEnabled(bool enabled) {
+    mConfig.stylusPointerIconEnabled = enabled;
+}
+
 void FakeInputReaderPolicy::getReaderConfiguration(InputReaderConfiguration* outConfig) {
     *outConfig = mConfig;
 }
diff --git a/services/inputflinger/tests/FakeInputReaderPolicy.h b/services/inputflinger/tests/FakeInputReaderPolicy.h
index 9ec3217..28ac505 100644
--- a/services/inputflinger/tests/FakeInputReaderPolicy.h
+++ b/services/inputflinger/tests/FakeInputReaderPolicy.h
@@ -76,6 +76,7 @@
     float getPointerGestureZoomSpeedRatio();
     void setVelocityControlParams(const VelocityControlParameters& params);
     void setStylusButtonMotionEventsEnabled(bool enabled);
+    void setStylusPointerIconEnabled(bool enabled);
 
 private:
     void getReaderConfiguration(InputReaderConfiguration* outConfig) override;
diff --git a/services/inputflinger/tests/FakePointerController.cpp b/services/inputflinger/tests/FakePointerController.cpp
index ab7879f..28dad95 100644
--- a/services/inputflinger/tests/FakePointerController.cpp
+++ b/services/inputflinger/tests/FakePointerController.cpp
@@ -65,6 +65,10 @@
     ASSERT_NEAR(y, actualY, 1);
 }
 
+bool FakePointerController::isPointerShown() {
+    return mIsPointerShown;
+}
+
 bool FakePointerController::getBounds(float* outMinX, float* outMinY, float* outMaxX,
                                       float* outMaxY) const {
     *outMinX = mMinX;
@@ -83,6 +87,13 @@
     if (mY > mMaxY) mY = mMaxY;
 }
 
+void FakePointerController::fade(Transition) {
+    mIsPointerShown = false;
+}
+void FakePointerController::unfade(Transition) {
+    mIsPointerShown = true;
+}
+
 void FakePointerController::setSpots(const PointerCoords*, const uint32_t*, BitSet32 spotIdBits,
                                      int32_t displayId) {
     std::vector<int32_t> newSpots;
diff --git a/services/inputflinger/tests/FakePointerController.h b/services/inputflinger/tests/FakePointerController.h
index d10cbcd..dd56e65 100644
--- a/services/inputflinger/tests/FakePointerController.h
+++ b/services/inputflinger/tests/FakePointerController.h
@@ -39,12 +39,13 @@
     void setDisplayViewport(const DisplayViewport& viewport) override;
 
     void assertPosition(float x, float y);
+    bool isPointerShown();
 
 private:
     bool getBounds(float* outMinX, float* outMinY, float* outMaxX, float* outMaxY) const override;
     void move(float deltaX, float deltaY) override;
-    void fade(Transition) override {}
-    void unfade(Transition) override {}
+    void fade(Transition) override;
+    void unfade(Transition) override;
     void setPresentation(Presentation) override {}
     void setSpots(const PointerCoords*, const uint32_t*, BitSet32 spotIdBits,
                   int32_t displayId) override;
@@ -55,6 +56,7 @@
     float mX{0}, mY{0};
     int32_t mButtonState{0};
     int32_t mDisplayId{ADISPLAY_ID_DEFAULT};
+    bool mIsPointerShown{false};
 
     std::map<int32_t, std::vector<int32_t>> mSpotsByDisplay;
 };
diff --git a/services/inputflinger/tests/InputReader_test.cpp b/services/inputflinger/tests/InputReader_test.cpp
index e1c54e9..1b04375 100644
--- a/services/inputflinger/tests/InputReader_test.cpp
+++ b/services/inputflinger/tests/InputReader_test.cpp
@@ -6678,6 +6678,52 @@
     ASSERT_EQ(AINPUT_SOURCE_TOUCH_NAVIGATION, mapper.getSources());
 }
 
+TEST_F(SingleTouchInputMapperTest, Process_WhenConfigEnabled_ShouldShowDirectStylusPointer) {
+    std::shared_ptr<FakePointerController> fakePointerController =
+            std::make_shared<FakePointerController>();
+    addConfigurationProperty("touch.deviceType", "touchScreen");
+    prepareDisplay(ui::ROTATION_0);
+    prepareButtons();
+    prepareAxes(POSITION);
+    mFakeEventHub->addKey(EVENTHUB_ID, BTN_TOOL_PEN, 0, AKEYCODE_UNKNOWN, 0);
+    mFakePolicy->setPointerController(fakePointerController);
+    mFakePolicy->setStylusPointerIconEnabled(true);
+    SingleTouchInputMapper& mapper = addMapperAndConfigure<SingleTouchInputMapper>();
+
+    processKey(mapper, BTN_TOOL_PEN, 1);
+    processMove(mapper, 100, 200);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS),
+                  WithPointerCoords(0, toDisplayX(100), toDisplayY(200)))));
+    ASSERT_TRUE(fakePointerController->isPointerShown());
+    ASSERT_NO_FATAL_FAILURE(
+            fakePointerController->assertPosition(toDisplayX(100), toDisplayY(200)));
+}
+
+TEST_F(SingleTouchInputMapperTest, Process_WhenConfigDisabled_ShouldNotShowDirectStylusPointer) {
+    std::shared_ptr<FakePointerController> fakePointerController =
+            std::make_shared<FakePointerController>();
+    addConfigurationProperty("touch.deviceType", "touchScreen");
+    prepareDisplay(ui::ROTATION_0);
+    prepareButtons();
+    prepareAxes(POSITION);
+    mFakeEventHub->addKey(EVENTHUB_ID, BTN_TOOL_PEN, 0, AKEYCODE_UNKNOWN, 0);
+    mFakePolicy->setPointerController(fakePointerController);
+    mFakePolicy->setStylusPointerIconEnabled(false);
+    SingleTouchInputMapper& mapper = addMapperAndConfigure<SingleTouchInputMapper>();
+
+    processKey(mapper, BTN_TOOL_PEN, 1);
+    processMove(mapper, 100, 200);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS),
+                  WithPointerCoords(0, toDisplayX(100), toDisplayY(200)))));
+    ASSERT_FALSE(fakePointerController->isPointerShown());
+}
+
 // --- TouchDisplayProjectionTest ---
 
 class TouchDisplayProjectionTest : public SingleTouchInputMapperTest {
@@ -9757,6 +9803,58 @@
                   WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS))));
 }
 
+TEST_F(MultiTouchInputMapperTest, Process_WhenConfigEnabled_ShouldShowDirectStylusPointer) {
+    addConfigurationProperty("touch.deviceType", "touchScreen");
+    prepareDisplay(ui::ROTATION_0);
+    prepareAxes(POSITION | ID | SLOT | TOOL_TYPE | PRESSURE);
+    // Add BTN_TOOL_PEN to statically show stylus support, since using ABS_MT_TOOL_TYPE can only
+    // indicate stylus presence dynamically.
+    mFakeEventHub->addKey(EVENTHUB_ID, BTN_TOOL_PEN, 0, AKEYCODE_UNKNOWN, 0);
+    std::shared_ptr<FakePointerController> fakePointerController =
+            std::make_shared<FakePointerController>();
+    mFakePolicy->setPointerController(fakePointerController);
+    mFakePolicy->setStylusPointerIconEnabled(true);
+    MultiTouchInputMapper& mapper = addMapperAndConfigure<MultiTouchInputMapper>();
+
+    processId(mapper, FIRST_TRACKING_ID);
+    processPressure(mapper, RAW_PRESSURE_MIN);
+    processPosition(mapper, 100, 200);
+    processToolType(mapper, MT_TOOL_PEN);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS),
+                  WithPointerCoords(0, toDisplayX(100), toDisplayY(200)))));
+    ASSERT_TRUE(fakePointerController->isPointerShown());
+    ASSERT_NO_FATAL_FAILURE(
+            fakePointerController->assertPosition(toDisplayX(100), toDisplayY(200)));
+}
+
+TEST_F(MultiTouchInputMapperTest, Process_WhenConfigDisabled_ShouldNotShowDirectStylusPointer) {
+    addConfigurationProperty("touch.deviceType", "touchScreen");
+    prepareDisplay(ui::ROTATION_0);
+    prepareAxes(POSITION | ID | SLOT | TOOL_TYPE | PRESSURE);
+    // Add BTN_TOOL_PEN to statically show stylus support, since using ABS_MT_TOOL_TYPE can only
+    // indicate stylus presence dynamically.
+    mFakeEventHub->addKey(EVENTHUB_ID, BTN_TOOL_PEN, 0, AKEYCODE_UNKNOWN, 0);
+    std::shared_ptr<FakePointerController> fakePointerController =
+            std::make_shared<FakePointerController>();
+    mFakePolicy->setPointerController(fakePointerController);
+    mFakePolicy->setStylusPointerIconEnabled(false);
+    MultiTouchInputMapper& mapper = addMapperAndConfigure<MultiTouchInputMapper>();
+
+    processId(mapper, FIRST_TRACKING_ID);
+    processPressure(mapper, RAW_PRESSURE_MIN);
+    processPosition(mapper, 100, 200);
+    processToolType(mapper, MT_TOOL_PEN);
+    processSync(mapper);
+    ASSERT_NO_FATAL_FAILURE(mFakeListener->assertNotifyMotionWasCalled(
+            AllOf(WithMotionAction(AMOTION_EVENT_ACTION_HOVER_ENTER),
+                  WithToolType(AMOTION_EVENT_TOOL_TYPE_STYLUS),
+                  WithPointerCoords(0, toDisplayX(100), toDisplayY(200)))));
+    ASSERT_FALSE(fakePointerController->isPointerShown());
+}
+
 // --- MultiTouchInputMapperTest_ExternalDevice ---
 
 class MultiTouchInputMapperTest_ExternalDevice : public MultiTouchInputMapperTest {