[go: nahoru, domu]

Add Uninstall UI for Crostini Apps

Add UI to handle the "uninstall" menu option when right-clicking on a
Crostini app. This change brings up a confirmation dialog; if the user
confirms, it starts up the container and sends an uninstall message.

UI mocks https://docs.google.com/presentation/d/1akjEdjidRcMH54SK9zMmQVIPaPkwVuatXuKZLJKUqzA/edit

Several known bugs:
 * chromium:909071
 * chromium:909063
 * chromium:898295
so this is behind a flag (CrostiniAppUninstallGui)

BUG=chromium:822514
TEST=Uninstalled multiple applications. Uninstalled applications at the same time for
queuing. Closed notifications while applications were uninstalling or queued. Installed
and uninstalled applications at the same time.

Change-Id: If5bb80cb867e55c3f082c40f89357f05db151985
Reviewed-on: https://chromium-review.googlesource.com/c/1275292
Commit-Queue: Ian Barkley-Yeung <iby@chromium.org>
Reviewed-by: Timothy Loh <timloh@chromium.org>
Reviewed-by: Scott Violet <sky@chromium.org>
Reviewed-by: Anand Mistry <amistry@chromium.org>
Reviewed-by: Dan Erat <derat@chromium.org>
Cr-Commit-Position: refs/heads/master@{#616567}
diff --git a/chrome/app/chromeos_strings.grdp b/chrome/app/chromeos_strings.grdp
index 381a398..e5c8729 100644
--- a/chrome/app/chromeos_strings.grdp
+++ b/chrome/app/chromeos_strings.grdp
@@ -3590,6 +3590,41 @@
     An error occurred during installation of your Linux application.
   </message>
 
+  <!-- Linux application uninstaller messages -->
+  <message name="IDS_CROSTINI_APPLICATION_UNINSTALL_CONFIRM_TITLE" desc="Title of the Crostini application uninstaller, a confirmation dialog for uninstalling a Linux application.">
+    Uninstall app?
+  </message>
+  <message name="IDS_CROSTINI_APPLICATION_UNINSTALL_CONFIRM_BODY" desc="Description for the Crostini application uninstaller, a confirmation dialog for uninstalling a Linux application.">
+    <ph name="LINUX_APP_NAME">$1<ex>GIMP</ex></ph> and the data associated with it will be removed from this device.
+  </message>
+  <message name="IDS_CROSTINI_APPLICATION_UNINSTALL_UNINSTALL_BUTTON" desc="Label for the button in the Linux application uninstaller dialog to uninstall the application">
+    Uninstall
+  </message>
+  <message name="IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_DISPLAY_SOURCE" desc="Source of the Notification for Linux application uninstallation.">
+    Linux uninstaller
+  </message>
+  <message name="IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_QUEUED_TITLE" desc="Title of the Notification when an Linux application is queued waiting for other operations to finish.">
+    <ph name="LINUX_APP_NAME">$1<ex>GIMP</ex></ph>
+  </message>
+  <message name="IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_QUEUED_MESSAGE" desc="Message of the Notification when an Linux application is queued waiting for other operations to finish.">
+    Uninstall pending
+  </message>
+  <message name="IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_IN_PROGRESS_TITLE" desc="Title of the Notification for an in-progress Linux application uninstallation.">
+    Uninstalling <ph name="LINUX_APP_NAME">$1<ex>GIMP</ex></ph>...
+  </message>
+  <message name="IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_COMPLETED_TITLE" desc="Title of the Notification for Linux application uninstallation once uninstallation has completed successfully.">
+    <ph name="LINUX_APP_NAME">$1<ex>GIMP</ex></ph>
+  </message>
+  <message name="IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_COMPLETED_MESSAGE" desc="Message of the Notification for Linux application uninstallation once uninstallation has completed successfully.">
+    Uninstall complete
+  </message>
+  <message name="IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_ERROR_TITLE" desc="Title of the Notification for Linux application uninstallation when there is an error during uninstallation.">
+    <ph name="LINUX_APP_NAME">$1<ex>GIMP</ex></ph>
+  </message>
+  <message name="IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_ERROR_MESSAGE" desc="Message in the Notification for Linux application uninstallation when there is an error during uninstallation.">
+    An error occurred during uninstallation. Please uninstall through the Terminal.
+  </message>
+
   <!-- Time limit notification -->
   <message name="IDS_SCREEN_TIME_NOTIFICATION_TITLE" desc="The title of the notification when screen usage limit reaches before locking the device.">
     Almost time for a break
diff --git a/chrome/browser/chromeos/BUILD.gn b/chrome/browser/chromeos/BUILD.gn
index 348f15e..1ca0e4d 100644
--- a/chrome/browser/chromeos/BUILD.gn
+++ b/chrome/browser/chromeos/BUILD.gn
@@ -622,10 +622,11 @@
     "crostini/crostini_mime_types_service.h",
     "crostini/crostini_mime_types_service_factory.cc",
     "crostini/crostini_mime_types_service_factory.h",
-    "crostini/crostini_package_installer_notification.cc",
-    "crostini/crostini_package_installer_notification.h",
-    "crostini/crostini_package_installer_service.cc",
-    "crostini/crostini_package_installer_service.h",
+    "crostini/crostini_package_notification.cc",
+    "crostini/crostini_package_notification.h",
+    "crostini/crostini_package_operation_status.h",
+    "crostini/crostini_package_service.cc",
+    "crostini/crostini_package_service.h",
     "crostini/crostini_pref_names.cc",
     "crostini/crostini_pref_names.h",
     "crostini/crostini_registry_service.cc",
diff --git a/chrome/browser/chromeos/crostini/crostini_manager.cc b/chrome/browser/chromeos/crostini/crostini_manager.cc
index cb093c0..2c59762e 100644
--- a/chrome/browser/chromeos/crostini/crostini_manager.cc
+++ b/chrome/browser/chromeos/crostini/crostini_manager.cc
@@ -1006,8 +1006,7 @@
     // detect when the install completes, successfully or otherwise.
     LOG(ERROR)
         << "Attempted to install package when progress signal not connected.";
-    std::move(callback).Run(CrostiniResult::INSTALL_LINUX_PACKAGE_FAILED,
-                            std::string());
+    std::move(callback).Run(CrostiniResult::INSTALL_LINUX_PACKAGE_FAILED);
     return;
   }
 
@@ -1023,6 +1022,32 @@
                      weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
 }
 
+void CrostiniManager::UninstallPackageOwningFile(
+    std::string vm_name,
+    std::string container_name,
+    std::string desktop_file_id,
+    UninstallPackageOwningFileCallback callback) {
+  if (!GetCiceroneClient()->IsUninstallPackageProgressSignalConnected()) {
+    // Technically we could still start the uninstall, but we wouldn't be able
+    // to detect when the uninstall completes, successfully or otherwise.
+    LOG(ERROR)
+        << "Attempted to uninstall package when progress signal not connected.";
+    std::move(callback).Run(CrostiniResult::UNINSTALL_PACKAGE_FAILED);
+    return;
+  }
+
+  vm_tools::cicerone::UninstallPackageOwningFileRequest request;
+  request.set_owner_id(owner_id_);
+  request.set_vm_name(std::move(vm_name));
+  request.set_container_name(std::move(container_name));
+  request.set_desktop_file_id(std::move(desktop_file_id));
+
+  GetCiceroneClient()->UninstallPackageOwningFile(
+      std::move(request),
+      base::BindOnce(&CrostiniManager::OnUninstallPackageOwningFile,
+                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+}
+
 void CrostiniManager::GetContainerSshKeys(
     std::string vm_name,
     std::string container_name,
@@ -1187,14 +1212,14 @@
   remove_crostini_callbacks_.emplace_back(std::move(remove_callback));
 }
 
-void CrostiniManager::AddInstallLinuxPackageProgressObserver(
-    InstallLinuxPackageProgressObserver* observer) {
-  install_linux_package_progress_observers_.AddObserver(observer);
+void CrostiniManager::AddLinuxPackageOperationProgressObserver(
+    LinuxPackageOperationProgressObserver* observer) {
+  linux_package_operation_progress_observers_.AddObserver(observer);
 }
 
-void CrostiniManager::RemoveInstallLinuxPackageProgressObserver(
-    InstallLinuxPackageProgressObserver* observer) {
-  install_linux_package_progress_observers_.RemoveObserver(observer);
+void CrostiniManager::RemoveLinuxPackageOperationProgressObserver(
+    LinuxPackageOperationProgressObserver* observer) {
+  linux_package_operation_progress_observers_.RemoveObserver(observer);
 }
 
 void CrostiniManager::OnCreateDiskImage(
@@ -1419,6 +1444,7 @@
       break;
     case vm_tools::cicerone::InstallLinuxPackageProgressSignal::FAILED:
       status = InstallLinuxPackageProgressStatus::FAILED;
+      LOG(ERROR) << "Install failed: " << signal.failure_details();
       break;
     case vm_tools::cicerone::InstallLinuxPackageProgressSignal::DOWNLOADING:
       status = InstallLinuxPackageProgressStatus::DOWNLOADING;
@@ -1430,13 +1456,79 @@
       NOTREACHED();
   }
 
-  for (auto& observer : install_linux_package_progress_observers_) {
-    observer.OnInstallLinuxPackageProgress(
-        signal.vm_name(), signal.container_name(), status,
-        signal.progress_percent(), signal.failure_details());
+  for (auto& observer : linux_package_operation_progress_observers_) {
+    observer.OnInstallLinuxPackageProgress(signal.vm_name(),
+                                           signal.container_name(), status,
+                                           signal.progress_percent());
   }
 }
 
+void CrostiniManager::OnUninstallPackageProgress(
+    const vm_tools::cicerone::UninstallPackageProgressSignal& signal) {
+  if (signal.owner_id() != owner_id_)
+    return;
+
+  if (signal.progress_percent() < 0 || signal.progress_percent() > 100) {
+    LOG(ERROR) << "Received uninstall progress with invalid progress of "
+               << signal.progress_percent() << "%.";
+    return;
+  }
+
+  UninstallPackageProgressStatus status;
+  switch (signal.status()) {
+    case vm_tools::cicerone::UninstallPackageProgressSignal::SUCCEEDED:
+      status = UninstallPackageProgressStatus::SUCCEEDED;
+      break;
+    case vm_tools::cicerone::UninstallPackageProgressSignal::FAILED:
+      status = UninstallPackageProgressStatus::FAILED;
+      LOG(ERROR) << "Uninstalled failed: " << signal.failure_details();
+      break;
+    case vm_tools::cicerone::UninstallPackageProgressSignal::UNINSTALLING:
+      status = UninstallPackageProgressStatus::UNINSTALLING;
+      break;
+    default:
+      NOTREACHED();
+  }
+
+  for (auto& observer : linux_package_operation_progress_observers_) {
+    observer.OnUninstallPackageProgress(signal.vm_name(),
+                                        signal.container_name(), status,
+                                        signal.progress_percent());
+  }
+}
+
+void CrostiniManager::OnUninstallPackageOwningFile(
+    UninstallPackageOwningFileCallback callback,
+    base::Optional<vm_tools::cicerone::UninstallPackageOwningFileResponse>
+        reply) {
+  if (!reply.has_value()) {
+    LOG(ERROR) << "Failed to uninstall Linux package. Empty response.";
+    std::move(callback).Run(CrostiniResult::UNINSTALL_PACKAGE_FAILED);
+    return;
+  }
+  vm_tools::cicerone::UninstallPackageOwningFileResponse response =
+      reply.value();
+
+  if (response.status() ==
+      vm_tools::cicerone::UninstallPackageOwningFileResponse::FAILED) {
+    LOG(ERROR) << "Failed to uninstall Linux package: "
+               << response.failure_reason();
+    std::move(callback).Run(CrostiniResult::UNINSTALL_PACKAGE_FAILED);
+    return;
+  }
+
+  if (response.status() ==
+      vm_tools::cicerone::UninstallPackageOwningFileResponse::
+          BLOCKING_OPERATION_IN_PROGRESS) {
+    LOG(WARNING) << "Failed to uninstall Linux package, another operation is "
+                    "already active.";
+    std::move(callback).Run(CrostiniResult::BLOCKING_OPERATION_ALREADY_ACTIVE);
+    return;
+  }
+
+  std::move(callback).Run(CrostiniResult::SUCCESS);
+}
+
 void CrostiniManager::OnCreateLxdContainer(
     std::string vm_name,
     std::string container_name,
@@ -1671,8 +1763,8 @@
     base::Optional<vm_tools::cicerone::InstallLinuxPackageResponse> reply) {
   if (!reply.has_value()) {
     LOG(ERROR) << "Failed to install Linux package. Empty response.";
-    std::move(callback).Run(CrostiniResult::LAUNCH_CONTAINER_APPLICATION_FAILED,
-                            std::string());
+    std::move(callback).Run(
+        CrostiniResult::LAUNCH_CONTAINER_APPLICATION_FAILED);
     return;
   }
   vm_tools::cicerone::InstallLinuxPackageResponse response = reply.value();
@@ -1681,20 +1773,18 @@
       vm_tools::cicerone::InstallLinuxPackageResponse::FAILED) {
     LOG(ERROR) << "Failed to install Linux package: "
                << response.failure_reason();
-    std::move(callback).Run(CrostiniResult::INSTALL_LINUX_PACKAGE_FAILED,
-                            response.failure_reason());
+    std::move(callback).Run(CrostiniResult::INSTALL_LINUX_PACKAGE_FAILED);
     return;
   }
 
   if (response.status() ==
       vm_tools::cicerone::InstallLinuxPackageResponse::INSTALL_ALREADY_ACTIVE) {
     LOG(WARNING) << "Failed to install Linux package, install already active.";
-    std::move(callback).Run(
-        CrostiniResult::INSTALL_LINUX_PACKAGE_ALREADY_ACTIVE, std::string());
+    std::move(callback).Run(CrostiniResult::BLOCKING_OPERATION_ALREADY_ACTIVE);
     return;
   }
 
-  std::move(callback).Run(CrostiniResult::SUCCESS, std::string());
+  std::move(callback).Run(CrostiniResult::SUCCESS);
 }
 
 void CrostiniManager::OnGetContainerSshKeys(
diff --git a/chrome/browser/chromeos/crostini/crostini_manager.h b/chrome/browser/chromeos/crostini/crostini_manager.h
index 468ce0c2..dbba332 100644
--- a/chrome/browser/chromeos/crostini/crostini_manager.h
+++ b/chrome/browser/chromeos/crostini/crostini_manager.h
@@ -47,7 +47,8 @@
   CONTAINER_START_FAILED,
   LAUNCH_CONTAINER_APPLICATION_FAILED,
   INSTALL_LINUX_PACKAGE_FAILED,
-  INSTALL_LINUX_PACKAGE_ALREADY_ACTIVE,
+  BLOCKING_OPERATION_ALREADY_ACTIVE,
+  UNINSTALL_PACKAGE_FAILED,
   SSHFS_MOUNT_ERROR,
   OFFLINE_WHEN_UPGRADE_REQUIRED,
   LOAD_COMPONENT_FAILED,
@@ -67,6 +68,12 @@
   STOPPING,
 };
 
+enum class UninstallPackageProgressStatus {
+  SUCCEEDED,
+  FAILED,
+  UNINSTALLING,  // In progress
+};
+
 // Return type when getting app icons from within a container.
 struct Icon {
   std::string desktop_file_id;
@@ -91,19 +98,24 @@
   std::string description;
 };
 
-class InstallLinuxPackageProgressObserver {
+class LinuxPackageOperationProgressObserver {
  public:
   // A successfully started package install will continually fire progress
   // events until it returns a status of SUCCEEDED or FAILED. The
   // |progress_percent| field is given as a percentage of the given step,
-  // DOWNLOADING or INSTALLING. |failure_reason| is returned from the container
-  // for a FAILED case, and not necessarily localized.
+  // DOWNLOADING or INSTALLING.
   virtual void OnInstallLinuxPackageProgress(
       const std::string& vm_name,
       const std::string& container_name,
       InstallLinuxPackageProgressStatus status,
-      int progress_percent,
-      const std::string& failure_reason) = 0;
+      int progress_percent) = 0;
+
+  // A successfully started package uninstall will continually fire progress
+  // events until it returns a status of SUCCEEDED or FAILED.
+  virtual void OnUninstallPackageProgress(const std::string& vm_name,
+                                          const std::string& container_name,
+                                          UninstallPackageProgressStatus status,
+                                          int progress_percent) = 0;
 };
 
 // CrostiniManager is a singleton which is used to check arguments for
@@ -152,11 +164,9 @@
   using GetLinuxPackageInfoCallback =
       base::OnceCallback<void(const LinuxPackageInfo&)>;
   // The type of the callback for CrostiniManager::InstallLinuxPackage.
-  // |failure_reason| is returned from the container upon failure
-  // (INSTALL_LINUX_PACKAGE_FAILED), and not necessarily localized.
-  using InstallLinuxPackageCallback =
-      base::OnceCallback<void(CrostiniResult result,
-                              const std::string& failure_reason)>;
+  using InstallLinuxPackageCallback = CrostiniResultCallback;
+  // The type of the callback for CrostiniManager::UninstallPackageOwningFile.
+  using UninstallPackageOwningFileCallback = CrostiniResultCallback;
   // The type of the callback for CrostiniManager::GetContainerSshKeys.
   using GetContainerSshKeysCallback =
       base::OnceCallback<void(CrostiniResult result,
@@ -319,12 +329,22 @@
 
   // Begin installation of a Linux Package inside the container. If the
   // installation is successfully started, further updates will be sent to
-  // added InstallLinuxPackageProgressObservers.
+  // added LinuxPackageOperationProgressObservers.
   void InstallLinuxPackage(std::string vm_name,
                            std::string container_name,
                            std::string package_path,
                            InstallLinuxPackageCallback callback);
 
+  // Begin uninstallation of a Linux Package inside the container. The package
+  // is identified by its associated .desktop file's ID; we don't use package_id
+  // to avoid problems with stale package_ids (such as after upgrades). If the
+  // uninstallation is successfully started, further updates will be sent to
+  // added LinuxPackageOperationProgressObservers.
+  void UninstallPackageOwningFile(std::string vm_name,
+                                  std::string container_name,
+                                  std::string desktop_file_id,
+                                  UninstallPackageOwningFileCallback callback);
+
   // Asynchronously gets SSH server public key of container and trusted SSH
   // client private key which can be used to connect to the container.
   // |callback| is called after the method call finishes.
@@ -375,11 +395,11 @@
   // Adds a callback to receive uninstall notification.
   void AddRemoveCrostiniCallback(RemoveCrostiniCallback remove_callback);
 
-  // Add/remove observers for package install progress.
-  void AddInstallLinuxPackageProgressObserver(
-      InstallLinuxPackageProgressObserver* observer);
-  void RemoveInstallLinuxPackageProgressObserver(
-      InstallLinuxPackageProgressObserver* observer);
+  // Add/remove observers for package install and uninstall progress.
+  void AddLinuxPackageOperationProgressObserver(
+      LinuxPackageOperationProgressObserver* observer);
+  void RemoveLinuxPackageOperationProgressObserver(
+      LinuxPackageOperationProgressObserver* observer);
 
   // ConciergeClient::Observer:
   void OnContainerStartupFailed(
@@ -393,6 +413,9 @@
   void OnInstallLinuxPackageProgress(
       const vm_tools::cicerone::InstallLinuxPackageProgressSignal& signal)
       override;
+  void OnUninstallPackageProgress(
+      const vm_tools::cicerone::UninstallPackageProgressSignal& signal)
+      override;
   void OnLxdContainerCreated(
       const vm_tools::cicerone::LxdContainerCreatedSignal& signal) override;
   void OnLxdContainerDownloading(
@@ -530,6 +553,12 @@
       InstallLinuxPackageCallback callback,
       base::Optional<vm_tools::cicerone::InstallLinuxPackageResponse> reply);
 
+  // Callback for CrostiniManager::UninstallPackageOwningFile.
+  void OnUninstallPackageOwningFile(
+      UninstallPackageOwningFileCallback callback,
+      base::Optional<vm_tools::cicerone::UninstallPackageOwningFileResponse>
+          reply);
+
   // Callback for CrostiniManager::GetContainerSshKeys. Called after the
   // Concierge service finishes.
   void OnGetContainerSshKeys(
@@ -591,8 +620,8 @@
 
   std::vector<RemoveCrostiniCallback> remove_crostini_callbacks_;
 
-  base::ObserverList<InstallLinuxPackageProgressObserver>::Unchecked
-      install_linux_package_progress_observers_;
+  base::ObserverList<LinuxPackageOperationProgressObserver>::Unchecked
+      linux_package_operation_progress_observers_;
 
   // Restarts by <vm_name, container_name>. Only one restarter flow is actually
   // running for a given container, other restarters will just have their
diff --git a/chrome/browser/chromeos/crostini/crostini_manager_unittest.cc b/chrome/browser/chromeos/crostini/crostini_manager_unittest.cc
index 3c4a751..2589c0b9 100644
--- a/chrome/browser/chromeos/crostini/crostini_manager_unittest.cc
+++ b/chrome/browser/chromeos/crostini/crostini_manager_unittest.cc
@@ -119,11 +119,15 @@
 
   void InstallLinuxPackageCallback(base::OnceClosure closure,
                                    CrostiniResult expected_result,
-                                   const std::string& expected_failure_reason,
-                                   CrostiniResult result,
-                                   const std::string& failure_reason) {
+                                   CrostiniResult result) {
     EXPECT_EQ(expected_result, result);
-    EXPECT_EQ(expected_failure_reason, failure_reason);
+    std::move(closure).Run();
+  }
+
+  void UninstallPackageOwningFileCallback(base::OnceClosure closure,
+                                          CrostiniResult expected_result,
+                                          CrostiniResult result) {
+    EXPECT_EQ(expected_result, result);
     std::move(closure).Run();
   }
 
@@ -314,8 +318,7 @@
       kVmName, kContainerName, "/tmp/package.deb",
       base::BindOnce(&CrostiniManagerTest::InstallLinuxPackageCallback,
                      base::Unretained(this), run_loop()->QuitClosure(),
-                     CrostiniResult::INSTALL_LINUX_PACKAGE_FAILED,
-                     std::string()));
+                     CrostiniResult::INSTALL_LINUX_PACKAGE_FAILED));
   run_loop()->Run();
 }
 
@@ -327,7 +330,7 @@
       kVmName, kContainerName, "/tmp/package.deb",
       base::BindOnce(&CrostiniManagerTest::InstallLinuxPackageCallback,
                      base::Unretained(this), run_loop()->QuitClosure(),
-                     CrostiniResult::SUCCESS, std::string()));
+                     CrostiniResult::SUCCESS));
   run_loop()->Run();
 }
 
@@ -341,8 +344,70 @@
       kVmName, kContainerName, "/tmp/package.deb",
       base::BindOnce(&CrostiniManagerTest::InstallLinuxPackageCallback,
                      base::Unretained(this), run_loop()->QuitClosure(),
-                     CrostiniResult::INSTALL_LINUX_PACKAGE_FAILED,
-                     failure_reason));
+                     CrostiniResult::INSTALL_LINUX_PACKAGE_FAILED));
+  run_loop()->Run();
+}
+
+TEST_F(CrostiniManagerTest, InstallLinuxPackageSignalOperationBlocked) {
+  vm_tools::cicerone::InstallLinuxPackageResponse response;
+  response.set_status(
+      vm_tools::cicerone::InstallLinuxPackageResponse::INSTALL_ALREADY_ACTIVE);
+  fake_cicerone_client_->set_install_linux_package_response(response);
+  crostini_manager()->InstallLinuxPackage(
+      kVmName, kContainerName, "/tmp/package.deb",
+      base::BindOnce(&CrostiniManagerTest::InstallLinuxPackageCallback,
+                     base::Unretained(this), run_loop()->QuitClosure(),
+                     CrostiniResult::BLOCKING_OPERATION_ALREADY_ACTIVE));
+  run_loop()->Run();
+}
+
+TEST_F(CrostiniManagerTest, UninstallPackageOwningFileSignalNotConnectedError) {
+  fake_cicerone_client_->set_uninstall_package_progress_signal_connected(false);
+  crostini_manager()->UninstallPackageOwningFile(
+      kVmName, kContainerName, "emacs",
+      base::BindOnce(&CrostiniManagerTest::UninstallPackageOwningFileCallback,
+                     base::Unretained(this), run_loop()->QuitClosure(),
+                     CrostiniResult::UNINSTALL_PACKAGE_FAILED));
+  run_loop()->Run();
+}
+
+TEST_F(CrostiniManagerTest, UninstallPackageOwningFileSignalSuccess) {
+  vm_tools::cicerone::UninstallPackageOwningFileResponse response;
+  response.set_status(
+      vm_tools::cicerone::UninstallPackageOwningFileResponse::STARTED);
+  fake_cicerone_client_->set_uninstall_package_owning_file_response(response);
+  crostini_manager()->UninstallPackageOwningFile(
+      kVmName, kContainerName, "emacs",
+      base::BindOnce(&CrostiniManagerTest::UninstallPackageOwningFileCallback,
+                     base::Unretained(this), run_loop()->QuitClosure(),
+                     CrostiniResult::SUCCESS));
+  run_loop()->Run();
+}
+
+TEST_F(CrostiniManagerTest, UninstallPackageOwningFileSignalFailure) {
+  vm_tools::cicerone::UninstallPackageOwningFileResponse response;
+  response.set_status(
+      vm_tools::cicerone::UninstallPackageOwningFileResponse::FAILED);
+  response.set_failure_reason("Didn't feel like it");
+  fake_cicerone_client_->set_uninstall_package_owning_file_response(response);
+  crostini_manager()->UninstallPackageOwningFile(
+      kVmName, kContainerName, "emacs",
+      base::BindOnce(&CrostiniManagerTest::UninstallPackageOwningFileCallback,
+                     base::Unretained(this), run_loop()->QuitClosure(),
+                     CrostiniResult::UNINSTALL_PACKAGE_FAILED));
+  run_loop()->Run();
+}
+
+TEST_F(CrostiniManagerTest, UninstallPackageOwningFileSignalOperationBlocked) {
+  vm_tools::cicerone::UninstallPackageOwningFileResponse response;
+  response.set_status(vm_tools::cicerone::UninstallPackageOwningFileResponse::
+                          BLOCKING_OPERATION_IN_PROGRESS);
+  fake_cicerone_client_->set_uninstall_package_owning_file_response(response);
+  crostini_manager()->UninstallPackageOwningFile(
+      kVmName, kContainerName, "emacs",
+      base::BindOnce(&CrostiniManagerTest::UninstallPackageOwningFileCallback,
+                     base::Unretained(this), run_loop()->QuitClosure(),
+                     CrostiniResult::BLOCKING_OPERATION_ALREADY_ACTIVE));
   run_loop()->Run();
 }
 
diff --git a/chrome/browser/chromeos/crostini/crostini_package_installer_notification.cc b/chrome/browser/chromeos/crostini/crostini_package_installer_notification.cc
deleted file mode 100644
index 3594aa82..0000000
--- a/chrome/browser/chromeos/crostini/crostini_package_installer_notification.cc
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "chrome/browser/chromeos/crostini/crostini_package_installer_notification.h"
-
-#include "ash/public/cpp/notification_utils.h"
-#include "ash/public/cpp/vector_icons/vector_icons.h"
-#include "chrome/browser/chromeos/crostini/crostini_package_installer_service.h"
-#include "chrome/browser/notifications/notification_display_service.h"
-#include "chrome/grit/generated_resources.h"
-#include "ui/base/l10n/l10n_util.h"
-#include "ui/message_center/public/cpp/message_center_constants.h"
-#include "ui/message_center/public/cpp/notification.h"
-#include "ui/message_center/public/cpp/notification_delegate.h"
-
-namespace crostini {
-
-namespace {
-
-constexpr char kNotifierCrostiniPackageInstaller[] =
-    "crostini.package_installer";
-
-}  // namespace
-
-CrostiniPackageInstallerNotification::CrostiniPackageInstallerNotification(
-    Profile* profile,
-    const std::string& notification_id,
-    CrostiniPackageInstallerService* installer_service)
-    : installer_service_(installer_service),
-      profile_(profile),
-      weak_ptr_factory_(this) {
-  message_center::RichNotificationData rich_notification_data;
-  rich_notification_data.vector_small_image = &ash::kNotificationLinuxIcon;
-  rich_notification_data.never_timeout = true;
-  rich_notification_data.accent_color = ash::kSystemNotificationColorNormal;
-
-  notification_ = std::make_unique<message_center::Notification>(
-      message_center::NOTIFICATION_TYPE_PROGRESS, notification_id,
-      l10n_util::GetStringUTF16(
-          IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_IN_PROGRESS_TITLE),
-      base::string16(),  // body
-      gfx::Image(),      // icon
-      l10n_util::GetStringUTF16(
-          IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_DISPLAY_SOURCE),
-      GURL(),  // origin_url
-      message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
-                                 kNotifierCrostiniPackageInstaller),
-      rich_notification_data,
-      base::MakeRefCounted<message_center::ThunkNotificationDelegate>(
-          weak_ptr_factory_.GetWeakPtr()));
-
-  UpdateDisplayedNotification();
-}
-
-CrostiniPackageInstallerNotification::~CrostiniPackageInstallerNotification() =
-    default;
-
-// TODO(timloh): This doesn't get called if the user shuts down Crostini, so
-// the notification will be stuck at whatever percentage it is at.
-void CrostiniPackageInstallerNotification::UpdateProgress(
-    InstallLinuxPackageProgressStatus result,
-    int progress_percent,
-    const std::string& failure_reason) {
-  if (result == InstallLinuxPackageProgressStatus::SUCCEEDED ||
-      result == InstallLinuxPackageProgressStatus::FAILED) {
-    // The package installer service will stop sending us updates after this.
-    int title_id = IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_COMPLETED_TITLE;
-    int message_id =
-        IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_COMPLETED_MESSAGE;
-    if (result != InstallLinuxPackageProgressStatus::SUCCEEDED) {
-      title_id = IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_ERROR_TITLE;
-      message_id = IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_ERROR_MESSAGE;
-      notification_->set_accent_color(
-          ash::kSystemNotificationColorCriticalWarning);
-    }
-    notification_->set_title(l10n_util::GetStringUTF16(title_id));
-    notification_->set_message(l10n_util::GetStringUTF16(message_id));
-    notification_->set_type(message_center::NOTIFICATION_TYPE_SIMPLE);
-    notification_->set_never_timeout(false);
-  } else {
-    int display_progress = progress_percent / 2;
-    if (result == InstallLinuxPackageProgressStatus::INSTALLING)
-      display_progress += 50;
-    else
-      DCHECK_EQ(InstallLinuxPackageProgressStatus::DOWNLOADING, result);
-
-    notification_->set_progress(display_progress);
-  }
-
-  UpdateDisplayedNotification();
-}
-
-void CrostiniPackageInstallerNotification::Close(bool by_user) {
-  // This call deletes us.
-  installer_service_->NotificationClosed(this);
-}
-
-void CrostiniPackageInstallerNotification::UpdateDisplayedNotification() {
-  NotificationDisplayService* display_service =
-      NotificationDisplayService::GetForProfile(profile_);
-  display_service->Display(NotificationHandler::Type::TRANSIENT,
-                           *notification_);
-}
-
-}  // namespace crostini
diff --git a/chrome/browser/chromeos/crostini/crostini_package_installer_notification.h b/chrome/browser/chromeos/crostini/crostini_package_installer_notification.h
deleted file mode 100644
index d95f98c..0000000
--- a/chrome/browser/chromeos/crostini/crostini_package_installer_notification.h
+++ /dev/null
@@ -1,52 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_INSTALLER_NOTIFICATION_H_
-#define CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_INSTALLER_NOTIFICATION_H_
-
-#include "base/memory/weak_ptr.h"
-#include "chrome/browser/chromeos/crostini/crostini_manager.h"
-#include "ui/message_center/public/cpp/notification_delegate.h"
-
-namespace message_center {
-class Notification;
-}
-
-namespace crostini {
-
-class CrostiniPackageInstallerService;
-
-class CrostiniPackageInstallerNotification
-    : public message_center::NotificationObserver {
- public:
-  CrostiniPackageInstallerNotification(
-      Profile* profile,
-      const std::string& notification_id,
-      CrostiniPackageInstallerService* installer_service);
-  virtual ~CrostiniPackageInstallerNotification();
-
-  void UpdateProgress(InstallLinuxPackageProgressStatus result,
-                      int progress_percent,
-                      const std::string& failure_reason);
-
-  // message_center::NotificationObserver:
-  void Close(bool by_user) override;
-
- private:
-  void UpdateDisplayedNotification();
-
-  // These notifications are owned by the installer service.
-  CrostiniPackageInstallerService* installer_service_;
-  Profile* profile_;
-
-  std::unique_ptr<message_center::Notification> notification_;
-
-  base::WeakPtrFactory<CrostiniPackageInstallerNotification> weak_ptr_factory_;
-
-  DISALLOW_COPY_AND_ASSIGN(CrostiniPackageInstallerNotification);
-};
-
-}  // namespace crostini
-
-#endif  // CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_INSTALLER_NOTIFICATION_H_
diff --git a/chrome/browser/chromeos/crostini/crostini_package_installer_service.cc b/chrome/browser/chromeos/crostini/crostini_package_installer_service.cc
deleted file mode 100644
index 646ad50..0000000
--- a/chrome/browser/chromeos/crostini/crostini_package_installer_service.cc
+++ /dev/null
@@ -1,176 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#include "chrome/browser/chromeos/crostini/crostini_package_installer_service.h"
-
-#include "base/bind.h"
-#include "base/files/file_path.h"
-#include "base/no_destructor.h"
-#include "chrome/browser/chromeos/crostini/crostini_manager_factory.h"
-#include "chrome/browser/profiles/profile.h"
-#include "components/keyed_service/content/browser_context_dependency_manager.h"
-#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
-
-namespace crostini {
-
-namespace {
-
-class CrostiniPackageInstallerServiceFactory
-    : public BrowserContextKeyedServiceFactory {
- public:
-  static CrostiniPackageInstallerService* GetForProfile(Profile* profile) {
-    return static_cast<CrostiniPackageInstallerService*>(
-        GetInstance()->GetServiceForBrowserContext(profile, true));
-  }
-
-  static CrostiniPackageInstallerServiceFactory* GetInstance() {
-    static base::NoDestructor<CrostiniPackageInstallerServiceFactory> factory;
-    return factory.get();
-  }
-
- private:
-  friend class base::NoDestructor<CrostiniPackageInstallerServiceFactory>;
-
-  CrostiniPackageInstallerServiceFactory()
-      : BrowserContextKeyedServiceFactory(
-            "CrostiniPackageInstallerService",
-            BrowserContextDependencyManager::GetInstance()) {
-    DependsOn(CrostiniManagerFactory::GetInstance());
-  }
-
-  ~CrostiniPackageInstallerServiceFactory() override = default;
-
-  // BrowserContextKeyedServiceFactory:
-  KeyedService* BuildServiceInstanceFor(
-      content::BrowserContext* context) const override {
-    Profile* profile = Profile::FromBrowserContext(context);
-    return new CrostiniPackageInstallerService(profile);
-  }
-};
-
-}  // namespace
-
-CrostiniPackageInstallerService* CrostiniPackageInstallerService::GetForProfile(
-    Profile* profile) {
-  return CrostiniPackageInstallerServiceFactory::GetForProfile(profile);
-}
-
-CrostiniPackageInstallerService::CrostiniPackageInstallerService(
-    Profile* profile)
-    : profile_(profile), weak_ptr_factory_(this) {
-  CrostiniManager::GetForProfile(profile)
-      ->AddInstallLinuxPackageProgressObserver(this);
-}
-
-CrostiniPackageInstallerService::~CrostiniPackageInstallerService() = default;
-
-void CrostiniPackageInstallerService::Shutdown() {
-  CrostiniManager::GetForProfile(profile_)
-      ->RemoveInstallLinuxPackageProgressObserver(this);
-}
-
-void CrostiniPackageInstallerService::NotificationClosed(
-    CrostiniPackageInstallerNotification* notification) {
-  for (auto it = running_notifications_.begin();
-       it != running_notifications_.end(); ++it) {
-    if (it->second.get() == notification) {
-      running_notifications_.erase(it);
-      return;
-    }
-  }
-
-  for (auto it = finished_notifications_.begin();
-       it != finished_notifications_.end(); ++it) {
-    if (it->get() == notification) {
-      finished_notifications_.erase(it);
-      return;
-    }
-  }
-
-  NOTREACHED();
-}
-
-void CrostiniPackageInstallerService::GetLinuxPackageInfo(
-    const std::string& vm_name,
-    const std::string& container_name,
-    const std::string& package_path,
-    CrostiniManager::GetLinuxPackageInfoCallback callback) {
-  CrostiniManager::GetForProfile(profile_)->GetLinuxPackageInfo(
-      profile_, vm_name, container_name, package_path,
-      base::BindOnce(&CrostiniPackageInstallerService::OnGetLinuxPackageInfo,
-                     weak_ptr_factory_.GetWeakPtr(), vm_name, container_name,
-                     std::move(callback)));
-}
-
-void CrostiniPackageInstallerService::InstallLinuxPackage(
-    const std::string& vm_name,
-    const std::string& container_name,
-    const std::string& package_path,
-    CrostiniManager::InstallLinuxPackageCallback callback) {
-  CrostiniManager::GetForProfile(profile_)->InstallLinuxPackage(
-      vm_name, container_name, package_path,
-      base::BindOnce(&CrostiniPackageInstallerService::OnInstallLinuxPackage,
-                     weak_ptr_factory_.GetWeakPtr(), vm_name, container_name,
-                     std::move(callback)));
-}
-
-void CrostiniPackageInstallerService::OnInstallLinuxPackageProgress(
-    const std::string& vm_name,
-    const std::string& container_name,
-    InstallLinuxPackageProgressStatus result,
-    int progress_percent,
-    const std::string& failure_reason) {
-  auto it =
-      running_notifications_.find(std::make_pair(vm_name, container_name));
-  if (it == running_notifications_.end())
-    return;
-  it->second->UpdateProgress(result, progress_percent, failure_reason);
-
-  if (result == InstallLinuxPackageProgressStatus::SUCCEEDED ||
-      result == InstallLinuxPackageProgressStatus::FAILED) {
-    finished_notifications_.emplace_back(std::move(it->second));
-    running_notifications_.erase(it);
-  }
-}
-
-void CrostiniPackageInstallerService::OnGetLinuxPackageInfo(
-    const std::string& vm_name,
-    const std::string& container_name,
-    CrostiniManager::GetLinuxPackageInfoCallback callback,
-    const LinuxPackageInfo& linux_package_info) {
-  std::move(callback).Run(linux_package_info);
-  if (!linux_package_info.success)
-    return;
-}
-
-void CrostiniPackageInstallerService::OnInstallLinuxPackage(
-    const std::string& vm_name,
-    const std::string& container_name,
-    CrostiniManager::InstallLinuxPackageCallback callback,
-    CrostiniResult result,
-    const std::string& failure_reason) {
-  std::move(callback).Run(result, failure_reason);
-  if (result != CrostiniResult::SUCCESS)
-    return;
-
-  std::unique_ptr<CrostiniPackageInstallerNotification>& notification =
-      running_notifications_[std::make_pair(vm_name, container_name)];
-  if (notification) {
-    // We could reach this if the final progress update signal from a previous
-    // package install doesn't get sent, so we wouldn't end up moving the
-    // previous notification out of running_notifications_.
-    LOG(ERROR) << "Notification for package install already exists.";
-    return;
-  }
-
-  notification = std::make_unique<CrostiniPackageInstallerNotification>(
-      profile_, GetUniqueNotificationId(), this);
-}
-
-std::string CrostiniPackageInstallerService::GetUniqueNotificationId() {
-  return base::StringPrintf("crostini_package_install_%d",
-                            next_notification_id++);
-}
-
-}  // namespace crostini
diff --git a/chrome/browser/chromeos/crostini/crostini_package_installer_service.h b/chrome/browser/chromeos/crostini/crostini_package_installer_service.h
deleted file mode 100644
index fdc5926..0000000
--- a/chrome/browser/chromeos/crostini/crostini_package_installer_service.h
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright 2018 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-#ifndef CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_INSTALLER_SERVICE_H_
-#define CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_INSTALLER_SERVICE_H_
-
-#include <map>
-#include <string>
-#include <vector>
-
-#include "base/memory/weak_ptr.h"
-#include "chrome/browser/chromeos/crostini/crostini_manager.h"
-#include "chrome/browser/chromeos/crostini/crostini_package_installer_notification.h"
-#include "components/keyed_service/core/keyed_service.h"
-
-namespace crostini {
-
-class CrostiniPackageInstallerService
-    : public KeyedService,
-      public InstallLinuxPackageProgressObserver {
- public:
-  static CrostiniPackageInstallerService* GetForProfile(Profile* profile);
-
-  explicit CrostiniPackageInstallerService(Profile* profile);
-  ~CrostiniPackageInstallerService() override;
-
-  // KeyedService:
-  void Shutdown() override;
-
-  void NotificationClosed(CrostiniPackageInstallerNotification* notification);
-
-  // The package installer service caches the most recent retrieved package
-  // info, for use in a package install notification.
-  // TODO(timloh): Actually cache the values.
-  void GetLinuxPackageInfo(
-      const std::string& vm_name,
-      const std::string& container_name,
-      const std::string& package_path,
-      CrostiniManager::GetLinuxPackageInfoCallback callback);
-
-  // Install a Linux package. If successfully started, a system notification
-  // will be used to display further updates.
-  void InstallLinuxPackage(
-      const std::string& vm_name,
-      const std::string& container_name,
-      const std::string& package_path,
-      CrostiniManager::InstallLinuxPackageCallback callback);
-
-  // InstallLinuxPackageProgressObserver:
-  void OnInstallLinuxPackageProgress(
-      const std::string& vm_name,
-      const std::string& container_name,
-      InstallLinuxPackageProgressStatus result,
-      int progress_percent,
-      const std::string& failure_reason) override;
-
- private:
-  // Wraps the callback provided in GetLinuxPackageInfo().
-  void OnGetLinuxPackageInfo(
-      const std::string& vm_name,
-      const std::string& container_name,
-      CrostiniManager::GetLinuxPackageInfoCallback callback,
-      const LinuxPackageInfo& linux_package_info);
-
-  // Wraps the callback provided in InstallLinuxPackage().
-  void OnInstallLinuxPackage(
-      const std::string& vm_name,
-      const std::string& container_name,
-      CrostiniManager::InstallLinuxPackageCallback callback,
-      CrostiniResult result,
-      const std::string& failure_reason);
-
-  std::string GetUniqueNotificationId();
-
-  Profile* profile_;
-
-  // Keyed on <vm_name, container_name>. A container can only have one install
-  // running at a time, but we need to keep notifications around until they're
-  // dismissed.
-  std::map<std::pair<std::string, std::string>,
-           std::unique_ptr<CrostiniPackageInstallerNotification>>
-      running_notifications_;
-  std::vector<std::unique_ptr<CrostiniPackageInstallerNotification>>
-      finished_notifications_;
-
-  int next_notification_id = 0;
-
-  base::WeakPtrFactory<CrostiniPackageInstallerService> weak_ptr_factory_;
-
-  DISALLOW_COPY_AND_ASSIGN(CrostiniPackageInstallerService);
-};
-
-}  // namespace crostini
-
-#endif  // CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_INSTALLER_SERVICE_H_
diff --git a/chrome/browser/chromeos/crostini/crostini_package_notification.cc b/chrome/browser/chromeos/crostini/crostini_package_notification.cc
new file mode 100644
index 0000000..4746209
--- /dev/null
+++ b/chrome/browser/chromeos/crostini/crostini_package_notification.cc
@@ -0,0 +1,231 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/chromeos/crostini/crostini_package_notification.h"
+
+#include "ash/public/cpp/notification_utils.h"
+#include "ash/public/cpp/vector_icons/vector_icons.h"
+#include "chrome/browser/chromeos/crostini/crostini_package_service.h"
+#include "chrome/browser/notifications/notification_display_service.h"
+#include "chrome/grit/generated_resources.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/base/l10n/time_format.h"
+#include "ui/message_center/public/cpp/message_center_constants.h"
+#include "ui/message_center/public/cpp/notification.h"
+#include "ui/message_center/public/cpp/notification_delegate.h"
+
+namespace crostini {
+
+namespace {
+
+constexpr char kNotifierCrostiniPackageOperation[] =
+    "crostini.package_operation";
+
+}  // namespace
+
+CrostiniPackageNotification::NotificationSettings::NotificationSettings() {}
+CrostiniPackageNotification::NotificationSettings::NotificationSettings(
+    const NotificationSettings& rhs) = default;
+CrostiniPackageNotification::NotificationSettings::~NotificationSettings() {}
+
+CrostiniPackageNotification::CrostiniPackageNotification(
+    Profile* profile,
+    NotificationType notification_type,
+    PackageOperationStatus status,
+    const base::string16& app_name,
+    const std::string& notification_id,
+    CrostiniPackageService* package_service)
+    : notification_type_(notification_type),
+      current_status_(status),
+      package_service_(package_service),
+      profile_(profile),
+      notification_settings_(
+          GetNotificationSettingsForTypeAndAppName(notification_type,
+                                                   app_name)),
+      weak_ptr_factory_(this) {
+  if (status == PackageOperationStatus::RUNNING) {
+    running_start_time_ = base::Time::Now();
+  }
+  message_center::RichNotificationData rich_notification_data;
+  rich_notification_data.vector_small_image = &ash::kNotificationLinuxIcon;
+  rich_notification_data.never_timeout = true;
+  rich_notification_data.accent_color = ash::kSystemNotificationColorNormal;
+
+  notification_ = std::make_unique<message_center::Notification>(
+      message_center::NOTIFICATION_TYPE_PROGRESS, notification_id,
+      base::string16(), base::string16(),
+      gfx::Image(),  // icon
+      notification_settings_.source,
+      GURL(),  // origin_url
+      message_center::NotifierId(message_center::NotifierType::SYSTEM_COMPONENT,
+                                 kNotifierCrostiniPackageOperation),
+      rich_notification_data,
+      base::MakeRefCounted<message_center::ThunkNotificationDelegate>(
+          weak_ptr_factory_.GetWeakPtr()));
+
+  // Sets title and body
+  UpdateProgress(status, 0 /*progress_percent*/);
+}
+
+CrostiniPackageNotification::~CrostiniPackageNotification() = default;
+
+// static
+CrostiniPackageNotification::NotificationSettings
+CrostiniPackageNotification::GetNotificationSettingsForTypeAndAppName(
+    NotificationType notification_type,
+    const base::string16& app_name) {
+  NotificationSettings result;
+
+  switch (notification_type) {
+    case NotificationType::PACKAGE_INSTALL:
+      DCHECK(app_name.empty());
+      result.source = l10n_util::GetStringUTF16(
+          IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_DISPLAY_SOURCE);
+      result.progress_title = l10n_util::GetStringUTF16(
+          IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_IN_PROGRESS_TITLE);
+      result.progress_body.clear();
+      result.success_title = l10n_util::GetStringUTF16(
+          IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_COMPLETED_TITLE);
+      result.success_body = l10n_util::GetStringUTF16(
+          IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_COMPLETED_MESSAGE);
+      result.failure_title = l10n_util::GetStringUTF16(
+          IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_ERROR_TITLE);
+      result.failure_body = l10n_util::GetStringUTF16(
+          IDS_CROSTINI_PACKAGE_INSTALL_NOTIFICATION_ERROR_MESSAGE);
+      break;
+
+    case NotificationType::APPLICATION_UNINSTALL:
+      result.source = l10n_util::GetStringUTF16(
+          IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_DISPLAY_SOURCE);
+      result.queued_title = l10n_util::GetStringFUTF16(
+          IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_QUEUED_TITLE,
+          app_name);
+      result.queued_body = l10n_util::GetStringUTF16(
+          IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_QUEUED_MESSAGE);
+      result.progress_title = l10n_util::GetStringFUTF16(
+          IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_IN_PROGRESS_TITLE,
+          app_name);
+      result.success_title = l10n_util::GetStringFUTF16(
+          IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_COMPLETED_TITLE,
+          app_name);
+      result.success_body = l10n_util::GetStringUTF16(
+          IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_COMPLETED_MESSAGE);
+      result.failure_title = l10n_util::GetStringFUTF16(
+          IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_ERROR_TITLE,
+          app_name);
+      result.failure_body = l10n_util::GetStringUTF16(
+          IDS_CROSTINI_APPLICATION_UNINSTALL_NOTIFICATION_ERROR_MESSAGE);
+      break;
+
+    default:
+      NOTREACHED();
+  }
+
+  return result;
+}
+
+// TODO(timloh): This doesn't get called if the user shuts down Crostini, so
+// the notification will be stuck at whatever percentage it is at.
+void CrostiniPackageNotification::UpdateProgress(PackageOperationStatus status,
+                                                 int progress_percent) {
+  if (status == PackageOperationStatus::RUNNING &&
+      current_status_ != PackageOperationStatus::RUNNING) {
+    running_start_time_ = base::Time::Now();
+  }
+  current_status_ = status;
+
+  base::string16 title;
+  base::string16 body;
+  message_center::NotificationType notification_type =
+      message_center::NOTIFICATION_TYPE_SIMPLE;
+  bool never_timeout = false;
+
+  switch (status) {
+    case PackageOperationStatus::SUCCEEDED:
+      title = notification_settings_.success_title;
+      body = notification_settings_.success_body;
+      break;
+
+    case PackageOperationStatus::FAILED:
+      title = notification_settings_.failure_title;
+      body = notification_settings_.failure_body;
+      notification_->set_accent_color(
+          ash::kSystemNotificationColorCriticalWarning);
+      break;
+
+    case PackageOperationStatus::RUNNING:
+      never_timeout = true;
+      notification_type = message_center::NOTIFICATION_TYPE_PROGRESS;
+      title = notification_settings_.progress_title;
+      if (notification_type_ == NotificationType::APPLICATION_UNINSTALL) {
+        // Uninstalls have a time remaining instead of a fixed message.
+        base::TimeDelta time_since_started_running =
+            base::Time::Now() - running_start_time_;
+
+        // Don't estimate if we don't have enough data yet. At the moment we
+        // start the uninstall, we have no idea how long it will take. Only
+        // estimate once we've spent at least 3 seconds OR gotten 10% of the
+        // way through the uninstall.
+        constexpr base::TimeDelta kMinTimeForEstimate =
+            base::TimeDelta::FromSeconds(3);
+        constexpr base::TimeDelta kTimeDeltaZero =
+            base::TimeDelta::FromSeconds(0);
+        constexpr int kMinPercentForEstimate = 10;
+        if ((time_since_started_running >= kMinTimeForEstimate &&
+             progress_percent > 0) ||
+            (progress_percent >= kMinPercentForEstimate &&
+             time_since_started_running > kTimeDeltaZero)) {
+          base::TimeDelta total_time_expected =
+              (time_since_started_running * 100) / progress_percent;
+          base::TimeDelta time_remaining =
+              total_time_expected - time_since_started_running;
+          body = ui::TimeFormat::Simple(ui::TimeFormat::FORMAT_REMAINING,
+                                        ui::TimeFormat::LENGTH_SHORT,
+                                        time_remaining);
+        }
+        // else leave body blank
+      } else {
+        body = notification_settings_.progress_body;
+      }
+      break;
+
+    case PackageOperationStatus::QUEUED:
+      // We don't have queued strings for some NotificationTypes; we shouldn't
+      // be asked to move to QUEUED status for those,
+      DCHECK(!notification_settings_.queued_title.empty());
+      DCHECK(!notification_settings_.queued_body.empty());
+      title = notification_settings_.queued_title;
+      body = notification_settings_.queued_body;
+      break;
+
+    default:
+      NOTREACHED();
+  }
+
+  notification_->set_title(title);
+  notification_->set_message(body);
+  notification_->set_type(notification_type);
+  notification_->set_progress(progress_percent);
+  notification_->set_never_timeout(never_timeout);
+  UpdateDisplayedNotification();
+}
+
+void CrostiniPackageNotification::ForceAllowAutoHide() {
+  notification_->set_never_timeout(false);
+  UpdateDisplayedNotification();
+}
+
+void CrostiniPackageNotification::Close(bool by_user) {
+  // This call deletes us.
+  package_service_->NotificationClosed(this);
+}
+
+void CrostiniPackageNotification::UpdateDisplayedNotification() {
+  NotificationDisplayService* display_service =
+      NotificationDisplayService::GetForProfile(profile_);
+  display_service->Display(NotificationHandler::Type::TRANSIENT,
+                           *notification_);
+}
+
+}  // namespace crostini
diff --git a/chrome/browser/chromeos/crostini/crostini_package_notification.h b/chrome/browser/chromeos/crostini/crostini_package_notification.h
new file mode 100644
index 0000000..f94f3d99
--- /dev/null
+++ b/chrome/browser/chromeos/crostini/crostini_package_notification.h
@@ -0,0 +1,98 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_NOTIFICATION_H_
+#define CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_NOTIFICATION_H_
+
+#include <memory>
+#include <ostream>
+#include <string>
+
+#include "base/memory/weak_ptr.h"
+#include "base/time/time.h"
+#include "chrome/browser/chromeos/crostini/crostini_manager.h"
+#include "chrome/browser/chromeos/crostini/crostini_package_operation_status.h"
+#include "ui/message_center/public/cpp/notification_delegate.h"
+
+namespace message_center {
+class Notification;
+}
+
+namespace crostini {
+
+class CrostiniPackageService;
+
+// Notification for various Crostini package operations, such as installing
+// from a package or uninstalling an existing app.
+class CrostiniPackageNotification
+    : public message_center::NotificationObserver {
+ public:
+  enum class NotificationType { PACKAGE_INSTALL, APPLICATION_UNINSTALL };
+
+  // |app_name| should be empty for PACKAGE_INSTALL, non-empty for
+  // APPLICATION_UNINSTALL.
+  CrostiniPackageNotification(Profile* profile,
+                              NotificationType notification_type,
+                              PackageOperationStatus status,
+                              const base::string16& app_name,
+                              const std::string& notification_id,
+                              CrostiniPackageService* installer_service);
+  virtual ~CrostiniPackageNotification();
+
+  void UpdateProgress(PackageOperationStatus status, int progress_percent);
+
+  void ForceAllowAutoHide();
+
+  // message_center::NotificationObserver:
+  void Close(bool by_user) override;
+
+ private:
+  // A type giving the string, etc displayed for each notification type. Note
+  // that we have the complete strings here, not just the string IDs, because
+  // the call needed to generate the strings is slightly different between
+  // notification types (specifically, uninstall notification strings usually
+  // need an app_name, while installs do not).
+  struct NotificationSettings {
+    NotificationSettings();
+    NotificationSettings(const NotificationSettings& rhs);
+    ~NotificationSettings();
+    base::string16 source;
+    base::string16 queued_title;
+    base::string16 queued_body;
+    base::string16 progress_title;
+    base::string16 progress_body;
+    base::string16 success_title;
+    base::string16 success_body;
+    base::string16 failure_title;
+    base::string16 failure_body;
+  };
+
+  void UpdateDisplayedNotification();
+
+  static NotificationSettings GetNotificationSettingsForTypeAndAppName(
+      NotificationType notification_type,
+      const base::string16& app_name);
+
+  const NotificationType notification_type_;
+  PackageOperationStatus current_status_;
+
+  // The most-recent time we entered the "RUNNING" state. Used for
+  // guesstimating when we'll be done.
+  base::Time running_start_time_;
+
+  // These notifications are owned by the package service.
+  CrostiniPackageService* package_service_;
+  Profile* profile_;
+  const NotificationSettings notification_settings_;
+
+  std::unique_ptr<message_center::Notification> notification_;
+
+  base::WeakPtrFactory<CrostiniPackageNotification> weak_ptr_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(CrostiniPackageNotification);
+};
+
+}  // namespace crostini
+
+#endif  // CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_NOTIFICATION_H_
diff --git a/chrome/browser/chromeos/crostini/crostini_package_operation_status.h b/chrome/browser/chromeos/crostini/crostini_package_operation_status.h
new file mode 100644
index 0000000..69bcda82
--- /dev/null
+++ b/chrome/browser/chromeos/crostini/crostini_package_operation_status.h
@@ -0,0 +1,18 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_OPERATION_STATUS_H_
+#define CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_OPERATION_STATUS_H_
+
+#include <ostream>
+
+namespace crostini {
+
+// Status of an operation in CrostiniPackageService &
+// CrostiniPackageNotification.
+enum class PackageOperationStatus { QUEUED, SUCCEEDED, FAILED, RUNNING };
+
+}  // namespace crostini
+
+#endif  // CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_OPERATION_STATUS_H_
diff --git a/chrome/browser/chromeos/crostini/crostini_package_service.cc b/chrome/browser/chromeos/crostini/crostini_package_service.cc
new file mode 100644
index 0000000..615dbd1
--- /dev/null
+++ b/chrome/browser/chromeos/crostini/crostini_package_service.cc
@@ -0,0 +1,437 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/chromeos/crostini/crostini_package_service.h"
+
+#include "base/bind.h"
+#include "base/files/file_path.h"
+#include "base/no_destructor.h"
+#include "base/strings/strcat.h"
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/chromeos/crostini/crostini_manager_factory.h"
+#include "chrome/browser/chromeos/crostini/crostini_registry_service_factory.h"
+#include "chrome/browser/chromeos/crostini/crostini_util.h"
+#include "chrome/browser/profiles/profile.h"
+#include "components/keyed_service/content/browser_context_dependency_manager.h"
+#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
+
+namespace crostini {
+
+namespace {
+
+class CrostiniPackageServiceFactory : public BrowserContextKeyedServiceFactory {
+ public:
+  static CrostiniPackageService* GetForProfile(Profile* profile) {
+    return static_cast<CrostiniPackageService*>(
+        GetInstance()->GetServiceForBrowserContext(profile, true));
+  }
+
+  static CrostiniPackageServiceFactory* GetInstance() {
+    static base::NoDestructor<CrostiniPackageServiceFactory> factory;
+    return factory.get();
+  }
+
+ private:
+  friend class base::NoDestructor<CrostiniPackageServiceFactory>;
+
+  CrostiniPackageServiceFactory()
+      : BrowserContextKeyedServiceFactory(
+            "CrostiniPackageService",
+            BrowserContextDependencyManager::GetInstance()) {
+    DependsOn(CrostiniManagerFactory::GetInstance());
+  }
+
+  ~CrostiniPackageServiceFactory() override = default;
+
+  // BrowserContextKeyedServiceFactory:
+  KeyedService* BuildServiceInstanceFor(
+      content::BrowserContext* context) const override {
+    Profile* profile = Profile::FromBrowserContext(context);
+    return new CrostiniPackageService(profile);
+  }
+};
+
+PackageOperationStatus InstallStatusToOperationStatus(
+    InstallLinuxPackageProgressStatus status) {
+  switch (status) {
+    case InstallLinuxPackageProgressStatus::SUCCEEDED:
+      return PackageOperationStatus::SUCCEEDED;
+    case InstallLinuxPackageProgressStatus::FAILED:
+      return PackageOperationStatus::FAILED;
+    case InstallLinuxPackageProgressStatus::DOWNLOADING:
+    case InstallLinuxPackageProgressStatus::INSTALLING:
+      return PackageOperationStatus::RUNNING;
+    default:
+      NOTREACHED();
+  }
+}
+
+PackageOperationStatus UninstallStatusToOperationStatus(
+    UninstallPackageProgressStatus status) {
+  switch (status) {
+    case UninstallPackageProgressStatus::SUCCEEDED:
+      return PackageOperationStatus::SUCCEEDED;
+    case UninstallPackageProgressStatus::FAILED:
+      return PackageOperationStatus::FAILED;
+    case UninstallPackageProgressStatus::UNINSTALLING:
+      return PackageOperationStatus::RUNNING;
+    default:
+      NOTREACHED();
+  }
+}
+
+}  // namespace
+
+struct CrostiniPackageService::QueuedUninstall {
+  QueuedUninstall(
+      const std::string& app_id,
+      std::unique_ptr<CrostiniPackageNotification> notification_argument)
+      : app_id(app_id), notification(std::move(notification_argument)) {}
+  ~QueuedUninstall() = default;
+
+  // App to uninstall
+  std::string app_id;
+
+  // Notification displaying "uninstall queued"
+  std::unique_ptr<CrostiniPackageNotification> notification;
+};
+
+CrostiniPackageService* CrostiniPackageService::GetForProfile(
+    Profile* profile) {
+  return CrostiniPackageServiceFactory::GetForProfile(profile);
+}
+
+CrostiniPackageService::CrostiniPackageService(Profile* profile)
+    : profile_(profile), weak_ptr_factory_(this) {
+  CrostiniManager* manager = CrostiniManager::GetForProfile(profile);
+
+  manager->AddLinuxPackageOperationProgressObserver(this);
+}
+
+CrostiniPackageService::~CrostiniPackageService() = default;
+
+void CrostiniPackageService::Shutdown() {
+  CrostiniManager* manager = CrostiniManager::GetForProfile(profile_);
+  manager->RemoveLinuxPackageOperationProgressObserver(this);
+}
+
+void CrostiniPackageService::NotificationClosed(
+    CrostiniPackageNotification* notification) {
+  for (auto it = running_notifications_.begin();
+       it != running_notifications_.end(); ++it) {
+    if (it->second.get() == notification) {
+      running_notifications_.erase(it);
+      return;
+    }
+  }
+
+  for (auto it = finished_notifications_.begin();
+       it != finished_notifications_.end(); ++it) {
+    if (it->get() == notification) {
+      finished_notifications_.erase(it);
+      return;
+    }
+  }
+
+  for (auto it = queued_uninstalls_.begin(); it != queued_uninstalls_.end();
+       ++it) {
+    std::deque<QueuedUninstall>& queue = it->second;
+    for (auto it2 = queue.begin(); it2 != queue.end(); ++it2) {
+      if (it2->notification.get() == notification) {
+        // We need to delete the notification, but we still want to run the
+        // uninstall, so don't do erase(it2).
+        it2->notification.reset();
+        return;
+      }
+    }
+  }
+  NOTREACHED();
+}
+
+void CrostiniPackageService::GetLinuxPackageInfo(
+    const std::string& vm_name,
+    const std::string& container_name,
+    const std::string& package_path,
+    CrostiniManager::GetLinuxPackageInfoCallback callback) {
+  CrostiniManager::GetForProfile(profile_)->GetLinuxPackageInfo(
+      profile_, vm_name, container_name, package_path,
+      base::BindOnce(&CrostiniPackageService::OnGetLinuxPackageInfo,
+                     weak_ptr_factory_.GetWeakPtr(), vm_name, container_name,
+                     std::move(callback)));
+}
+
+void CrostiniPackageService::InstallLinuxPackage(
+    const std::string& vm_name,
+    const std::string& container_name,
+    const std::string& package_path,
+    CrostiniManager::InstallLinuxPackageCallback callback) {
+  CrostiniManager::GetForProfile(profile_)->InstallLinuxPackage(
+      vm_name, container_name, package_path,
+      base::BindOnce(&CrostiniPackageService::OnInstallLinuxPackage,
+                     weak_ptr_factory_.GetWeakPtr(), vm_name, container_name,
+                     std::move(callback)));
+}
+
+void CrostiniPackageService::OnInstallLinuxPackageProgress(
+    const std::string& vm_name,
+    const std::string& container_name,
+    InstallLinuxPackageProgressStatus status,
+    int progress_percent) {
+  // Linux package install has two phases, downloading and installing, which we
+  // map to a single progess percentage amount by dividing the range in half --
+  // 0-50% for the downloading phase, 51-100% for the installing phase.
+  int display_progress = progress_percent / 2;
+  if (status == InstallLinuxPackageProgressStatus::INSTALLING)
+    display_progress += 50;  // Second phase
+
+  UpdatePackageOperationStatus(std::make_pair(vm_name, container_name),
+                               InstallStatusToOperationStatus(status),
+                               display_progress);
+}
+
+void CrostiniPackageService::OnUninstallPackageProgress(
+    const std::string& vm_name,
+    const std::string& container_name,
+    UninstallPackageProgressStatus status,
+    int progress_percent) {
+  UpdatePackageOperationStatus(ContainerIdentifier(vm_name, container_name),
+                               UninstallStatusToOperationStatus(status),
+                               progress_percent);
+}
+
+void CrostiniPackageService::QueueUninstallApplication(
+    const std::string& app_id) {
+  auto registration =
+      CrostiniRegistryServiceFactory::GetForProfile(profile_)->GetRegistration(
+          app_id);
+  DCHECK(registration);
+  const std::string vm_name = registration->VmName();
+  const std::string container_name = registration->ContainerName();
+  const std::string app_name = registration->Name();
+
+  const ContainerIdentifier container_id(vm_name, container_name);
+  if (ContainerHasRunningOperation(container_id)) {
+    CreateQueuedUninstall(container_id, app_id, app_name);
+    return;
+  }
+
+  CreateRunningNotification(
+      container_id,
+      CrostiniPackageNotification::NotificationType::APPLICATION_UNINSTALL,
+      app_name);
+  containers_with_running_operations_.insert(container_id);
+
+  UninstallApplication(*registration, app_id);
+}
+
+std::string CrostiniPackageService::ContainerIdentifierToString(
+    const ContainerIdentifier& container_id) const {
+  return base::StrCat(
+      {"(", container_id.first, ", ", container_id.second, ")"});
+}
+
+bool CrostiniPackageService::ContainerHasRunningOperation(
+    const ContainerIdentifier& container_id) const {
+  return containers_with_running_operations_.find(container_id) !=
+         containers_with_running_operations_.end();
+}
+
+void CrostiniPackageService::CreateRunningNotification(
+    const ContainerIdentifier& container_id,
+    CrostiniPackageNotification::NotificationType notification_type,
+    const std::string& app_name) {
+  {  // Scope limit for |it|, which will become invalid shortly.
+    auto it = running_notifications_.find(container_id);
+    if (it != running_notifications_.end()) {
+      // We could reach this if the final progress update signal from a previous
+      // operation doesn't get sent, so we wouldn't end up moving the
+      // previous notification out of running_notifications_. Clear it out by
+      // moving to finished_notifications_.
+      LOG(ERROR) << "Notification for package operation already exists.";
+      it->second->ForceAllowAutoHide();
+      finished_notifications_.emplace_back(std::move(it->second));
+      running_notifications_.erase(it);
+    }
+  }
+
+  running_notifications_[container_id] =
+      std::make_unique<CrostiniPackageNotification>(
+          profile_, notification_type, PackageOperationStatus::RUNNING,
+          base::UTF8ToUTF16(app_name), GetUniqueNotificationId(), this);
+}
+
+void CrostiniPackageService::CreateQueuedUninstall(
+    const ContainerIdentifier& container_id,
+    const std::string& app_id,
+    const std::string& app_name) {
+  queued_uninstalls_[container_id].emplace_back(
+      app_id,
+      std::make_unique<CrostiniPackageNotification>(
+          profile_,
+          CrostiniPackageNotification::NotificationType::APPLICATION_UNINSTALL,
+          PackageOperationStatus::QUEUED, base::UTF8ToUTF16(app_name),
+          GetUniqueNotificationId(), this));
+}
+
+void CrostiniPackageService::UpdatePackageOperationStatus(
+    const ContainerIdentifier& container_id,
+    PackageOperationStatus status,
+    int progress_percent) {
+  // Update the notification window, if any. User may have closed it while it
+  // was in progress, so don't complain if not found.
+  auto it = running_notifications_.find(container_id);
+  if (it != running_notifications_.end()) {
+    DCHECK(it->second) << ContainerIdentifierToString(container_id)
+                       << " has null notification pointer";
+    it->second->UpdateProgress(status, progress_percent);
+
+    if (status == PackageOperationStatus::SUCCEEDED ||
+        status == PackageOperationStatus::FAILED) {
+      finished_notifications_.emplace_back(std::move(it->second));
+      running_notifications_.erase(it);
+    }
+  }
+
+  // Update our state and kick off the next operation if we just finished an
+  // operation.
+  DCHECK(ContainerHasRunningOperation(container_id))
+      << "containers_with_running_operations_["
+      << ContainerIdentifierToString(container_id) << "] not found";
+  if (status == PackageOperationStatus::SUCCEEDED ||
+      status == PackageOperationStatus::FAILED) {
+    auto queued_iter = queued_uninstalls_.find(container_id);
+    if (queued_iter == queued_uninstalls_.end() ||
+        queued_iter->second.empty()) {
+      containers_with_running_operations_.erase(container_id);
+    } else {
+      StartQueuedUninstall(container_id);
+    }
+  }
+}
+
+void CrostiniPackageService::OnGetLinuxPackageInfo(
+    const std::string& vm_name,
+    const std::string& container_name,
+    CrostiniManager::GetLinuxPackageInfoCallback callback,
+    const LinuxPackageInfo& linux_package_info) {
+  std::move(callback).Run(linux_package_info);
+}
+
+void CrostiniPackageService::OnInstallLinuxPackage(
+    const std::string& vm_name,
+    const std::string& container_name,
+    CrostiniManager::InstallLinuxPackageCallback callback,
+    CrostiniResult result) {
+  std::move(callback).Run(result);
+  if (result != CrostiniResult::SUCCESS)
+    return;
+  const ContainerIdentifier container_id(vm_name, container_name);
+  CreateRunningNotification(
+      container_id,
+      CrostiniPackageNotification::NotificationType::PACKAGE_INSTALL,
+      "" /* app_name */);
+  containers_with_running_operations_.insert(container_id);
+}
+
+void CrostiniPackageService::UninstallApplication(
+    const CrostiniRegistryService::Registration& registration,
+    const std::string& app_id) {
+  const std::string vm_name = registration.VmName();
+  const std::string container_name = registration.ContainerName();
+  const ContainerIdentifier container_id(vm_name, container_name);
+
+  // Policies can change under us, and crostini may now be forbidden.
+  if (!IsCrostiniUIAllowedForProfile(profile_)) {
+    LOG(ERROR) << "Can't uninstall because policy no longer allows Crostini";
+    UpdatePackageOperationStatus(container_id, PackageOperationStatus::FAILED,
+                                 0);
+    return;
+  }
+
+  // If Crostini is not running, launch it. This is a no-op if Crostini is
+  // already running.
+  CrostiniManager::GetForProfile(profile_)->RestartCrostini(
+      vm_name, container_name,
+      base::BindOnce(&CrostiniPackageService::OnCrostiniRunningForUninstall,
+                     weak_ptr_factory_.GetWeakPtr(), container_id,
+                     registration.DesktopFileId()));
+}
+
+void CrostiniPackageService::OnCrostiniRunningForUninstall(
+    const ContainerIdentifier& container_id,
+    const std::string& desktop_file_id,
+    CrostiniResult result) {
+  if (result != CrostiniResult::SUCCESS) {
+    LOG(ERROR) << "Failed to launch Crostini; uninstall aborted";
+    UpdatePackageOperationStatus(container_id, PackageOperationStatus::FAILED,
+                                 0);
+    return;
+  }
+  const std::string& vm_name = container_id.first;
+  const std::string& container_name = container_id.second;
+
+  CrostiniManager::GetForProfile(profile_)->UninstallPackageOwningFile(
+      vm_name, container_name, desktop_file_id,
+      base::BindOnce(&CrostiniPackageService::OnUninstallPackageOwningFile,
+                     weak_ptr_factory_.GetWeakPtr(), container_id));
+}
+
+void CrostiniPackageService::OnUninstallPackageOwningFile(
+    const ContainerIdentifier& container_id,
+    CrostiniResult result) {
+  if (result != CrostiniResult::SUCCESS) {
+    // Let user know the uninstall failed.
+    UpdatePackageOperationStatus(container_id, PackageOperationStatus::FAILED,
+                                 0);
+    return;
+  }
+  // Otherwise, just leave the notification alone in the "running" state.
+  // SUCCESS just means we successfully *started* the uninstall.
+}
+
+void CrostiniPackageService::StartQueuedUninstall(
+    const ContainerIdentifier& container_id) {
+  std::string app_id;
+  auto uninstall_queue_iter = queued_uninstalls_.find(container_id);
+  if (uninstall_queue_iter == queued_uninstalls_.end()) {
+    return;
+  }
+  std::deque<QueuedUninstall>& uninstall_queue = uninstall_queue_iter->second;
+  {  // Scope |next|; it becomes an invalid reference when we pop_front()
+    QueuedUninstall& next = uninstall_queue.front();
+
+    // User may have closed notification while still queued; don't complain if
+    // notification is nullptr.
+    if (next.notification) {
+      next.notification->UpdateProgress(PackageOperationStatus::RUNNING, 0);
+      running_notifications_.emplace(container_id,
+                                     std::move(next.notification));
+    }
+
+    app_id = next.app_id;
+    uninstall_queue.pop_front();  // Invalidates |next|
+  }
+  // containers_with_running_operations_ should be set from before and not
+  // cleared.
+  DCHECK(ContainerHasRunningOperation(container_id));
+
+  auto registration =
+      CrostiniRegistryServiceFactory::GetForProfile(profile_)->GetRegistration(
+          app_id);
+  DCHECK(registration);
+  UninstallApplication(*registration, app_id);
+
+  // Clean up memory.
+  if (uninstall_queue.empty()) {
+    queued_uninstalls_.erase(container_id);
+    // Invalidates uninstall_queue
+  }
+}
+
+std::string CrostiniPackageService::GetUniqueNotificationId() {
+  return base::StringPrintf("crostini_package_operation_%d",
+                            next_notification_id_++);
+}
+
+}  // namespace crostini
diff --git a/chrome/browser/chromeos/crostini/crostini_package_service.h b/chrome/browser/chromeos/crostini/crostini_package_service.h
new file mode 100644
index 0000000..7c24538e
--- /dev/null
+++ b/chrome/browser/chromeos/crostini/crostini_package_service.h
@@ -0,0 +1,176 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_SERVICE_H_
+#define CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_SERVICE_H_
+
+#include <deque>
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/memory/weak_ptr.h"
+#include "chrome/browser/chromeos/crostini/crostini_manager.h"
+#include "chrome/browser/chromeos/crostini/crostini_package_notification.h"
+#include "chrome/browser/chromeos/crostini/crostini_package_operation_status.h"
+#include "chrome/browser/chromeos/crostini/crostini_registry_service.h"
+#include "components/keyed_service/core/keyed_service.h"
+
+namespace crostini {
+
+class CrostiniPackageService : public KeyedService,
+                               public LinuxPackageOperationProgressObserver {
+ public:
+  static CrostiniPackageService* GetForProfile(Profile* profile);
+
+  explicit CrostiniPackageService(Profile* profile);
+  ~CrostiniPackageService() override;
+
+  // KeyedService:
+  void Shutdown() override;
+
+  void NotificationClosed(CrostiniPackageNotification* notification);
+
+  // The package installer service caches the most recent retrieved package
+  // info, for use in a package install notification.
+  // TODO(timloh): Actually cache the values.
+  void GetLinuxPackageInfo(
+      const std::string& vm_name,
+      const std::string& container_name,
+      const std::string& package_path,
+      CrostiniManager::GetLinuxPackageInfoCallback callback);
+
+  // Install a Linux package. If successfully started, a system notification
+  // will be used to display further updates.
+  void InstallLinuxPackage(
+      const std::string& vm_name,
+      const std::string& container_name,
+      const std::string& package_path,
+      CrostiniManager::InstallLinuxPackageCallback callback);
+
+  // LinuxPackageOperationProgressObserver:
+  void OnInstallLinuxPackageProgress(const std::string& vm_name,
+                                     const std::string& container_name,
+                                     InstallLinuxPackageProgressStatus status,
+                                     int progress_percent) override;
+
+  void OnUninstallPackageProgress(const std::string& vm_name,
+                                  const std::string& container_name,
+                                  UninstallPackageProgressStatus status,
+                                  int progress_percent) override;
+
+  // (Eventually) uninstall the package identified by |app_id|. If successfully
+  // started, a system notification will be used to display further updates.
+  void QueueUninstallApplication(const std::string& app_id);
+
+ private:
+  // A unique identifier for our containers. This is <vm_name, container_name>.
+  using ContainerIdentifier = std::pair<std::string, std::string>;
+
+  // The user can request new uninstalls while a different operation is in
+  // progress. Rather than sending a request which will fail, just queue the
+  // request until the previous one is done.
+  struct QueuedUninstall;
+
+  std::string ContainerIdentifierToString(
+      const ContainerIdentifier& container_id) const;
+
+  bool ContainerHasRunningOperation(
+      const ContainerIdentifier& container_id) const;
+
+  // Creates a new notification and adds it to running_notifications_.
+  // |app_name| is the name of the application being modified, if any -- for
+  // installs, it will be blank, but for uninstalls, it will have the localized
+  // name of the application in UTF8.
+  // If there is a running notification, it will be set to error state. Caller
+  // should check before calling this if a different behavior is desired.
+  void CreateRunningNotification(
+      const ContainerIdentifier& container_id,
+      CrostiniPackageNotification::NotificationType notification_type,
+      const std::string& app_name);
+
+  // Creates a new uninstall notification and adds it to queued_uninstalls_.
+  void CreateQueuedUninstall(const ContainerIdentifier& container_id,
+                             const std::string& app_id,
+                             const std::string& app_name);
+
+  // Sets the operation status of the current operation. Sets the notification
+  // window's current state and updates containers_with_running_operations_.
+  // Note that if status is |SUCCEEDED| or |FAILED|, this may kick off another
+  // operation from the queued_uninstalls_ list.
+  void UpdatePackageOperationStatus(const ContainerIdentifier& container_id,
+                                    PackageOperationStatus status,
+                                    int progress_percent);
+
+  // Wraps the callback provided in GetLinuxPackageInfo().
+  void OnGetLinuxPackageInfo(
+      const std::string& vm_name,
+      const std::string& container_name,
+      CrostiniManager::GetLinuxPackageInfoCallback callback,
+      const LinuxPackageInfo& linux_package_info);
+
+  // Wraps the callback provided in InstallLinuxPackage().
+  void OnInstallLinuxPackage(
+      const std::string& vm_name,
+      const std::string& container_name,
+      CrostiniManager::InstallLinuxPackageCallback callback,
+      CrostiniResult result);
+
+  // Kicks off an uninstall of the given app. Never queues the operation. Helper
+  // for QueueUninstallApplication (if the operation can be performed
+  // immediately) and StartQueuedUninstall.
+  void UninstallApplication(
+      const CrostiniRegistryService::Registration& registration,
+      const std::string& app_id);
+
+  // Callback when the Crostini container is up and ready to accept messages.
+  // Used by the uninstall flow only.
+  void OnCrostiniRunningForUninstall(const ContainerIdentifier& container_id,
+                                     const std::string& desktop_file_id,
+                                     CrostiniResult result);
+
+  // Callback for CrostiniManager::UninstallPackageOwningFile().
+  void OnUninstallPackageOwningFile(const ContainerIdentifier& container_id,
+                                    CrostiniResult result);
+
+  // Kick off the next operation in the queue for the given container.
+  void StartQueuedUninstall(const ContainerIdentifier& container_id);
+
+  std::string GetUniqueNotificationId();
+
+  Profile* profile_;
+
+  // The notifications in the RUNNING state for each container.
+  std::map<ContainerIdentifier, std::unique_ptr<CrostiniPackageNotification>>
+      running_notifications_;
+
+  // A list of containers with running operations. Generally, matches the list
+  // of containers with notifications, but we need a separate copy of the state
+  // in case the user closes the notification while still running.
+  std::set<ContainerIdentifier> containers_with_running_operations_;
+
+  // Uninstalls we want to run when the current one is done. Operations are
+  // queued in FIFO order (but we can't use std::queue because we sometimes need
+  // to erase a notification window pointer in the middle of the queue).
+  std::map<ContainerIdentifier, std::deque<QueuedUninstall>> queued_uninstalls_;
+
+  // Notifications in a finished state (either SUCCEEDED or FAILED). We need
+  // to keep notifications around until they are dismissed even if we don't
+  // update them any more.
+  std::vector<std::unique_ptr<CrostiniPackageNotification>>
+      finished_notifications_;
+
+  int next_notification_id_ = 0;
+
+  base::WeakPtrFactory<CrostiniPackageService> weak_ptr_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(CrostiniPackageService);
+};
+
+}  // namespace crostini
+
+#endif  // CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_PACKAGE_SERVICE_H_
diff --git a/chrome/browser/chromeos/crostini/crostini_util.h b/chrome/browser/chromeos/crostini/crostini_util.h
index 86c0364c..e1a6fae 100644
--- a/chrome/browser/chromeos/crostini/crostini_util.h
+++ b/chrome/browser/chromeos/crostini/crostini_util.h
@@ -6,6 +6,7 @@
 #define CHROME_BROWSER_CHROMEOS_CROSTINI_CROSTINI_UTIL_H_
 
 #include <string>
+#include <vector>
 
 #include "base/callback.h"
 #include "base/optional.h"
@@ -107,6 +108,9 @@
 // Shows the Crostini Uninstaller dialog.
 void ShowCrostiniUninstallerView(Profile* profile,
                                  CrostiniUISurface ui_surface);
+// Shows the Crostini App Uninstaller dialog.
+void ShowCrostiniAppUninstallerView(Profile* profile,
+                                    const std::string& app_id);
 // Shows the Crostini Upgrade dialog.
 void ShowCrostiniUpgradeView(Profile* profile, CrostiniUISurface ui_surface);
 
diff --git a/chrome/browser/chromeos/extensions/file_manager/private_api_misc.cc b/chrome/browser/chromeos/extensions/file_manager/private_api_misc.cc
index 41e67ce..fca554ec 100644
--- a/chrome/browser/chromeos/extensions/file_manager/private_api_misc.cc
+++ b/chrome/browser/chromeos/extensions/file_manager/private_api_misc.cc
@@ -20,7 +20,7 @@
 #include "base/strings/stringprintf.h"
 #include "base/strings/utf_string_conversions.h"
 #include "chrome/browser/browser_process.h"
-#include "chrome/browser/chromeos/crostini/crostini_package_installer_service.h"
+#include "chrome/browser/chromeos/crostini/crostini_package_service.h"
 #include "chrome/browser/chromeos/crostini/crostini_share_path.h"
 #include "chrome/browser/chromeos/crostini/crostini_util.h"
 #include "chrome/browser/chromeos/drive/drive_integration_service.h"
@@ -784,14 +784,12 @@
     return RespondNow(Error("Invalid url: " + params->url));
   }
 
-  crostini::CrostiniPackageInstallerService::GetForProfile(profile)
-      ->GetLinuxPackageInfo(
-          crostini::kCrostiniDefaultVmName,
-          crostini::kCrostiniDefaultContainerName, path.value(),
-          base::BindOnce(
-              &FileManagerPrivateInternalGetLinuxPackageInfoFunction::
-                  OnGetLinuxPackageInfo,
-              this));
+  crostini::CrostiniPackageService::GetForProfile(profile)->GetLinuxPackageInfo(
+      crostini::kCrostiniDefaultVmName, crostini::kCrostiniDefaultContainerName,
+      path.value(),
+      base::BindOnce(&FileManagerPrivateInternalGetLinuxPackageInfoFunction::
+                         OnGetLinuxPackageInfo,
+                     this));
   return RespondLater();
 }
 
@@ -832,20 +830,17 @@
     return RespondNow(Error("Invalid url: " + params->url));
   }
 
-  crostini::CrostiniPackageInstallerService::GetForProfile(profile)
-      ->InstallLinuxPackage(
-          crostini::kCrostiniDefaultVmName,
-          crostini::kCrostiniDefaultContainerName, path.value(),
-          base::BindOnce(
-              &FileManagerPrivateInternalInstallLinuxPackageFunction::
-                  OnInstallLinuxPackage,
-              this));
+  crostini::CrostiniPackageService::GetForProfile(profile)->InstallLinuxPackage(
+      crostini::kCrostiniDefaultVmName, crostini::kCrostiniDefaultContainerName,
+      path.value(),
+      base::BindOnce(&FileManagerPrivateInternalInstallLinuxPackageFunction::
+                         OnInstallLinuxPackage,
+                     this));
   return RespondLater();
 }
 
 void FileManagerPrivateInternalInstallLinuxPackageFunction::
-    OnInstallLinuxPackage(crostini::CrostiniResult result,
-                          const std::string& failure_reason) {
+    OnInstallLinuxPackage(crostini::CrostiniResult result) {
   extensions::api::file_manager_private::InstallLinuxPackageResponse response;
   switch (result) {
     case crostini::CrostiniResult::SUCCESS:
@@ -856,16 +851,15 @@
       response = extensions::api::file_manager_private::
           INSTALL_LINUX_PACKAGE_RESPONSE_FAILED;
       break;
-    case crostini::CrostiniResult::INSTALL_LINUX_PACKAGE_ALREADY_ACTIVE:
+    case crostini::CrostiniResult::BLOCKING_OPERATION_ALREADY_ACTIVE:
       response = extensions::api::file_manager_private::
           INSTALL_LINUX_PACKAGE_RESPONSE_INSTALL_ALREADY_ACTIVE;
       break;
     default:
       NOTREACHED();
   }
-  Respond(ArgumentList(
-      extensions::api::file_manager_private_internal::InstallLinuxPackage::
-          Results::Create(response, failure_reason)));
+  Respond(ArgumentList(extensions::api::file_manager_private_internal::
+                           InstallLinuxPackage::Results::Create(response)));
 }
 
 FileManagerPrivateInternalGetCustomActionsFunction::
diff --git a/chrome/browser/chromeos/extensions/file_manager/private_api_misc.h b/chrome/browser/chromeos/extensions/file_manager/private_api_misc.h
index 95af4da..59e36d91 100644
--- a/chrome/browser/chromeos/extensions/file_manager/private_api_misc.h
+++ b/chrome/browser/chromeos/extensions/file_manager/private_api_misc.h
@@ -399,8 +399,7 @@
 
  private:
   ResponseAction Run() override;
-  void OnInstallLinuxPackage(crostini::CrostiniResult result,
-                             const std::string& failure_reason);
+  void OnInstallLinuxPackage(crostini::CrostiniResult result);
   DISALLOW_COPY_AND_ASSIGN(
       FileManagerPrivateInternalInstallLinuxPackageFunction);
 };
diff --git a/chrome/browser/ui/BUILD.gn b/chrome/browser/ui/BUILD.gn
index 0fb40a6..1972d17 100644
--- a/chrome/browser/ui/BUILD.gn
+++ b/chrome/browser/ui/BUILD.gn
@@ -3286,6 +3286,8 @@
         "views/arc_data_removal_dialog_view.cc",
         "views/crostini/crostini_app_restart_view.cc",
         "views/crostini/crostini_app_restart_view.h",
+        "views/crostini/crostini_app_uninstaller_view.cc",
+        "views/crostini/crostini_app_uninstaller_view.h",
         "views/crostini/crostini_installer_view.cc",
         "views/crostini/crostini_installer_view.h",
         "views/crostini/crostini_uninstaller_view.cc",
diff --git a/chrome/browser/ui/app_list/crostini/crostini_app_context_menu.cc b/chrome/browser/ui/app_list/crostini/crostini_app_context_menu.cc
index 57a57ea3..ef73e52d 100644
--- a/chrome/browser/ui/app_list/crostini/crostini_app_context_menu.cc
+++ b/chrome/browser/ui/app_list/crostini/crostini_app_context_menu.cc
@@ -76,9 +76,10 @@
       if (app_id() == crostini::kCrostiniTerminalId) {
         crostini::ShowCrostiniUninstallerView(
             profile(), crostini::CrostiniUISurface::kAppList);
-        return;
+      } else {
+        crostini::ShowCrostiniAppUninstallerView(profile(), app_id());
       }
-      break;
+      return;
 
     case ash::STOP_APP:
       if (app_id() == crostini::kCrostiniTerminalId) {
diff --git a/chrome/browser/ui/browser_dialogs.h b/chrome/browser/ui/browser_dialogs.h
index 5efd8e2..b3c7567 100644
--- a/chrome/browser/ui/browser_dialogs.h
+++ b/chrome/browser/ui/browser_dialogs.h
@@ -269,6 +269,7 @@
   HATS_BUBBLE = 90,
   CROSTINI_APP_RESTART = 91,
   INCOGNITO_WINDOW_COUNTER = 92,
+  CROSTINI_APP_UNINSTALLER = 93,
   MAX_VALUE
 };
 
diff --git a/chrome/browser/ui/views/crostini/crostini_app_uninstaller_view.cc b/chrome/browser/ui/views/crostini/crostini_app_uninstaller_view.cc
new file mode 100644
index 0000000..2f583ab
--- /dev/null
+++ b/chrome/browser/ui/views/crostini/crostini_app_uninstaller_view.cc
@@ -0,0 +1,109 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chrome/browser/ui/views/crostini/crostini_app_uninstaller_view.h"
+
+#include <memory>
+
+#include "base/strings/utf_string_conversions.h"
+#include "chrome/browser/chromeos/crostini/crostini_package_service.h"
+#include "chrome/browser/chromeos/crostini/crostini_registry_service.h"
+#include "chrome/browser/chromeos/crostini/crostini_registry_service_factory.h"
+#include "chrome/browser/chromeos/crostini/crostini_util.h"
+#include "chrome/browser/ui/browser_dialogs.h"
+#include "chrome/browser/ui/views/chrome_layout_provider.h"
+#include "chrome/grit/generated_resources.h"
+#include "ui/base/l10n/l10n_util.h"
+#include "ui/strings/grit/ui_strings.h"
+#include "ui/views/controls/label.h"
+#include "ui/views/layout/box_layout.h"
+
+namespace crostini {
+
+// Declaration in crostini_util.h, definition here. Needed because of include
+// restrictions.
+void ShowCrostiniAppUninstallerView(Profile* profile,
+                                    const std::string& app_id) {
+  CrostiniAppUninstallerView::Show(profile, app_id);
+}
+
+}  // namespace crostini
+
+// static
+void CrostiniAppUninstallerView::Show(Profile* profile,
+                                      const std::string& app_id) {
+  DCHECK(crostini::IsCrostiniUIAllowedForProfile(profile));
+  views::DialogDelegate::CreateDialogWidget(
+      new CrostiniAppUninstallerView(profile, app_id), nullptr, nullptr)
+      ->Show();
+}
+
+int CrostiniAppUninstallerView::GetDialogButtons() const {
+  return ui::DIALOG_BUTTON_OK | ui::DIALOG_BUTTON_CANCEL;
+}
+
+base::string16 CrostiniAppUninstallerView::GetDialogButtonLabel(
+    ui::DialogButton button) const {
+  if (button == ui::DIALOG_BUTTON_OK)
+    return l10n_util::GetStringUTF16(
+        IDS_CROSTINI_APPLICATION_UNINSTALL_UNINSTALL_BUTTON);
+  DCHECK_EQ(button, ui::DIALOG_BUTTON_CANCEL);
+  return l10n_util::GetStringUTF16(IDS_APP_CANCEL);
+}
+
+base::string16 CrostiniAppUninstallerView::GetWindowTitle() const {
+  return l10n_util::GetStringUTF16(
+      IDS_CROSTINI_APPLICATION_UNINSTALL_CONFIRM_TITLE);
+}
+
+bool CrostiniAppUninstallerView::ShouldShowCloseButton() const {
+  return false;
+}
+
+bool CrostiniAppUninstallerView::Accept() {
+  // Switch over to the notification service to uninstall the package and
+  // display notifications related to the uninstall.
+  crostini::CrostiniPackageService::GetForProfile(profile_)
+      ->QueueUninstallApplication(app_id_);
+  return true;  // Should close the dialog
+}
+
+gfx::Size CrostiniAppUninstallerView::CalculatePreferredSize() const {
+  const int dialog_width = ChromeLayoutProvider::Get()->GetDistanceMetric(
+                               DISTANCE_MODAL_DIALOG_PREFERRED_WIDTH) -
+                           margins().width();
+  return gfx::Size(dialog_width, GetHeightForWidth(dialog_width));
+}
+
+CrostiniAppUninstallerView::CrostiniAppUninstallerView(
+    Profile* profile,
+    const std::string& app_id)
+    : profile_(profile), app_id_(app_id), weak_ptr_factory_(this) {
+  views::LayoutProvider* provider = views::LayoutProvider::Get();
+  SetLayoutManager(std::make_unique<views::BoxLayout>(
+      views::BoxLayout::kVertical,
+      provider->GetInsetsMetric(views::InsetsMetric::INSETS_DIALOG),
+      provider->GetDistanceMetric(views::DISTANCE_RELATED_CONTROL_VERTICAL)));
+  set_margins(provider->GetDialogInsetsForContentType(
+      views::DialogContentType::TEXT, views::DialogContentType::TEXT));
+
+  crostini::CrostiniRegistryService* registry =
+      crostini::CrostiniRegistryServiceFactory::GetForProfile(profile);
+  DCHECK(registry);
+  auto app_registration = registry->GetRegistration(app_id);
+  DCHECK(app_registration);
+  const base::string16 app_name = base::UTF8ToUTF16(app_registration->Name());
+
+  const base::string16 message = l10n_util::GetStringFUTF16(
+      IDS_CROSTINI_APPLICATION_UNINSTALL_CONFIRM_BODY, app_name);
+  auto* message_label = new views::Label(message);
+  message_label->SetMultiLine(true);
+  message_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
+  AddChildView(message_label);
+
+  chrome::RecordDialogCreation(
+      chrome::DialogIdentifier::CROSTINI_APP_UNINSTALLER);
+}
+
+CrostiniAppUninstallerView::~CrostiniAppUninstallerView() = default;
diff --git a/chrome/browser/ui/views/crostini/crostini_app_uninstaller_view.h b/chrome/browser/ui/views/crostini/crostini_app_uninstaller_view.h
new file mode 100644
index 0000000..b468e7c
--- /dev/null
+++ b/chrome/browser/ui/views/crostini/crostini_app_uninstaller_view.h
@@ -0,0 +1,43 @@
+// Copyright 2018 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CHROME_BROWSER_UI_VIEWS_CROSTINI_CROSTINI_APP_UNINSTALLER_VIEW_H_
+#define CHROME_BROWSER_UI_VIEWS_CROSTINI_CROSTINI_APP_UNINSTALLER_VIEW_H_
+
+#include <string>
+
+#include "ui/views/window/dialog_delegate.h"
+
+class Profile;
+
+// The Crostini application uninstaller. Displays a confirmation prompt,
+// and kicks off the uninstall if the user confirms that they want the app
+// uninstalled. Subsequent notifications are handled by CrostiniPackageService.
+class CrostiniAppUninstallerView : public views::DialogDelegateView {
+ public:
+  // Show the "are you sure?"-style confirmation prompt. |app_id| should be an
+  // ID understood by CrostiniRegistryService::GetRegistration().
+  static void Show(Profile* profile, const std::string& app_id);
+
+  // views::DialogDelegateView:
+  int GetDialogButtons() const override;
+  base::string16 GetDialogButtonLabel(ui::DialogButton button) const override;
+  base::string16 GetWindowTitle() const override;
+  bool ShouldShowCloseButton() const override;
+  bool Accept() override;
+  gfx::Size CalculatePreferredSize() const override;
+
+ private:
+  CrostiniAppUninstallerView(Profile* profile, const std::string& app_id);
+  ~CrostiniAppUninstallerView() override;
+
+  Profile* profile_;
+  std::string app_id_;
+
+  base::WeakPtrFactory<CrostiniAppUninstallerView> weak_ptr_factory_;
+
+  DISALLOW_COPY_AND_ASSIGN(CrostiniAppUninstallerView);
+};
+
+#endif  // CHROME_BROWSER_UI_VIEWS_CROSTINI_CROSTINI_APP_UNINSTALLER_VIEW_H_
diff --git a/chrome/common/extensions/api/file_manager_private_internal.idl b/chrome/common/extensions/api/file_manager_private_internal.idl
index 9fb75d3..97bfd0b 100644
--- a/chrome/common/extensions/api/file_manager_private_internal.idl
+++ b/chrome/common/extensions/api/file_manager_private_internal.idl
@@ -37,8 +37,7 @@
   callback GetLinuxPackageInfoCallback =
       void(fileManagerPrivate.LinuxPackageInfo linux_package_info);
   callback InstallLinuxPackageCallback =
-      void(fileManagerPrivate.InstallLinuxPackageResponse response,
-           optional DOMString failure_reason);
+      void(fileManagerPrivate.InstallLinuxPackageResponse response);
   callback GetThumbnailCallback = void(DOMString ThumbnailDataUrl);
 
   interface Functions {
diff --git a/chromeos/dbus/cicerone_client.cc b/chromeos/dbus/cicerone_client.cc
index 16837d4..d6fb71d 100644
--- a/chromeos/dbus/cicerone_client.cc
+++ b/chromeos/dbus/cicerone_client.cc
@@ -4,6 +4,9 @@
 
 #include "chromeos/dbus/cicerone_client.h"
 
+#include <string>
+#include <utility>
+
 #include "base/bind.h"
 #include "base/location.h"
 #include "base/observer_list.h"
@@ -51,6 +54,10 @@
     return is_install_linux_package_progress_signal_connected_;
   }
 
+  bool IsUninstallPackageProgressSignalConnected() override {
+    return is_uninstall_package_progress_signal_connected_;
+  }
+
   bool IsLxdContainerCreatedSignalConnected() override {
     return is_lxd_container_created_signal_connected_;
   }
@@ -157,6 +164,31 @@
                        weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
   }
 
+  void UninstallPackageOwningFile(
+      const vm_tools::cicerone::UninstallPackageOwningFileRequest& request,
+      DBusMethodCallback<vm_tools::cicerone::UninstallPackageOwningFileResponse>
+          callback) override {
+    dbus::MethodCall method_call(
+        vm_tools::cicerone::kVmCiceroneInterface,
+        vm_tools::cicerone::kUninstallPackageOwningFileMethod);
+    dbus::MessageWriter writer(&method_call);
+
+    if (!writer.AppendProtoAsArrayOfBytes(request)) {
+      LOG(ERROR)
+          << "Failed to encode UninstallPackageOwningFileRequest protobuf";
+      base::ThreadTaskRunnerHandle::Get()->PostTask(
+          FROM_HERE, base::BindOnce(std::move(callback), base::nullopt));
+      return;
+    }
+
+    cicerone_proxy_->CallMethod(
+        &method_call, dbus::ObjectProxy::TIMEOUT_USE_DEFAULT,
+        base::BindOnce(
+            &CiceroneClientImpl::OnDBusProtoResponse<
+                vm_tools::cicerone::UninstallPackageOwningFileResponse>,
+            weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
+  }
+
   void CreateLxdContainer(
       const vm_tools::cicerone::CreateLxdContainerRequest& request,
       DBusMethodCallback<vm_tools::cicerone::CreateLxdContainerResponse>
@@ -286,6 +318,14 @@
                        weak_ptr_factory_.GetWeakPtr()));
     cicerone_proxy_->ConnectToSignal(
         vm_tools::cicerone::kVmCiceroneInterface,
+        vm_tools::cicerone::kUninstallPackageProgressSignal,
+        base::BindRepeating(
+            &CiceroneClientImpl::OnUninstallPackageProgressSignal,
+            weak_ptr_factory_.GetWeakPtr()),
+        base::BindOnce(&CiceroneClientImpl::OnSignalConnected,
+                       weak_ptr_factory_.GetWeakPtr()));
+    cicerone_proxy_->ConnectToSignal(
+        vm_tools::cicerone::kVmCiceroneInterface,
         vm_tools::cicerone::kLxdContainerCreatedSignal,
         base::BindRepeating(&CiceroneClientImpl::OnLxdContainerCreatedSignal,
                             weak_ptr_factory_.GetWeakPtr()),
@@ -362,6 +402,18 @@
     }
   }
 
+  void OnUninstallPackageProgressSignal(dbus::Signal* signal) {
+    vm_tools::cicerone::UninstallPackageProgressSignal proto;
+    dbus::MessageReader reader(signal);
+    if (!reader.PopArrayOfBytesAsProto(&proto)) {
+      LOG(ERROR) << "Failed to parse proto from DBus Signal";
+      return;
+    }
+    for (auto& observer : observer_list_) {
+      observer.OnUninstallPackageProgress(proto);
+    }
+  }
+
   void OnLxdContainerCreatedSignal(dbus::Signal* signal) {
     vm_tools::cicerone::LxdContainerCreatedSignal proto;
     dbus::MessageReader reader(signal);
@@ -413,6 +465,9 @@
     } else if (signal_name ==
                vm_tools::cicerone::kInstallLinuxPackageProgressSignal) {
       is_install_linux_package_progress_signal_connected_ = is_connected;
+    } else if (signal_name ==
+               vm_tools::cicerone::kUninstallPackageProgressSignal) {
+      is_uninstall_package_progress_signal_connected_ = is_connected;
     } else if (signal_name == vm_tools::cicerone::kLxdContainerCreatedSignal) {
       is_lxd_container_created_signal_connected_ = is_connected;
     } else if (signal_name ==
@@ -432,6 +487,7 @@
   bool is_container_started_signal_connected_ = false;
   bool is_container_shutdown_signal_connected_ = false;
   bool is_install_linux_package_progress_signal_connected_ = false;
+  bool is_uninstall_package_progress_signal_connected_ = false;
   bool is_lxd_container_created_signal_connected_ = false;
   bool is_lxd_container_downloading_signal_connected_ = false;
   bool is_tremplin_started_signal_connected_ = false;
diff --git a/chromeos/dbus/cicerone_client.h b/chromeos/dbus/cicerone_client.h
index 6b832e9..aa74506 100644
--- a/chromeos/dbus/cicerone_client.h
+++ b/chromeos/dbus/cicerone_client.h
@@ -36,6 +36,11 @@
         const vm_tools::cicerone::InstallLinuxPackageProgressSignal&
             signal) = 0;
 
+    // This is signaled from the container while a package is being uninstalled
+    // via UninstallPackageOwningFile.
+    virtual void OnUninstallPackageProgress(
+        const vm_tools::cicerone::UninstallPackageProgressSignal& signal) = 0;
+
     // OnLxdContainerCreated is signaled from Cicerone when the long running
     // creation of an Lxd container is complete.
     virtual void OnLxdContainerCreated(
@@ -75,6 +80,9 @@
   // This should be true prior to calling InstallLinuxPackage.
   virtual bool IsInstallLinuxPackageProgressSignalConnected() = 0;
 
+  // This should be true prior to calling UninstallPackageOwningFile.
+  virtual bool IsUninstallPackageProgressSignalConnected() = 0;
+
   // This should be true prior to calling CreateLxdContainer or
   // StartLxdContainer.
   virtual bool IsLxdContainerCreatedSignalConnected() = 0;
@@ -115,6 +123,13 @@
       DBusMethodCallback<vm_tools::cicerone::InstallLinuxPackageResponse>
           callback) = 0;
 
+  // Uninstalls the package that owns the indicated .desktop file.
+  // |callback| is called after the method call finishes.
+  virtual void UninstallPackageOwningFile(
+      const vm_tools::cicerone::UninstallPackageOwningFileRequest& request,
+      DBusMethodCallback<vm_tools::cicerone::UninstallPackageOwningFileResponse>
+          callback) = 0;
+
   // Creates a new Lxd Container.
   // |callback| is called to indicate creation status.
   // |Observer::OnLxdContainerCreated| will be called on completion.
diff --git a/chromeos/dbus/fake_cicerone_client.cc b/chromeos/dbus/fake_cicerone_client.cc
index 6404da7..f972995 100644
--- a/chromeos/dbus/fake_cicerone_client.cc
+++ b/chromeos/dbus/fake_cicerone_client.cc
@@ -11,29 +11,24 @@
 namespace chromeos {
 
 FakeCiceroneClient::FakeCiceroneClient() {
-  launch_container_application_response_.Clear();
   launch_container_application_response_.set_success(true);
 
-  container_app_icon_response_.Clear();
-
-  get_linux_package_info_response_.Clear();
   get_linux_package_info_response_.set_success(true);
   get_linux_package_info_response_.set_package_id("Fake Package;1.0;x86-64");
   get_linux_package_info_response_.set_summary("A package that is fake");
 
-  install_linux_package_response_.Clear();
   install_linux_package_response_.set_status(
       vm_tools::cicerone::InstallLinuxPackageResponse::STARTED);
 
-  create_lxd_container_response_.Clear();
+  uninstall_package_owning_file_response_.set_status(
+      vm_tools::cicerone::UninstallPackageOwningFileResponse::STARTED);
+
   create_lxd_container_response_.set_status(
       vm_tools::cicerone::CreateLxdContainerResponse::CREATING);
 
-  start_lxd_container_response_.Clear();
   start_lxd_container_response_.set_status(
       vm_tools::cicerone::StartLxdContainerResponse::STARTED);
 
-  setup_lxd_container_user_response_.Clear();
   setup_lxd_container_user_response_.set_status(
       vm_tools::cicerone::SetUpLxdContainerUserResponse::SUCCESS);
 }
@@ -72,6 +67,10 @@
   return is_install_linux_package_progress_signal_connected_;
 }
 
+bool FakeCiceroneClient::IsUninstallPackageProgressSignalConnected() {
+  return is_uninstall_package_progress_signal_connected_;
+}
+
 void FakeCiceroneClient::LaunchContainerApplication(
     const vm_tools::cicerone::LaunchContainerApplicationRequest& request,
     DBusMethodCallback<vm_tools::cicerone::LaunchContainerApplicationResponse>
@@ -106,6 +105,15 @@
       base::BindOnce(std::move(callback), install_linux_package_response_));
 }
 
+void FakeCiceroneClient::UninstallPackageOwningFile(
+    const vm_tools::cicerone::UninstallPackageOwningFileRequest& request,
+    DBusMethodCallback<vm_tools::cicerone::UninstallPackageOwningFileResponse>
+        callback) {
+  base::ThreadTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE, base::BindOnce(std::move(callback),
+                                uninstall_package_owning_file_response_));
+}
+
 void FakeCiceroneClient::WaitForServiceToBeAvailable(
     dbus::ObjectProxy::WaitForServiceToBeAvailableCallback callback) {
   base::ThreadTaskRunnerHandle::Get()->PostTask(
diff --git a/chromeos/dbus/fake_cicerone_client.h b/chromeos/dbus/fake_cicerone_client.h
index c81875c..fa62094 100644
--- a/chromeos/dbus/fake_cicerone_client.h
+++ b/chromeos/dbus/fake_cicerone_client.h
@@ -32,6 +32,9 @@
   // This should be true prior to calling InstallLinuxPackage.
   bool IsInstallLinuxPackageProgressSignalConnected() override;
 
+  // This should be true prior to calling UninstallPackageOwningFile.
+  bool IsUninstallPackageProgressSignalConnected() override;
+
   // This should be true prior to calling CreateLxdContainer or
   // StartLxdContainer.
   bool IsLxdContainerCreatedSignalConnected() override;
@@ -73,6 +76,14 @@
       DBusMethodCallback<vm_tools::cicerone::InstallLinuxPackageResponse>
           callback) override;
 
+  // Fake version of the method that uninstalls an application inside a running
+  // Container. |callback| is called after the method call finishes. This does
+  // not cause progress events to be fired.
+  void UninstallPackageOwningFile(
+      const vm_tools::cicerone::UninstallPackageOwningFileRequest& request,
+      DBusMethodCallback<vm_tools::cicerone::UninstallPackageOwningFileResponse>
+          callback) override;
+
   // Fake version of the method that creates a new Container.
   // |callback| is called to indicate creation status.
   void CreateLxdContainer(
@@ -121,6 +132,11 @@
     is_install_linux_package_progress_signal_connected_ = connected;
   }
 
+  // Set IsUninstallPackageProgressSignalConnected state
+  void set_uninstall_package_progress_signal_connected(bool connected) {
+    is_uninstall_package_progress_signal_connected_ = connected;
+  }
+
   // Set LxdContainerCreatedSignalConnected state
   void set_lxd_container_created_signal_connected(bool connected) {
     is_lxd_container_created_signal_connected_ = connected;
@@ -165,6 +181,13 @@
     install_linux_package_response_ = install_linux_package_response;
   }
 
+  void set_uninstall_package_owning_file_response(
+      const vm_tools::cicerone::UninstallPackageOwningFileResponse&
+          uninstall_package_owning_file_response) {
+    uninstall_package_owning_file_response_ =
+        uninstall_package_owning_file_response;
+  }
+
   void set_create_lxd_container_response(
       const vm_tools::cicerone::CreateLxdContainerResponse&
           create_lxd_container_response) {
@@ -204,6 +227,7 @@
   bool is_container_started_signal_connected_ = true;
   bool is_container_shutdown_signal_connected_ = true;
   bool is_install_linux_package_progress_signal_connected_ = true;
+  bool is_uninstall_package_progress_signal_connected_ = true;
   bool is_lxd_container_created_signal_connected_ = true;
   bool is_lxd_container_downloading_signal_connected_ = true;
   bool is_tremplin_started_signal_connected_ = true;
@@ -218,6 +242,8 @@
   vm_tools::cicerone::LinuxPackageInfoResponse get_linux_package_info_response_;
   vm_tools::cicerone::InstallLinuxPackageResponse
       install_linux_package_response_;
+  vm_tools::cicerone::UninstallPackageOwningFileResponse
+      uninstall_package_owning_file_response_;
   vm_tools::cicerone::CreateLxdContainerResponse create_lxd_container_response_;
   vm_tools::cicerone::StartLxdContainerResponse start_lxd_container_response_;
   vm_tools::cicerone::GetLxdContainerUsernameResponse