Merge "Build an apex shim with different certificate"
diff --git a/apexd/apex_shim.cpp b/apexd/apex_shim.cpp
index a896984..7394e6d 100644
--- a/apexd/apex_shim.cpp
+++ b/apexd/apex_shim.cpp
@@ -26,6 +26,7 @@
 #include <sstream>
 #include <unordered_set>
 
+#include "apex_constants.h"
 #include "apex_file.h"
 #include "string_log.h"
 
@@ -78,7 +79,7 @@
   return ss.str();
 }
 
-Result<std::vector<std::string>> ReadSha512(const std::string& path) {
+Result<std::vector<std::string>> GetAllowedHashes(const std::string& path) {
   using android::base::ReadFileToString;
   using android::base::StringPrintf;
   const std::string& file_path =
@@ -88,7 +89,14 @@
   if (!ReadFileToString(file_path, &hash, false /* follows symlinks */)) {
     return ErrnoError() << "Failed to read " << file_path;
   }
-  return android::base::Split(hash, "\n");
+  std::vector<std::string> allowed_hashes = android::base::Split(hash, "\n");
+  auto system_shim_hash = CalculateSha512(
+      StringPrintf("%s/%s", kApexPackageSystemDir, shim::kSystemShimApexName));
+  if (!system_shim_hash) {
+    return system_shim_hash.error();
+  }
+  allowed_hashes.push_back(std::move(*system_shim_hash));
+  return allowed_hashes;
 }
 
 Result<void> IsRegularFile(const fs::directory_entry& entry) {
@@ -205,7 +213,7 @@
                             const std::string& new_apex_path) {
   LOG(DEBUG) << "Validating update of shim apex to " << new_apex_path
              << " using system shim apex " << system_apex_path;
-  auto allowed = ReadSha512(system_apex_path);
+  auto allowed = GetAllowedHashes(system_apex_path);
   if (!allowed) {
     return allowed.error();
   }
diff --git a/apexd/apexd.cpp b/apexd/apexd.cpp
index a81a194..ceca6ec 100644
--- a/apexd/apexd.cpp
+++ b/apexd/apexd.cpp
@@ -51,6 +51,7 @@
 #include <dirent.h>
 #include <fcntl.h>
 #include <linux/loop.h>
+#include <sys/inotify.h>
 #include <sys/ioctl.h>
 #include <sys/mount.h>
 #include <sys/stat.h>
@@ -65,6 +66,7 @@
 #include <memory>
 #include <optional>
 #include <string>
+#include <thread>
 #include <unordered_map>
 #include <unordered_set>
 
@@ -1623,6 +1625,66 @@
   return 0;
 }
 
+Result<void> remountApexFile(const std::string& path) {
+  auto ret = deactivatePackage(path);
+  if (!ret) return ret.error();
+
+  ret = activatePackage(path);
+  if (!ret) return ret.error();
+
+  return {};
+}
+
+Result<void> monitorBuiltinDirs() {
+  int fd = inotify_init1(IN_CLOEXEC);
+  if (fd == -1) {
+    return ErrnoErrorf("inotify_init failed");
+  }
+  std::map<int, std::string> desc_to_dir;
+  for (const auto& dir : kApexPackageBuiltinDirs) {
+    int desc = inotify_add_watch(fd, dir.c_str(), IN_CREATE | IN_MODIFY);
+    if (desc == -1 && errno != ENOENT) {
+      // don't complain about missing directories like /product/apex
+      return ErrnoErrorf("failed to add watch on {}", dir);
+    }
+    desc_to_dir.emplace(desc, dir);
+  }
+  static std::thread th([fd, desc_to_dir]() -> void {
+    constexpr int num_events = 100;
+    constexpr size_t average_path_length = 50;
+    char buffer[num_events *
+                (sizeof(struct inotify_event) + average_path_length)];
+    while (true) {
+      ssize_t length = read(fd, buffer, sizeof(buffer));
+      if (length < 0) {
+        PLOG(ERROR) << "failed to read inotify event: " << strerror(errno);
+        continue;
+      }
+      int i = 0;
+      while (i < length) {
+        struct inotify_event* e = (struct inotify_event*)&buffer[i];
+        if (e->len > 0 && (e->mask & (IN_CREATE | IN_MODIFY)) != 0) {
+          if (desc_to_dir.find(e->wd) == desc_to_dir.end()) {
+            LOG(ERROR) << "unexpected watch descriptor " << e->wd
+                       << " for name: " << e->name;
+          } else {
+            std::string path = desc_to_dir.at(e->wd) + "/" + e->name;
+            auto ret = remountApexFile(path);
+            if (!ret) {
+              LOG(ERROR) << ret.error().message();
+            } else {
+              LOG(INFO) << path << " remounted because it was changed";
+            }
+          }
+        }
+        i += sizeof(struct inotify_event) + e->len;
+      }
+    }
+  });
+
+  return {};
+}
+
 void onStart(CheckpointInterface* checkpoint_service) {
   LOG(INFO) << "Marking APEXd as starting";
   if (!android::base::SetProperty(kApexStatusSysprop, kApexStatusStarting)) {
@@ -1709,6 +1771,13 @@
                  << status.error();
     }
   }
+
+  if (android::base::GetBoolProperty("ro.debuggable", false)) {
+    status = monitorBuiltinDirs();
+    if (!status) {
+      LOG(ERROR) << "cannot monitor built-in dirs: " << status.error();
+    }
+  }
 }
 
 void onAllPackagesReady() {
diff --git a/apexd/apexd_utils.h b/apexd/apexd_utils.h
index feb6a08..4d35249 100644
--- a/apexd/apexd_utils.h
+++ b/apexd/apexd_utils.h
@@ -36,6 +36,7 @@
 #include "string_log.h"
 
 using android::base::ErrnoError;
+using android::base::ErrnoErrorf;
 using android::base::Error;
 using android::base::Result;
 
diff --git a/apexd/apexservice.cpp b/apexd/apexservice.cpp
index 6a8a4df..5830c4b 100644
--- a/apexd/apexservice.cpp
+++ b/apexd/apexservice.cpp
@@ -138,7 +138,8 @@
     int session_id, const std::vector<int>& child_session_ids,
     ApexInfoList* apex_info_list) {
   LOG(DEBUG) << "submitStagedSession() received by ApexService, session id "
-             << session_id;
+             << session_id << " child sessions: ["
+             << android::base::Join(child_session_ids, ',') << "]";
 
   Result<std::vector<ApexFile>> packages =
       ::android::apex::submitStagedSession(session_id, child_session_ids);
diff --git a/apexd/apexservice_test.cpp b/apexd/apexservice_test.cpp
index caa822e..e574899 100644
--- a/apexd/apexservice_test.cpp
+++ b/apexd/apexservice_test.cpp
@@ -1979,6 +1979,29 @@
   ASSERT_FALSE(IsOk(service_->submitStagedSession(42, {}, &list)));
 }
 
+TEST_F(ApexShimUpdateTest, UpdateToV1Success) {
+  PrepareTestApexForInstall installer(
+      GetTestFile("com.android.apex.cts.shim.apex"));
+
+  if (!installer.Prepare()) {
+    FAIL() << GetDebugStr(&installer);
+  }
+
+  ASSERT_TRUE(IsOk(service_->stagePackages({installer.test_file})));
+}
+
+TEST_F(ApexShimUpdateTest, SubmitStagedSessionV1ShimApexSuccess) {
+  PrepareTestApexForInstall installer(
+      GetTestFile("com.android.apex.cts.shim.apex"),
+      "/data/app-staging/session_97", "staging_data_file");
+  if (!installer.Prepare()) {
+    FAIL() << GetDebugStr(&installer);
+  }
+
+  ApexInfoList list;
+  ASSERT_TRUE(IsOk(service_->submitStagedSession(97, {}, &list)));
+}
+
 TEST_F(ApexServiceTest, SubmitStagedSessionCorruptApexFails) {
   PrepareTestApexForInstall installer(
       GetTestFile("apex.apexd_test_corrupt_apex.apex"),
diff --git a/docs/README.md b/docs/README.md
index 495ebb4..5ba0ab9 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -487,6 +487,17 @@
 <device.mk>:
 
 PRODUCT_PROPERTY_OVERRIDES += ro.apex.updatable=true
+
+BoardConfig.mk:
+TARGET_FLATTEN_APEX := false
+```
+
+or just
+
+```
+<device.mk>:
+
+$(call inherit-product, $(SRC_TARGET_DIR)/product/updatable_apex.mk)
 ```
 
 ## Flattened APEX
@@ -504,22 +515,21 @@
 Activating a flattened APEX doesn't involve the loop device. The entire
 directory `/system/apex/my.apex` is directly bind-mounted to `/apex/name@ver`.
 
-To build flattened APEXs, build the platform with the following flag.
-
-```
-BoardConfig.mk:
-
-TARGET_FLATTEN_APEX := true
-
-<device.mk>:
-
-PRODUCT_PROPERTY_OVERRIDES += ro.apex.updatable=false
-```
-
 Flattened APEXs can't be updated by downloading updated versions
 of the APEXs from network because the downloaded APEXs can't be flattened.
 Flattened APEXs can be updated only via a regular OTA.
 
+Note that flattened APEX is the default configuration for now. This means all
+APEXes are by default flattened unless you explicitly configure your device
+to support updatable APEX (explained above).
+
+Also note that, mixing flattened and non-flattened APEXes in a device is NOT
+supported. It should be either all non-flattened or all flattened. This is
+especially important when shipping pre-signed APEX prebuilts for the projects
+like Mainline. APEXes that are not pre-signed (i.e. built from the source)
+should also be non-flattened and signed with proper keys in that case. The
+device should inherit from `updatable_apex.mk` as explained above.
+
 ## Alternatives considered when developing APEX
 
 Here are some options that we considered when designing the APEX file
diff --git a/proto/Android.bp b/proto/Android.bp
index 962542b..1ec12b4 100644
--- a/proto/Android.bp
+++ b/proto/Android.bp
@@ -42,6 +42,16 @@
     },
 }
 
+java_library_static {
+    name: "apex_manifest_proto_java",
+    host_supported: true,
+    device_supported: false,
+    proto: {
+        type: "full",
+    },
+    srcs: ["apex_manifest.proto"],
+}
+
 cc_library_static {
     name: "lib_apex_session_state_proto",
     host_supported: true,
diff --git a/proto/apex_manifest.proto b/proto/apex_manifest.proto
index f9531b0..3795acb 100644
--- a/proto/apex_manifest.proto
+++ b/proto/apex_manifest.proto
@@ -18,6 +18,9 @@
 
 package apex.proto;
 
+option java_package = "com.android.apex";
+option java_outer_classname = "Protos";
+
 message ApexManifest {
 
   // Package Name
diff --git a/tests/Android.bp b/tests/Android.bp
index cabf3ad..c44acec 100644
--- a/tests/Android.bp
+++ b/tests/Android.bp
@@ -141,3 +141,18 @@
         ":com.android.apex.cts.shim.v2_prebuilt",
     ],
 }
+
+java_test_host {
+    name: "apex_remount_tests",
+    srcs:  ["src/**/ApexRemountTest.java"],
+    libs: ["tradefed"],
+    static_libs: [
+        "module_test_util",
+        "truth-prebuilt",
+        "apex_manifest_proto_java",
+    ],
+    test_config: "apex-remount-tests.xml",
+    test_suites: ["general-tests"],
+
+    data: [":com.android.apex.cts.shim.v2_prebuilt"],
+}
\ No newline at end of file
diff --git a/tests/apex-remount-tests.xml b/tests/apex-remount-tests.xml
new file mode 100644
index 0000000..50bf95f
--- /dev/null
+++ b/tests/apex-remount-tests.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2019 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.
+-->
+<configuration description="Runs the apex remount test cases">
+    <option name="test-suite-tag" value="apex_remount_tests" />
+    <option name="test-suite-tag" value="apct" />
+    <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+    <test class="com.android.tradefed.testtype.HostTest" >
+        <option name="jar" value="apex_remount_tests.jar" />
+    </test>
+</configuration>
diff --git a/tests/src/com/android/tests/apex/ApexRemountTest.java b/tests/src/com/android/tests/apex/ApexRemountTest.java
new file mode 100644
index 0000000..bb8aa59
--- /dev/null
+++ b/tests/src/com/android/tests/apex/ApexRemountTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.tests.apex;
+
+import com.android.apex.Protos.ApexManifest;
+import com.android.tests.util.ModuleTestUtils;
+import com.android.tradefed.device.DeviceNotAvailableException;
+import com.android.tradefed.testtype.DeviceJUnit4ClassRunner;
+import com.android.tradefed.testtype.junit4.BaseHostJUnit4Test;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.protobuf.InvalidProtocolBufferException;
+import com.google.protobuf.util.JsonFormat;
+
+import static org.junit.Assume.assumeTrue;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.io.File;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests for automatic remount of APEXes when they are updated via `adb sync`
+ */
+@RunWith(DeviceJUnit4ClassRunner.class)
+public class ApexRemountTest extends BaseHostJUnit4Test {
+    private File mSavedShimFile;
+
+    private static final String SHIM_APEX_PACKAGE_NAME = "com.android.apex.cts.shim";
+    private static final String SHIM_APEX_PATH = "/system/apex/com.android.apex.cts.shim.apex";
+
+    @Before
+    public void setUp() throws Exception {
+        mSavedShimFile = getDevice().pullFile(SHIM_APEX_PATH);
+    }
+
+    @After
+    public void tearDown() throws Exception {
+        if (mSavedShimFile != null) {
+            getDevice().remountSystemWritable();
+            getDevice().pushFile(mSavedShimFile, SHIM_APEX_PATH);
+            getDevice().reboot();
+            assertThat(getShimApexManifest().getVersion()).isEqualTo(1L);
+        }
+    }
+
+    @Test
+    public void testApexIsRemountedUponUpdate() throws Exception {
+        assumeTrue("APEXes on the device are flattened", hasNonFlattenedApex());
+
+        ModuleTestUtils utils = new ModuleTestUtils(this);
+        File updatedFile = utils.getTestFile("com.android.apex.cts.shim.v2.apex");
+
+        getDevice().remountSystemWritable();
+        getDevice().pushFile(updatedFile, SHIM_APEX_PATH);
+
+        // Wait some time until the update is detected by apexd and remount is done
+        TimeUnit.SECONDS.sleep(5);
+
+        assertThat(getShimApexManifest().getVersion()).isEqualTo(2L);
+    }
+
+    private ApexManifest getShimApexManifest() throws DeviceNotAvailableException,
+            InvalidProtocolBufferException {
+        String json = getDevice().executeShellCommand(
+                "cat /apex/com.android.apex.cts.shim/apex_manifest.json");
+        ApexManifest.Builder builder = ApexManifest.newBuilder();
+        JsonFormat.parser().merge(json, builder);
+        return builder.build();
+    }
+
+    private boolean hasNonFlattenedApex() throws Exception {
+        return "true".equals(getDevice().getProperty("ro.apex.updatable"));
+    }
+}