Repository: beeware/briefcase

Issue:
Briefcase Does Not Detect Failure to Install an APK on an Android version which is too old
### Describe the bug

During `briefcase run android`, the attempt to install the APK on the device can fail if the API level of the APK is beyond what the device can support. Briefcase does not detect this error and eventually errors out when attempting to run the app.

```
           >>> Running Command:                                                                                                                                    subprocess.py:665
           >>>     'C:\Users\domin\AppData\Local\BeeWare\briefcase\Cache\tools\android_sdk\platform-tools\adb.exe' -s PLI5T31F7E install -r                        subprocess.py:666
           'C:\Users\domin\beeware-tutorial\smartmirrortab\android\gradle\SmartMirrorTab\app\build\outputs\apk\debug\app-debug.apk'                                                 
           >>> Working Directory:                                                                                                                                  subprocess.py:673
           >>>     C:\Users\domin\beeware-tutorial\smartmirrortab                                                                                                  subprocess.py:674
[17:31:11] >>> Command Output:                                                                                                                                     subprocess.py:690
           >>>     Performing Push Install                                                                                                                         subprocess.py:692
           >>>     C:\Users\domin\beeware-tutorial\smartmirrortab\android\gradle\SmartMirrorTab\app\build\outputs\apk\debug\app-debug.apk: 1 file pushed, 0        subprocess.py:692
           skipped. 5.5 MB/s (33125287 bytes in 5.768s)                                                                                                                             
           >>>             pkg: /data/local/tmp/app-debug.apk                                                                                                      subprocess.py:692
           >>>                                                                                                                                                     subprocess.py:692
           >>>     Failure [INSTALL_FAILED_OLDER_SDK]                                                                                                              subprocess.py:692
           >>>                                                                                                                                                     subprocess.py:692
           >>> Return code: 0                                                                                                                                      subprocess.py:701
           Installing new app version... done                                                                                                                          gradle.py:336
                                                                                                                                                                   subprocess.py:664
           >>> Running Command:                                                                                                                                    subprocess.py:665
           >>>     'C:\Users\domin\AppData\Local\BeeWare\briefcase\Cache\tools\android_sdk\platform-tools\adb.exe' -s PLI5T31F7E shell am start                    subprocess.py:666
           com.example.smartmirrortab/org.beeware.android.MainActivity -a android.intent.action.MAIN -c android.intent.category.LAUNCHER                                            
           >>> Working Directory:                                                                                                                                  subprocess.py:673
           >>>     C:\Users\domin\beeware-tutorial\smartmirrortab                                                                                                  subprocess.py:674
           >>> Command Output:                                                                                                                                     subprocess.py:690
           >>>     Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]                                                        subprocess.py:692
           cmp=com.example.smartmirrortab/org.beeware.android.MainActivity }                                                                                                        
           >>>                                                                                                                                                     subprocess.py:692
           >>>     Error type 3                                                                                                                                    subprocess.py:692
           >>>                                                                                                                                                     subprocess.py:692
           >>>     Error: Activity class {com.example.smartmirrortab/org.beeware.android.MainActivity} does not exist.                                             subprocess.py:692
           >>>                                                                                                                                                     subprocess.py:692
           >>> Return code: 0                                                                                                                                      subprocess.py:701
[17:31:12] Launching app...                                                                                                                                            gradle.py:355
                                                                                                                                                                      __main__.py:30
           Activity class not found while starting app.                                                                                                               __main__.py:31
                                                                                                                                                                      __main__.py:31
           `adb` output:                                                                                                                                              __main__.py:31
                                                                                                                                                                      __main__.py:31
               Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]                                                               __main__.py:31
           cmp=com.example.smartmirrortab/org.beeware.android.MainActivity }                                                                                                        
                                                                                                                                                                      __main__.py:31
           Error type 3                                                                                                                                               __main__.py:31
                                                                                                                                                                      __main__.py:31
           Error: Activity class {com.example.smartmirrortab/org.beeware.android.MainActivity} does not exist.                                                        __main__.py:31
```

### Steps to reproduce

Attempt `briefcase run android` with a device running Android 4.

### Expected behavior

Briefcase should detect the failure to install the APK and inform the user as such.

### Screenshots

_No response_

### Environment

- Operating System: Windows 10
- Python version: Python 3.8
- Software versions:
  - Briefcase: 0.3.12


### Logs

[briefcase.2023_03_30-17_31_12.run.log](https://github.com/beeware/briefcase/files/11113658/briefcase.2023_03_30-17_31_12.run.log)


### Additional context

Reported in [discord](https://discord.com/channels/836455665257021440/836455665257021443/1091023037513605261).

Comments:

Patch:
diff --git a/changes/1157.bugfix.rst b/changes/1157.bugfix.rst
new file mode 100644
index 00000000..cb9c13db
--- /dev/null
+++ b/changes/1157.bugfix.rst
@@ -0,0 +1,1 @@
+Briefcase will detect if you attempt to launch an Android app on a device whose OS doesn't meet minimum version requirements.
diff --git a/src/briefcase/integrations/android_sdk.py b/src/briefcase/integrations/android_sdk.py
index 59bf8e77..2147ab07 100644
--- a/src/briefcase/integrations/android_sdk.py
+++ b/src/briefcase/integrations/android_sdk.py
@@ -706,7 +706,6 @@ connection.
             output = self.tools.subprocess.check_output(
                 [os.fsdecode(self.emulator_path), "-list-avds"]
             ).strip()
-
             # AVD names are returned one per line.
             if len(output) == 0:
                 return []
@@ -720,7 +719,6 @@ connection.
             output = self.tools.subprocess.check_output(
                 [os.fsdecode(self.adb_path), "devices", "-l"]
             ).strip()
-
             # Process the output of `adb devices -l`.
             # The first line is header information.
             # Each subsequent line is a single device descriptor.
@@ -1326,7 +1324,7 @@ class ADB:
         # checking that they are valid, then parsing output to notice errors.
         # This keeps performance good in the success case.
         try:
-            return self.tools.subprocess.check_output(
+            output = self.tools.subprocess.check_output(
                 [
                     os.fsdecode(self.tools.android_sdk.adb_path),
                     "-s",
@@ -1338,6 +1336,14 @@ class ADB:
                 ],
                 quiet=quiet,
             )
+            # add returns status code 0 in the case of failure. The only tangible evidence
+            # of failure is the message "Failure [INSTALL_FAILED_OLDER_SDK]" in the,
+            # console output; so if that message exists in the output, raise an exception.
+            if "Failure [INSTALL_FAILED_OLDER_SDK]" in output:
+                raise BriefcaseCommandError(
+                    "Your device doesn't meet the minimum SDK requirements of this app."
+                )
+            return output
         except subprocess.CalledProcessError as e:
             if any(DEVICE_NOT_FOUND.match(line) for line in e.output.split("\n")):
                 raise InvalidDeviceError("device id", self.device) from e


Test patch:
diff --git a/tests/integrations/android_sdk/ADB/test_run.py b/tests/integrations/android_sdk/ADB/test_run.py
index 0a828ebf..a6b114d5 100644
--- a/tests/integrations/android_sdk/ADB/test_run.py
+++ b/tests/integrations/android_sdk/ADB/test_run.py
@@ -5,7 +5,7 @@ from pathlib import Path
 
 import pytest
 
-from briefcase.exceptions import InvalidDeviceError
+from briefcase.exceptions import BriefcaseCommandError, InvalidDeviceError
 
 
 def test_simple_command(mock_tools, adb, tmp_path):
@@ -99,3 +99,20 @@ def test_error_handling(mock_tools, adb, name, exception, tmp_path):
         ],
         quiet=False,
     )
+
+
+def test_older_sdk_error(mock_tools, adb):
+    """Failure [INSTALL_FAILED_OLDER_SDK] needs to be catched manually."""
+    mock_tools.subprocess.check_output.return_value = "\n".join(
+        [
+            "Performing Push Install",
+            "C:/.../app-debug.apk: 1 file pushed, 0 skipped. 5.5 MB/s (33125287 bytes in 5.768s)",
+            "         pkg: /data/local/tmp/app-debug.apk",
+            "Failure [INSTALL_FAILED_OLDER_SDK]",
+        ]
+    )
+    with pytest.raises(
+        BriefcaseCommandError,
+        match=r"Your device doesn't meet the minimum SDK requirements of this app",
+    ):
+        adb.run("example", "command")