Repository: beeware/briefcase

Issue:
Add support for ANDROID_HOME environment variable
**Describe the bug**
After creating a project, building for Android and accepting the license, I had to delete it (because of #462). Then, when creating a new, clean project and building it for Android again, it did not prompt me to accept a license but instead gave me an error that the licenses weren't accepted.

**To Reproduce**
Steps to reproduce the behavior:
1. Create a project
2. Build for Android and accept the license
3. Delete the folder of this project
4. Create another project
5. Build for Android
6. See error

**Expected behavior**
For the application to build successfully.

**Screenshots**
![Screenshots2020 08 01-19 05 43 screenshot](https://user-images.githubusercontent.com/51785314/89107607-1e486f80-d42a-11ea-865e-143848775b3a.png)

**Environment:**
 - Operating System: Linux 5.7.10-arch1-1
 - Python version: Python 3.8.4
 - Software versions:
   - Briefcase: 0.3.3
   - Toga: 0.3.0


Discussion:

Patch:
diff --git a/changes/463.feature.rst b/changes/463.feature.rst
new file mode 100644
index 00000000..ac4b5644
--- /dev/null
+++ b/changes/463.feature.rst
@@ -0,0 +1,1 @@
+An Android SDK specified in ``ANDROID_HOME`` is respected now and will take precedence over the setting of ``ANDROID_SDK_ROOT``.
diff --git a/docs/reference/platforms/android.rst b/docs/reference/platforms/android.rst
index 3579b0ee..4123ddaf 100644
--- a/docs/reference/platforms/android.rst
+++ b/docs/reference/platforms/android.rst
@@ -4,6 +4,21 @@ Android
 
 When generating an Android project, Briefcase produces a Gradle project.
 
+Gradle requires an install of the Android SDK and a Java 17 JDK.
+
+If you have an existing install of the Android SDK, it will be used by Briefcase
+if the ``ANDROID_HOME`` environment variable is set. If ``ANDROID_HOME`` is not
+present in the environment, Briefcase will honor the deprecated
+``ANDROID_SDK_ROOT`` environment variable.
+
+If you have an existing install of a Java 17 JDK, it will be used by Briefcase
+if the ``JAVA_HOME`` environment variable is set. On macOS, if ``JAVA_HOME`` is
+not set, Briefcase will use the ``/usr/libexec/java_home`` tool to find an
+existing JDK install.
+
+If the above methods fail to find an Android SDK or Java JDK, Briefcase will
+download and install an isolated copy in its data directory.
+
 Icon format
 ===========
 
diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py
index fe2c1f77..a20356ce 100644
--- a/src/briefcase/integrations/android_sdk.py
+++ b/src/briefcase/integrations/android_sdk.py
@@ -118,6 +118,7 @@ class AndroidSDK(ManagedTool):
     @property
     def env(self) -> dict[str, str]:
         return {
+            "ANDROID_HOME": os.fsdecode(self.root_path),
             "ANDROID_SDK_ROOT": os.fsdecode(self.root_path),
             "JAVA_HOME": str(self.tools.java.java_home),
         }
@@ -147,6 +148,57 @@ class AndroidSDK(ManagedTool):
     def DEFAULT_SYSTEM_IMAGE(self) -> str:
         return f"system-images;android-31;default;{self.emulator_abi}"
 
+    @classmethod
+    def sdk_path_from_env(cls, tools: ToolCache) -> tuple[str | None, str | None]:
+        """Determine the file path to an Android SDK from the environment.
+
+        Android has historically supported several env vars to set the location of an
+        Android SDK for build tools. The currently preferred source is ANDROID_HOME;
+        however, ANDROID_SDK_ROOT is also supported as a deprecated setting.
+
+        These values must be same if both set; otherwise, Gradle will error.
+
+        :param tools: ToolCache of available tools
+        :returns: Tuple of path to SDK and the env var name that provided that path
+        """
+        android_home = tools.os.environ.get("ANDROID_HOME")
+        android_sdk_root = tools.os.environ.get("ANDROID_SDK_ROOT")
+
+        if android_home:
+            if android_sdk_root and android_sdk_root != android_home:
+                tools.logger.warning(
+                    f"""
+*************************************************************************
+** WARNING: ANDROID_HOME and ANDROID_SDK_ROOT are inconsistent         **
+*************************************************************************
+
+    The ANDROID_HOME and ANDROID_SDK_ROOT environment variables are set
+    to different paths:
+
+        ANDROID_HOME:     {android_home}
+        ANDROID_SDK_ROOT: {android_sdk_root}
+
+    Briefcase will ignore ANDROID_SDK_ROOT and only use the path
+    specified by ANDROID_HOME.
+
+    You should update your environment configuration to either not set
+    ANDROID_SDK_ROOT, or set both environment variables to the same
+    path.
+
+*************************************************************************
+"""
+                )
+            sdk_root = android_home
+            sdk_source = "ANDROID_HOME"
+        elif android_sdk_root:
+            sdk_root = android_sdk_root
+            sdk_source = "ANDROID_SDK_ROOT"
+        else:
+            sdk_root = None
+            sdk_source = None
+
+        return sdk_root, sdk_source
+
     @classmethod
     def verify_install(
         cls,
@@ -156,11 +208,11 @@ class AndroidSDK(ManagedTool):
     ) -> AndroidSDK:
         """Verify an Android SDK is available.
 
-        If the ANDROID_SDK_ROOT environment variable is set, that location will
+        The file paths in ANDROID_HOME and ANDROID_SDK_ROOT environment variables will
         be checked for a valid SDK.
 
-        If the location provided doesn't contain an SDK, or no location is provided,
-        an SDK is downloaded.
+        If those file paths do not contain an SDK, or no file path is provided, an SDK
+        is downloaded.
 
         :param tools: ToolCache of available tools
         :param install: Should the tool be installed if it is not found?
@@ -174,21 +226,41 @@ class AndroidSDK(ManagedTool):
         JDK.verify(tools=tools, install=install)
 
         sdk = None
-        sdk_root = tools.os.environ.get("ANDROID_SDK_ROOT")
+        sdk_root, sdk_env_source = cls.sdk_path_from_env(tools=tools)
+
         if sdk_root:
             sdk = AndroidSDK(tools=tools, root_path=Path(sdk_root))
 
             if sdk.exists():
+                if sdk_env_source == "ANDROID_SDK_ROOT":
+                    tools.logger.warning(
+                        """
+*************************************************************************
+** WARNING: Using Android SDK from ANDROID_SDK_ROOT                    **
+*************************************************************************
+
+    Briefcase is using the Android SDK specified by the ANDROID_SDK_ROOT
+    environment variable.
+
+    Android has deprecated ANDROID_SDK_ROOT in favor of the
+    ANDROID_HOME environment variable.
+
+    Update your environment configuration to set ANDROID_HOME instead of
+    ANDROID_SDK_ROOT to ensure future compatibility.
+
+*************************************************************************
+"""
+                    )
                 sdk.verify_license()
             else:
                 sdk = None
                 tools.logger.warning(
                     f"""
 *************************************************************************
-** WARNING: ANDROID_SDK_ROOT does not point to an Android SDK          **
+** {f"WARNING: {sdk_env_source} does not point to an Android SDK":67} **
 *************************************************************************
 
-    The location pointed to by the ANDROID_SDK_ROOT environment
+    The location pointed to by the {sdk_env_source} environment
     variable:
 
     {sdk_root}


Test patch:
diff --git a/tests/console/test_Log.py b/tests/console/test_Log.py
index 48191330..1a3e9655 100644
--- a/tests/console/test_Log.py
+++ b/tests/console/test_Log.py
@@ -89,7 +89,7 @@ def test_save_log_to_file_no_exception(mock_now, command, tmp_path):
     stacktrace if one is not captured."""
     command.tools.os.environ = {
         "GITHUB_KEY": "super-secret-key",
-        "ANDROID_SDK_ROOT": "/androidsdk",
+        "ANDROID_HOME": "/androidsdk",
     }
 
     logger = Log(verbosity=2)
@@ -131,11 +131,11 @@ def test_save_log_to_file_no_exception(mock_now, command, tmp_path):
     assert "this is log output" in log_contents
     assert "this is console output" not in log_contents
     # Environment variables are in the output
-    assert "ANDROID_SDK_ROOT=/androidsdk" in log_contents
+    assert "ANDROID_HOME=/androidsdk" in log_contents
     assert "GITHUB_KEY=********************" in log_contents
     assert "GITHUB_KEY=super-secret-key" not in log_contents
     # Environment variables are sorted
-    assert log_contents.index("ANDROID_SDK_ROOT") < log_contents.index("GITHUB_KEY")
+    assert log_contents.index("ANDROID_HOME") < log_contents.index("GITHUB_KEY")
 
     assert TRACEBACK_HEADER not in log_contents
     assert EXTRA_HEADER not in log_contents
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_properties.py b/tests/integrations/android_sdk/AndroidSDK/test_properties.py
index 4a32f2a0..9aae960f 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_properties.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_properties.py
@@ -89,6 +89,7 @@ def test_simple_env(mock_tools, android_sdk, tmp_path):
     """The SDK Environment can be constructed."""
     assert android_sdk.env == {
         "JAVA_HOME": os.fsdecode(Path("/path/to/jdk")),
+        "ANDROID_HOME": os.fsdecode(tmp_path / "sdk"),
         "ANDROID_SDK_ROOT": os.fsdecode(tmp_path / "sdk"),
     }
 
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify.py b/tests/integrations/android_sdk/AndroidSDK/test_verify.py
index b6ba106f..c3871898 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_verify.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_verify.py
@@ -113,8 +113,49 @@ def test_succeeds_immediately_in_happy_path(mock_tools, tmp_path):
     assert sdk.root_path == android_sdk_root_path
 
 
-def test_user_provided_sdk(mock_tools, tmp_path):
-    """If the user specifies a valid ANDROID_SDK_ROOT, it is used."""
+@pytest.mark.parametrize("env_var", ["ANDROID_HOME", "ANDROID_SDK_ROOT"])
+def test_user_provided_sdk(mock_tools, env_var, tmp_path, capsys):
+    """If the user environment specifies a valid Android SDK, it is used."""
+    # Increase the log level.
+    mock_tools.logger.verbosity = 2
+
+    # Create `sdkmanager` and the license file.
+    existing_android_sdk_root_path = tmp_path / "other_sdk"
+    tools_bin = existing_android_sdk_root_path / "cmdline-tools" / "latest" / "bin"
+    tools_bin.mkdir(parents=True, mode=0o755)
+    if platform.system() == "Windows":
+        sdk_manager = tools_bin / "sdkmanager.bat"
+        sdk_manager.touch()
+    else:
+        sdk_manager = tools_bin / "sdkmanager"
+        sdk_manager.touch(mode=0o755)
+
+    # Pre-accept the license
+    accept_license(existing_android_sdk_root_path)()
+
+    # Set the environment to specify ANDROID_SDK_ROOT
+    mock_tools.os.environ = {env_var: os.fsdecode(existing_android_sdk_root_path)}
+
+    # Expect verify() to succeed
+    sdk = AndroidSDK.verify(mock_tools)
+
+    # No calls to download or unpack anything.
+    mock_tools.download.file.assert_not_called()
+    mock_tools.shutil.unpack_archive.assert_not_called()
+
+    # The returned SDK has the expected root path.
+    assert sdk.root_path == existing_android_sdk_root_path
+
+    # User is warned about using ANDROID_SDK_ROOT
+    if env_var == "ANDROID_SDK_ROOT":
+        assert "Using Android SDK from ANDROID_SDK_ROOT" in capsys.readouterr().out
+    else:
+        assert capsys.readouterr().out == ""
+
+
+def test_consistent_user_provided_sdk(mock_tools, tmp_path, capsys):
+    """If the user environment specifies a valid Android SDK in both ANDROID_HOME and
+    ANDROID_SDK_ROOT, it is used."""
     # Increase the log level.
     mock_tools.logger.verbosity = 2
 
@@ -134,7 +175,8 @@ def test_user_provided_sdk(mock_tools, tmp_path):
 
     # Set the environment to specify ANDROID_SDK_ROOT
     mock_tools.os.environ = {
-        "ANDROID_SDK_ROOT": os.fsdecode(existing_android_sdk_root_path)
+        "ANDROID_HOME": os.fsdecode(existing_android_sdk_root_path),
+        "ANDROID_SDK_ROOT": os.fsdecode(existing_android_sdk_root_path),
     }
 
     # Expect verify() to succeed
@@ -147,9 +189,54 @@ def test_user_provided_sdk(mock_tools, tmp_path):
     # The returned SDK has the expected root path.
     assert sdk.root_path == existing_android_sdk_root_path
 
+    # User is not warned about configuration
+    assert capsys.readouterr().out == ""
 
-def test_invalid_user_provided_sdk(mock_tools, tmp_path):
-    """If the user specifies an invalid ANDROID_SDK_ROOT, it is ignored."""
+
+def test_inconsistent_user_provided_sdk(mock_tools, tmp_path, capsys):
+    """A warning is logged if ANDROID_HOME and ANDROID_SDK_ROOT are not the same."""
+    # Increase the log level.
+    mock_tools.logger.verbosity = 2
+
+    # Create `sdkmanager` and the license file.
+    existing_android_sdk_root_path = tmp_path / "other_sdk"
+    tools_bin = existing_android_sdk_root_path / "cmdline-tools" / "latest" / "bin"
+    tools_bin.mkdir(parents=True, mode=0o755)
+    if platform.system() == "Windows":
+        sdk_manager = tools_bin / "sdkmanager.bat"
+        sdk_manager.touch()
+    else:
+        sdk_manager = tools_bin / "sdkmanager"
+        sdk_manager.touch(mode=0o755)
+
+    # Pre-accept the license
+    accept_license(existing_android_sdk_root_path)()
+
+    # Set the environment to specify ANDROID_HOME and ANDROID_SDK_ROOT
+    mock_tools.os.environ = {
+        "ANDROID_HOME": os.fsdecode(existing_android_sdk_root_path),
+        "ANDROID_SDK_ROOT": "/opt/sdk2",
+    }
+
+    # Expect verify() to succeed
+    sdk = AndroidSDK.verify(mock_tools)
+
+    # No calls to download or unpack anything.
+    mock_tools.download.file.assert_not_called()
+    mock_tools.shutil.unpack_archive.assert_not_called()
+
+    # The returned SDK has the expected root path.
+    assert sdk.root_path == existing_android_sdk_root_path
+
+    # User is warned about using ANDROID_SDK_ROOT
+    assert (
+        "ANDROID_HOME and ANDROID_SDK_ROOT are inconsistent" in capsys.readouterr().out
+    )
+
+
+@pytest.mark.parametrize("env_var", ["ANDROID_HOME", "ANDROID_SDK_ROOT"])
+def test_invalid_user_provided_sdk(mock_tools, env_var, tmp_path, capsys):
+    """If the user's environment specifies an invalid Android SDK, it is ignored."""
 
     # Create `sdkmanager` and the license file
     # for the *briefcase* managed version of the SDK.
@@ -167,7 +254,7 @@ def test_invalid_user_provided_sdk(mock_tools, tmp_path):
     accept_license(android_sdk_root_path)()
 
     # Set the environment to specify an ANDROID_SDK_ROOT that doesn't exist
-    mock_tools.os.environ = {"ANDROID_SDK_ROOT": os.fsdecode(tmp_path / "other_sdk")}
+    mock_tools.os.environ = {env_var: os.fsdecode(tmp_path / "other_sdk")}
 
     # Expect verify() to succeed
     sdk = AndroidSDK.verify(mock_tools)
@@ -180,6 +267,91 @@ def test_invalid_user_provided_sdk(mock_tools, tmp_path):
     # The returned SDK has the expected root path.
     assert sdk.root_path == android_sdk_root_path
 
+    # User is informed about invalid env var setting
+    assert f"{env_var} does not point to an Android SDK" in capsys.readouterr().out
+
+
+def test_consistent_invalid_user_provided_sdk(mock_tools, tmp_path, capsys):
+    """If the user's environment specifies an invalid Android SDK in both ANDROID_HOME
+    and ANDROID_SDK_ROOT, they are ignored."""
+
+    # Create `sdkmanager` and the license file
+    # for the *briefcase* managed version of the SDK.
+    android_sdk_root_path = tmp_path / "tools" / "android_sdk"
+    tools_bin = android_sdk_root_path / "cmdline-tools" / "latest" / "bin"
+    tools_bin.mkdir(parents=True, mode=0o755)
+    if platform.system() == "Windows":
+        sdk_manager = tools_bin / "sdkmanager.bat"
+        sdk_manager.touch()
+    else:
+        sdk_manager = tools_bin / "sdkmanager"
+        sdk_manager.touch(mode=0o755)
+
+    # Pre-accept the license
+    accept_license(android_sdk_root_path)()
+
+    # Set the environment to specify an ANDROID_SDK_ROOT that doesn't exist
+    mock_tools.os.environ = {
+        "ANDROID_HOME": os.fsdecode(tmp_path / "other_sdk"),
+        "ANDROID_SDK_ROOT": os.fsdecode(tmp_path / "other_sdk"),
+    }
+
+    # Expect verify() to succeed
+    sdk = AndroidSDK.verify(mock_tools)
+
+    # No calls to download, run or unpack anything.
+    mock_tools.download.file.assert_not_called()
+    mock_tools.subprocess.run.assert_not_called()
+    mock_tools.shutil.unpack_archive.assert_not_called()
+
+    # The returned SDK has the expected root path.
+    assert sdk.root_path == android_sdk_root_path
+
+    # User is informed about invalid env var setting
+    assert "ANDROID_HOME does not point to an Android SDK" in capsys.readouterr().out
+
+
+def test_inconsistent_invalid_user_provided_sdk(mock_tools, tmp_path, capsys):
+    """If the user's environment specifies an invalid Android SDK in both ANDROID_HOME
+    and ANDROID_SDK_ROOT...and they are both different, they are ignored."""
+
+    # Create `sdkmanager` and the license file
+    # for the *briefcase* managed version of the SDK.
+    android_sdk_root_path = tmp_path / "tools" / "android_sdk"
+    tools_bin = android_sdk_root_path / "cmdline-tools" / "latest" / "bin"
+    tools_bin.mkdir(parents=True, mode=0o755)
+    if platform.system() == "Windows":
+        sdk_manager = tools_bin / "sdkmanager.bat"
+        sdk_manager.touch()
+    else:
+        sdk_manager = tools_bin / "sdkmanager"
+        sdk_manager.touch(mode=0o755)
+
+    # Pre-accept the license
+    accept_license(android_sdk_root_path)()
+
+    # Set the environment to specify an ANDROID_SDK_ROOT that doesn't exist
+    mock_tools.os.environ = {
+        "ANDROID_HOME": os.fsdecode(tmp_path / "other_sdk1"),
+        "ANDROID_SDK_ROOT": os.fsdecode(tmp_path / "other_sdk2"),
+    }
+
+    # Expect verify() to succeed
+    sdk = AndroidSDK.verify(mock_tools)
+
+    # No calls to download, run or unpack anything.
+    mock_tools.download.file.assert_not_called()
+    mock_tools.subprocess.run.assert_not_called()
+    mock_tools.shutil.unpack_archive.assert_not_called()
+
+    # The returned SDK has the expected root path.
+    assert sdk.root_path == android_sdk_root_path
+
+    # User is informed about invalid env var setting
+    output = capsys.readouterr().out
+    assert "ANDROID_HOME and ANDROID_SDK_ROOT are inconsistent" in output
+    assert "ANDROID_HOME does not point to an Android SDK" in output
+
 
 def test_download_sdk(mock_tools, tmp_path):
     """If an SDK is not available, one will be downloaded."""
diff --git a/tests/platforms/android/gradle/test_run.py b/tests/platforms/android/gradle/test_run.py
index aa7aaf14..6ae9e01a 100644
--- a/tests/platforms/android/gradle/test_run.py
+++ b/tests/platforms/android/gradle/test_run.py
@@ -542,6 +542,7 @@ def test_log_file_extra(run_command, monkeypatch):
     run_command.tools.subprocess.check_output.assert_called_once_with(
         [normpath(sdk_manager), "--list_installed"],
         env={
+            "ANDROID_HOME": str(run_command.tools.android_sdk.root_path),
             "ANDROID_SDK_ROOT": str(run_command.tools.android_sdk.root_path),
             "JAVA_HOME": str(run_command.tools.java.java_home),
         },