diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml
index 89ee08f6..903b6408 100644
--- a/.github/workflows/macos.yml
+++ b/.github/workflows/macos.yml
@@ -39,6 +39,9 @@ jobs:
           sudo chmod a+x /usr/local/bin/apktool
           sudo wget -q "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_${APKTOOL_VERSION}.jar" -O /usr/local/bin/apktool.jar
           sudo chmod a+x /usr/local/bin/apktool.jar
+          # Install BundleDecompiler.
+          sudo wget -q https://github.com/TamilanPeriyasamy/BundleDecompiler/raw/master/build/libs/BundleDecompiler-0.0.2.jar -O /usr/local/bin/BundleDecompiler.jar
+          sudo chmod a+x /usr/local/bin/BundleDecompiler.jar
 
       - name: Run tests
         run: |
diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml
index b21b11ae..f8d95f3f 100644
--- a/.github/workflows/ubuntu.yml
+++ b/.github/workflows/ubuntu.yml
@@ -39,6 +39,8 @@ jobs:
           sudo chmod a+x /usr/local/bin/apktool
           sudo wget -q "https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_${APKTOOL_VERSION}.jar" -O /usr/local/bin/apktool.jar
           sudo chmod a+x /usr/local/bin/apktool.jar
+          sudo wget -q https://github.com/TamilanPeriyasamy/BundleDecompiler/raw/master/build/libs/BundleDecompiler-0.0.2.jar -O /usr/local/bin/BundleDecompiler.jar
+          sudo chmod a+x /usr/local/bin/BundleDecompiler.jar
 
       - name: Run tests
         run: |
diff --git a/README.md b/README.md
old mode 100644
new mode 100755
index 8d2a9f03..2199d887
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
 [![Windows Build Status](https://github.com/ClaudiuGeorgiu/Obfuscapk/workflows/Windows/badge.svg)](https://github.com/ClaudiuGeorgiu/Obfuscapk/actions?query=workflow%3AWindows)
 [![MacOS Build Status](https://github.com/ClaudiuGeorgiu/Obfuscapk/workflows/MacOS/badge.svg)](https://github.com/ClaudiuGeorgiu/Obfuscapk/actions?query=workflow%3AMacOS)
 [![Docker Hub](https://img.shields.io/docker/cloud/build/claudiugeorgiu/obfuscapk)](https://hub.docker.com/r/claudiugeorgiu/obfuscapk)
-[![Python Version](https://img.shields.io/badge/Python-3.6%2B-green.svg?logo=python&logoColor=white)](https://www.python.org/downloads/)
+[![Python Version](https://img.shields.io/badge/Python-3.7%2B-green.svg?logo=python&logoColor=white)](https://www.python.org/downloads/)
 [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ClaudiuGeorgiu/Obfuscapk/blob/master/LICENSE)
 
 
@@ -20,6 +20,22 @@ obfuscated app retains the same functionality as the original one, but the diffe
 under the hood sometimes make the new application very different from the original
 (e.g., to signature-based antivirus software).
 
+### :new: Android App Bundle support :new:
+
+Obfuscapk is adding support for
+[Android App Bundles](https://developer.android.com/guide/app-bundle) (aab files) by
+using [BundleDecompiler](https://github.com/TamilanPeriyasamy/BundleDecompiler) (see
+[#121](https://github.com/ClaudiuGeorgiu/Obfuscapk/pull/121)). In order to use this new
+feature, download the latest version of BundleDecompiler available from
+[here](https://github.com/TamilanPeriyasamy/BundleDecompiler/tree/master/build/libs) and
+save it as `BundleDecompiler.jar` in a directory included in `PATH` (e.g., in Ubuntu,
+`/usr/local/bin` or `/usr/bin`).
+
+`NOTE:` BundleDecompiler doesn't work on Windows yet, so app bundle obfuscation is not
+supported by Obfuscapk on Windows platform. Also, app bundle support is still in early
+development, so if you faced any problems or if you want to help us improve, please see
+[contributing](#-contributing).
+
 
 
 ## ❱ Publication
@@ -109,7 +125,7 @@ Docker version 19.03.0, build aeac949
 #### Official Docker Hub image
 
 The [official Obfuscapk Docker image](https://hub.docker.com/r/claudiugeorgiu/obfuscapk)
-is available on Docker Hub (automatically built from this repository):
+is available on Docker Hub:
 
 ```Shell
 $ # Download the Docker image.
@@ -136,7 +152,7 @@ installed correctly:
 
 ```Shell
 $ docker run --rm -it obfuscapk --help
-usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK]
+usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK_OR_AAB]
 ...
 ```
 
@@ -174,15 +190,22 @@ Copyright (C) 2009 The Android Open Source Project
 ...
 ```
 
-To install and use `apktool` you need a recent version of Java. 
+To support app bundles obfuscation you need
+[BundleDecompiler](https://github.com/TamilanPeriyasamy/BundleDecompiler), so download
+the latest available version from
+[here](https://github.com/TamilanPeriyasamy/BundleDecompiler/tree/master/build/libs) and
+save it as `BundleDecompiler.jar` in a directory included in `PATH` (e.g., in Ubuntu,
+`/usr/local/bin` or `/usr/bin`).
+
+To use BundleDecompiler and `apktool` you also need a recent version of Java. 
 `zipalign` and `apksigner` are included in the Android SDK. The location of the
 executables can also be specified through the following environment variables:
-`APKTOOL_PATH`, `APKSIGNER_PATH` and `ZIPALIGN_PATH` (e.g., in Ubuntu, run
-`export APKTOOL_PATH=/custom/location/apktool` before running Obfuscapk in the same
-terminal).
+`APKTOOL_PATH`, `BUNDLE_DECOMPILER_PATH`, `APKSIGNER_PATH` and `ZIPALIGN_PATH` (e.g.,
+in Ubuntu, run `export APKTOOL_PATH=/custom/location/apktool` before running Obfuscapk
+in the same terminal).
 
 Apart from the above tools, the only requirement of this project is a working
-`Python 3` (at least `3.6`) installation (along with its package manager `pip`).
+`Python 3` (at least `3.7`) installation (along with its package manager `pip`).
 
 #### Install
 
@@ -209,7 +232,7 @@ $ cd src/
 $ # The following command has to be executed always from Obfuscapk/src/ directory
 $ # or by adding Obfuscapk/src/ directory to PYTHONPATH environment variable.
 $ python3 -m obfuscapk.cli --help
-usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK]
+usage: python3 -m obfuscapk.cli [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK_OR_AAB]
 ...
 ```
 
@@ -249,17 +272,17 @@ Let's start by looking at the help message:
 
 ```Shell
 $ obfuscapk --help
-obfuscapk [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK] [-i] [-p] [-k VT_API_KEY]
+obfuscapk [-h] -o OBFUSCATOR [-w DIR] [-d OUT_APK_OR_AAB] [-i] [-p] [-k VT_API_KEY]
           [--keystore-file KEYSTORE_FILE] [--keystore-password KEYSTORE_PASSWORD]
           [--key-alias KEY_ALIAS] [--key-password KEY_PASSWORD] [--use-aapt2]
-          <APK_FILE>
+          <APK_OR_BUNDLE_FILE>
 ```
 
-There are two mandatory parameters: `<APK_FILE>`, the path (relative or absolute) to
-the apk file to obfuscate and the list with the names of the obfuscation techniques to
-apply (specified with a `-o` option that can be used multiple times, e.g.,
-`-o Rebuild -o NewAlignment -o NewSignature`). The other optional arguments are as
-follows:
+There are two mandatory parameters: `<APK_OR_BUNDLE_FILE>`, the path (relative or
+absolute) to the apk or app bundle file to obfuscate and the list with the names of the
+obfuscation techniques to apply (specified with a `-o` option that can be used multiple
+times, e.g., `-o Rebuild -o NewAlignment -o NewSignature`). The other optional arguments
+are as follows:
 
 * `-w DIR` is used to set the working directory where to save the intermediate files
 (generated by `apktool`). If not specified, a directory named `obfuscation_working_dir`
@@ -267,10 +290,11 @@ is created in the same directory as the input application. This can be useful fo
 debugging purposes, but if it's not needed it can be set to a temporary directory
 (e.g., `-w /tmp/`).
 
-* `-d OUT_APK` is used to set the path of the destination file: the apk file generated
-by the obfuscation process (e.g., `-d /home/user/Desktop/obfuscated.apk`). If not
-specified, the final obfuscated file will be saved inside the working directory.
-Note: existing files will be overwritten without any warning.
+* `-d OUT_APK_OR_AAB` is used to set the path of the destination file: the apk file
+generated by the obfuscation process (e.g., `-d /home/user/Desktop/obfuscated.apk` or
+`-d /home/user/Desktop/obfuscated.aab`). If not specified, the final obfuscated file
+will be saved inside the working directory. Note: existing files will be overwritten
+without any warning.
 
 * `-i` is a flag for ignoring known third party libraries during the obfuscation
 process, to use fewer resources, to increase performances and to reduce the risk of
@@ -302,8 +326,8 @@ shown in the example below:
     com.mycompany.dontobfuscate
     com.mycompany.ignore
     ...
-    ``` 
-* `--use-aapt2` is a flag for use aapt2 option to rebuild app when using apktool.
+    ```
+* `--use-aapt2` is a flag for using aapt2 option when rebuilding an app with `apktool`.
 
 Let's consider now a simple working example to see how Obfuscapk works:
 
diff --git a/src/obfuscapk/cli.py b/src/obfuscapk/cli.py
index bad56459..a380bd7b 100644
--- a/src/obfuscapk/cli.py
+++ b/src/obfuscapk/cli.py
@@ -21,13 +21,14 @@ def get_cmd_args(args: list = None):
 
     parser = argparse.ArgumentParser(
         prog="python3 -m obfuscapk.cli",
-        description="Obfuscate an application (.apk) without needing its source code.",
+        description="Obfuscate an application (.apk/.aab) without needing its "
+        "source code.",
     )
     parser.add_argument(
         "apk_file",
         type=str,
-        metavar="<APK_FILE>",
-        help="The path to the application (.apk) to obfuscate",
+        metavar="<APK_OR_BUNDLE_FILE>",
+        help="The path to the application (.apk/.aab) to obfuscate",
     )
     parser.add_argument(
         "-o",
@@ -54,9 +55,9 @@ def get_cmd_args(args: list = None):
         "-d",
         "--destination",
         type=str,
-        metavar="OUT_APK",
-        help="The path where to save the obfuscated .apk file. By default the file "
-        "will be saved in the working directory",
+        metavar="OUT_APK_OR_AAB",
+        help="The path where to save the obfuscated .apk/.aab file. By default the "
+        "file will be saved in the working directory",
     )
     parser.add_argument(
         "-i",
diff --git a/src/obfuscapk/main.py b/src/obfuscapk/main.py
index aaa7ddf9..a31a2cf7 100644
--- a/src/obfuscapk/main.py
+++ b/src/obfuscapk/main.py
@@ -8,6 +8,7 @@
 from obfuscapk.obfuscation import Obfuscation
 from obfuscapk.obfuscator_manager import ObfuscatorManager
 from obfuscapk.tool import Apktool, Zipalign, ApkSigner
+from obfuscapk.toolbundledecompiler import BundleDecompiler
 
 if "LOG_LEVEL" in os.environ:
     log_level = os.environ["LOG_LEVEL"]
@@ -38,6 +39,7 @@ def check_external_tool_dependencies():
     # an exception will be thrown by the corresponding constructor.
     logger.debug("Checking external tool dependencies")
     Apktool()
+    BundleDecompiler()
     ApkSigner()
     Zipalign()
 
diff --git a/src/obfuscapk/obfuscation.py b/src/obfuscapk/obfuscation.py
index 59b77deb..ca7ce11b 100644
--- a/src/obfuscapk/obfuscation.py
+++ b/src/obfuscapk/obfuscation.py
@@ -7,7 +7,8 @@
 from typing import List, Union
 
 from obfuscapk import util
-from obfuscapk.tool import Apktool, Zipalign, ApkSigner
+from obfuscapk.tool import Apktool, ApkSigner, Zipalign
+from obfuscapk.toolbundledecompiler import BundleDecompiler, AABSigner
 
 
 class Obfuscation(object):
@@ -46,6 +47,10 @@ def __init__(
         self.key_password: str = key_password
         self.ignore_packages_file: str = ignore_packages_file
         self.use_aapt2 = use_aapt2
+        if apk_path.endswith('aab'):
+            self.is_bundle = True
+        else:
+            self.is_bundle = False
 
         # Random string (32 chars long) generation with ASCII letters and digits
         self.encryption_secret = "".join(
@@ -114,12 +119,20 @@ def __init__(
         # If the path of the output obfuscated apk is not specified, save it in the
         # working directory.
         if not self.obfuscated_apk_path:
-            self.obfuscated_apk_path = "{0}_obfuscated.apk".format(
-                os.path.join(
-                    self.working_dir_path,
-                    os.path.splitext(os.path.basename(self.apk_path))[0],
+            if (self.is_bundle):
+                self.obfuscated_apk_path = "{0}_obfuscated.aab".format(
+                    os.path.join(
+                        self.working_dir_path,
+                        os.path.splitext(os.path.basename(self.apk_path))[0],
+                    )
+                )
+            else:
+                self.obfuscated_apk_path = "{0}_obfuscated.apk".format(
+                    os.path.join(
+                        self.working_dir_path,
+                        os.path.splitext(os.path.basename(self.apk_path))[0],
+                    )
                 )
-            )
             self.logger.debug(
                 "No obfuscated apk path provided, the result will be saved "
                 'as "{0}"'.format(self.obfuscated_apk_path)
@@ -329,8 +342,9 @@ def decode_apk(self) -> None:
 
         if not self._is_decoded:
 
-            # The input apk will be decoded with apktool.
+            # The input apk will be decoded with apktool or BundleDecompiler.
             apktool: Apktool = Apktool()
+            bundledecompiler: BundleDecompiler = BundleDecompiler()
 
             # <working_directory>/<apk_path>/
             self._decoded_apk_path = os.path.join(
@@ -338,15 +352,24 @@ def decode_apk(self) -> None:
                 os.path.splitext(os.path.basename(self.apk_path))[0],
             )
             try:
-                apktool.decode(self.apk_path, self._decoded_apk_path, force=True)
+                if (self.is_bundle):
+                    bundledecompiler.decode(self.apk_path, self._decoded_apk_path, force=False)
+                else:
+                    apktool.decode(self.apk_path, self._decoded_apk_path, force=True)
+                
 
                 # Path to the decoded manifest file.
-                self._manifest_file = os.path.join(
+                if (self.is_bundle):
+                    self._manifest_file = os.path.join(
+                        self._decoded_apk_path, "base", "manifest", "AndroidManifest.xml",
+                    )
+                else:
+                    self._manifest_file = os.path.join(
                     self._decoded_apk_path, "AndroidManifest.xml"
                 )
 
                 # A list containing the paths to all the smali files obtained with
-                # apktool.
+                # apktool or bundledecompiler.
                 self._smali_files = [
                     os.path.join(root, file_name)
                     for root, dir_names, file_names in os.walk(self._decoded_apk_path)
@@ -389,19 +412,31 @@ def decode_apk(self) -> None:
                 self._smali_files.sort()
 
                 # Check if multidex.
-                if os.path.isdir(
-                    os.path.join(self._decoded_apk_path, "smali_classes2")
-                ):
-                    self._is_multidex = True
-
+                if (self.is_bundle):
+                    if os.path.isdir(
+                        os.path.join(self._decoded_apk_path, "base", "dex", "smali_classes2")
+                    ):
+                        self._is_multidex = True
+                else:
+                    if os.path.isdir(
+                        os.path.join(self._decoded_apk_path, "smali_classes2")
+                    ):
+                        self._is_multidex = True
+
+                if (self._is_multidex):
                     smali_directories = ["smali"]
                     for i in range(2, 15):
                         smali_directories.append("smali_classes{0}".format(i))
 
                     for smali_directory in smali_directories:
-                        current_directory = os.path.join(
-                            self._decoded_apk_path, smali_directory, ""
-                        )
+                        if (self.is_bundle):
+                            current_directory = os.path.join(
+                                self._decoded_apk_path, "base", "dex", smali_directory, ""
+                            )
+                        else:
+                            current_directory = os.path.join(
+                                self._decoded_apk_path, smali_directory, ""
+                            )
                         if os.path.isdir(current_directory):
                             self._multidex_smali_files.append(
                                 [
@@ -499,13 +534,15 @@ def build_obfuscated_apk(self) -> None:
         if not self._is_decoded:
             self.decode_apk()
 
-        # The obfuscated apk will be built with apktool.
+        # The obfuscated apk will be built with apktool or BundleDecompiler.
         apktool: Apktool = Apktool()
+        bundledecompiler: BundleDecompiler = BundleDecompiler()
 
         try:
-            apktool.build(
-                self._decoded_apk_path, self.obfuscated_apk_path, self.use_aapt2
-            )
+            if (self.is_bundle):
+                bundledecompiler.build(self._decoded_apk_path, self.obfuscated_apk_path)
+            else:
+                apktool.build(self._decoded_apk_path, self.obfuscated_apk_path)
         except Exception as e:
             self.logger.error("Error during apk building: {0}".format(e))
             raise
@@ -514,7 +551,8 @@ def sign_obfuscated_apk(self) -> None:
 
         # This method must be called AFTER the obfuscated apk has been built.
 
-        # The obfuscated apk will be signed with apksigner.
+        # The obfuscated apk will be signed with APKSigner or BundleDecompiler.
+        aabsigner: AABSigner = AABSigner()
         apksigner: ApkSigner = ApkSigner()
 
         # If a custom keystore file is not provided, use the default one bundled with
@@ -541,13 +579,18 @@ def sign_obfuscated_apk(self) -> None:
                 )
 
         try:
-            apksigner.resign(
-                self.obfuscated_apk_path,
-                self.keystore_file,
-                self.keystore_password,
-                self.key_alias,
-                self.key_password,
-            )
+            if (self.is_bundle):
+                aabsigner.sign(
+                    self.obfuscated_apk_path,
+                )
+            else:
+                apksigner.resign(
+                    self.obfuscated_apk_path,
+                    self.keystore_file,
+                    self.keystore_password,
+                    self.key_alias,
+                    self.key_password,
+                )
         except Exception as e:
             self.logger.error("Error during apk signing: {0}".format(e))
             raise
@@ -558,6 +601,8 @@ def align_obfuscated_apk(self) -> None:
 
         # The obfuscated apk will be aligned with zipalign.
         zipalign: Zipalign = Zipalign()
+        if (self.is_bundle):
+            return
 
         try:
             zipalign.align(self.obfuscated_apk_path)
@@ -607,7 +652,10 @@ def get_assets_directory(self) -> str:
             self.decode_apk()
 
         # '.join(x, "")' is used to add a trailing slash.
-        return os.path.join(self._decoded_apk_path, "assets", "")
+        if (self.is_bundle):
+            return os.path.join(self._decoded_apk_path, "base", "assets", "")
+        else:
+            return os.path.join(self._decoded_apk_path, "assets", "")
 
     def get_resource_directory(self) -> str:
 
@@ -615,7 +663,11 @@ def get_resource_directory(self) -> str:
             self.decode_apk()
 
         # '.join(x, "")' is used to add a trailing slash.
-        return os.path.join(self._decoded_apk_path, "res", "")
+        if (self.is_bundle):
+            return os.path.join(self._decoded_apk_path, "base", "res", "")
+        else:
+            return os.path.join(self._decoded_apk_path, "res", "")
+        
 
     def get_ignore_package_names(self) -> List[str]:
         ignore_package_list = []
diff --git a/src/obfuscapk/toolbundledecompiler.py b/src/obfuscapk/toolbundledecompiler.py
new file mode 100644
index 00000000..8fc450ed
--- /dev/null
+++ b/src/obfuscapk/toolbundledecompiler.py
@@ -0,0 +1,262 @@
+#!/usr/bin/env python3
+
+import logging
+import os
+import platform
+import shutil
+import subprocess
+from typing import List
+
+
+class BundleDecompiler(object):
+    def __init__(self):
+        self.logger = logging.getLogger(
+            "{0}.{1}".format(__name__, self.__class__.__name__)
+        )
+
+        if platform.system() == "Windows":
+            self.logger.warning("BundleDecompiler is not yet available on Windows platform")
+            return
+
+        if "BUNDLE_DECOMPILER_PATH" in os.environ:
+            self.bundledecompiler_path: str = os.environ["BUNDLE_DECOMPILER_PATH"]
+        else:
+            self.bundledecompiler_path: str = "BundleDecompiler.jar"
+
+        full_bundledecompiler_path = shutil.which(self.bundledecompiler_path)
+
+        # Make sure bundle decompiler is available
+        if not os.path.isfile(full_bundledecompiler_path):
+            raise RuntimeError(
+                'Cannot find BundleDecompiler with executable "{0}"'.format(full_bundledecompiler_path)
+            )
+
+        # Make sure to use the full path of the executable (needed for cross-platform
+        # compatibility).
+        if full_bundledecompiler_path is None:
+            raise RuntimeError(
+                'Something is wrong with executable "{0}"'.format(self.bundledecompiler_path)
+            )
+        else:
+            self.bundledecompiler_path = full_bundledecompiler_path
+
+    def decode(
+        self, aab_path: str, output_dir_path: str = None, force: bool = False
+    ) -> str:
+        if platform.system() == "Windows":
+            raise NotImplementedError("BundleDecompiler is not yet available on Windows platform")
+
+        # Check if the aab file to decode is a valid file.
+        if not os.path.isfile(aab_path):
+            self.logger.error('Unable to find file "{0}"'.format(aab_path))
+            raise FileNotFoundError('Unable to find file "{0}"'.format(aab_path))
+
+        # If no output directory is specified, use a new directory in the same
+        # directory as the aab file to decode.
+        if not output_dir_path:
+            output_dir_path = os.path.join(
+                os.path.dirname(aab_path),
+                os.path.splitext(os.path.basename(aab_path))[0],
+            )
+            self.logger.debug(
+                "No output directory provided, the result will be saved in the "
+                "same directory as the input file, in a directory with the same "
+                'name as the input file: "{0}"'.format(output_dir_path)
+            )
+
+        # If an output directory is provided, make sure that the path to that
+        # directory exists (the final directory will be created by aabtool).
+        elif not os.path.isdir(os.path.dirname(output_dir_path)):
+            self.logger.error(
+                'Unable to find output directory "{0}", aabtool won\'t be able to '
+                'create the directory "{1}"'.format(
+                    os.path.dirname(output_dir_path), output_dir_path
+                )
+            )
+            raise NotADirectoryError(
+                'Unable to find output directory "{0}", aabtool won\'t be able to '
+                'create the directory "{1}"'.format(
+                    os.path.dirname(output_dir_path), output_dir_path
+                )
+            )
+
+        # Inform the user if an existing output directory is provided without the
+        # "force" flag.
+        if os.path.isdir(output_dir_path) and not force:
+            self.logger.error(
+                'Output directory "{0}" already exists, use the "force" flag '
+                "to overwrite".format(output_dir_path)
+            )
+            raise FileExistsError(
+                'Output directory "{0}" already exists, use the "force" flag '
+                "to overwrite".format(output_dir_path)
+            )
+
+        decode_cmd: List[str] = [
+            "java",
+            "-jar",
+            self.bundledecompiler_path,
+            "d",
+            "--in=" + aab_path,
+            "--out=" + output_dir_path,
+        ]
+
+        if force:
+            self.logger.warning(
+                'Bundle Decompiler does not support force'
+            )
+
+        try:
+            self.logger.info(
+                'Running decode command "{0}"'.format(" ".join(decode_cmd))
+            )
+            # A new line character is sent as input since newer versions of aabtool
+            # have an interactive prompt on Windows where the user should press a key.
+            output = subprocess.check_output(
+                decode_cmd, stderr=subprocess.STDOUT, input=b"\n"
+            ).strip()
+            if b"Exception in thread " in output:
+                # Report exception raised in aabtool.
+                raise subprocess.CalledProcessError(1, decode_cmd, output)
+            return output.decode(errors="replace")
+        except subprocess.CalledProcessError as e:
+            self.logger.error(
+                "Error during decode command: {0}".format(
+                    e.output.decode(errors="replace") if e.output else e
+                )
+            )
+            raise
+        except Exception as e:
+            self.logger.error("Error during decoding: {0}".format(e))
+            raise
+
+    def build(self, source_dir_path: str, output_aab_path: str = None) -> str:
+        if platform.system() == "Windows":
+            raise NotImplementedError("BundleDecompiler is not yet available on Windows platform")
+
+        # Check if the input directory exists.
+        if not os.path.isdir(source_dir_path):
+            self.logger.error(
+                'Unable to find source directory "{0}"'.format(source_dir_path)
+            )
+            raise NotADirectoryError(
+                'Unable to find source directory "{0}"'.format(source_dir_path)
+            )
+
+        # If no output aab path is specified, the new aab will be saved in the
+        # default path: <source_dir_path>/dist/<source_dir_name>.aab
+        if not output_aab_path:
+            output_aab_path = os.path.join(
+                source_dir_path,
+                "output",
+                "{0}.aab".format(os.path.basename(source_dir_path)),
+            )
+            self.logger.debug(
+                "No output aab path provided, the new aab will be saved in the "
+                'default path: "{0}"'.format(output_aab_path)
+            )
+
+        build_cmd: List[str] = [
+            "java",
+            "-jar",
+            self.bundledecompiler_path,
+            "b",
+            "--in=" + source_dir_path,
+            "--out=" + output_aab_path,
+        ]
+
+        try:
+            self.logger.info('Running build command "{0}"'.format(" ".join(build_cmd)))
+            # A new line character is sent as input since newer versions of aabtool
+            # have an interactive prompt on Windows where the user should press a key.
+            output = subprocess.check_output(
+                build_cmd, stderr=subprocess.STDOUT, input=b"\n"
+            ).strip()
+            if (
+                b"brut.directory.PathNotExist: " in output
+                or b"Exception in thread " in output
+            ):
+                # Report exception raised in aabtool.
+                raise subprocess.CalledProcessError(1, build_cmd, output)
+
+            if not os.path.isfile(output_aab_path):
+                raise FileNotFoundError(
+                    '"{0}" was not built correctly. aabtool output:\n{1}'.format(
+                        output_aab_path, output.decode(errors="replace")
+                    )
+                )
+
+            return output.decode(errors="replace")
+        except subprocess.CalledProcessError as e:
+            self.logger.error(
+                "Error during build command: {0}".format(
+                    e.output.decode(errors="replace") if e.output else e
+                )
+            )
+            raise
+        except Exception as e:
+            self.logger.error("Error during building: {0}".format(e))
+            raise
+
+
+class AABSigner(object):
+    def __init__(self):
+        self.logger = logging.getLogger(
+            "{0}.{1}".format(__name__, self.__class__.__name__)
+        )
+
+        if platform.system() == "Windows":
+            self.logger.warning("BundleDecompiler is not yet available on Windows platform")
+            return
+
+        if "BUNDLE_DECOMPILER_PATH" in os.environ:
+            self.aabsigner_path: str = os.environ["BUNDLE_DECOMPILER_PATH"]
+        else:
+            self.aabsigner_path: str = "BundleDecompiler.jar"
+
+        full_aabsigner_path = shutil.which(self.aabsigner_path)
+
+        # Make sure to use the full path of the executable (needed for cross-platform
+        # compatibility).
+        if full_aabsigner_path is None:
+            raise RuntimeError(
+                'Something is wrong with executable "{0}"'.format(self.aabsigner_path)
+            )
+        else:
+            self.aabsigner_path = full_aabsigner_path
+
+    def sign(
+        self,
+        aab_path: str,
+    ) -> str:
+        if platform.system() == "Windows":
+            raise NotImplementedError("BundleDecompiler is not yet available on Windows platform")
+
+        # Check if the aab file to sign is a valid file.
+        if not os.path.isfile(aab_path):
+            self.logger.error('Unable to find file "{0}"'.format(aab_path))
+            raise FileNotFoundError('Unable to find file "{0}"'.format(aab_path))
+
+        sign_cmd: List[str] = [
+            "java",
+            "-jar",
+            self.aabsigner_path,
+            "sign-bundle",
+            "--in=" + aab_path,
+            "--out=" + aab_path.replace(".aab","_signed.aab")
+        ]
+
+        try:
+            self.logger.info('Running sign command "{0}"'.format(" ".join(sign_cmd)))
+            output = subprocess.check_output(sign_cmd, stderr=subprocess.STDOUT).strip()           
+            return output.decode(errors="replace")
+        except subprocess.CalledProcessError as e:
+            self.logger.error(
+                "Error during sign command: {0}".format(
+                    e.output.decode(errors="replace") if e.output else e
+                )
+            )
+            raise
+        except Exception as e:
+            self.logger.error("Error during signing: {0}".format(e))
+            raise