<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- UPnP/DLNA Discovery Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />
<!-- WAKE_LOCK is required by jUPnP's AndroidRouter, which acquires a
WifiLock (NOT a wake lock — confusingly named) to keep the Wi-Fi
radio in high-performance mode during UPnP discovery. WifiLock.acquire()
is gated on this permission at the system level. Without it, the very
first call into UpnpServiceImpl.startup() throws SecurityException and
the entire UPnP stack fails to initialize — silently, because the
binder catches it. The phone shows zero servers and there is no
user-visible error. Symptom indistinguishable from "no servers on
network." Discovered via logcat: AndroidRouter.setWifiLock →
WifiLock.acquire → SecurityException. -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Playback Service Permission -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
<!-- Local media library — read audio files from device storage.
READ_MEDIA_AUDIO is the granular permission on API 33+;
READ_EXTERNAL_STORAGE is the legacy fallback for API 29-32. -->
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- Voice search delegates to the system's RecognizerIntent (the same
path the system keyboard's mic button uses). Audio capture happens
in the system's recognizer activity, not in our process — so we
don't declare RECORD_AUDIO. The <queries> block below is kept so
PackageManager.queryIntentActivities can resolve the recognizer
activity from our process; without it, on API 30+ launching the
intent silently no-ops. -->
<queries>
<intent>
<action android:name="android.speech.RecognitionService" />
</intent>
<intent>
<action android:name="android.speech.action.RECOGNIZE_SPEECH" />
</intent>
<!-- Kiosk hand-back: resolve the launch intent of the `returnTo`
package named by an OBSERVE launch (FaveatHaus on the wall
screens). API 30+ package-visibility filtering makes
getLaunchIntentForPackage return null for unqueried packages —
these two filters grant visibility of launcher-able apps on
both touch and leanback form factors. -->
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent>
</queries>
<!-- TV: declares leanback support. required=false so the app can also
install on phones for Android Auto. The LEANBACK_LAUNCHER category
in the activity's intent-filter is what makes it appear on TV. -->
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<!-- Decision, 2026-10-29: backup rules (D9). Auto Backup used to copy
everything, including the settings DataStore that holds this
device's identity (renderer UDN, kiosk return package, observed Cast
device, last-known hosts), so a restored phone impersonated the old
one. backup_rules / data_extraction_rules exclude only that file. -->
<application
android:name=".EarhornApp"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/data_extraction_rules"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:logo="@mipmap/ic_launcher"
android:banner="@drawable/tv_banner"
android:theme="@style/Theme.Earhorn"
android:usesCleartextTraffic="true"
tools:targetApi="35">
<!-- launchMode=singleTask: required for the voice-cast auto-observe
entry point. FaveatHaus fires OBSERVE while Earhorn is in the
BACKGROUND; with the default launchMode that would stack a second
MainActivity instance (and onNewIntent would never fire).
singleTask guarantees exactly one instance, brings its task
forward, and delivers repeat launches through onNewIntent — which
is also what the notification deep-links already assumed. -->
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTask"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
android:theme="@style/Theme.Earhorn">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<!-- This line is critical for Android TV visibility -->
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<!-- E1 primary: exported observe entry point for the voice-cast
pipeline (musicnow > FaveatHaus > us). String extra "group"
carries the Cast group friendly name exactly as Google Home
spells it (UTF-8; spaces and umlauts intact).
Verification command: see docs/voice-cast-observe.md
(adb "am start" with the extra; the literal flag can't be
quoted here because XML comments forbid double hyphens).
DEFAULT category is mandatory: implicit startActivity (and
adb "am start" by action) only resolves filters carrying it. -->
<intent-filter>
<action android:name="com.faveat.earhorn.action.OBSERVE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<!-- E1 secondary: earhorn://observe?group=<url-encoded name> -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="earhorn" android:host="observe" />
</intent-filter>
</activity>
<!-- jUPnP UPnP Service (successor to the abandoned Cling project) -->
<service
android:name="org.jupnp.android.AndroidUpnpServiceImpl"
android:enabled="true"
android:exported="false" />
<!-- Media3 Playback + Browse Service. Extends MediaLibraryService so
both the TV launcher (MediaSession) and Android Auto (browse tree)
work from the same service. exported=true is required for Auto to
bind from the car head unit process. -->
<service
android:name=".playback.PlaybackService"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
<!-- Decision, 2026-10-29: Media3's media-button receiver, so a
Bluetooth / headset Play reaches PlaybackService (and playback
resumption) even when the app isn't running (D8). Subclassed so
it only starts the service when there is a queue to resume —
otherwise Android kills the app for a foreground service that
never posted its notification. -->
<receiver
android:name=".playback.EarhornMediaButtonReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
</receiver>
<!-- Android Auto: declares this app provides media playback. -->
<meta-data
android:name="com.google.android.gms.car.application"
android:resource="@xml/automotive_app_desc" />
<service
android:name=".cartemplate.EarhornCarAppService"
android:exported="true">
<intent-filter>
<action android:name="androidx.car.app.CarAppService" />
<category android:name="androidx.car.app.category.MEDIA" />
</intent-filter>
<meta-data
android:name="androidx.car.app.minCarApiLevel"
android:value="7" />
</service>
</application>
</manifest>