Update sdk derivation logic

Add logic to read the extension database and update the logic that
figures out the current SDK level based on the modules present.

For each extension (R, S etc), we find the highest version where all the
modules in that extension fulfil the requirements, and set the extension
sysprops accordingly.

This follows the design in go/mainline-sdk-versioning-impl.

Update the derive_sdk_test with a few more interesting cases (though no
multi-extension tests yet, that's coming next).

For the java tests, add a database that is compatible with the existing
test apexes. Future changes will add more apex versions into the mix to
make the tests more interesting.

Bug: 173188089
Test: atest --test-mapping packages/modules/SdkExtensions
Change-Id: I8b550f18c19330b65d250ffc6d7db43976cf6f80
diff --git a/Android.bp b/Android.bp
index be282ed..6d8a3c6 100644
--- a/Android.bp
+++ b/Android.bp
@@ -21,7 +21,10 @@
     defaults: [ "com.android.sdkext-defaults" ],
     binaries: [ "derive_sdk" ],
     java_libs: [ "framework-sdkextensions" ],
-    prebuilts: [ "cur_sdkinfo" ],
+    prebuilts: [
+        "cur_sdkinfo",
+        "extensions_db",
+    ],
     manifest: "manifest.json",
     min_sdk_version: "30",
 }
diff --git a/derive_sdk/derive_sdk.cpp b/derive_sdk/derive_sdk.cpp
index c117120..98b84a4 100644
--- a/derive_sdk/derive_sdk.cpp
+++ b/derive_sdk/derive_sdk.cpp
@@ -34,7 +34,85 @@
 namespace android {
 namespace derivesdk {
 
+static const std::unordered_map<std::string, SdkModule> kApexNameToModule = {
+    {"com.android.ipsec", SdkModule::IPSEC},
+    {"com.android.media", SdkModule::MEDIA},
+    {"com.android.mediaprovider", SdkModule::MEDIA_PROVIDER},
+    {"com.android.permission", SdkModule::PERMISSIONS},
+    {"com.android.sdkext", SdkModule::SDK_EXTENSIONS},
+    {"com.android.os.statsd", SdkModule::STATSD},
+    {"com.android.tethering", SdkModule::TETHERING},
+};
+
+static const std::unordered_set<SdkModule> kRModules = {
+    SdkModule::IPSEC,          SdkModule::MEDIA,
+    SdkModule::MEDIA_PROVIDER, SdkModule::PERMISSIONS,
+    SdkModule::SDK_EXTENSIONS, SdkModule::STATSD,
+    SdkModule::TETHERING,
+};
+
+static const std::unordered_set<SdkModule> kSModules = {};
+
+bool ReadDatabase(const std::string& db_path, ExtensionDatabase& db) {
+  std::string contents;
+  if (!android::base::ReadFileToString(db_path, &contents, true)) {
+    PLOG(ERROR) << "failed to read " << db_path << ": ";
+    return false;
+  }
+  if (!db.ParseFromString(contents)) {
+    LOG(ERROR) << "failed to parse " << db_path;
+    return false;
+  }
+  return true;
+}
+
+bool VersionRequirementsMet(
+    const ExtensionVersion& ext_version,
+    const std::unordered_set<SdkModule>& relevant_modules,
+    const std::unordered_map<SdkModule, int>& module_versions) {
+  for (const auto& requirement : ext_version.requirements()) {
+    // Only requirements on modules relevant for this extension matter.
+    if (relevant_modules.find(requirement.module()) == relevant_modules.end())
+      continue;
+
+    auto version = module_versions.find(requirement.module());
+    if (version == module_versions.end()) {
+      LOG(DEBUG) << "Not version " << ext_version.version() << ": Module "
+                 << requirement.module() << " is missing";
+      return false;
+    }
+    if (version->second < requirement.version().version()) {
+      LOG(DEBUG) << "Not version " << ext_version.version() << ": Module "
+                 << requirement.module() << " version (" << version->second
+                 << ") too low. Needed " << requirement.version().version();
+      return false;
+    }
+  }
+  return true;
+}
+
+int GetSdkLevel(const ExtensionDatabase& db,
+                const std::unordered_set<SdkModule>& relevant_modules,
+                const std::unordered_map<SdkModule, int>& module_versions) {
+  int max = 0;
+
+  for (const auto& ext_version : db.versions()) {
+    if (ext_version.version() > max &&
+        VersionRequirementsMet(ext_version, relevant_modules,
+                               module_versions)) {
+      max = ext_version.version();
+    }
+  }
+  return max;
+}
+
 bool SetSdkLevels(const std::string& mountpath) {
+  ExtensionDatabase db;
+  if (!ReadDatabase(
+          mountpath + "/com.android.sdkext/etc/extensions_db.binarypb", db)) {
+    LOG(ERROR) << "Failed to read database";
+    return false;
+  }
   std::unique_ptr<DIR, decltype(&closedir)> apex(opendir(mountpath.c_str()),
                                                  closedir);
   if (!apex) {
@@ -42,7 +120,7 @@
     return false;
   }
   struct dirent* de;
-  std::vector<std::string> paths;
+  std::unordered_map<SdkModule, int> versions;
   while ((de = readdir(apex.get()))) {
     std::string name = de->d_name;
     if (name[0] == '.' || name.find('@') != std::string::npos) {
@@ -51,13 +129,14 @@
     }
     std::string path = mountpath + "/" + name + "/etc/sdkinfo.binarypb";
     struct stat statbuf;
-    if (stat(path.c_str(), &statbuf) == 0) {
-      paths.push_back(path);
+    if (stat(path.c_str(), &statbuf) != 0) {
+      continue;
     }
-  }
-
-  std::vector<int> versions;
-  for (const auto& path : paths) {
+    auto module_itr = kApexNameToModule.find(name);
+    if (module_itr == kApexNameToModule.end()) {
+      LOG(WARNING) << "Found sdkinfo in unexpected apex " << name;
+      continue;
+    }
     std::string contents;
     if (!android::base::ReadFileToString(path, &contents, true)) {
       LOG(ERROR) << "failed to read " << path;
@@ -68,24 +147,29 @@
       LOG(ERROR) << "failed to parse " << path;
       continue;
     }
-    LOG(INFO) << "Read version " << sdk_version.version() << " from " << path;
-    versions.push_back(sdk_version.version());
+    SdkModule module = module_itr->second;
+    LOG(INFO) << "Read version " << sdk_version.version() << " from " << module;
+    versions[module] = sdk_version.version();
   }
-  auto itr = std::min_element(versions.begin(), versions.end());
-  std::string prop_value = itr == versions.end() ? "0" : std::to_string(*itr);
 
-  if (!android::base::SetProperty("build.version.extensions.r", prop_value)) {
+  int version_R = GetSdkLevel(db, kRModules, versions);
+  LOG(INFO) << "R extension version is " << version_R;
+
+  if (!android::base::SetProperty("build.version.extensions.r",
+                                  std::to_string(version_R))) {
     LOG(ERROR) << "failed to set r sdk_info prop";
     return false;
   }
   if (android::modules::sdklevel::IsAtLeastS()) {
-    if (!android::base::SetProperty("build.version.extensions.s", prop_value)) {
+    int version_S = GetSdkLevel(db, kSModules, versions);
+    LOG(INFO) << "S extension version is " << version_S;
+    if (!android::base::SetProperty("build.version.extensions.s",
+                                    std::to_string(version_S))) {
       LOG(ERROR) << "failed to set s sdk_info prop";
       return false;
     }
   }
 
-  LOG(INFO) << "Extension version is " << prop_value;
   return true;
 }
 
diff --git a/derive_sdk/derive_sdk_test.cpp b/derive_sdk/derive_sdk_test.cpp
index 0f4ca72..d4bae6d 100644
--- a/derive_sdk/derive_sdk_test.cpp
+++ b/derive_sdk/derive_sdk_test.cpp
@@ -23,6 +23,7 @@
 #include <android-base/properties.h>
 #include <android-modules-utils/sdk_level.h>
 #include <gtest/gtest.h>
+#include <stdlib.h>
 #include <sys/stat.h>
 
 #include <cstdlib>
@@ -33,18 +34,41 @@
  protected:
   void TearDown() override { android::derivesdk::SetSdkLevels("/apex"); }
 
-  std::string dir() { return std::string(dir_.path); }
+  const std::string dir() { return std::string(dir_.path); }
 
-  void MakeSdkVersion(std::string apex, int version) {
+  const std::string EtcDir(const std::string& apex) {
+    return dir() + "/" + apex + "/etc";
+  }
+
+  void AddExtensionVersion(
+      const int version,
+      const std::unordered_map<SdkModule, int>& requirements) {
+    ExtensionVersion* sdk = db_.add_versions();
+    sdk->set_version(version);
+    for (auto pair : requirements) {
+      ExtensionVersion_ModuleRequirement* req = sdk->add_requirements();
+      req->set_module(pair.first);
+      req->mutable_version()->set_version(pair.second);
+    }
+    WriteProto(db_, EtcDir("com.android.sdkext") + "/extensions_db.binarypb");
+
+    android::derivesdk::SetSdkLevels(dir());
+  }
+
+  void SetApexVersion(const std::string apex, int version) {
     SdkVersion sdk_version;
     sdk_version.set_version(version);
+    WriteProto(sdk_version, EtcDir(apex) + "/sdkinfo.binarypb");
+
+    android::derivesdk::SetSdkLevels(dir());
+  }
+
+  void WriteProto(const google::protobuf::MessageLite& proto,
+                  const std::string& path) {
     std::string buf;
-    ASSERT_TRUE(sdk_version.SerializeToString(&buf));
-    std::string path = dir() + "/" + apex;
-    ASSERT_EQ(0, mkdir(path.c_str(), 0755));
-    path += "/etc";
-    ASSERT_EQ(0, mkdir(path.c_str(), 0755));
-    path += "/sdkinfo.binarypb";
+    proto.SerializeToString(&buf);
+    std::string cmd("mkdir -p " + path.substr(0, path.find_last_of('/')));
+    ASSERT_EQ(0, system(cmd.c_str()));
     ASSERT_TRUE(android::base::WriteStringToFile(buf, path, true));
   }
 
@@ -59,6 +83,7 @@
     EXPECT_EQ(S, android::modules::sdklevel::IsAtLeastS() ? n : -1);
   }
 
+  ExtensionDatabase db_;
   TemporaryDir dir_;
 };
 
@@ -67,26 +92,62 @@
   EXPECT_S(0);
 }
 
-TEST_F(DeriveSdkTest, OneApex) {
-  MakeSdkVersion("a", 3);
+TEST_F(DeriveSdkTest, OneDessert_OneVersion_OneApex) {
+  AddExtensionVersion(3, {{SdkModule::SDK_EXTENSIONS, 2}});
+  EXPECT_S(3);
 
-  android::derivesdk::SetSdkLevels(dir());
+  SetApexVersion("com.android.sdkext", 3);
 
   EXPECT_R(3);
   EXPECT_S(3);
 }
 
-TEST_F(DeriveSdkTest, TwoApexes) {
-  MakeSdkVersion("a", 3);
-  MakeSdkVersion("b", 5);
+TEST_F(DeriveSdkTest, OneDessert_OneVersion_TwoApexes) {
+  AddExtensionVersion(5, {
+                             {SdkModule::MEDIA, 5},
+                             {SdkModule::SDK_EXTENSIONS, 2},
+                         });
+  EXPECT_R(0);
+  EXPECT_S(5);
 
-  android::derivesdk::SetSdkLevels(dir());
-
-  EXPECT_R(3);
-  EXPECT_S(3);
+  SetApexVersion("com.android.sdkext", 2);
+  EXPECT_R(0);
+  SetApexVersion("com.android.media", 5);
+  EXPECT_R(5);
 }
 
-int main(int argc, char **argv) {
+TEST_F(DeriveSdkTest, OneDessert_ManyVersions) {
+  AddExtensionVersion(1, {
+                             {SdkModule::MEDIA, 1},
+                         });
+  AddExtensionVersion(2, {
+                             {SdkModule::MEDIA, 1},
+                             {SdkModule::MEDIA_PROVIDER, 2},
+                             {SdkModule::SDK_EXTENSIONS, 2},
+                         });
+  AddExtensionVersion(3, {
+                             {SdkModule::MEDIA, 3},
+                             {SdkModule::MEDIA_PROVIDER, 2},
+                             {SdkModule::SDK_EXTENSIONS, 3},
+                         });
+  EXPECT_R(0);
+  EXPECT_S(3);
+
+  SetApexVersion("com.android.media", 1);
+  EXPECT_R(1);
+
+  SetApexVersion("com.android.mediaprovider", 2);
+  EXPECT_R(1);
+  SetApexVersion("com.android.sdkext", 2);
+  EXPECT_R(2);
+
+  SetApexVersion("com.android.media", 3);
+  EXPECT_R(2);
+  SetApexVersion("com.android.sdkext", 3);
+  EXPECT_R(3);
+}
+
+int main(int argc, char** argv) {
   ::testing::InitGoogleTest(&argc, argv);
   return RUN_ALL_TESTS();
 }
diff --git a/gen_sdk/Android.bp b/gen_sdk/Android.bp
index c5890fb..e2e876d 100644
--- a/gen_sdk/Android.bp
+++ b/gen_sdk/Android.bp
@@ -43,3 +43,10 @@
     tools: ["gen_sdk"],
     cmd: "$(location gen_sdk) --action print_binary --database $(location extensions_db.textpb) > $(out)",
 }
+
+prebuilt_etc {
+    name: "extensions_db",
+    src: ":extensions_db.binarypb",
+    filename: "extensions_db.binarypb",
+    installable: false,
+}
diff --git a/testing/Android.bp b/testing/Android.bp
index defe2a0..ff34809 100644
--- a/testing/Android.bp
+++ b/testing/Android.bp
@@ -17,7 +17,10 @@
     defaults: ["com.android.sdkext-defaults"],
     java_libs: [ "test_framework-sdkextensions" ],
     manifest: "test_manifest.json",
-    prebuilts: [ "sdkinfo_45" ],
+    prebuilts: [
+        "sdkinfo_45",
+        "test_extensions_db",
+    ],
     file_contexts: ":com.android.sdkext-file_contexts",
     installable: false, // Should never be installed on the systemimage
     multilib: {
@@ -49,6 +52,23 @@
     ],
 }
 
+genrule {
+    name: "test_extensions_db.binarypb",
+    srcs: ["test_extensions_db.textpb"],
+    out: ["test_extensions_db.binarypb"],
+    tools: ["gen_sdk"],
+    cmd: "$(location gen_sdk) --action print_binary --database $(location test_extensions_db.textpb) > $(out)",
+    visibility: ["//visibility:private"],
+}
+
+prebuilt_etc {
+    name: "test_extensions_db",
+    src: ":test_extensions_db.binarypb",
+    filename: "extensions_db.binarypb",
+    installable: false,
+    visibility: ["//visibility:private"],
+}
+
 filegroup {
     name: "test_framework-sdkextensions-sources",
     srcs: ["impl-src/**/*.java"],
diff --git a/testing/test_extensions_db.textpb b/testing/test_extensions_db.textpb
new file mode 100644
index 0000000..e02680c
--- /dev/null
+++ b/testing/test_extensions_db.textpb
@@ -0,0 +1,24 @@
+versions {
+  version: 12
+  requirements {
+    module: SDK_EXTENSIONS
+    version {
+      version: 45
+    }
+  }
+}
+versions {
+  version: 45
+  requirements {
+    module: SDK_EXTENSIONS
+    version {
+      version: 45
+    }
+  }
+  requirements {
+    module: MEDIA
+    version {
+      version: 45
+    }
+  }
+}
diff --git a/tests/e2e/test-src/com/android/tests/apex/sdkextensions/SdkExtensionsHostTest.java b/tests/e2e/test-src/com/android/tests/apex/sdkextensions/SdkExtensionsHostTest.java
index ea84eeb..ecee769 100644
--- a/tests/e2e/test-src/com/android/tests/apex/sdkextensions/SdkExtensionsHostTest.java
+++ b/tests/e2e/test-src/com/android/tests/apex/sdkextensions/SdkExtensionsHostTest.java
@@ -84,18 +84,16 @@
 
     @Test
     public void upgradeOneApexWithBump()  throws Exception {
-        // On the system image, sdkextensions is the only apex with sdkinfo, and it's version 0.
-        // Verify that installing a new version of it with sdk version 45 bumps the version.
+        // Version 12 requires test_com.android.sdkext.
         assertVersion0();
         mInstallUtils.installApexes(SDKEXTENSIONS_FILENAME);
         reboot();
-        assertVersion45();
+        assertVersion12();
     }
 
     @Test
     public void upgradeOneApex() throws Exception {
-        // On the system image, sdkextensions is the only apex with sdkinfo, and it's version 0.
-        // This test verifies that installing media with sdk version 45 doesn't bump the version.
+        // Version 45 requires updated sdkext and media, so updating just media changes nothing.
         assertVersion0();
         mInstallUtils.installApexes(MEDIA_FILENAME);
         reboot();
@@ -104,9 +102,7 @@
 
     @Test
     public void upgradeTwoApexes() throws Exception {
-        // On the system image, sdkextensions is the only apex with sdkinfo, and it's version 0.
-        // This test verifies that installing media with sdk version 45 *and* a new sdkext does bump
-        // the version.
+        // Updating sdkext and media bumps the version to 45.
         assertVersion0();
         mInstallUtils.installApexes(MEDIA_FILENAME, SDKEXTENSIONS_FILENAME);
         reboot();
@@ -141,6 +137,11 @@
         assertEquals("true", broadcast("MAKE_CALLS_0"));
     }
 
+    private void assertVersion12() throws Exception {
+        assertEquals(12, getExtensionVersionR());
+        assertEquals("true", broadcast("MAKE_CALLS_45")); // sdkext 45 APIs are available in 12 too.
+    }
+
     private void assertVersion45() throws Exception {
         assertEquals(45, getExtensionVersionR());
         assertEquals("true", broadcast("MAKE_CALLS_45"));