Repository: beeware/briefcase

Issue:
Android SDK Installation Path Cannot Contain Whitespace
**Describe the bug**
Parts of the Android SDK do not support running from an installation path that contains whitespace, e.g. `/sdks/android sdk/`.

Briefcase's data directory was moved from `~/.briefcase` to OS-native locations in #777; for macOS, this location is `~/Library/Application Support/briefcase` and encountered this existing issue for Android SDK.

```
(venv-3.9) user@users-MacBook-Pro helloworld % briefcase run android -v

The Android tools provided by Google have license terms that you must accept
before you may use those tools.

>>> Running Command:
>>>     '/Users/user/Library/Application Support/briefcase/tools/android_sdk/cmdline-tools/latest/bin/sdkmanager' --licenses
>>> Environment Overrides:
>>>     ANDROID_SDK_ROOT=/Users/user/Library/Application Support/briefcase/tools/android_sdk
>>>     JAVA_HOME=/Users/user/Library/Application Support/briefcase/tools/java/Contents/Home
Error: Could not find or load main class Support.briefcase.tools.android_sdk.cmdline-tools.latest
>>> Return code: 1
```

In android/ndk#1400, it was noted this is unlikely to ever be fixed. And the internet is full of people encountering this [problem](https://stackoverflow.com/questions/37052934/android-sdk-location-should-not-contain-whitespace-as-this-cause-problems-with).

Of note, though, this is not limited to macOS.
On Linux, if you set `XDG_DATA_HOME=/mnt/my data/`, for example, you'll have the same problem.
On Windows, the user's home directory will contain a space if the user specifies a space in their "name" during account creation. Therefore, while the specific issue outlined above may not surface, other issues with Android SDK and whitespace could occur on Windows.

**To Reproduce**
Steps to reproduce the behavior (on macOS):
1. Install affected briefcase version: 
- `pip install git+https://github.com/beeware/briefcase.git@baf2942d08e09f8bb61fcecafb148904bedec6c2`
2. Run `briefcase new --no-input && cd helloworld && briefcase run android`

**Expected behavior**
The Android SDK should properly run from briefcase's default installation location regardless of platform or user/system configuration.

Discussion:

Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1daaa183..5a2835c8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -141,15 +141,15 @@ jobs:
         framework: ['toga', 'pyside2', 'pyside6', 'ppb']
         include:
         - platform: macos-latest
-          briefcase-data-dir: ~/Library/Application Support/briefcase
+          briefcase-data-dir: ~/Library/Caches/org.beeware.briefcase
           pip-cache-dir: ~/Library/Caches/pip
           docker-cache-dir: ~/Library/Containers/com.docker.docker/Data/vms/0/
         - platform: windows-latest
-          briefcase-data-dir: ~\AppData\Local\BeeWare\briefcase
+          briefcase-data-dir: ~\AppData\Local\BeeWare\briefcase\Cache
           pip-cache-dir: ~\AppData\Local\pip\Cache
           docker-cache-dir: C:\ProgramData\DockerDesktop
         - platform: ubuntu-latest
-          briefcase-data-dir: ~/.local/share/briefcase
+          briefcase-data-dir: ~/.cache/briefcase
           pip-cache-dir: ~/.cache/pip
           # cache action cannot cache docker images (actions/cache#31)
           # docker-cache-dir: /var/lib/docker
diff --git a/changes/374.feature.rst b/changes/374.feature.rst
index 7f58fd4e..7d9e19cd 100644
--- a/changes/374.feature.rst
+++ b/changes/374.feature.rst
@@ -1,1 +1,1 @@
-SDKs, tools, and other downloads needed to support app builds are now stored in an OS-native user data directory instead of ``~/.briefcase``.
+SDKs, tools, and other downloads needed to support app builds are now stored in an OS-native user cache directory instead of ``~/.briefcase``.
diff --git a/changes/789.feature.rst b/changes/789.feature.rst
new file mode 100644
index 00000000..e8490399
--- /dev/null
+++ b/changes/789.feature.rst
@@ -0,0 +1,1 @@
+Added support for the user to define a ``BRIEFCASE_HOME`` environment variable. This enables the user to specify the location of the Briefcase tool cache, allowing the user to avoid issues with spaces in paths or disk space limitations.
diff --git a/docs/how-to/code-signing/android.rst b/docs/how-to/code-signing/android.rst
index ff953516..c77970d7 100644
--- a/docs/how-to/code-signing/android.rst
+++ b/docs/how-to/code-signing/android.rst
@@ -48,28 +48,28 @@ Google Play.
     .. code-block:: bash
 
       $ mkdir -p ~/.android
-      $ ~/Library/Application\ Support/briefcase/tools/java/Contents/Home/bin/keytool -keyalg RSA -deststoretype pkcs12 -genkey -v -storepass android -keystore ~/.android/upload-key-helloworld.jks -keysize 2048 -dname "cn=Upload Key" -alias upload-key -validity 10000
+      $ ~/Library/Caches/org.beeware.briefcase/tools/java/Contents/Home/bin/keytool -keyalg RSA -deststoretype pkcs12 -genkey -v -storepass android -keystore ~/.android/upload-key-helloworld.jks -keysize 2048 -dname "cn=Upload Key" -alias upload-key -validity 10000
 
   .. group-tab:: Linux
 
     .. code-block:: bash
 
       $ mkdir -p ~/.android
-      $ ~/.local/share/briefcase/tools/java/bin/keytool -keyalg RSA -deststoretype pkcs12 -genkey -v -storepass android -keystore ~/.android/upload-key-helloworld.jks -keysize 2048 -dname "cn=Upload Key" -alias upload-key -validity 10000
+      $ ~/.cache/briefcase/tools/java/bin/keytool -keyalg RSA -deststoretype pkcs12 -genkey -v -storepass android -keystore ~/.android/upload-key-helloworld.jks -keysize 2048 -dname "cn=Upload Key" -alias upload-key -validity 10000
 
   .. group-tab:: Windows (PowerShell)
 
     .. code-block:: powershell
 
       C:\...>If (-Not (Test-Path "$env:HOMEPATH/.android")) { New-Item -Path "$env:HOMEPATH\.android" -ItemType Directory }
-      C:\...>& "$env:LOCALAPPDATA\BeeWare\briefcase\tools\java\bin\keytool.exe" -keyalg RSA -deststoretype pkcs12 -genkey -v -storepass android -keystore "$env:HOMEPATH\.android\upload-key-helloworld.jks" -keysize 2048 -dname "cn=Upload Key" -alias upload-key -validity 10000
+      C:\...>& "$env:LOCALAPPDATA\BeeWare\briefcase\Cache\tools\java\bin\keytool.exe" -keyalg RSA -deststoretype pkcs12 -genkey -v -storepass android -keystore "$env:HOMEPATH\.android\upload-key-helloworld.jks" -keysize 2048 -dname "cn=Upload Key" -alias upload-key -validity 10000
 
   .. group-tab:: Windows (cmd)
 
     .. code-block:: doscon
 
       C:\...>IF not exist %HOMEPATH%\.android mkdir %HOMEPATH%\.android
-      C:\...>%LOCALAPPDATA%\BeeWare\briefcase\tools\java\bin\keytool.exe -keyalg RSA -deststoretype pkcs12 -genkey -v -storepass android -keystore %HOMEPATH%\.android\upload-key-helloworld.jks -keysize 2048 -dname "cn=Upload Key" -alias upload-key -validity 10000
+      C:\...>%LOCALAPPDATA%\BeeWare\briefcase\Cache\tools\java\bin\keytool.exe -keyalg RSA -deststoretype pkcs12 -genkey -v -storepass android -keystore %HOMEPATH%\.android\upload-key-helloworld.jks -keysize 2048 -dname "cn=Upload Key" -alias upload-key -validity 10000
 
 
 This creates a 2048-bit key and stores it in a Java keystore located in the
diff --git a/docs/how-to/publishing/android.rst b/docs/how-to/publishing/android.rst
index a74ca5ac..b5642716 100644
--- a/docs/how-to/publishing/android.rst
+++ b/docs/how-to/publishing/android.rst
@@ -76,7 +76,7 @@ name.
 
     .. code-block::
 
-      $ ~/Library/Application\ Support/briefcase/tools/java/Contents/Home/bin/jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/.android/upload-key-helloworld.jks "android/gradle/Hello World/app/build/outputs/bundle/release/app-release.aab" upload-key -storepass android
+      $ ~/Library/Caches/org.beeware.briefcase/tools/java/Contents/Home/bin/jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/.android/upload-key-helloworld.jks "android/gradle/Hello World/app/build/outputs/bundle/release/app-release.aab" upload-key -storepass android
          adding: META-INF/MANIFEST.MF
          adding: META-INF/UPLOAD-K.SF
          adding: META-INF/UPLOAD-K.RSA
@@ -101,7 +101,7 @@ name.
 
     .. code-block::
 
-      $ ~/.local/share/briefcase/tools/java/bin/jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/.android/upload-key-helloworld.jks "android/gradle/Hello World/app/build/outputs/bundle/release/app-release.aab" upload-key -storepass android
+      $ ~/.cache/briefcase/tools/java/bin/jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore ~/.android/upload-key-helloworld.jks "android/gradle/Hello World/app/build/outputs/bundle/release/app-release.aab" upload-key -storepass android
          adding: META-INF/MANIFEST.MF
          adding: META-INF/UPLOAD-K.SF
          adding: META-INF/UPLOAD-K.RSA
@@ -126,7 +126,7 @@ name.
 
     .. code-block::
 
-      C:\...>& "$env:LOCALAPPDATA\BeeWare\briefcase\tools\java\bin\jarsigner.exe" -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore "$env:HOMEPATH\.android\upload-key-helloworld.jks" "android\gradle\Hello World\app\build\outputs\bundle\release\app-release.aab" upload-key -storepass android
+      C:\...>& "$env:LOCALAPPDATA\BeeWare\briefcase\Cache\tools\java\bin\jarsigner.exe" -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore "$env:HOMEPATH\.android\upload-key-helloworld.jks" "android\gradle\Hello World\app\build\outputs\bundle\release\app-release.aab" upload-key -storepass android
          adding: META-INF/MANIFEST.MF
          adding: META-INF/UPLOAD-K.SF
          adding: META-INF/UPLOAD-K.RSA
@@ -151,7 +151,7 @@ name.
 
     .. code-block:: doscon
 
-      C:\...>%LOCALAPPDATA%\BeeWare\briefcase\tools\java\bin\jarsigner.exe -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore %HOMEPATH%\.android\upload-key-helloworld.jks "android\gradle\Hello World\app\build\outputs\bundle\release\app-release.aab" upload-key -storepass android
+      C:\...>%LOCALAPPDATA%\BeeWare\briefcase\Cache\tools\java\bin\jarsigner.exe -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore %HOMEPATH%\.android\upload-key-helloworld.jks "android\gradle\Hello World\app\build\outputs\bundle\release\app-release.aab" upload-key -storepass android
          adding: META-INF/MANIFEST.MF
          adding: META-INF/UPLOAD-K.SF
          adding: META-INF/UPLOAD-K.RSA
diff --git a/docs/reference/configuration.rst b/docs/reference/configuration.rst
index b2227085..9e0f6fee 100644
--- a/docs/reference/configuration.rst
+++ b/docs/reference/configuration.rst
@@ -1,12 +1,12 @@
-=====================
-Configuration options
-=====================
+=============================
+Project configuration options
+=============================
 
 Briefcase is a `PEP518 <https://www.python.org/dev/peps/pep-0518/>`__-compliant
 build tool. It uses a ``pyproject.toml`` file, in the root directory of your
 project, to provide build instructions for the packaged file.
 
-If you have an application called "My App", with source code in the `src/myapp`
+If you have an application called "My App", with source code in the ``src/myapp``
 directory, the simplest possible ``pyproject.toml`` Briefcase configuration
 file would be::
 
diff --git a/docs/reference/environment.rst b/docs/reference/environment.rst
new file mode 100644
index 00000000..d910265e
--- /dev/null
+++ b/docs/reference/environment.rst
@@ -0,0 +1,29 @@
+===============================
+Briefcase configuration options
+===============================
+
+Environment variables
+=====================
+
+``BRIEFCASE_HOME``
+~~~~~~~~~~~~~~~~~~
+
+When briefcase runs, it will download the support files, tools, and SDKs
+necessary to support building and packaging apps. By default, it will store the
+files in a platform-native cache folder:
+
+* macOS: ``~/Library/Caches/org.beeware.briefcase``
+* Windows: ``%LOCALAPPDATA%\BeeWare\briefcase\Cache``
+* Linux: ``~/.cache/briefcase``
+
+If you want to use a different folder to store the Briefcase resources, you can
+define a ``BRIEFCASE_HOME`` environment variable.
+
+There are three restrictions on this path specification:
+
+1. The path must already exist. If it doesn't exist, you should create it manually.
+2. It *must not* contain any spaces.
+3. It *must not* be on a network drive.
+
+The second two restrictions both exist because some of the tools that Briefcase
+uses (in particular, the Android SDK) do not work in these locations.
diff --git a/docs/reference/index.rst b/docs/reference/index.rst
index 72c0224e..0fe0d7fd 100644
--- a/docs/reference/index.rst
+++ b/docs/reference/index.rst
@@ -9,6 +9,7 @@ This is the technical reference for public APIs provided by Briefcase.
 .. toctree::
    :maxdepth: 2
 
+   environment
    configuration
    commands/index
    platforms/index
diff --git a/src/briefcase/commands/base.py b/src/briefcase/commands/base.py
index 8aeebfe9..27e9120e 100644
--- a/src/briefcase/commands/base.py
+++ b/src/briefcase/commands/base.py
@@ -117,24 +117,72 @@ class BaseCommand(ABC):
     def __init__(
         self,
         base_path,
-        home_path=Path.home(),
-        data_path=PlatformDirs(appname="briefcase", appauthor="BeeWare").user_data_path,
+        home_path=None,
+        data_path=None,
         apps=None,
         input_enabled=True,
     ):
-        self.base_path = base_path
-        self.home_path = home_path
-        self.data_path = data_path
+        # Some details about the host machine
+        self.host_arch = platform.machine()
+        self.host_os = platform.system()
+
+        self.base_path = Path(base_path)
+
+        # If a home path is provided during construction, use it. This usually
+        # indicates we're under test conditions.
+        if home_path is None:
+            home_path = Path.home()
+        self.home_path = Path(home_path)
+
+        # If a data path is provided during construction, use it. This usually
+        # indicates we're under test conditions. If there's no data path
+        # provided, look for a BRIEFCASE_HOME environment variable. If that
+        # isn't defined, use a platform-specific default data path.
+        if data_path is None:
+            try:
+                briefcase_home = os.environ["BRIEFCASE_HOME"]
+                data_path = Path(briefcase_home).resolve()
+                # Path("") converts to ".", so check for that edge case.
+                if briefcase_home == "" or not data_path.exists():
+                    raise BriefcaseCommandError(
+                        "The path specified by BRIEFCASE_HOME does not exist."
+                    )
+            except KeyError:
+                if self.host_os == "Darwin":
+                    # macOS uses a bundle name, rather than just the app name
+                    app_name = "org.beeware.briefcase"
+                else:
+                    app_name = "briefcase"
+
+                data_path = PlatformDirs(
+                    appname=app_name,
+                    appauthor="BeeWare",
+                ).user_cache_path
+
+        if " " in os.fsdecode(data_path):
+            raise BriefcaseCommandError(
+                f"""
+The location Briefcase will use to store tools and support files:
+
+    {data_path}
+
+contains spaces. This will cause problems with some tools, preventing
+you from building and packaging applications.
+
+You can set the environment variable BRIEFCASE_HOME to specify
+a custom location for Briefcase's tools.
+
+"""
+            )
+
+        self.data_path = Path(data_path)
+
         self.tools_path = self.data_path / "tools"
 
         self.global_config = None
         self.apps = {} if apps is None else apps
         self._path_index = {}
 
-        # Some details about the host machine
-        self.host_arch = platform.machine()
-        self.host_os = platform.system()
-
         # External service APIs.
         # These are abstracted to enable testing without patching.
         self.cookiecutter = cookiecutter
@@ -163,9 +211,14 @@ class BaseCommand(ABC):
         """
         dot_briefcase_path = self.home_path / ".briefcase"
 
+        # If there's no .briefcase path, no need to check for migration.
         if not dot_briefcase_path.exists():
             return
 
+        # If the data path is user-provided, don't check for migration.
+        if "BRIEFCASE_HOME" in os.environ:
+            return
+
         if self.data_path.exists():
             self.logger.warning(
                 f"""\


Test patch:
diff --git a/tests/commands/base/test_check_obsolete_data_dir.py b/tests/commands/base/test_check_obsolete_data_dir.py
index 5607b171..2253f8ed 100644
--- a/tests/commands/base/test_check_obsolete_data_dir.py
+++ b/tests/commands/base/test_check_obsolete_data_dir.py
@@ -1,3 +1,4 @@
+import os
 from unittest.mock import MagicMock
 
 import pytest
@@ -18,12 +19,38 @@ def test_skip_if_dot_briefcase_nonexistent(capsys, tmp_path):
         home_path=home_path,
         data_path=tmp_path / "data_dir",
     )
+    cmd.input.boolean_input = MagicMock()
+
     cmd.check_obsolete_data_dir()
 
+    cmd.input.boolean_input.assert_not_called()
     assert not dot_briefcase_dir.exists()
     assert capsys.readouterr().out == ""
 
 
+def test_skip_if_data_dir_from_environment(monkeypatch, capsys, tmp_path):
+    """If the data directory came from the user environment, don't run the
+    check, even if .briefcase exists."""
+    monkeypatch.setenv("BRIEFCASE_HOME", os.fsdecode(tmp_path / "custom"))
+
+    home_path = tmp_path / "home"
+    dot_briefcase_dir = home_path / ".briefcase"
+    dot_briefcase_dir.mkdir(parents=True)
+
+    cmd = DummyCommand(
+        base_path=tmp_path / "base",
+        home_path=home_path,
+        data_path=tmp_path / "data_dir",
+    )
+    cmd.input.boolean_input = MagicMock()
+
+    cmd.check_obsolete_data_dir()
+
+    cmd.input.boolean_input.assert_not_called()
+    assert dot_briefcase_dir.exists()
+    assert capsys.readouterr().out == ""
+
+
 def test_first_notice_if_dot_briefcase_exists(capsys, tmp_path):
     """Obsolete data dir check shows full notice of transition if .briefcase
     directory exists but new data directory does not exist."""
diff --git a/tests/commands/base/test_paths.py b/tests/commands/base/test_paths.py
new file mode 100644
index 00000000..31834a5a
--- /dev/null
+++ b/tests/commands/base/test_paths.py
@@ -0,0 +1,231 @@
+import os
+import platform
+from pathlib import Path
+
+import pytest
+
+from briefcase.exceptions import BriefcaseCommandError
+
+from .conftest import DummyCommand
+
+
+def test_space_in_path(tmp_path):
+    """The briefcase data path cannot contain spaces."""
+    with pytest.raises(
+        BriefcaseCommandError,
+        match=r"contains spaces. This will cause problems with some tools",
+    ):
+        DummyCommand(tmp_path / "base", data_path=tmp_path / "somewhere bad")
+
+
+def test_empty_custom_path(monkeypatch, tmp_path):
+    """If the environment-specified BRIEFCASE_HOME is defined, but empty, an
+    error is raised."""
+    monkeypatch.setenv("BRIEFCASE_HOME", "")
+
+    with pytest.raises(
+        BriefcaseCommandError,
+        match=r"The path specified by BRIEFCASE_HOME does not exist.",
+    ):
+        DummyCommand(tmp_path / "base")
+
+
+def test_custom_path_does_not_exist(monkeypatch, tmp_path):
+    """If the environment-specified BRIEFCASE_HOME doesn't exist, an error is
+    raised."""
+    monkeypatch.setenv("BRIEFCASE_HOME", tmp_path / "custom")
+
+    with pytest.raises(
+        BriefcaseCommandError,
+        match=r"The path specified by BRIEFCASE_HOME does not exist.",
+    ):
+        DummyCommand(tmp_path / "base")
+
+
+def templated_path_test(
+    monkeypatch,
+    tmp_path,
+    home_path,
+    data_path,
+    environ_path,
+    expected_home_path,
+    expected_data_path,
+):
+    if environ_path:
+        monkeypatch.setenv("BRIEFCASE_HOME", environ_path.format(tmp_path=tmp_path))
+        Path(environ_path.format(tmp_path=tmp_path)).mkdir(parents=True)
+    else:
+        monkeypatch.delenv("BRIEFCASE_HOME", raising=False)
+
+    command = DummyCommand(
+        tmp_path / "base",
+        home_path=home_path,
+        data_path=data_path.format(tmp_path=tmp_path) if data_path else None,
+    )
+    assert command.base_path == tmp_path / "base"
+    assert command.home_path == Path(os.path.expanduser(expected_home_path))
+    assert command.data_path == Path(
+        os.path.expanduser(expected_data_path.format(tmp_path=tmp_path))
+    )
+
+
+@pytest.mark.skipif(platform.system() != "Darwin", reason="macOS specific tests")
+@pytest.mark.parametrize(
+    "home_path, data_path, environ_path, expected_home_path, expected_data_path",
+    [
+        (  # All default values
+            None,
+            None,
+            None,
+            "~",
+            "~/Library/Caches/org.beeware.briefcase",
+        ),
+        (  # Environment variable for the data home
+            None,
+            None,
+            "{tmp_path}custom/briefcase/path",
+            "~",
+            "{tmp_path}custom/briefcase/path",
+        ),
+        (  # Explicit paths
+            "/path/to/home",
+            "/path/to/data",
+            None,
+            "/path/to/home",
+            "/path/to/data",
+        ),
+        (  # Explicit paths and an environment variable present
+            "/path/to/home",
+            "/path/to/data",
+            "{tmp_path}custom/briefcase/path",
+            "/path/to/home",
+            "/path/to/data",
+        ),
+    ],
+)
+def test_macOS_paths(
+    monkeypatch,
+    tmp_path,
+    home_path,
+    data_path,
+    environ_path,
+    expected_home_path,
+    expected_data_path,
+):
+    templated_path_test(
+        monkeypatch,
+        tmp_path,
+        home_path,
+        data_path,
+        environ_path,
+        expected_home_path,
+        expected_data_path,
+    )
+
+
+@pytest.mark.skipif(platform.system() != "Windows", reason="Windoes specific tests")
+@pytest.mark.parametrize(
+    "home_path, data_path, environ_path, expected_home_path, expected_data_path",
+    [
+        (  # All default values
+            None,
+            None,
+            None,
+            "~",
+            "~/AppData/Local/BeeWare/briefcase/Cache",
+        ),
+        (  # Environment variable for the data home
+            None,
+            None,
+            "{tmp_path}custom\\briefcase\\path",
+            "~",
+            "{tmp_path}custom\\briefcase\\path",
+        ),
+        (  # Explicit paths
+            "Y:\\path\\to\\home",
+            "Z:\\path\\to\\data",
+            None,
+            "Y:\\path\\to\\home",
+            "Z:\\path\\to\\data",
+        ),
+        (  # Explicit paths and an environment variable present
+            "Y:\\path\\to\\home",
+            "Z:\\path\\to\\data",
+            "{tmp_path}\\briefcase\\path",
+            "Y:\\path\\to\\home",
+            "Z:\\path\\to\\data",
+        ),
+    ],
+)
+def test_windows_paths(
+    monkeypatch,
+    tmp_path,
+    home_path,
+    data_path,
+    environ_path,
+    expected_home_path,
+    expected_data_path,
+):
+    templated_path_test(
+        monkeypatch,
+        tmp_path,
+        home_path,
+        data_path,
+        environ_path,
+        expected_home_path,
+        expected_data_path,
+    )
+
+
+@pytest.mark.skipif(platform.system() != "Linux", reason="Linux specific tests")
+@pytest.mark.parametrize(
+    "home_path, data_path, environ_path, expected_home_path, expected_data_path",
+    [
+        (  # All default values
+            None,
+            None,
+            None,
+            "~",
+            "~/.cache/briefcase",
+        ),
+        (  # Environment variable for the data home
+            None,
+            None,
+            "{tmp_path}custom/briefcase/path",
+            "~",
+            "{tmp_path}custom/briefcase/path",
+        ),
+        (  # Explicit paths
+            "/path/to/home",
+            "/path/to/data",
+            None,
+            "/path/to/home",
+            "/path/to/data",
+        ),
+        (  # Explicit paths and an environment variable present
+            "/path/to/home",
+            "/path/to/data",
+            "{tmp_path}custom/briefcase/path",
+            "/path/to/home",
+            "/path/to/data",
+        ),
+    ],
+)
+def test_linux_paths(
+    monkeypatch,
+    tmp_path,
+    home_path,
+    data_path,
+    environ_path,
+    expected_home_path,
+    expected_data_path,
+):
+    templated_path_test(
+        monkeypatch,
+        tmp_path,
+        home_path,
+        data_path,
+        environ_path,
+        expected_home_path,
+        expected_data_path,
+    )
diff --git a/tests/integrations/android_sdk/AndroidSDK/test_verify.py b/tests/integrations/android_sdk/AndroidSDK/test_verify.py
index 4fbe7df7..a3f941de 100644
--- a/tests/integrations/android_sdk/AndroidSDK/test_verify.py
+++ b/tests/integrations/android_sdk/AndroidSDK/test_verify.py
@@ -22,8 +22,11 @@ def mock_command(tmp_path):
     command.sys = MagicMock()
     command.tools_path = tmp_path / "tools"
 
-    # Make the `os` module and `host_os` live.
-    command.os = os
+    # Mock the os environment, but copy over other key functions.
+    command.os = MagicMock()
+    command.os.environ = {}
+    command.os.fsdecode = os.fsdecode
+    command.os.access = os.access
 
     # Identify the host platform
     command.host_os = platform.system()