Uninstall apk if installed before re-installing

PiperOrigin-RevId: 575106519
diff --git a/ui_automator/android_device.py b/ui_automator/android_device.py
new file mode 100644
index 0000000..5d1f224
--- /dev/null
+++ b/ui_automator/android_device.py
@@ -0,0 +1,48 @@
+"""Android device related methods which extend from UI Automator.
+
+Android device class is defined in `mobly.controller`.
+"""
+
+from mobly import utils
+from mobly.controllers import android_device
+
+
+def is_apk_installed(
+    device: android_device.AndroidDevice, package_name: str
+) -> bool:
+  """Checks if given package is already installed on the Android device.
+
+  Args:
+    device: An AndroidDevice object.
+    package_name: The Android app package name.
+
+  Raises:
+    adb.AdbError: When executing adb command fails.
+
+  Returns:
+    True if package is installed. False otherwise.
+  """
+  out = device.adb.shell(['pm', 'list', 'package'])
+  return bool(utils.grep('^package:%s$' % package_name, out))
+
+
+def install_apk(device: android_device.AndroidDevice, apk_path: str) -> None:
+  """Installs required apk snippet to the given Android device.
+
+  Args:
+    device: An AndroidDevice object.
+    apk_path: The absolute file path where the apk is located.
+  """
+  device.adb.install(['-r', '-g', apk_path])
+
+
+def uninstall_apk(
+    device: android_device.AndroidDevice, package_name: str
+) -> None:
+  """Uninstalls required apk snippet on given Android device.
+
+  Args:
+    device: An AndroidDevice object.
+    package_name: The Android app package name.
+  """
+  device.adb.uninstall(package_name)
diff --git a/ui_automator/android_device_test.py b/ui_automator/android_device_test.py
new file mode 100644
index 0000000..9f1fd46
--- /dev/null
+++ b/ui_automator/android_device_test.py
@@ -0,0 +1,73 @@
+"""Unittest of android device extended methods."""
+import unittest
+from unittest import mock
+
+from mobly.controllers import android_device
+from mobly.controllers.android_device_lib import adb
+
+from ui_automator import android_device as ad
+
+
+class UIAutomatorTest(unittest.TestCase):
+
+  def setUp(self):
+    """This method will be run before each of the test methods in the class."""
+    super().setUp()
+    self.mock_android_device = mock.patch.object(
+        android_device, 'AndroidDevice'
+    ).start()
+
+  def test_is_apk_installed_returns_true_with_installed_apk(self):
+    self.mock_android_device.adb.shell.return_value = (
+        b'package:installed.apk\npackage:a.apk\npackage:b.apk\n'
+    )
+
+    is_apk_installed = ad.is_apk_installed(
+        self.mock_android_device, 'installed.apk'
+    )
+
+    self.assertTrue(is_apk_installed)
+
+  def test_is_apk_installed_returns_false_with_not_installed_apk(self):
+    self.mock_android_device.adb.shell.return_value = (
+        b'package:installed.apk\npackage:a.apk\npackage:b.apk\n'
+    )
+
+    is_apk_installed = ad.is_apk_installed(
+        self.mock_android_device, 'notinstalled.apk'
+    )
+
+    self.assertFalse(is_apk_installed)
+
+  def test_is_apk_installed_raises_an_error(self):
+    self.mock_android_device.adb.shell.side_effect = adb.AdbError(
+        cmd='adb.shell',
+        stderr=b'Run adb command failed.',
+        stdout=b'',
+        ret_code=1,
+    )
+
+    with self.assertRaisesRegex(adb.AdbError, r'Run adb command failed\.'):
+      ad.is_apk_installed(self.mock_android_device, 'fake.apk')
+
+  def test_install_apk_installs_apk_with_correct_path(self):
+    apk_path = '/path/to/fake.apk'
+
+    ad.install_apk(self.mock_android_device, apk_path)
+
+    self.mock_android_device.adb.install.assert_called_once_with(
+        ['-r', '-g', apk_path]
+    )
+
+  def test_uninstall_apk_uninstalls_apk_with_given_package_name(self):
+    package_to_be_uninstalled = 'notinstalled.apk'
+
+    ad.uninstall_apk(self.mock_android_device, package_to_be_uninstalled)
+
+    self.mock_android_device.adb.uninstall.assert_called_once_with(
+        package_to_be_uninstalled
+    )
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/ui_automator/ui_automator.py b/ui_automator/ui_automator.py
index fef9fea..63343c6 100644
--- a/ui_automator/ui_automator.py
+++ b/ui_automator/ui_automator.py
@@ -26,10 +26,10 @@
 from absl import app
 from absl import flags
 import inflection
-from mobly import utils
 from mobly.controllers import android_device
 from mobly.controllers.android_device_lib import adb
 from mobly.snippet import errors as snippet_errors
+from ui_automator import android_device as ad
 
 from ui_automator import errors
 
@@ -184,8 +184,10 @@
           'No Android device connected to the host computer.'
       )
 
-    if not self._is_apk_installed(self._connected_device, _MOBLY_SNIPPET_APK):
-      self._install_apk(self._connected_device, self._get_mbs_apk_path())
+    if ad.is_apk_installed(self._connected_device, _MOBLY_SNIPPET_APK):
+      ad.uninstall_apk(self._connected_device, _MOBLY_SNIPPET_APK)
+
+    ad.install_apk(self._connected_device, self._get_mbs_apk_path())
 
     try:
       self._connected_device.load_snippet(
@@ -255,35 +257,6 @@
           f' device({self._connected_device.device_info["serial"]}).'
       ) from e
 
-  def _is_apk_installed(
-      self, device: android_device.AndroidDevice, package_name: str
-  ) -> bool:
-    """Checks if given package is already installed on the Android device.
-
-    Args:
-      device: An AndroidDevice object.
-      package_name: The Android app package name.
-
-    Raises:
-      adb.AdbError: When executing adb command fails.
-
-    Returns:
-      True if package is installed. False otherwise.
-    """
-    out = device.adb.shell(['pm', 'list', 'package'])
-    return bool(utils.grep('^package:%s$' % package_name, out))
-
-  def _install_apk(
-      self, device: android_device.AndroidDevice, apk_path: str
-  ) -> None:
-    """Installs required apk snippet to the given Android device.
-
-    Args:
-      device: An AndroidDevice object.
-      apk_path: The absolute file path where the apk is located.
-    """
-    device.adb.install(['-r', '-g', apk_path])
-
   def _get_mbs_apk_path(self) -> str:
     return os.path.join(
         os.path.dirname(os.path.abspath(__file__)),
diff --git a/ui_automator/ui_automator_test.py b/ui_automator/ui_automator_test.py
index 74617a2..707264a 100644
--- a/ui_automator/ui_automator_test.py
+++ b/ui_automator/ui_automator_test.py
@@ -184,11 +184,14 @@
     self.mock_android_device.adb.install.assert_called_once_with(
         ['-r', '-g', '/path/to/android/app/snippet-0.0.0.apk']
     )
+    self.mock_android_device.adb.uninstall.assert_not_called()
     mock_dirname.assert_called_once()
 
   @mock.patch.object(android_device, 'get_all_instances', autospec=True)
-  @mock.patch.object(os.path, 'dirname', autospec=True)
-  def test_load_snippet_should_not_install_apk_when_apk_is_installed(
+  @mock.patch.object(
+      os.path, 'dirname', autospec=True, return_value='/path/to/'
+  )
+  def test_load_snippet_uninstalls_apk_before_installing_it_when_installed(
       self, mock_dirname, mock_get_all_instances
   ):
     self.mock_android_device.adb.shell.return_value = (
@@ -199,8 +202,13 @@
 
     self.ui_automator.load_snippet()
 
-    self.mock_android_device.adb.install.assert_not_called()
-    mock_dirname.assert_not_called()
+    self.mock_android_device.adb.uninstall.assert_called_with(
+        'com.chip.interop.moblysnippet'
+    )
+    self.mock_android_device.adb.install.assert_called_once_with(
+        ['-r', '-g', '/path/to/android/app/snippet-0.0.0.apk']
+    )
+    mock_dirname.assert_called_once()
 
   @mock.patch.object(ui_automator.UIAutomator, 'load_device')
   @mock.patch.object(ui_automator.UIAutomator, 'load_snippet')