AndroidManifest.xml
186 lines · 9.7 KB
Changed in: 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> |


