Repository: beeware/briefcase

Issue:
Certain characters in formal names cause problems with Android

Certain characters (`/\:<>"?*|`) are prohibited from project paths on Android. These names are legal on other platforms

In addition, the character `!` causes problems when used as part of a path name.

**To Reproduce**
Steps to reproduce the behavior:
1. Run `briefcase new`, using a formal name of `Hello!` or `Hello?`
2. Run `briefcase run android`

Using `Hello!`, the build will fail with the error:
```
[hello] Building Android APK...
Exception in thread "main" java.lang.RuntimeException: Could not determine wrapper version.
        at org.gradle.wrapper.GradleWrapperMain.wrapperVersion(GradleWrapperMain.java:111)
        at org.gradle.wrapper.GradleWrapperMain.main(GradleWrapperMain.java:61)
Caused by: java.lang.RuntimeException: No build receipt resource found.
        at org.gradle.wrapper.GradleWrapperMain.wrapperVersion(GradleWrapperMain.java:97)
        ... 1 more

Error while building project.
```
[hello] Building Android APK...

FAILURE: Build failed with an exception.

* Where:
Settings file '/Users/rkm/projects/beeware/briefcase/local/hello/android/gradle/Hello?/settings.gradle' line: 2

* What went wrong:
A problem occurred evaluating settings 'Hello?'.
> The project name 'Hello?' must not contain any of the following characters: [/, \, :, <, >, ", ?, *, |]. Set the 'rootProject.name' or adjust the 'include' statement (see https://docs.gradle.org/7.2/dsl/org.gradle.api.initialization.Settings.html#org.gradle.api.initialization.Settings:include(java.lang.String[]) for more details).

* Try:
Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 633ms

Error while building project.
```

**Expected behavior**

App should build and start.

**Environment:**
 - Operating System: All
 - Python version: All
 - Software versions:
   - Briefcase: 0.3.6




Comments:

Patch:
diff --git a/changes/696.bugfix.rst b/changes/696.bugfix.rst
new file mode 100644
index 00000000..07c83479
--- /dev/null
+++ b/changes/696.bugfix.rst
@@ -0,0 +1,1 @@
+Android projects that have punctuation in their formal names can now build without error.
diff --git a/src/briefcase/platforms/android/gradle.py b/src/briefcase/platforms/android/gradle.py
index f7c9ab0f..632294d9 100644
--- a/src/briefcase/platforms/android/gradle.py
+++ b/src/briefcase/platforms/android/gradle.py
@@ -1,3 +1,4 @@
+import re
 import subprocess
 
 from briefcase.commands import (
@@ -13,6 +14,20 @@ from briefcase.exceptions import BriefcaseCommandError
 from briefcase.integrations.android_sdk import AndroidSDK
 
 
+def safe_formal_name(name):
+    """Converts the name into a safe name on Android.
+
+    Certain characters (``/\\:<>"?*|``) can't be used as app names
+    on Android; ``!`` causes problems with Android build tooling.
+    Also ensure that trailing, leading, and consecutive whitespace
+    caused by removing punctuation is collapsed.
+
+    :param name: The candidate name
+    :returns: The safe version of the name.
+    """
+    return re.sub(r'\s+', ' ', re.sub(r'[!/\\:<>"\?\*\|]', "", name)).strip()
+
+
 class GradleMixin:
     output_format = "gradle"
     platform = "android"
@@ -28,6 +43,20 @@ class GradleMixin:
     def __init__(self, *args, **kwargs):
         super().__init__(*args, **kwargs)
 
+    def bundle_path(self, app):
+        """
+        The path to the bundle for the app in the output format.
+
+        The bundle is the template-generated source form of the app.
+        The path will usually be a directory, the existence of which is
+        indicative that the template has been rolled out for an app.
+
+        This overrides the default behavior, using a "safe" formal name
+
+        :param app: The app config
+        """
+        return self.platform_path / self.output_format / safe_formal_name(app.formal_name)
+
     def binary_path(self, app):
         return (
             self.bundle_path(app)
@@ -88,6 +117,7 @@ class GradleCreateCommand(GradleMixin, CreateCommand):
 
         return {
             'version_code': version_code,
+            'safe_formal_name': safe_formal_name(app.formal_name),
         }
 
 


Test patch:
diff --git a/tests/platforms/android/gradle/test_create.py b/tests/platforms/android/gradle/test_create.py
index 429035aa..49c5ecb3 100644
--- a/tests/platforms/android/gradle/test_create.py
+++ b/tests/platforms/android/gradle/test_create.py
@@ -43,7 +43,8 @@ def test_version_code(create_command, first_app_config, version, build, version_
     if build:
         first_app_config.build = build
     assert create_command.output_format_template_context(first_app_config) == {
-        'version_code': version_code
+        'version_code': version_code,
+        'safe_formal_name': 'First App',
     }
     # Version code must be less than a 32 bit signed integer MAXINT.
     assert int(version_code) < 2147483647
diff --git a/tests/platforms/android/gradle/test_safe_formal_name.py b/tests/platforms/android/gradle/test_safe_formal_name.py
new file mode 100644
index 00000000..b0919524
--- /dev/null
+++ b/tests/platforms/android/gradle/test_safe_formal_name.py
@@ -0,0 +1,38 @@
+import pytest
+
+from briefcase.platforms.android.gradle import safe_formal_name
+
+
+@pytest.mark.parametrize(
+    'formal_name, safe_name',
+    [
+        ('Hello World', 'Hello World'),
+
+        # The invalid list is all stripped
+        ('Hello/World/', 'HelloWorld'),
+        ('Hello\\World', 'HelloWorld'),
+        ('Hello:World', 'HelloWorld'),
+        ('Hello<World', 'HelloWorld'),
+        ('Hello>World', 'HelloWorld'),
+        ('Hello "World"', 'Hello World'),
+        ('Hello World?', 'Hello World'),
+        ('Hello|World', 'HelloWorld'),
+        ('Hello World!', 'Hello World'),
+
+        # All invalid punctuation is removed
+        # Valid punctuation is preserved
+        ('Hello! (World?)', 'Hello (World)'),
+
+        # Position of punctuation doesn't matter
+        ('Hello! World', 'Hello World'),
+        ('!Hello World', 'Hello World'),
+
+        # If removing punctuation leads to double spaces, reduce the double spaces
+        ('Hello | World', 'Hello World'),
+        ('Hello World |', 'Hello World'),
+        ('| Hello World', 'Hello World'),
+
+    ]
+)
+def test_safe_formal_name(formal_name, safe_name):
+    assert safe_formal_name(formal_name) == safe_name