Merge "Mark 24Q4 as merged in aosp-main-future" into aosp-main-future
diff --git a/atest/atest_execution_info.py b/atest/atest_execution_info.py
index 0f35b98..064f096 100644
--- a/atest/atest_execution_info.py
+++ b/atest/atest_execution_info.py
@@ -389,10 +389,10 @@
       print(f'Test logs: {log_path / "log"}')
     log_link = html_path if html_path else log_path
     if log_link:
-      print(f'Log file list: {atest_utils.mark_magenta(f"file://{log_link}")}')
+      print(atest_utils.mark_magenta(f'Log file list: file://{log_link}'))
     bug_report_url = AtestExecutionInfo._create_bug_report_url()
     if bug_report_url:
-      print(f'Issue report: {bug_report_url}')
+      print(atest_utils.mark_magenta(f"Bug report: {bug_report_url}"))
     print()
 
     # Do not send stacktrace with send_exit_event when exit code is not
diff --git a/atest/atest_main.py b/atest/atest_main.py
index fe12d41..590dcad 100755
--- a/atest/atest_main.py
+++ b/atest/atest_main.py
@@ -916,12 +916,6 @@
       logging.debug('"--test" mode detected, will not rebuild module-info.')
       return False
     if self._args.rebuild_module_info:
-      msg = (
-          f'`{constants.REBUILD_MODULE_INFO_FLAG}` is no longer needed '
-          f'since Atest can smartly rebuild {module_info._MODULE_INFO} '
-          r'only when needed.'
-      )
-      atest_utils.colorful_print(msg, constants.YELLOW)
       return True
     logging.debug('Examinating the consistency of build files...')
     if not atest_utils.build_files_integrity_is_ok():
diff --git a/atest/integration_tests/atest_dry_run_diff_tests.py b/atest/integration_tests/atest_dry_run_diff_tests.py
index b58b6ed..5031b5c 100644
--- a/atest/integration_tests/atest_dry_run_diff_tests.py
+++ b/atest/integration_tests/atest_dry_run_diff_tests.py
@@ -97,6 +97,7 @@
         step_in,
         include_device_serial=False,
         use_prebuilt_atest_binary=use_prod,
+        pipe_to_stdin='n',
     )
     get_prod_result = functools.partial(run_command, True)
     get_dev_result = functools.partial(run_command, False)
diff --git a/atest/integration_tests/atest_integration_test.py b/atest/integration_tests/atest_integration_test.py
index bf14881..c8416d6 100644
--- a/atest/integration_tests/atest_integration_test.py
+++ b/atest/integration_tests/atest_integration_test.py
@@ -344,6 +344,7 @@
       include_device_serial: bool,
       print_output: bool = True,
       use_prebuilt_atest_binary=None,
+      pipe_to_stdin: str = None,
   ) -> AtestRunResult:
     """Run either `atest-dev` or `atest` command through subprocess.
 
@@ -359,6 +360,8 @@
           is running.
         use_prebuilt_atest_binary: Whether to run the command using the prebuilt
           atest binary instead of the atest-dev binary.
+        pipe_to_stdin: A string value to pipe continuously to the stdin of the
+          command subprocess.
 
     Returns:
         An AtestRunResult object containing the run information.
@@ -387,6 +390,7 @@
         env=step_in.get_env(),
         cwd=step_in.get_repo_root(),
         print_output=print_output,
+        pipe_to_stdin=pipe_to_stdin,
     )
     elapsed_time = time.time() - start_time
     result = AtestRunResult(
@@ -419,39 +423,53 @@
       env: dict[str, str],
       cwd: str,
       print_output: bool = True,
+      pipe_to_stdin: str = None,
   ) -> subprocess.CompletedProcess[str]:
     """Execute shell command with real time output printing and capture."""
 
-    def read_output(read_src, print_dst, capture_dst):
+    def read_output(process, read_src, print_dst, capture_dst):
       while (output := read_src.readline()) or process.poll() is None:
         if output:
           if print_output:
             print(output, end='', file=print_dst)
           capture_dst.append(output)
 
-    with subprocess.Popen(
-        cmd,
-        stdout=subprocess.PIPE,
-        stderr=subprocess.PIPE,
-        text=True,
-        env=env,
-        cwd=cwd,
-    ) as process:
-      stdout = []
-      stderr = []
-      with concurrent.futures.ThreadPoolExecutor() as executor:
-        stdout_future = executor.submit(
-            read_output, process.stdout, sys.stdout, stdout
-        )
-        stderr_future = executor.submit(
-            read_output, process.stderr, sys.stderr, stderr
-        )
-      stdout_future.result()
-      stderr_future.result()
+    # Disable log uploading when running locally.
+    env['ENABLE_ATEST_LOG_UPLOADING'] = 'false'
 
-      return subprocess.CompletedProcess(
-          cmd, process.poll(), ''.join(stdout), ''.join(stderr)
-      )
+    def run_popen(stdin=None):
+      with subprocess.Popen(
+          cmd,
+          stdout=subprocess.PIPE,
+          stderr=subprocess.PIPE,
+          stdin=stdin,
+          text=True,
+          env=env,
+          cwd=cwd,
+      ) as process:
+        stdout = []
+        stderr = []
+        with concurrent.futures.ThreadPoolExecutor() as executor:
+          stdout_future = executor.submit(
+              read_output, process, process.stdout, sys.stdout, stdout
+          )
+          stderr_future = executor.submit(
+              read_output, process, process.stderr, sys.stderr, stderr
+          )
+        stdout_future.result()
+        stderr_future.result()
+
+        return subprocess.CompletedProcess(
+            cmd, process.poll(), ''.join(stdout), ''.join(stderr)
+        )
+
+    if pipe_to_stdin:
+      with subprocess.Popen(
+          ['yes', pipe_to_stdin], stdout=subprocess.PIPE
+      ) as yes_process:
+        return run_popen(yes_process.stdout)
+
+    return run_popen()
 
   @staticmethod
   def _get_jdk_path_list() -> str:
diff --git a/experiments/a/a.py b/experiments/a/a.py
index 4399ac2..da66e54 100644
--- a/experiments/a/a.py
+++ b/experiments/a/a.py
@@ -35,22 +35,20 @@
 def run():
   """Entry point for tool."""
   parser = argparse.ArgumentParser(
-      description='Run workflows to build update and test modules',
+      description='A runs tools and workflows for local Android development',
       formatter_class=argparse.RawDescriptionHelpFormatter,
   )
-  parser.add_argument(
-      '-q',
-      '--quiet',
-      action='store_true',
-      help='Do not display progress updates',
-  )
-  subparsers = parser.add_subparsers(dest='tool', required=True)
+  subparsers = parser.add_subparsers(dest='tool')
   for _, tool_class in tools_map.items():
     tool_class.add_parser(subparsers)
 
   args = parser.parse_args()
 
   # Tool
+  if not args.tool:
+    print('Error: Please specify a tool (eg. update)')
+    parser.print_help()
+    exit(1)
   tool_name = args.tool.lower()
   tool = tools_map[tool_name](args)
   return tool.main()
diff --git a/experiments/a/tools/update.py b/experiments/a/tools/update.py
index a505af7..ea6663d 100644
--- a/experiments/a/tools/update.py
+++ b/experiments/a/tools/update.py
@@ -18,13 +18,13 @@
 """Update Tool."""
 
 import argparse
-import inspect
-import os
-import sys
 
 from core.errors import WorkflowError
 from core.task_runner import Task
 from core.task_runner import TaskRunner
+from tools.update_aliases import get_aliases
+from tools.update_utils import combine_build_commands
+from tools.update_utils import combine_update_commands
 
 
 class Update:
@@ -37,9 +37,13 @@
   def add_parser(cls, subparsers):
     """Parse command line update arguments."""
 
+    aliases = get_aliases()
     epilog = 'Aliases:\n'
     for alias in get_aliases().keys():
-      epilog += f'  {alias}\n'
+      name = alias
+      build_commands = (';').join(aliases[name].build())
+      update_commands = (';').join(aliases[name].update())
+      epilog += f'  {name}:\n\t{build_commands}\n\t{update_commands}\n'
 
     parser = subparsers.add_parser(
         'update', epilog=epilog, formatter_class=argparse.RawTextHelpFormatter
@@ -49,12 +53,15 @@
     parser.add_argument(
         '--build-only',
         action='store_true',
-        help='Only build the specified targets, do not update the device.',
+        help='only build the specified targets, do not update the device.',
     )
     parser.add_argument(
         '--update-only',
         action='store_true',
-        help='Only update the device with prebuilt targets, do not build.',
+        help=(
+            'only update the device with prebuilt targets, do not build'
+            ' targets.'
+        ),
     )
 
   def main(self):
@@ -65,23 +72,23 @@
   def gather_tasks(self):
     """Gathers tasks to run based on alias."""
     tasks = []
-
-    requested_aliases = self.args.alias
-    aliases = get_aliases()
-    if len(requested_aliases) >= 1:
-      for a in requested_aliases:
-        if a not in aliases:
-          raise WorkflowError(f'Unknown Alias {a}')
-
     build_tasks = []
     update_tasks = []
 
-    if len(requested_aliases) == 1:
-      a = requested_aliases[0]
+    requested_aliases = self.args.alias
+    aliases = get_aliases()
+    for a in requested_aliases:
+      if a not in aliases:
+        raise WorkflowError(f'unknown alias: {a}')
       config = aliases[a]
       build_tasks += config.build()
       update_tasks += config.update()
 
+    # combine build tasks
+    build_tasks = combine_build_commands(build_tasks)
+    # combine update tasks
+    update_tasks = combine_update_commands(update_tasks)
+
     if self.args.build_only:
       tasks = build_tasks
     elif self.args.update_only:
@@ -115,424 +122,3 @@
       else:
         task_runner.add_task(task)
     task_runner.start()
-
-
-class Alias:
-  """Base class for defining an alias."""
-
-  def build(self):
-    return []
-
-  def update(self):
-    return []
-
-
-class Core(Alias):
-  """Alias for Core."""
-
-  def build(self):
-    return ['m framework framework-minus-apex']
-
-  def update(self):
-    return [
-        'adevice update',
-    ]
-
-
-class SystemServer(Alias):
-  """Alias for SystemServer."""
-
-  def update(self):
-    return [
-        'adevice update --restart=none',
-        'adb kill systemserver',
-    ]
-
-
-class SysUI(Alias):
-  """Alias for SystemUI."""
-
-  def build(self):
-    if is_nexus():
-      raise WorkflowError(
-          "Target 'sysui' is not allowed on Nexus Experience devices.\n"
-          'Try sysuig (with g at the end) or sysuititan'
-      )
-    return ['m framework framework-minus-apex SystemUI']
-
-  def update(self):
-    target = 'com.android.systemui'
-    return [
-        'adevice update --restart=none',
-        f'adb shell "am force-stop {target}"',
-    ]
-
-
-class SysUIG(Alias):
-  """Alias for SystemUI for Google Devices."""
-
-  def build(self):
-    if not is_nexus():
-      raise WorkflowError(
-          "Target 'sysuig' is only allowed on Nexus Experience devices.\n"
-          'Try sysui (no g at the end)'
-      )
-    return ['m framework framework-minus-apex SystemUIGoogle']
-
-  def update(self):
-    target = 'com.android.systemui'
-    return [
-        'adevice update --restart=none',
-        f'adb shell am force-stop {target}',
-    ]
-
-
-class SysUITitan(Alias):
-  """Alias for SystemUI Titan devices."""
-
-  def build(self):
-    if not is_nexus():
-      raise WorkflowError(
-          "Target 'sysuititan' is only allowed on Nexus Experience devices.\n"
-          'Try sysui (no g at the end)'
-      )
-    return ['m framework framework-minus-apex SystemUITitan']
-
-  def update(self):
-    target = 'com.android.systemui'
-    return [
-        'adevice update --restart=none',
-        f'adb shell am force-stop {target}',
-    ]
-
-
-class SysUIGo(Alias):
-  """Alias for SystemUI."""
-
-  def build(self):
-    if not is_nexus():
-      raise WorkflowError(
-          "Target 'sysuigo' is only allowed on Nexus Experience devices.\n"
-          'Try sysui (no go at the end)'
-      )
-    return ['m framework framework-minus-apex SystemUIGo']
-
-  def update(self):
-    target = 'com.android.systemui'
-    return [
-        'adevice update --restart=none',
-        f'adb shell am force-stop {target}',
-    ]
-
-
-class CarSysUI(Alias):
-  """Alias for CarSystemUI."""
-
-  def build(self):
-    return ['m framework framework-minus-apex CarSystemUI']
-
-  def update(self):
-    target = 'com.android.systemui'
-    return [
-        'adevice update --restart=none',
-        f'adb shell am force-stop {target}',
-    ]
-
-
-class CarSysUIG(Alias):
-  """Alias for CarSystemUI."""
-
-  def build(self):
-    return ['m framework framework-minus-apex AAECarSystemUI']
-
-  def update(self):
-    target = 'com.android.systemui'
-    return [
-        'adevice update --restart=none',
-        f'adb shell am force-stop {target}',
-    ]
-
-
-# These definitions are imported from makepush
-# https://team.git.corp.google.com/android-framework/toolbox/+/refs/heads/main/makepush/makepush.sh
-alias_definitions = {
-    'droid': {
-        'build': 'droid',
-        'update': 'flashall',
-    },
-    'snod': {
-        'build': 'snod',
-        'update': 'flashall',
-    },
-    'core_jni': {'build': 'libandroid_runtime'},
-    'res_jni': {'build': 'libandroidfw libidmap2'},
-    'idmap2': {'build': 'idmap2 idmap2d'},
-    'sf': {'build': 'surfaceflinger'},
-    'res': {'build': 'framework-res'},
-    'services': {'build': 'services protolog.conf.json.gz'},
-    'inputflinger': {'build': 'libinputflinger'},
-    'carsysui': {
-        'build': 'carSystemUI',
-        'update': 'adb shell am force-stop com.android.systemui',
-    },
-    'carsysuig': {
-        'build': 'AAECarSystemUI',
-        'update': 'adb shell am force-stop com.android.systemui',
-    },
-    'car-mainline': {
-        'build': 'AAECarSystemUI',
-        'update': (
-            'adb install -r --staged --enable-rollback'
-            ' $OUT/system/apex/com.android.car.framework.apex'
-        ),
-    },
-    'carfwk': {'build': 'carfwk car-frameworks-service'},
-    'carfwk-module': {'build': 'car-frameworks-service-module'},
-    'carsettings': {
-        'build': 'carSettings',
-        'update': 'adb shell am force-stop com.android.car.settings',
-    },
-    'carks': {
-        'build': 'EmbeddedKitchenSinkApp',
-        'update': 'adb shell am force-stop com.google.android.car.kitchensink',
-    },
-    'carlauncher': {
-        'build': 'carLauncher',
-        'update': 'adb shell am force-stop com.android.car.carlauncher',
-    },
-    'carlauncherg': {
-        'build': 'GASCarLauncher',
-        'update': 'adb shell am force-stop com.android.car.carlauncher',
-    },
-    'car-md-launcher': {
-        'build': 'MultiDisplaySecondaryHomeTestLauncher',
-        'update': (
-            'adb install'
-            ' $OUT/system/priv-app/MultiDisplaySecondaryHomeTestLauncher/MultiDisplaySecondaryHomeTestLauncher.apk'
-        ),
-    },
-    'carsuw': {
-        'build': 'carProvision',
-        'update': 'adb shell am force-stop com.android.car.provision',
-    },
-    'car': {'build': 'android.car'},
-    'car-builtin': {'build': 'android.car.builtin'},
-    'vhal-legacy': {
-        'build': 'android.hardware.automotive.vehicle@2.0-service',
-        'update': (
-            'adb shell am force-stop'
-            ' android.hardware.automotive.vehicle@2.0-service'
-        ),
-    },
-    'vhal': {
-        'build': 'android.hardware.automotive.vehicle@V1-default-service',
-        'update': (
-            'adb shell am force-stop'
-            ' android.hardware.automotive.vehicle@V1-default-service'
-        ),
-    },
-    'vhal-pasa': {
-        'build': 'android.hardware.automotive.vehicle@V1-pasa-service',
-        'update': (
-            'adb shell am force-stop'
-            ' android.hardware.automotive.vehicle@V1-pasa-service'
-        ),
-    },
-    'launcher': {'build': 'NexusLauncherRelease'},
-    'launcherd': {
-        'build': 'nexusLauncherDebug',
-        'update': (
-            'adb install'
-            ' $OUT/anywhere/priv-app/NexusLauncherDebug/NexusLauncherDebug.apk'
-        ),
-    },
-    'launchergo': {
-        'build': 'launcherGoGoogle',
-        'update': 'adb shell am force-stop com.android.launcher3',
-    },
-    'intentresolver': {
-        'build': 'intentResolver',
-        'update': 'adb shell am force-stop com.android.intentresolver',
-    },
-    'sysuig': {
-        'build': 'systemUIGoogle',
-        'update': 'adb shell am force-stop com.android.systemui',
-    },
-    'sysuititan': {
-        'build': 'systemUITitan',
-        'update': 'adb shell am force-stop com.android.systemui',
-    },
-    'sysuigo': {
-        'build': 'systemUIGo',
-        'update': 'adb shell am force-stop com.android.systemui',
-    },
-    'flagflipper': {
-        'build': 'theFlippinApp',
-        'update': 'adb shell am force-stop com.android.theflippinapp',
-    },
-    'docsui': {
-        'build': 'documentsUI',
-        'update': 'adb shell am force-stop com.android.documentsui',
-    },
-    'docsuig': {
-        'build': 'documentsUIGoogle',
-        'update': 'adb shell am force-stop com.google.android.documentsui',
-    },
-    'settings': {
-        'build': 'settings',
-        'update': 'adb shell am force-stop com.android.settings',
-    },
-    'settingsg': {
-        'build': 'SettingsGoogle',
-        'update': 'adb shell am force-stop com.google.android.settings',
-    },
-    'settingsgf': {
-        'build': 'SettingsGoogleFutureFaceEnroll',
-        'update': (
-            'adb shell am force-stop'
-            ' com.google.android.settings.future.biometrics.faceenroll'
-        ),
-    },
-    'settings_provider': {'build': 'SettingsProvider'},
-    'apidemos': {
-        'build': 'ApiDemos',
-        'update': (
-            'adb install'
-            ' $OUT/testcases/ApiDemos/$var_cache_TARGET_ARCH/ApiDemos.apk'
-        ),
-    },
-    'teleservice': {
-        'build': 'TeleService',
-        'update': 'adb shell am force-stop com.android.phone',
-    },
-    'managed_provisioning': {
-        'build': 'ManagedProvisioning',
-        'update': 'adb shell am force-stop com.android.managedprovisioning',
-    },
-    'car_managed_provisioning': {
-        'build': 'carManagedProvisioning',
-        'update': (
-            'adb install'
-            ' $OUT/anywhere/priv-app/CarManagedProvisioning/CarManagedProvisioning.apk'
-        ),
-    },
-    'ctsv': {
-        'build': 'ctsVerifier',
-        'update': (
-            'adb install'
-            ' $OUT/testcases/CtsVerifier/$var_cache_TARGET_ARCH/CtsVerifier.apk'
-        ),
-    },
-    'gtsv': {
-        'build': 'gtsVerifier',
-        'update': (
-            'adb install'
-            ' $OUT/testcases/GtsVerifier/$var_cache_TARGET_ARCH/GtsVerifier.apk'
-        ),
-    },
-    'suw': {
-        'build': 'Provision',
-        'update': 'adb shell am force-stop com.android.provision',
-    },
-    'pkg_installer': {
-        'build': 'PackageInstaller',
-        'update': 'adb shell am force-stop com.android.packageinstaller',
-    },
-    'pkg_installer_g': {
-        'build': 'GooglePackageInstaller',
-        'update': 'adb shell am force-stop com.google.android.packageinstaller',
-    },
-    'perm_controller': {
-        'build': 'PermissionController',
-        'update': (
-            'adb install'
-            ' $OUT/apex/com.android.permission/priv-app/PermissionController/PermissionController.apk'
-        ),
-    },
-    'perm_controller_g': {
-        'build': 'GooglePermissionController',
-        'update': (
-            'adb install -r'
-            ' $OUT/apex/com.google.android.permission/priv-app/GooglePermissionController/GooglePermissionController.apk'
-        ),
-    },
-    'wifi': {
-        'build': 'wifi',
-        'update': (
-            'adb install -r --staged --enable-rollback'
-            ' $OUT/system/apex/com.android.wifi && adb shell am force-stop'
-            ' com.android.wifi'
-        ),
-    },
-    'vold': {'build': 'vold', 'update': 'adb shell am force-stop vold'},
-    'multidisplay': {
-        'build': 'multiDisplayProvider',
-        'update': 'adb shell am force-stop com.android.emulator.multidisplay',
-    },
-    'wm_ext': {
-        'build': 'androidx.window.extensions',
-    },
-    'rb': {
-        'build': 'adServicesApk',
-        'update': (
-            'adb install'
-            ' $OUT/apex/com.android.adservices/priv-app/AdServices/AdServices.apk'
-        ),
-    },
-    'rb_g': {
-        'build': 'adServicesApkGoogle',
-        'update': (
-            'adb install'
-            ' $OUT/apex/com.google.android.adservices/priv-app/AdServicesApkGoogle@MASTER/AdServicesApkGoogle.apk'
-        ),
-    },
-    'sdk_sandbox': {
-        'build': 'sdkSandbox',
-        'update': (
-            'adb install'
-            ' $OUT/apex/com.google.android.adservices/app/SdkSandboxGoogle@MASTER/SdkSandboxGoogle.apk'
-        ),
-    },
-}
-
-
-# Utilities to get type of target
-def is_nexus():
-  target_product = os.getenv('TARGET_PRODUCT')
-  return (
-      target_product.startswith('.aosp')
-      or 'wembley' in target_product
-      or 'gms_humuhumu' in target_product
-  )
-
-
-def create_alias_from_config(config):
-  """Generates a Alias class from json."""
-  alias = Alias()
-  build = config.get('build', None)
-  if build:
-    alias.build = lambda: [f'm {build}']
-
-  update = config.get('update', None)
-  if update:
-    alias.update = lambda: [
-        'adevice update --restart=None',
-        update,
-    ]
-  return alias
-
-
-def get_aliases():
-  """Dynamically find all aliases."""
-  # definitions that subclass the Alias class
-  aliases = {
-      name.lower(): cls()
-      for name, cls in inspect.getmembers(
-          sys.modules[__name__], inspect.isclass
-      )
-      if issubclass(cls, Alias) and cls != Alias
-  }
-  # definitions that are defined in alias_definitions
-  for name, config in alias_definitions.items():
-    aliases[name.lower()] = create_alias_from_config(config)
-  return aliases
diff --git a/experiments/a/tools/update_aliases.py b/experiments/a/tools/update_aliases.py
new file mode 100644
index 0000000..e22dfee
--- /dev/null
+++ b/experiments/a/tools/update_aliases.py
@@ -0,0 +1,458 @@
+#!/usr/bin/env python3
+#
+# Copyright 2024 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+"""Update Aliases."""
+
+import inspect
+import os
+import sys
+from core.errors import WorkflowError
+
+
+class Alias:
+  """Base class for defining an alias."""
+
+  def build(self):
+    return []
+
+  def update(self):
+    return []
+
+
+class Core(Alias):
+  """Alias for Core."""
+
+  def build(self):
+    return ['m framework framework-minus-apex']
+
+  def update(self):
+    return [
+        'adevice update',
+    ]
+
+
+class SystemServer(Alias):
+  """Alias for SystemServer."""
+
+  def update(self):
+    return [
+        'adevice update --restart=none',
+        'adb kill systemserver',
+    ]
+
+
+class SysUI(Alias):
+  """Alias for SystemUI."""
+
+  def build(self):
+    if is_nexus():
+      raise WorkflowError(
+          "Target 'sysui' is not allowed on Nexus Experience devices.\n"
+          'Try sysuig (with g at the end) or sysuititan'
+      )
+    return ['m framework framework-minus-apex SystemUI']
+
+  def update(self):
+    target = 'com.android.systemui'
+    return [
+        'adevice update --restart=none',
+        f'adb shell "am force-stop {target}"',
+    ]
+
+
+class SysUIG(Alias):
+  """Alias for SystemUI for Google Devices."""
+
+  def build(self):
+    if not is_nexus():
+      raise WorkflowError(
+          "Target 'sysuig' is only allowed on Nexus Experience devices.\n"
+          'Try sysui (no g at the end)'
+      )
+    return ['m framework framework-minus-apex SystemUIGoogle']
+
+  def update(self):
+    target = 'com.android.systemui'
+    return [
+        'adevice update --restart=none',
+        f'adb shell am force-stop {target}',
+    ]
+
+
+class SysUITitan(Alias):
+  """Alias for SystemUI Titan devices."""
+
+  def build(self):
+    if not is_nexus():
+      raise WorkflowError(
+          "Target 'sysuititan' is only allowed on Nexus Experience devices.\n"
+          'Try sysui (no g at the end)'
+      )
+    return ['m framework framework-minus-apex SystemUITitan']
+
+  def update(self):
+    target = 'com.android.systemui'
+    return [
+        'adevice update --restart=none',
+        f'adb shell am force-stop {target}',
+    ]
+
+
+class SysUIGo(Alias):
+  """Alias for SystemUI."""
+
+  def build(self):
+    if not is_nexus():
+      raise WorkflowError(
+          "Target 'sysuigo' is only allowed on Nexus Experience devices.\n"
+          'Try sysui (no go at the end)'
+      )
+    return ['m framework framework-minus-apex SystemUIGo']
+
+  def update(self):
+    target = 'com.android.systemui'
+    return [
+        'adevice update --restart=none',
+        f'adb shell am force-stop {target}',
+    ]
+
+
+class CarSysUI(Alias):
+  """Alias for CarSystemUI."""
+
+  def build(self):
+    return ['m framework framework-minus-apex CarSystemUI']
+
+  def update(self):
+    target = 'com.android.systemui'
+    return [
+        'adevice update --restart=none',
+        f'adb shell am force-stop {target}',
+    ]
+
+
+class CarSysUIG(Alias):
+  """Alias for CarSystemUI."""
+
+  def build(self):
+    return ['m framework framework-minus-apex AAECarSystemUI']
+
+  def update(self):
+    target = 'com.android.systemui'
+    return [
+        'adevice update --restart=none',
+        f'adb shell am force-stop {target}',
+    ]
+
+
+class Droid(Alias):
+  """Alias for Droid."""
+
+  def build(self):
+    return ['m droid']
+
+  def update(self):
+    return ['flashall']
+
+
+class Snod(Alias):
+  """Alias for Snod."""
+
+  def build(self):
+    return ['m snod']
+
+  def update(self):
+    return ['flashall']
+
+
+# These definitions are imported from makepush
+# https://team.git.corp.google.com/android-framework/toolbox/+/refs/heads/main/makepush/makepush.sh
+alias_definitions = {
+    'core_jni': {'build': 'libandroid_runtime'},
+    'res_jni': {'build': 'libandroidfw libidmap2'},
+    'idmap2': {'build': 'idmap2 idmap2d'},
+    'sf': {'build': 'surfaceflinger'},
+    'res': {'build': 'framework-res'},
+    'services': {'build': 'services protolog.conf.json.gz'},
+    'inputflinger': {'build': 'libinputflinger'},
+    'carsysui': {
+        'build': 'carSystemUI',
+        'update': 'adb shell am force-stop com.android.systemui',
+    },
+    'carsysuig': {
+        'build': 'AAECarSystemUI',
+        'update': 'adb shell am force-stop com.android.systemui',
+    },
+    'car-mainline': {
+        'build': 'AAECarSystemUI',
+        'update': (
+            'adb install -r --staged --enable-rollback'
+            ' $OUT/system/apex/com.android.car.framework.apex'
+        ),
+    },
+    'carfwk': {'build': 'carfwk car-frameworks-service'},
+    'carfwk-module': {'build': 'car-frameworks-service-module'},
+    'carsettings': {
+        'build': 'carSettings',
+        'update': 'adb shell am force-stop com.android.car.settings',
+    },
+    'carks': {
+        'build': 'EmbeddedKitchenSinkApp',
+        'update': 'adb shell am force-stop com.google.android.car.kitchensink',
+    },
+    'carlauncher': {
+        'build': 'carLauncher',
+        'update': 'adb shell am force-stop com.android.car.carlauncher',
+    },
+    'carlauncherg': {
+        'build': 'GASCarLauncher',
+        'update': 'adb shell am force-stop com.android.car.carlauncher',
+    },
+    'car-md-launcher': {
+        'build': 'MultiDisplaySecondaryHomeTestLauncher',
+        'update': (
+            'adb install'
+            ' $OUT/system/priv-app/MultiDisplaySecondaryHomeTestLauncher/MultiDisplaySecondaryHomeTestLauncher.apk'
+        ),
+    },
+    'carsuw': {
+        'build': 'carProvision',
+        'update': 'adb shell am force-stop com.android.car.provision',
+    },
+    'car': {'build': 'android.car'},
+    'car-builtin': {'build': 'android.car.builtin'},
+    'vhal-legacy': {
+        'build': 'android.hardware.automotive.vehicle@2.0-service',
+        'update': (
+            'adb shell am force-stop'
+            ' android.hardware.automotive.vehicle@2.0-service'
+        ),
+    },
+    'vhal': {
+        'build': 'android.hardware.automotive.vehicle@V1-default-service',
+        'update': (
+            'adb shell am force-stop'
+            ' android.hardware.automotive.vehicle@V1-default-service'
+        ),
+    },
+    'vhal-pasa': {
+        'build': 'android.hardware.automotive.vehicle@V1-pasa-service',
+        'update': (
+            'adb shell am force-stop'
+            ' android.hardware.automotive.vehicle@V1-pasa-service'
+        ),
+    },
+    'launcher': {'build': 'NexusLauncherRelease'},
+    'launcherd': {
+        'build': 'nexusLauncherDebug',
+        'update': (
+            'adb install'
+            ' $OUT/anywhere/priv-app/NexusLauncherDebug/NexusLauncherDebug.apk'
+        ),
+    },
+    'launchergo': {
+        'build': 'launcherGoGoogle',
+        'update': 'adb shell am force-stop com.android.launcher3',
+    },
+    'intentresolver': {
+        'build': 'intentResolver',
+        'update': 'adb shell am force-stop com.android.intentresolver',
+    },
+    'sysuig': {
+        'build': 'systemUIGoogle',
+        'update': 'adb shell am force-stop com.android.systemui',
+    },
+    'sysuititan': {
+        'build': 'systemUITitan',
+        'update': 'adb shell am force-stop com.android.systemui',
+    },
+    'sysuigo': {
+        'build': 'systemUIGo',
+        'update': 'adb shell am force-stop com.android.systemui',
+    },
+    'flagflipper': {
+        'build': 'theFlippinApp',
+        'update': 'adb shell am force-stop com.android.theflippinapp',
+    },
+    'docsui': {
+        'build': 'documentsUI',
+        'update': 'adb shell am force-stop com.android.documentsui',
+    },
+    'docsuig': {
+        'build': 'documentsUIGoogle',
+        'update': 'adb shell am force-stop com.google.android.documentsui',
+    },
+    'settings': {
+        'build': 'settings',
+        'update': 'adb shell am force-stop com.android.settings',
+    },
+    'settingsg': {
+        'build': 'SettingsGoogle',
+        'update': 'adb shell am force-stop com.google.android.settings',
+    },
+    'settingsgf': {
+        'build': 'SettingsGoogleFutureFaceEnroll',
+        'update': (
+            'adb shell am force-stop'
+            ' com.google.android.settings.future.biometrics.faceenroll'
+        ),
+    },
+    'settings_provider': {'build': 'SettingsProvider'},
+    'apidemos': {
+        'build': 'ApiDemos',
+        'update': (
+            'adb install'
+            ' $OUT/testcases/ApiDemos/$var_cache_TARGET_ARCH/ApiDemos.apk'
+        ),
+    },
+    'teleservice': {
+        'build': 'TeleService',
+        'update': 'adb shell am force-stop com.android.phone',
+    },
+    'managed_provisioning': {
+        'build': 'ManagedProvisioning',
+        'update': 'adb shell am force-stop com.android.managedprovisioning',
+    },
+    'car_managed_provisioning': {
+        'build': 'carManagedProvisioning',
+        'update': (
+            'adb install'
+            ' $OUT/anywhere/priv-app/CarManagedProvisioning/CarManagedProvisioning.apk'
+        ),
+    },
+    'ctsv': {
+        'build': 'ctsVerifier',
+        'update': (
+            'adb install'
+            ' $OUT/testcases/CtsVerifier/$var_cache_TARGET_ARCH/CtsVerifier.apk'
+        ),
+    },
+    'gtsv': {
+        'build': 'gtsVerifier',
+        'update': (
+            'adb install'
+            ' $OUT/testcases/GtsVerifier/$var_cache_TARGET_ARCH/GtsVerifier.apk'
+        ),
+    },
+    'suw': {
+        'build': 'Provision',
+        'update': 'adb shell am force-stop com.android.provision',
+    },
+    'pkg_installer': {
+        'build': 'PackageInstaller',
+        'update': 'adb shell am force-stop com.android.packageinstaller',
+    },
+    'pkg_installer_g': {
+        'build': 'GooglePackageInstaller',
+        'update': 'adb shell am force-stop com.google.android.packageinstaller',
+    },
+    'perm_controller': {
+        'build': 'PermissionController',
+        'update': (
+            'adb install'
+            ' $OUT/apex/com.android.permission/priv-app/PermissionController/PermissionController.apk'
+        ),
+    },
+    'perm_controller_g': {
+        'build': 'GooglePermissionController',
+        'update': (
+            'adb install -r'
+            ' $OUT/apex/com.google.android.permission/priv-app/GooglePermissionController/GooglePermissionController.apk'
+        ),
+    },
+    'wifi': {
+        'build': 'wifi',
+        'update': (
+            'adb install -r --staged --enable-rollback'
+            ' $OUT/system/apex/com.android.wifi && adb shell am force-stop'
+            ' com.android.wifi'
+        ),
+    },
+    'vold': {'build': 'vold', 'update': 'adb shell am force-stop vold'},
+    'multidisplay': {
+        'build': 'multiDisplayProvider',
+        'update': 'adb shell am force-stop com.android.emulator.multidisplay',
+    },
+    'wm_ext': {
+        'build': 'androidx.window.extensions',
+    },
+    'rb': {
+        'build': 'adServicesApk',
+        'update': (
+            'adb install'
+            ' $OUT/apex/com.android.adservices/priv-app/AdServices/AdServices.apk'
+        ),
+    },
+    'rb_g': {
+        'build': 'adServicesApkGoogle',
+        'update': (
+            'adb install'
+            ' $OUT/apex/com.google.android.adservices/priv-app/AdServicesApkGoogle@MASTER/AdServicesApkGoogle.apk'
+        ),
+    },
+    'sdk_sandbox': {
+        'build': 'sdkSandbox',
+        'update': (
+            'adb install'
+            ' $OUT/apex/com.google.android.adservices/app/SdkSandboxGoogle@MASTER/SdkSandboxGoogle.apk'
+        ),
+    },
+}
+
+
+# Utilities to get type of target
+def is_nexus():
+  target_product = os.getenv('TARGET_PRODUCT')
+  return (
+      target_product.startswith('.aosp')
+      or 'wembley' in target_product
+      or 'gms_humuhumu' in target_product
+  )
+
+
+def create_alias_from_config(config):
+  """Generates a Alias class from json."""
+  alias = Alias()
+  build = config.get('build', None)
+  if build:
+    alias.build = lambda: [f'm {build}']
+
+  update = config.get('update', None)
+  if update:
+    alias.update = lambda: [
+        'adevice update --restart=none',
+        update,
+    ]
+  else:
+    alias.update = lambda: ['adevice update']
+  return alias
+
+
+def get_aliases():
+  """Dynamically find all aliases."""
+  # definitions that subclass the Alias class
+  aliases = {
+      name.lower(): cls()
+      for name, cls in inspect.getmembers(
+          sys.modules[__name__], inspect.isclass
+      )
+      if issubclass(cls, Alias) and cls != Alias
+  }
+  # definitions that are defined in alias_definitions
+  for name, config in alias_definitions.items():
+    aliases[name.lower()] = create_alias_from_config(config)
+  return aliases
diff --git a/experiments/a/tools/update_test.py b/experiments/a/tools/update_test.py
index 5121cd4..acc0d96 100644
--- a/experiments/a/tools/update_test.py
+++ b/experiments/a/tools/update_test.py
@@ -16,11 +16,15 @@
 #
 import argparse
 import unittest
-from .update import Core
-from .update import get_aliases
-from .update import SystemServer
-from .update import SysUI
 from .update import Update
+from .update_aliases import Core
+from .update_aliases import get_aliases
+from .update_aliases import SystemServer
+from .update_aliases import SysUI
+from .update_utils import combine_build_commands
+from .update_utils import combine_update_commands
+from .update_utils import remove_commands_that_starts_with
+from .update_utils import remove_duplicates_maintain_order
 
 
 class UpdateTest(unittest.TestCase):
@@ -55,10 +59,8 @@
     self.assertEqual(tasks[1].fall_back_tasks, ['m droid', 'flashall'])
 
   def test_gather_tasks_alias(self):
-
     self.args.alias = ['core']
     update = Update(self.args)
-
     tasks = update.gather_tasks()
     self.assertEqual(
         tasks, ['m framework framework-minus-apex', 'adevice update']
@@ -80,6 +82,68 @@
     tasks = update.gather_tasks()
     self.assertEqual(tasks, ['adevice update'])
 
+  def test_gather_tasks_multiple_alias(self):
+    self.args.alias = ['sf', 'res']
+    update = Update(self.args)
+    tasks = update.gather_tasks()
+    self.assertEqual(
+        tasks, ['m surfaceflinger framework-res', 'adevice update']
+    )
+
+
+class UpdateUtilsTest(unittest.TestCase):
+
+  def test_remove_duplicates_maintain_order(self):
+    self.assertEqual(
+        remove_duplicates_maintain_order(['1', '2', '1', '3']), ['1', '2', '3']
+    )
+
+  def test_remove_commands_that_starts_with_no_match(self):
+    self.assertEqual(
+        remove_commands_that_starts_with(
+            commands=[
+                'keep a',
+                'keep b',
+                'remove a',
+                'remove b',
+            ],
+            cmd_to_remove='remove',
+        ),
+        ['keep a', 'keep b'],
+    )
+
+  def test_combine_build_cmd(self):
+    self.assertEqual(combine_build_commands(['m foo', 'm bar']), ['m foo bar'])
+
+  def test_combine_update_cmds_adevice_update(self):
+    # adevice update restarts so remove unneeded force-stops
+    self.assertEqual(
+        combine_update_commands([
+            'adevice update',
+            'adb shell "am force-stop foo"',
+            'adevice update',
+            'adb shell "am force-stop bar"',
+            'adevice update restart=none',
+        ]),
+        ['adevice update'],
+    )
+
+  def test_combine_update_cmds_adevice_update_no_restart(self):
+    # adevice update will not restart so keep force-stops
+    self.assertEqual(
+        combine_update_commands([
+            'adevice update --restart=none',
+            'adb shell "am force-stop foo"',
+            'adevice update --restart=none',
+            'adb shell "am force-stop bar"',
+        ]),
+        [
+            'adevice update --restart=none',
+            'adb shell "am force-stop foo"',
+            'adb shell "am force-stop bar"',
+        ],
+    )
+
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/experiments/a/tools/update_utils.py b/experiments/a/tools/update_utils.py
new file mode 100644
index 0000000..7ae90c1
--- /dev/null
+++ b/experiments/a/tools/update_utils.py
@@ -0,0 +1,66 @@
+#!/usr/bin/env python3
+#
+# Copyright 2024 - The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+"""Update Utils."""
+
+
+def combine_build_commands(commands):
+  """Combines build commands so that m is called once."""
+  m_command = ''
+  result = []
+  for cmd in commands:
+    if cmd.startswith('m '):
+      m_command += cmd[2:] + ' '
+    else:
+      result.append(cmd)
+  if m_command:
+    result.insert(0, 'm ' + m_command.strip())
+  return result
+
+
+def combine_update_commands(commands):
+  """Combines update tasks so that a reboot/adevice update is called only once."""
+  commands = remove_duplicates_maintain_order(commands)
+
+  # if any command calls for a restart; just do that
+  # deduplicate and remove
+  if 'adevice update' in commands:
+    commands = remove_commands_that_starts_with(commands, 'adevice update')
+    commands = remove_commands_that_starts_with(
+        commands, 'adb shell "am force-stop'
+    )
+    commands = ['adevice update'] + commands
+  return commands
+
+
+def remove_duplicates_maintain_order(commands):
+  """Removes duplicates while maintaining order."""
+  seen = set()
+  result = []
+  for item in commands:
+    if item not in seen:
+      seen.add(item)
+      result.append(item)
+  return result
+
+
+def remove_commands_that_starts_with(commands, cmd_to_remove):
+  """Removes commands that start with a command."""
+  result = []
+  for cmd in commands:
+    if not cmd.startswith(cmd_to_remove):
+      result.append(cmd)
+  return result