AndroidManifest.xml
Each line, with the release that last changed it.
| 0.5.0 | 1 | <?xml version="1.0" encoding="utf-8"?> |
| 2 | <manifest xmlns:android="http://schemas.android.com/apk/res/android" | |
| 3 | xmlns:tools="http://schemas.android.com/tools"> | |
| 4 | ||
| 5 | <!-- UPnP/DLNA Discovery Permissions --> | |
| 6 | <uses-permission android:name="android.permission.INTERNET" /> | |
| 7 | <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> | |
| 8 | <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> | |
| 9 | <uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" /> | |
| 10 | <!-- WAKE_LOCK is required by jUPnP's AndroidRouter, which acquires a | |
| 11 | WifiLock (NOT a wake lock — confusingly named) to keep the Wi-Fi | |
| 12 | radio in high-performance mode during UPnP discovery. WifiLock.acquire() | |
| 13 | is gated on this permission at the system level. Without it, the very | |
| 14 | first call into UpnpServiceImpl.startup() throws SecurityException and | |
| 15 | the entire UPnP stack fails to initialize — silently, because the | |
| 16 | binder catches it. The phone shows zero servers and there is no | |
| 17 | user-visible error. Symptom indistinguishable from "no servers on | |
| 18 | network." Discovered via logcat: AndroidRouter.setWifiLock → | |
| 19 | WifiLock.acquire → SecurityException. --> | |
| 20 | <uses-permission android:name="android.permission.WAKE_LOCK" /> | |
| 21 | ||
| 22 | <!-- Playback Service Permission --> | |
| 23 | <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> | |
| 24 | <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" /> | |
| 25 | ||
| 26 | <!-- Local media library — read audio files from device storage. | |
| 27 | READ_MEDIA_AUDIO is the granular permission on API 33+; | |
| 28 | READ_EXTERNAL_STORAGE is the legacy fallback for API 29-32. --> | |
| 29 | <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" /> | |
| 30 | <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" | |
| 31 | android:maxSdkVersion="32" /> | |
| 32 | ||
| 33 | <!-- Voice search delegates to the system's RecognizerIntent (the same | |
| 34 | path the system keyboard's mic button uses). Audio capture happens | |
| 35 | in the system's recognizer activity, not in our process — so we | |
| 36 | don't declare RECORD_AUDIO. The <queries> block below is kept so | |
| 37 | PackageManager.queryIntentActivities can resolve the recognizer | |
| 38 | activity from our process; without it, on API 30+ launching the | |
| 39 | intent silently no-ops. --> | |
| 40 | ||
| 41 | <queries> | |
| 42 | <intent> | |
| 43 | <action android:name="android.speech.RecognitionService" /> | |
| 44 | </intent> | |
| 45 | <intent> | |
| 46 | <action android:name="android.speech.action.RECOGNIZE_SPEECH" /> | |
| 47 | </intent> | |
| 48 | <!-- Kiosk hand-back: resolve the launch intent of the `returnTo` | |
| 49 | package named by an OBSERVE launch (FaveatHaus on the wall | |
| 50 | screens). API 30+ package-visibility filtering makes | |
| 51 | getLaunchIntentForPackage return null for unqueried packages — | |
| 52 | these two filters grant visibility of launcher-able apps on | |
| 53 | both touch and leanback form factors. --> | |
| 54 | <intent> | |
| 55 | <action android:name="android.intent.action.MAIN" /> | |
| 56 | <category android:name="android.intent.category.LAUNCHER" /> | |
| 57 | </intent> | |
| 58 | <intent> | |
| 59 | <action android:name="android.intent.action.MAIN" /> | |
| 60 | <category android:name="android.intent.category.LEANBACK_LAUNCHER" /> | |
| 61 | </intent> | |
| 62 | </queries> | |
| 63 | ||
| 64 | <!-- TV: declares leanback support. required=false so the app can also | |
| 65 | install on phones for Android Auto. The LEANBACK_LAUNCHER category | |
| 66 | in the activity's intent-filter is what makes it appear on TV. --> | |
| 67 | <uses-feature android:name="android.software.leanback" android:required="false" /> | |
| 68 | <uses-feature android:name="android.hardware.touchscreen" android:required="false" /> | |
| 69 | ||
| 70 | <!-- Decision, 2026-10-29: backup rules (D9). Auto Backup used to copy | |
| 71 | everything, including the settings DataStore that holds this | |
| 72 | device's identity (renderer UDN, kiosk return package, observed Cast | |
| 73 | device, last-known hosts), so a restored phone impersonated the old | |
| 74 | one. backup_rules / data_extraction_rules exclude only that file. --> | |
| 75 | <application | |
| 76 | android:name=".EarhornApp" | |
| 77 | android:allowBackup="true" | |
| 78 | android:fullBackupContent="@xml/backup_rules" | |
| 79 | android:dataExtractionRules="@xml/data_extraction_rules" | |
| 80 | android:icon="@mipmap/ic_launcher" | |
| 81 | android:roundIcon="@mipmap/ic_launcher_round" | |
| 82 | android:label="@string/app_name" | |
| 83 | android:logo="@mipmap/ic_launcher" | |
| 84 | android:banner="@drawable/tv_banner" | |
| 85 | android:theme="@style/Theme.Earhorn" | |
| 86 | android:usesCleartextTraffic="true" | |
| 87 | tools:targetApi="35"> | |
| 88 | ||
| 89 | <!-- launchMode=singleTask: required for the voice-cast auto-observe | |
| 90 | entry point. FaveatHaus fires OBSERVE while Earhorn is in the | |
| 91 | BACKGROUND; with the default launchMode that would stack a second | |
| 92 | MainActivity instance (and onNewIntent would never fire). | |
| 93 | singleTask guarantees exactly one instance, brings its task | |
| 94 | forward, and delivers repeat launches through onNewIntent — which | |
| 95 | is also what the notification deep-links already assumed. --> | |
| 96 | <activity | |
| 97 | android:name=".MainActivity" | |
| 98 | android:exported="true" | |
| 99 | android:launchMode="singleTask" | |
| 100 | android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize" | |
| 101 | android:theme="@style/Theme.Earhorn"> | |
| 102 | <intent-filter> | |
| 103 | <action android:name="android.intent.action.MAIN" /> | |
| 104 | <category android:name="android.intent.category.LAUNCHER" /> | |
| 105 | <!-- This line is critical for Android TV visibility --> | |
| 106 | <category android:name="android.intent.category.LEANBACK_LAUNCHER" /> | |
| 107 | </intent-filter> | |
| 108 | ||
| 109 | <!-- E1 primary: exported observe entry point for the voice-cast | |
| 110 | pipeline (musicnow > FaveatHaus > us). String extra "group" | |
| 111 | carries the Cast group friendly name exactly as Google Home | |
| 112 | spells it (UTF-8; spaces and umlauts intact). | |
| 113 | Verification command: see docs/voice-cast-observe.md | |
| 114 | (adb "am start" with the extra; the literal flag can't be | |
| 115 | quoted here because XML comments forbid double hyphens). | |
| 116 | DEFAULT category is mandatory: implicit startActivity (and | |
| 117 | adb "am start" by action) only resolves filters carrying it. --> | |
| 118 | <intent-filter> | |
| 119 | <action android:name="com.faveat.earhorn.action.OBSERVE" /> | |
| 120 | <category android:name="android.intent.category.DEFAULT" /> | |
| 121 | </intent-filter> | |
| 122 | ||
| 123 | <!-- E1 secondary: earhorn://observe?group=<url-encoded name> --> | |
| 124 | <intent-filter> | |
| 125 | <action android:name="android.intent.action.VIEW" /> | |
| 126 | <category android:name="android.intent.category.DEFAULT" /> | |
| 127 | <category android:name="android.intent.category.BROWSABLE" /> | |
| 128 | <data android:scheme="earhorn" android:host="observe" /> | |
| 129 | </intent-filter> | |
| 130 | </activity> | |
| 131 | ||
| 132 | <!-- jUPnP UPnP Service (successor to the abandoned Cling project) --> | |
| 133 | <service | |
| 134 | android:name="org.jupnp.android.AndroidUpnpServiceImpl" | |
| 135 | android:enabled="true" | |
| 136 | android:exported="false" /> | |
| 137 | ||
| 138 | <!-- Media3 Playback + Browse Service. Extends MediaLibraryService so | |
| 139 | both the TV launcher (MediaSession) and Android Auto (browse tree) | |
| 140 | work from the same service. exported=true is required for Auto to | |
| 141 | bind from the car head unit process. --> | |
| 142 | <service | |
| 143 | android:name=".playback.PlaybackService" | |
| 144 | android:enabled="true" | |
| 145 | android:exported="true" | |
| 146 | android:foregroundServiceType="mediaPlayback"> | |
| 147 | <intent-filter> | |
| 148 | <action android:name="androidx.media3.session.MediaLibraryService" /> | |
| 149 | <action android:name="android.media.browse.MediaBrowserService" /> | |
| 150 | </intent-filter> | |
| 151 | </service> | |
| 152 | ||
| 153 | <!-- Decision, 2026-10-29: Media3's media-button receiver, so a | |
| 154 | Bluetooth / headset Play reaches PlaybackService (and playback | |
| 155 | resumption) even when the app isn't running (D8). Subclassed so | |
| 156 | it only starts the service when there is a queue to resume — | |
| 157 | otherwise Android kills the app for a foreground service that | |
| 158 | never posted its notification. --> | |
| 159 | <receiver | |
| 160 | android:name=".playback.EarhornMediaButtonReceiver" | |
| 161 | android:exported="true"> | |
| 162 | <intent-filter> | |
| 163 | <action android:name="android.intent.action.MEDIA_BUTTON" /> | |
| 164 | </intent-filter> | |
| 165 | </receiver> | |
| 166 | ||
| 167 | <!-- Android Auto: declares this app provides media playback. --> | |
| 168 | <meta-data | |
| 169 | android:name="com.google.android.gms.car.application" | |
| 170 | android:resource="@xml/automotive_app_desc" /> | |
| 171 | ||
| 172 | ||
| 173 | <service | |
| 174 | android:name=".cartemplate.EarhornCarAppService" | |
| 175 | android:exported="true"> | |
| 176 | <intent-filter> | |
| 177 | <action android:name="androidx.car.app.CarAppService" /> | |
| 178 | <category android:name="androidx.car.app.category.MEDIA" /> | |
| 179 | </intent-filter> | |
| 180 | <meta-data | |
| 181 | android:name="androidx.car.app.minCarApiLevel" | |
| 182 | android:value="7" /> | |
| 183 | </service> | |
| 184 | ||
| 185 | </application> | |
| 186 | </manifest> |


