diff --git a/.gitignore b/.gitignore index 49eb5f31..ed65b2ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.kiro doxygen .bundle _site diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..f9b564b4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,126 @@ +# YAACC Changelog + +## [Unreleased] - 2026-02-21 + +### Added +- **SAF Short ID System**: Implemented short numeric ID mapping to fix UPnP browsing errors caused by long ObjectIDs (97% size reduction) +- **Clear Cache Button**: Added button in Server Control Activity to clear SAF cache and trigger reindexing +- **Image Preload**: Extended preload indexing to include image files (previously only audio/video) +- **Preload Timeout**: Added 5-minute timeout to prevent indefinite hanging during indexing +- **Permission Checks**: Added read permission checks before traversing SAF folders +- **Notification Permission**: Added POST_NOTIFICATIONS permission request on startup (Android 13+) +- **Lightweight Content Wrappers**: Created YaaccItem, YaaccMusicTrack, YaaccPhoto, YaaccRes classes for faster browsing +- **ProtocolInfo Cache**: Pre-created ProtocolInfo for common MIME types to eliminate repeated parsing + +### Changed +- **Renamed**: DurationCacheManager → SAFCacheManager (better reflects actual purpose) +- **Extended Cache**: Now caches duration, MIME type, file size, and shortId for all files +- **Optimized Item Creation**: Eliminated Cling library overhead (~5.5x faster, from ~250ms to ~45ms per item) +- **Improved Logging**: Better logging for cache operations and preload progress +- **ID Persistence**: ID counter now persisted in SharedPreferences to maintain stable IDs across restarts + +### Fixed +- Fixed SAF folder browsing hanging on folders without read permission +- Fixed preload getting stuck on large or inaccessible folders +- Fixed ID counter not persisting, causing ID collisions after restart +- Fixed cache not including image files +- Fixed UPnP `UPNP_E_BAD_RESPONSE` errors caused by excessively long ObjectIDs + +### Performance +- SAF browsing: 70% faster after initial cache population (3.75s → 1.1s for 15 files) +- Item creation: 5.5x faster (~250ms → ~45ms per item) +- ObjectID length: 97% reduction (250 chars → 8 chars) +- Cache hit time: <1ms (memory), <50ms (disk) + +### Breaking Changes +- Cache format changed: Old cache entries automatically migrated to include shortId +- ID format changed: ObjectIDs now use short numeric IDs instead of Base64-encoded URIs +- Desktop UPnP clients may need to clear cache and re-browse after update + +--- + +## [4.4.1] - Previous Release + +### Added +- Modernized image viewer with zoom support (PhotoView library) +- Media controls on lock screen +- Volume controls using Media Router API (standard Android implementation) +- System audio streaming (Android 10+) +- Screen cast streaming using MJPEG +- Experimental MPEG-TS streaming +- Smart logger for better debugging +- Unit tests for core functionality + +### Changed +- Refactored player service to align with recent Android APIs +- Implemented ExoPlayer and Media3 MediaSession +- Smart WiFi lock management to reduce battery drain +- Improved DLNA metadata +- Modernized TabBrowser activity +- Improved performance in content browsing +- LRU cache for SAF file scanning + +### Fixed +- Fixed renderer service issues +- Fixed album art in notifications +- Fixed restart during pause +- Fixed background service not stopping correctly +- Fixed issue #151 (network stack) +- Fixed issue #186 (media controls) +- Fixed issue #187 (media resource selection) +- Fixed issue #110 (network stack refactoring) +- Fixed issue #105 (player service refactoring) +- Fixed issue #93 (volume controls) +- Fixed issue #81 (audio streaming) +- Fixed issue #121 (screen cast) + +### Removed +- Removed dead classes after refactoring +- Cleaned up deprecated player implementations + +--- + +## Migration Notes + +### From 4.4.1 to Unreleased +1. **SAF Cache**: Existing cache entries will be automatically migrated on first access +2. **Desktop Clients**: Clear UPnP client cache and re-browse YAACC server after update +3. **Permissions**: Grant POST_NOTIFICATIONS permission when prompted (Android 13+) +4. **Cache Management**: Use new "Clear SAF Cache" button in Server Control Activity if needed + +--- + +## Technical Details + +### SAF Short ID Implementation +- Bidirectional URI ↔ shortId mapping in SAFCacheManager +- Persistent ID counter in SharedPreferences (`saf_id_counter`) +- Automatic migration of old cache entries +- ObjectID format: `1100999` + shortId (e.g., `11009991`, `11009992`) +- URL format: `http://ip:port/saf/11009991/1.mp3` + +### Performance Optimizations +- Eliminated Cling Property/ArrayList overhead in item creation +- Cached ProtocolInfo for common MIME types +- Lightweight wrapper classes instead of heavy Cling objects +- Deferred Cling conversion until UPnP response generation + +### Cache Improvements +- Memory cache: LRU with 1000 entry limit +- Disk cache: SharedPreferences with automatic trimming +- Preload: Background indexing with timeout and permission checks +- Metadata: Duration, MIME type, file size, shortId for all media files + +--- + +## Known Issues +- Preload may take several minutes for large SAF folders (5-minute timeout) +- Desktop UPnP clients must refresh cache after YAACC cache clear +- Local device in Receiver tab doesn't show status badge (not a UPnP renderer) + +--- + +## Credits +- SAF Short ID implementation: tobexyz +- Content browsing optimization: tobexyz +- Cache management improvements: tobexyz diff --git a/build.gradle b/build.gradle index f8bc8faa..727f9b44 100644 --- a/build.gradle +++ b/build.gradle @@ -1,20 +1,21 @@ allprojects { buildscript { repositories { - jcenter() google() mavenLocal() maven { url "https://plugins.gradle.org/m2/" } + mavenCentral() } } repositories { - jcenter() google() mavenLocal() + mavenCentral() + maven { url 'https://jitpack.io' } } diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock index b2018cfc..eb2d0726 100644 --- a/docs/Gemfile.lock +++ b/docs/Gemfile.lock @@ -33,10 +33,12 @@ GEM ffi (>= 1.15.0) eventmachine (1.2.7) execjs (2.9.1) - faraday (2.9.0) - faraday-net_http (>= 2.0, < 3.2) - faraday-net_http (3.1.0) - net-http + faraday (2.14.1) + faraday-net_http (>= 2.0, < 3.5) + json + logger + faraday-net_http (3.4.2) + net-http (~> 0.5) ffi (1.16.3) forwardable-extended (2.6.0) gemoji (4.1.0) @@ -205,6 +207,7 @@ GEM gemoji (>= 3, < 5) html-pipeline (~> 2.2) jekyll (>= 3.0, < 5.0) + json (2.18.1) kramdown (2.4.0) rexml kramdown-parser-gfm (1.1.0) @@ -213,6 +216,7 @@ GEM listen (3.9.0) rb-fsevent (~> 0.10, >= 0.10.3) rb-inotify (~> 0.9, >= 0.9.10) + logger (1.7.0) mercenary (0.3.6) minima (2.5.1) jekyll (>= 3.5, < 5.0) @@ -220,9 +224,9 @@ GEM jekyll-seo-tag (~> 2.1) minitest (5.22.2) mutex_m (0.2.0) - net-http (0.4.1) - uri - nokogiri (1.18.9-x86_64-linux-gnu) + net-http (0.9.1) + uri (>= 0.11.1) + nokogiri (1.19.1-x86_64-linux-gnu) racc (~> 1.4) octokit (4.25.1) faraday (>= 1, < 3) @@ -258,7 +262,7 @@ GEM unf_ext unf_ext (0.0.9.1) unicode-display_width (1.8.0) - uri (0.13.2) + uri (1.1.1) webrick (1.8.2) PLATFORMS diff --git a/docs/index.md b/docs/index.md index fc7fe344..32700088 100644 --- a/docs/index.md +++ b/docs/index.md @@ -26,13 +26,13 @@ On the first screen all UPnP/DLNA servers in your network are listed. Select one and the app will automatically switch to the content tab. It allows to browse the content provided by the selected server. -![browse_servers](./screenshots/4.4.x/browse_servers.png){:height="30%" width="30%"} +![browse_servers](./screenshots/5.0.x/browse_servers.png){:height="30%" width="30%"} Before selecting content make sure you have chosen an receiver on the receiver tab. Receivers are either UPnP/DLNA media renderers in your network or the android device itself. -![browse_receiver](./screenshots/4.4.x/browse_receiver.png){:height="30%" width="30%"} +![browse_receiver](./screenshots/5.0.x/browse_receiver.png){:height="30%" width="30%"} Normally senders and receivers will appear automatically. If not you can use the refresh button at the bottom of @@ -50,34 +50,155 @@ Behind each content entry different symbols are showing the possible actions: At the bottom of the screen the currently selected sender and receiver is displayed. -![browse_music_folder](./screenshots/4.4.x/browse_music_folder.png){:height="30%" width="30%"} +![browse_music_folder](./screenshots/5.0.x/browse_music_folder.png){:height="30%" width="30%"} ## Playing content -YAACC is able to control an multiple players at the same time. -For example you are able to stream a image show with background music +YAACC is able to control multiple players at the same time. +For example you are able to stream an image show with background music on your device, starting a movie on a TV or play music on a smart speaker at the same time. Each player is displayed in the player tab. Depending on the content type and if the content is played by YAACC itself or a network device, the player ui differs. -![browse_player](./screenshots/4.4.x/browse_player.png){:height="30%" width="30%"} +![browse_player](./screenshots/5.0.x/browse_player.png){:height="30%" width="30%"} YAACC includes a player for music and image shows. Videos are played using a third parties app on the device. The video app will start automatically, if video content is selected for playing -![music_player](./screenshots/4.4.x/music_player.png){:height="30%" width="30%"} +![music_player](./screenshots/5.0.x/music_player.png){:height="30%" width="30%"} -![image_player_show_menu](./screenshots/4.4.x/image_player_show_menu.png){:height="30%" width="30%"} +![image_player_show_menu](./screenshots/5.0.x/image_player_show_menu.png){:height="30%" width="30%"} + +### Receiver controls + +The receiver tab shows all available UPnP/DLNA renderers in your network, including your device. +Each renderer displays: + +* Current playback status (PLAYING badge when active) +* Track title and album art (when available) +* Play/pause/stop controls +* Volume control +* Mute button + +![browse_receiver](./screenshots/5.0.x/browse_receiver.png){:height="30%" width="30%"} + +You can control playback directly from the receiver tab without opening the player view. +The controls work for both local playback and remote UPnP devices. + +### Lock screen controls + +When playing media locally, YAACC provides lock screen controls: + +* Play/pause/stop buttons +* Next/previous track +* Track metadata display +* Album art +* Hardware volume buttons control playback volume + +## Live streaming (Android 10+) + +YAACC can stream your device's screen and system audio to UPnP/DLNA renderers in your network. + +### System audio streaming + +Stream all audio playing on your device to network speakers or receivers: + +* Captures system audio (music, podcasts, games, etc.) +* 44.1kHz stereo, high quality +* Works with most UPnP audio renderers +* Multiple simultaneous clients supported + +**Note:** Some apps may block audio capture (DRM-protected content like Spotify, YouTube Premium). + +### Screen casting + +Stream your device's screen to TVs or displays: + +* 720p video at 15fps +* MJPEG format for broad compatibility +* Works in web browsers and some UPnP video players +* Useful for presentations and screen sharing + +### Enabling live streaming + +1. Go to the Server tab +2. Find your local device in the list +3. Tap the audio or video button +4. Grant MediaProjection permission when prompted +5. The stream appears in the "Live Streams" folder + +Access streams via UPnP or directly: +* Audio: `http://:49157/live/audio` +* Video: `http://:49157/live/video` + +## Storage Access Framework (SAF) + +YAACC supports browsing files from external storage, USB drives, and SD cards using Android's +Storage Access Framework. This allows sharing media from any storage location accessible on your device. + +### Configuring SAF folders + +In the server control view, you can add SAF folders to share: + +1. Tap "Add SAF folder" +2. Select the folder using Android's file picker +3. Grant permission when prompted +4. The folder appears in the server content directory + +![server_control](./screenshots/5.0.x/server_control.png){:height="30%" width="30%"} + +### Performance optimization + +YAACC caches metadata (duration, MIME type, file size) for faster browsing: + +* First browse: Extracts metadata from files +* Subsequent browses: Uses cached data +* Cache persists across app restarts +* Clear cache button available in server control view + +The cache significantly improves browsing performance, especially for large media collections. + +## Battery optimization + +YAACC includes several battery-saving features: + +### Smart WiFi lock management + +The WiFi lock is only held when actually needed: + +* Released when app is backgrounded (if server/renderer disabled) +* Released when no active streaming +* Automatically re-acquired when needed + + +### Smart foreground service + +The player service intelligently manages foreground state: + +* Foreground only when actively playing media +* Stops foreground when paused (notification hidden) +* Service stops completely when no players active + + +## Modern architecture + +YAACC uses modern Android APIs for better performance and system integration: + +* **Media3/ExoPlayer** - Modern media playback engine (replaces deprecated MediaPlayer) +* **MediaSession** - System-wide media controls and lock screen integration +* **Improved UPnP stack** - Enhanced reliability and network state handling +* **AudioPlaybackCapture** - System audio streaming (Android 10+) +* **MediaProjection** - Screen casting support ## Media server YAACC includes a media server service, which has to be enabled separately. A switch for this is located at the bottom of the server list tab. -![browse_servers](./screenshots/4.4.x/browse_servers.png){:height="30%" width="30%"} +![browse_servers](./screenshots/5.0.x/browse_servers.png){:height="30%" width="30%"} Depending on the configurations for the server in the settings, the server service is used as media provider, media renderer or proxy. @@ -91,7 +212,7 @@ are accessible for other UPnP/DLNA devices in you network. The shareable folders have to be configured using the server control view. All included files and folders are accessible for other devices in your network. -![server_control](./screenshots/4.4.x/server_control.png){:height="30%" width="30%"} +![server_control](./screenshots/5.0.x/server_control.png){:height="30%" width="30%"} The server control view is accessible through the YAACC entry in the server list tab or the YAACC server service notification. @@ -122,9 +243,9 @@ The shutdown timer stops all running players and the app after a certain time. The timer is settable and can be enabled at the bottom of the server list tab and the remaining time is displayed. -![browse_servers](./screenshots/4.4.x/browse_servers.png){:height="30%" width="30%"} +![browse_servers](./screenshots/5.0.x/browse_servers.png){:height="30%" width="30%"} -![shutdown_timer](./screenshots/4.4.x/shutdown_timer.png){:height="30%" width="30%"} +![shutdown_timer](./screenshots/5.0.x/shutdown_timer.png){:height="30%" width="30%"} | [Screenshots](screenshots/) | [Settings](settings/) | [About](about/) | [Code](doxygen/html/inherits.html) diff --git a/docs/screenshots/5.0.x/browse_content_folder.png b/docs/screenshots/5.0.x/browse_content_folder.png new file mode 100755 index 00000000..3b8aedbb Binary files /dev/null and b/docs/screenshots/5.0.x/browse_content_folder.png differ diff --git a/docs/screenshots/5.0.x/browse_content_folder_landscape.png b/docs/screenshots/5.0.x/browse_content_folder_landscape.png new file mode 100755 index 00000000..fd55a8b0 Binary files /dev/null and b/docs/screenshots/5.0.x/browse_content_folder_landscape.png differ diff --git a/docs/screenshots/5.0.x/browse_image_folder.png b/docs/screenshots/5.0.x/browse_image_folder.png new file mode 100755 index 00000000..6755af29 Binary files /dev/null and b/docs/screenshots/5.0.x/browse_image_folder.png differ diff --git a/docs/screenshots/5.0.x/browse_music_folder.png b/docs/screenshots/5.0.x/browse_music_folder.png new file mode 100755 index 00000000..e51c8ed5 Binary files /dev/null and b/docs/screenshots/5.0.x/browse_music_folder.png differ diff --git a/docs/screenshots/5.0.x/browse_player.png b/docs/screenshots/5.0.x/browse_player.png new file mode 100755 index 00000000..02dd31e3 Binary files /dev/null and b/docs/screenshots/5.0.x/browse_player.png differ diff --git a/docs/screenshots/5.0.x/browse_receiver.png b/docs/screenshots/5.0.x/browse_receiver.png new file mode 100755 index 00000000..b3c98347 Binary files /dev/null and b/docs/screenshots/5.0.x/browse_receiver.png differ diff --git a/docs/screenshots/5.0.x/browse_servers.png b/docs/screenshots/5.0.x/browse_servers.png new file mode 100755 index 00000000..e49c22b5 Binary files /dev/null and b/docs/screenshots/5.0.x/browse_servers.png differ diff --git a/docs/screenshots/5.0.x/image_player.png b/docs/screenshots/5.0.x/image_player.png new file mode 100755 index 00000000..969f1ab7 Binary files /dev/null and b/docs/screenshots/5.0.x/image_player.png differ diff --git a/docs/screenshots/5.0.x/image_player_show_menu.png b/docs/screenshots/5.0.x/image_player_show_menu.png new file mode 100755 index 00000000..a754237f Binary files /dev/null and b/docs/screenshots/5.0.x/image_player_show_menu.png differ diff --git a/docs/screenshots/5.0.x/music_player.png b/docs/screenshots/5.0.x/music_player.png new file mode 100755 index 00000000..a78e0dd2 Binary files /dev/null and b/docs/screenshots/5.0.x/music_player.png differ diff --git a/docs/screenshots/5.0.x/music_player_landscape.png b/docs/screenshots/5.0.x/music_player_landscape.png new file mode 100755 index 00000000..c8e5387f Binary files /dev/null and b/docs/screenshots/5.0.x/music_player_landscape.png differ diff --git a/docs/screenshots/5.0.x/playlist_during_playing_partially_editable.png b/docs/screenshots/5.0.x/playlist_during_playing_partially_editable.png new file mode 100644 index 00000000..dfe0bc0a Binary files /dev/null and b/docs/screenshots/5.0.x/playlist_during_playing_partially_editable.png differ diff --git a/docs/screenshots/5.0.x/playlist_fully_editable.png b/docs/screenshots/5.0.x/playlist_fully_editable.png new file mode 100644 index 00000000..5c95cf4e Binary files /dev/null and b/docs/screenshots/5.0.x/playlist_fully_editable.png differ diff --git a/docs/screenshots/5.0.x/remote_music_player.png b/docs/screenshots/5.0.x/remote_music_player.png new file mode 100755 index 00000000..e3b903fb Binary files /dev/null and b/docs/screenshots/5.0.x/remote_music_player.png differ diff --git a/docs/screenshots/5.0.x/server_control.png b/docs/screenshots/5.0.x/server_control.png new file mode 100755 index 00000000..0b55dac5 Binary files /dev/null and b/docs/screenshots/5.0.x/server_control.png differ diff --git a/docs/screenshots/5.0.x/server_control_land.png b/docs/screenshots/5.0.x/server_control_land.png new file mode 100755 index 00000000..ccccbed9 Binary files /dev/null and b/docs/screenshots/5.0.x/server_control_land.png differ diff --git a/docs/screenshots/5.0.x/shutdown_timer.png b/docs/screenshots/5.0.x/shutdown_timer.png new file mode 100755 index 00000000..dacbc741 Binary files /dev/null and b/docs/screenshots/5.0.x/shutdown_timer.png differ diff --git a/docs/screenshots/screenshots.md b/docs/screenshots/screenshots.md index c66a1a52..11b2938e 100644 --- a/docs/screenshots/screenshots.md +++ b/docs/screenshots/screenshots.md @@ -10,6 +10,7 @@ permalink: screenshots/ # Screenshot gallary of YAACC +- [Version 5.0.x](#5.0.x) - [Version 4.4.x](#4.4.x) - [Version 4.2.x](#4.2.x) - [Version 4.1.x](#4.1.x) @@ -17,6 +18,22 @@ permalink: screenshots/ - [Version 3.x.x](#3.x.x) - [Version 2.x.x](#2.x.x) +
+## Version 4.4.x + +![browse_servers](./5.0.x/browse_servers.png){:height="30%" width="30%"} +![browse_content_folder](./5.0.x/browse_content_folder.png){:height="30%" width="30%"} +![browse_image_folder](./5.0.x/browse_image_folder.png){:height="30%" width="30%"} +![browse_music_folder](./5.0.x/browse_music_folder.png){:height="30%" width="30%"} +![browse_receiver](./5.0.x/browse_receiver.png){:height="30%" width="30%"} +![music_player](./5.0.x/music_player.png){:height="30%" width="30%"} +![browse_player](./5.0.x/browse_player.png){:height="30%" width="30%"} +![image_player](./5.0.x/image_player.png){:height="30%" width="30%"} +![image_player_show_menu](./5.0.x/image_player_show_menu.png){:height="30%" width="30%"} +![server_control](./5.0.x/server_control.png){:height="30%" width="30%"} +![shutdown_timer](./5.0.x/shutdown_timer.png){:height="30%" width="30%"} + +
## Version 4.4.x diff --git a/fastlane/metadata/android/en-US/changelogs/50000.txt b/fastlane/metadata/android/en-US/changelogs/50000.txt new file mode 100644 index 00000000..4053e093 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/50000.txt @@ -0,0 +1,10 @@ +Version 5.0.0 +- Battery optimization: Smart WiFi lock and foreground service management +- Live streaming: Stream system audio and screen to network devices (Android 10+) +- Lock screen controls: Hardware volume buttons and media controls +- Modern architecture: Media3/ExoPlayer, improved UPnP stack reliability +- Performance: Faster browsing with metadata caching +- Receiver controls: Play/pause/stop directly from receiver tab with status display +- SAF support: Browse and share files from external storage, USB drives, SD cards +- UI improvements: Modernized browsing and image viewer +- UPnP fixes: Enhanced network state handling, retry logic for commands diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt index 9c4c204b..f8517c45 100644 --- a/fastlane/metadata/android/en-US/full_description.txt +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -7,8 +7,13 @@ YAACC (Yet Another Android Client Controller) allows you to play media from UPNP * UPNP/DLNA client - play media from a media server on your device * UPNP/DLNA media renderer - stream media from other devices to your device * UPNP/DLNA controller - control media renderer in the network +* Live streaming - stream system audio and screen to network devices (Android 10+) +* SAF support - browse and share files from external storage, USB drives, SD cards +* Lock screen controls - hardware volume buttons and media controls +* Receiver controls - play/pause/stop directly from receiver tab * Control multiple media renderer * Allow download files from a media server to the device * Allow sharing of urls with the app and sending them to the current media renderers +* Battery optimized with smart WiFi lock and foreground service management * Shutdown timer for app diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png old mode 100644 new mode 100755 index 65eb6f13..e49c22b5 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png old mode 100644 new mode 100755 index 3b2931c3..ccccbed9 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/10.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png old mode 100644 new mode 100755 index 9ed62e22..6755af29 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png old mode 100644 new mode 100755 index ab72c433..e51c8ed5 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png old mode 100644 new mode 100755 index 742bef3b..b3c98347 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png old mode 100644 new mode 100755 index e7c540ad..02dd31e3 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png old mode 100644 new mode 100755 index a6748689..a78e0dd2 Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png old mode 100644 new mode 100755 index 02c201f5..a754237f Binary files a/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png and b/fastlane/metadata/android/en-US/images/phoneScreenshots/9.png differ diff --git a/test/AndroidManifest.xml b/test/AndroidManifest.xml deleted file mode 100644 index 7225e784..00000000 --- a/test/AndroidManifest.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - diff --git a/test/ant.properties b/test/ant.properties deleted file mode 100644 index c8439949..00000000 --- a/test/ant.properties +++ /dev/null @@ -1,18 +0,0 @@ -# This file is used to override default values used by the Ant build system. -# -# This file must be checked into Version Control Systems, as it is -# integral to the build system of your project. - -# This file is only used by the Ant script. - -# You can use this to override default values such as -# 'source.dir' for the location of your java source folder and -# 'out.dir' for the location of your output folder. - -# You can also use it define how the release builds are signed by declaring -# the following properties: -# 'key.store' for the location of your keystore and -# 'key.alias' for the name of the key to use. -# The password will be asked during the build when you use the 'release' target. - -tested.project.dir=../main diff --git a/test/assets/CIMG5019.JPG b/test/assets/CIMG5019.JPG deleted file mode 100755 index ed9eed64..00000000 Binary files a/test/assets/CIMG5019.JPG and /dev/null differ diff --git a/test/assets/CIMG5019_1920x1080.jpg b/test/assets/CIMG5019_1920x1080.jpg deleted file mode 100755 index e57892bc..00000000 Binary files a/test/assets/CIMG5019_1920x1080.jpg and /dev/null differ diff --git a/test/build.xml b/test/build.xml deleted file mode 100644 index e85e428e..00000000 --- a/test/build.xml +++ /dev/null @@ -1,94 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/test/proguard-project.txt b/test/proguard-project.txt deleted file mode 100644 index f2fe1559..00000000 --- a/test/proguard-project.txt +++ /dev/null @@ -1,20 +0,0 @@ -# To enable ProGuard in your project, edit project.properties -# to define the proguard.config property as described in that file. -# -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in ${sdk.dir}/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the ProGuard -# include property in project.properties. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} diff --git a/test/src/de/yaacc/MainActivityTest.java b/test/src/de/yaacc/MainActivityTest.java deleted file mode 100644 index b5e11479..00000000 --- a/test/src/de/yaacc/MainActivityTest.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright (C) 2013 www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc; - -import android.annotation.TargetApi; -import android.test.ActivityInstrumentationTestCase2; -import de.yaacc.browser.BrowseActivity; - -/** - * This is a simple framework for a test of an Application. See - * {@link android.test.ApplicationTestCase ApplicationTestCase} for more information on - * how to write and extend Application tests. - *

- * To run this test, you can type: - * adb shell am instrument -w \ - * -e class de.yaacc.MainActivityTest \ - * de.yaacc.tests/android.test.InstrumentationTestRunner - */ -@TargetApi(3) -public class MainActivityTest extends ActivityInstrumentationTestCase2 { - - public MainActivityTest() { - super("de.yaacc", BrowseActivity.class); - } - -} diff --git a/test/src/de/yaacc/upnp/ActivityTest.java b/test/src/de/yaacc/upnp/ActivityTest.java deleted file mode 100644 index 17e8c32f..00000000 --- a/test/src/de/yaacc/upnp/ActivityTest.java +++ /dev/null @@ -1,257 +0,0 @@ -/* - * Copyright (C) 2013 www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; - -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.res.AssetManager; -import android.database.Cursor; -import android.net.Uri; -import android.provider.MediaStore; -import android.test.AndroidTestCase; -import android.util.Log; -import de.yaacc.imageviewer.ImageViewerActivity; - - -/** - * test cases for testing activities - * - * @author Tobias Schöne (openbit) - * - */ -public class ActivityTest extends AndroidTestCase { - - private static String[] imageFileNames = { "CIMG5019_1920x1080.jpg", "CIMG5019.JPG" }; - - - public void testImageViewerActivityHDImage() throws Exception{ - String filesDir = getContext().getFilesDir().toString(); - String fileName = "CIMG5019_1920x1080.jpg"; - copyAssetsToSdCard(fileName, filesDir); - Context context = getContext(); - - Intent intent = new Intent(context, ImageViewerActivity.class); - - intent.setDataAndType(Uri.parse("file:///"+filesDir+"/"+fileName), "image/jpeg"); - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - context.startActivity(intent); - myWait(); - } - - - public void testImageViewerActivityBigImageFile() throws Exception{ - String filesDir = getContext().getFilesDir().toString(); - String fileName = "CIMG5019.JPG"; - copyAssetsToSdCard(fileName, filesDir); - Context context = getContext(); - Intent intent = new Intent(context, ImageViewerActivity.class); - - intent.setDataAndType(Uri.parse("file:///"+filesDir+"/"+fileName), "image/jpeg"); - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - context.startActivity(intent); - myWait(); - } - - - - public void testImageViewerActivityPictureShow() throws Exception{ - ArrayList uris = new ArrayList(); -// String filesDir = getContext().getFilesDir().toString(); -// String fileName = "CIMG5019.JPG"; - //copyAssetsToSdCard(fileName, filesDir); - //uris.add(Uri.parse("file:///"+filesDir+"/"+fileName)); - uris.add(Uri.parse("http://kde-look.org/CONTENT/content-files/156304-DSC_0089-2-1600.jpg")); - uris.add(Uri.parse("http://kde-look.org/CONTENT/content-files/156246-DSC_0021-1600.jpg")); - uris.add(Uri.parse("http://kde-look.org/CONTENT/content-files/156225-raining-bolt-1920x1200.JPG")); - uris.add(Uri.parse("http://kde-look.org/CONTENT/content-files/156223-kungsleden1900x1200.JPG")); - uris.add(Uri.parse("http://kde-look.org/CONTENT/content-files/156218-DSC_0012-1600.jpg")); - Context context = getContext(); - Intent intent = new Intent(context, ImageViewerActivity.class); - intent.putExtra(ImageViewerActivity.URIS,uris); - intent.putExtra(ImageViewerActivity.AUTO_START_SHOW, true); - //Starting an activity form outside any other activity have to be allowed - //by this flag - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - context.startActivity(intent); - //while(true); - myWait(30000); - } - - - - public void testAddAssetsToMediaStore() throws Exception{ - addAssetsToMediaStore(); - } - - public void testMediaStoreAccess() throws Exception{ - addAssetsToMediaStore(); - - // Query for all images on external storage - String[] projection = { MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME ,MediaStore.Images.Media.DATA}; - String selection = ""; - String [] selectionArgs = null; - Cursor mImageCursor = getContext().getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - projection, selection, selectionArgs, null ); - - - if ( mImageCursor != null ) { - mImageCursor.moveToFirst(); - while(!mImageCursor.isAfterLast()){ - String id = mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)); - String name = mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns.DISPLAY_NAME)); - String data = mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)); - Log.d(getClass().getName(), "Image: " + id + " Name: " + name + " Data: " + data); - mImageCursor.moveToNext(); - } - } else { - Log.d(getClass().getName(), "System media store is empty."); - } - mImageCursor.close(); - removeAssestsFromMediaStore(); - } - - - - - public void testImageViewerActivityByUsingMediaStore() throws Exception{ - addAssetsToMediaStore(); - Context context = getContext(); - // Query for all images on external storage - String[] projection = { MediaStore.Images.Media.DATA }; - String selection = ""; - String [] selectionArgs = null; - Cursor mImageCursor = context.getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - projection, selection, selectionArgs, null ); - - if ( mImageCursor != null ) { - mImageCursor.moveToFirst(); - while(!mImageCursor.isAfterLast()){ - String data = mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)); - Log.d(getClass().getName(), "Image: " + " Data: " + data); - Intent intent = new Intent(Intent.ACTION_VIEW); - - intent = new Intent(context, ImageViewerActivity.class); - - intent.setDataAndType(Uri.parse("file:///"+data), "image/jpeg"); - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - context.startActivity(intent); - myWait(5000L); - mImageCursor.moveToNext(); - } - } else { - Log.d(getClass().getName(), "System media store is empty."); - } - mImageCursor.close(); - myWait(); - removeAssestsFromMediaStore(); - } - - - - protected void myWait() { - myWait(30000l); - } - - protected void myWait(final long millis) { - - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - } - - /* - * Copy all Test assets to the sdcard - * in order to access them from the main app. - */ - private void copyAssetsToSdCard(String fileName, String filesDir) throws Exception - { - - Context testContext = getContext().createPackageContext("de.yaacc.tests", - Context.CONTEXT_IGNORE_SECURITY); - AssetManager assets = testContext.getAssets(); - InputStream in = null; - OutputStream out = null; - File file = new File(filesDir, fileName); - try - { - - in = assets.open(fileName); - out = getContext().openFileOutput(file.getName(), Context.MODE_WORLD_READABLE); - - copyFile(in, out); - in.close(); - in = null; - out.flush(); - out.close(); - out = null; - } catch (Exception e) - { - Log.e("tag", e.getMessage()); - } - } - - private void copyFile(InputStream in, OutputStream out) throws IOException - { - byte[] buffer = new byte[1024]; - int read; - while ((read = in.read(buffer)) != -1) - { - out.write(buffer, 0, read); - } - } - - - private void addAssetsToMediaStore() throws Exception{ - String filesDir = getContext().getFilesDir().toString(); - for (String fileName : imageFileNames) { - copyAssetsToSdCard(fileName, filesDir); - ContentValues values = new ContentValues(3); - values.put(MediaStore.Video.Media.TITLE, fileName); - values.put(MediaStore.Video.Media.MIME_TYPE, "image/jpg"); - values.put(MediaStore.Video.Media.DATA, getContext().getFilesDir().getAbsolutePath() + "/" + fileName); - getContext().getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); - } - } - - /** - * - */ - private void removeAssestsFromMediaStore() { - for (String fileName : imageFileNames) { - getContext().getContentResolver().delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, MediaStore.Video.Media.TITLE + "='"+fileName+"'", null); - } - } -} diff --git a/test/src/de/yaacc/upnp/ContentDirectoryBrowser.java b/test/src/de/yaacc/upnp/ContentDirectoryBrowser.java deleted file mode 100644 index 6cf94c88..00000000 --- a/test/src/de/yaacc/upnp/ContentDirectoryBrowser.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright (C) 2013 www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp; - -import java.util.List; - -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.support.contentdirectory.callback.Browse; -import org.fourthline.cling.support.model.BrowseFlag; -import org.fourthline.cling.support.model.BrowseResult; -import org.fourthline.cling.support.model.DIDLContent; -import org.fourthline.cling.support.model.SortCriterion; -import org.fourthline.cling.support.model.container.Container; -import org.fourthline.cling.support.model.item.Item; - -/** - * Browser for ContentDirectories. - * Connect an instance of this class to a MediaServer-Service. - * After calling run you will browse the MediaServer-Directory asynchronously - * @author Tobias Schöne (openbit) - * - */ -public class ContentDirectoryBrowser extends Browse { - private Status status = Status.NO_CONTENT; - private List containers = null; - private List items = null; - - /** - * @param service - * @param objectID - * @param flag - * @param filter - * @param firstResult - * @param maxResults - * @param orderBy - */ - public ContentDirectoryBrowser(Service service, String objectID, - BrowseFlag flag, String filter, long firstResult, Long maxResults, - SortCriterion... orderBy) { - super(service, objectID, flag, filter, firstResult, maxResults, orderBy); - - } - - /** - * @param service - * @param containerId - * @param flag - */ - public ContentDirectoryBrowser(Service service, String containerId, - BrowseFlag flag) { - super(service, containerId, flag); - - } - - @Override - public void received(ActionInvocation actionInvocation, DIDLContent didl) { - containers = didl.getContainers(); - for (Container container : containers) { - System.out.println("Container " + container.getTitle() + " Id: " + container.getId() + " (" + container.getChildCount() + ")"); - } - items = didl.getItems(); - for (Item item : items) { - System.out.println( "Item: " + item.getTitle() + " Id: " + item.getId()); - } - - - } - - - - - - @Override - public boolean receivedRaw(ActionInvocation actionInvocation, - BrowseResult browseResult) { - - boolean result = super.receivedRaw(actionInvocation, browseResult); - System.out.println(browseResult.getResult()); - return result; - } - - @Override - public void updateStatus(Status status) { - System.out.println("updateStatus: " + status); - this.status = status; - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, - String defaultMsg) { - - } - - public Status getStatus() { - return status; - } - - /** - * @return the containers - */ - public List getContainers() { - return containers; - } - - /** - * @return the items - */ - public List getItems() { - return items; - } - -} \ No newline at end of file diff --git a/test/src/de/yaacc/upnp/OpenbitTestCases.java b/test/src/de/yaacc/upnp/OpenbitTestCases.java deleted file mode 100644 index fb82b95a..00000000 --- a/test/src/de/yaacc/upnp/OpenbitTestCases.java +++ /dev/null @@ -1,979 +0,0 @@ -/* - * Copyright (C) 2013 www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp; - -import java.util.Set; -import java.util.Timer; -import java.util.TimerTask; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.model.meta.Device; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.ServiceId; -import org.fourthline.cling.model.types.UDAServiceId; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.support.avtransport.callback.GetMediaInfo; -import org.fourthline.cling.support.avtransport.callback.GetPositionInfo; -import org.fourthline.cling.support.avtransport.callback.GetTransportInfo; -import org.fourthline.cling.support.avtransport.callback.Pause; -import org.fourthline.cling.support.avtransport.callback.Play; -import org.fourthline.cling.support.avtransport.callback.Seek; -import org.fourthline.cling.support.avtransport.callback.SetAVTransportURI; -import org.fourthline.cling.support.avtransport.callback.Stop; -import org.fourthline.cling.support.connectionmanager.callback.GetProtocolInfo; -import org.fourthline.cling.support.contentdirectory.DIDLParser; -import org.fourthline.cling.support.model.MediaInfo; -import org.fourthline.cling.support.model.PositionInfo; -import org.fourthline.cling.support.model.ProtocolInfos; -import org.fourthline.cling.support.model.TransportInfo; -import org.fourthline.cling.support.renderingcontrol.callback.GetMute; -import org.fourthline.cling.support.renderingcontrol.callback.GetVolume; -import org.fourthline.cling.support.renderingcontrol.callback.SetMute; -import org.fourthline.cling.support.renderingcontrol.callback.SetVolume; - -import android.util.Log; - -import de.yaacc.upnp.callback.contentdirectory.ContentDirectoryBrowseResult; - - -/** - * Special test cases only working in openbits network - * - * @author Tobias Schöne (openbit) - * - */ -public class OpenbitTestCases extends UpnpClientTest { - private static final String OPENBIT_MEDIA_SERVER = "c8236ca5-1995-4ad5-a682-edce874c81eb"; - private static final String OPENBIT_AVTRANSPORT_DEVICE = "00-30-8D-20-20-83";//"00-30-8D-20-20-8C"; - private static final String OPENBIT_AVTRANSPORT_DEVICE2 = "F00DBABE-SA5E-BABA-DADA00903EF555CB"; - private static final String OPENBIT_AVTRANSPORT_DEVICE3 = "00-30-8D-20-20-83"; - private static final String OPENBIT_TABLET = "357718866788936"; - //gz uuid:00-30-8D-20-20-83, Descriptor: http://192.168.0.98:62199/d - //wz F00DBABE-SA5E-BABA-DADA00903EF555CB, Descriptor: http://192.168.0.102:49153/nmrDescription.xml - //az 00-30-8D-20-20-8C, Descriptor: http://192.168.0.2:60826/ oder 51222 - //n uuid:65adeb42-L121-7607-70aa-01d221629, Descriptor: http://192.168.0.67:50226 - // uuid:76889b9e-6657-8799-ed4b-00308D20208C, Descriptor: http://192.168.0.2:63068/ - //tablet uuid:357718866788936 - //mt uuid:c8236ca5-1995-4ad5-a682-edce874c81eb, Descriptor: http://192.168.0.90:49153/description.xml - protected boolean actionFinished; - protected boolean watchdogFlag; - - private void waitForActionComplete() { - watchdogFlag = false; - new Timer().schedule(new TimerTask() { - - @Override - public void run() { - watchdogFlag = true; - } - }, 30000l); // 30sec. Watchdog - - while (!actionFinished && !watchdogFlag) { - // wait for local device is connected - } - assertFalse("Watchdog timeout!", watchdogFlag); - } - - - private void displaySuccess(ActionInvocation invocation) { - Log.d(getClass().getName(), "Success:" + invocation.getAction().getName()); - Set keySet = invocation.getOutputMap().keySet(); - for (Object key : keySet) { - Log.d(getClass().getName(), "Key: " + key + "Value: " + invocation.getOutputMap().get(key)); - - } - } - - public void testStreamMP3Album() throws Exception { - streamMP3Album("432498", OPENBIT_MEDIA_SERVER); - } - - public void testStreamMP3() throws Exception { - streamMp3("434406", OPENBIT_MEDIA_SERVER); - - } - - public void testStreamPictureWithMusicShow() throws Exception { - streamMusicWithPhotoShow("432498", "380077", OPENBIT_MEDIA_SERVER); - - } - - public void testStreamPhotoShow() throws Exception { - streamPhotoShow("380077", OPENBIT_MEDIA_SERVER); - - } - -// public void testUseCasePlayLocalPhotoShow() { -// UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_MEDIA_SERVER); -// Device device = upnpClient.getDevice(OPENBIT_MEDIA_SERVER); -// ContentDirectoryBrowseResult result = upnpClient.browseSync(device,"380077"); -// //MusicTrack -// assertNotNull(result); -// assertNotNull(result.getResult()); -// assertNotNull(result.getResult().getItems()); -// upnpClient.initializePlayer(result.getResult()).play(); -// -// myWait(120000); -// -// } - - private Service getAVTransportService(Device device) { - // urn:upnp-org:serviceId:urn:schemas-upnp-org:service:AVTransport - // urn:schemas-upnp-org:serviceId:AVTransport - // new ServiceId(UDAServiceId.BROKEN_DEFAULT_NAMESPACE,"AVTransport") - ServiceId serviceId = new ServiceId( - UDAServiceId.BROKEN_DEFAULT_NAMESPACE, "AVTransport"); - Service[] services = device.getServices(); - Service avservice = null; // device.findService(serviceId); - for (Service service : services) { - if (service.getServiceType().toFriendlyString() - .indexOf("AVTransport") > -1) { - Log.d(getClass().getName(), serviceId.toString()); - Log.d(getClass().getName(), service.getServiceType() - .toFriendlyString()); - avservice = service; - break; - } - } - assertNotNull(avservice); - Log.d(getClass().getName(), - "Service found: " + avservice.getServiceId() + " Type: " - + avservice.getServiceType()); - return avservice; - } - - private Service getRenderingControlService(Device device) { - // urn:upnp-org:serviceId:urn:schemas-upnp-org:service:AVTransport - // urn:schemas-upnp-org:serviceId:AVTransport - // new ServiceId(UDAServiceId.BROKEN_DEFAULT_NAMESPACE,"AVTransport") - ServiceId serviceId = new UDAServiceId("RenderingControl"); - // Service[] services = device.getServices(); - Service avservice = device.findService(serviceId); - // for (Service service : services) { - // if (service.getServiceType().toFriendlyString() - // .indexOf("AVTransport") > -1) { - // Log.d(getClass().getName(), serviceId.toString()); - // Log.d(getClass().getName(), service.getServiceType() - // .toFriendlyString()); - // avservice = service; - // break; - // } - // } - assertNotNull(avservice); - Log.d(getClass().getName(), - "Service found: " + avservice.getServiceId() + " Type: " - + avservice.getServiceType()); - return avservice; - } - - - - private Service getConnectionManagerService(Device device) { - ServiceId serviceId = new ServiceId( - UDAServiceId.BROKEN_DEFAULT_NAMESPACE, "ConnectionManager"); - Service[] services = device.getServices(); - Service avservice = null; // device.findService(serviceId); - for (Service service : services) { - if (service.getServiceType().toFriendlyString() - .indexOf("ConnectionManager") > -1) { - Log.d(getClass().getName(), serviceId.toString()); - Log.d(getClass().getName(), service.getServiceType() - .toFriendlyString()); - avservice = service; - break; - } - } - assertNotNull(avservice); - Log.d(getClass().getName(), - "Service found: " + avservice.getServiceId() + " Type: " - + avservice.getServiceType()); - return avservice; - } - - public void testAVTransportActionMediaInfo() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getAVTransportService(device); - - // MediaInfo - Log.d(getClass().getName(), "Action GetMediaInfo "); - actionFinished = false; - GetMediaInfo mediaInfoAC = new GetMediaInfo(avservice) { - @Override - public void failure(ActionInvocation arg0, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse + " String: " + s); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - } - - @Override - public void received(ActionInvocation actioninvocation, - MediaInfo mediainfo) { - Log.d(getClass().getName(), "Mediainfo: " + mediainfo); - Log.d(getClass().getName(), - "Mediainfo: " + mediainfo.getCurrentURI()); - Log.d(getClass().getName(), - "Mediainfo: " + mediainfo.getMediaDuration()); - Log.d(getClass().getName(), - "Mediainfo: " + mediainfo.getNextURI()); - Log.d(getClass().getName(), - "Mediainfo: " + mediainfo.getPlayMedium()); - Log.d(getClass().getName(), - "Mediainfo: " + mediainfo.getNumberOfTracks()); - actionFinished = true; - } - }; - upnpClient.getControlPoint().execute(mediaInfoAC); - waitForActionComplete(); - } - - public void testAVTransportActionGetPositionInfo() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getAVTransportService(device); - // GetPositionInfo - Log.d(getClass().getName(), "Action GetPositionInfo "); - actionFinished = false; - GetPositionInfo positionInfoAC = new GetPositionInfo(avservice) { - - - @Override - public void success(ActionInvocation invocation) { - displaySuccess(invocation); - super.success(invocation); - } - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - @Override - public void received(ActionInvocation actioninvocation, - PositionInfo positioninfo) { - Log.d(getClass().getName(), - "PositionInfo: " + positioninfo.getTrackDuration()); - Log.d(getClass().getName(), - "PositionInfo: " + positioninfo.getTrackMetaData()); - Log.d(getClass().getName(), - "PositionInfo: " + positioninfo.getAbsCount()); - Log.d(getClass().getName(), - "PositionInfo: " + positioninfo.getElapsedPercent()); - Log.d(getClass().getName(), - "PositionInfo: " + positioninfo.getTrackURI()); - Log.d(getClass().getName(), - "PositionInfo: " - + positioninfo.getTrackRemainingSeconds()); - actionFinished = true; - - } - }; - upnpClient.getControlPoint().execute(positionInfoAC); - waitForActionComplete(); - } - - public void testAVTransportActionPlay() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getAVTransportService(device); - // Stop - Log.d(getClass().getName(), "Action Play"); - actionFinished = false; - Play actionCallback = new Play(avservice) { - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - @Override - public void success(ActionInvocation actioninvocation) { - super.success(actioninvocation); - displaySuccess(actioninvocation); - - } - - }; - upnpClient.getControlPoint().execute(actionCallback); - myWait(20000l); - } - - - public void testAVTransportActionSeek() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getAVTransportService(device); - // - Log.d(getClass().getName(), "Action Seek"); - actionFinished = false; - Seek actionCallback = new Seek(avservice,"100") { - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - @Override - public void success(ActionInvocation actioninvocation) { - super.success(actioninvocation); - displaySuccess(actioninvocation); - - } - - }; - upnpClient.getControlPoint().execute(actionCallback); - myWait(20000l); - } - - public void testAVTransportActionPause() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getAVTransportService(device); - // - Log.d(getClass().getName(), "Action Pause"); - actionFinished = false; - Pause actionCallback = new Pause(avservice) { - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - @Override - public void success(ActionInvocation actioninvocation) { - super.success(actioninvocation); - displaySuccess(actioninvocation); - - } - - }; - upnpClient.getControlPoint().execute(actionCallback); - myWait(20000l); - } - - - public void testAVTransportActionStop() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getAVTransportService(device); - // Stop - Log.d(getClass().getName(), "Action Stop"); - actionFinished = false; - Stop stopAC = new Stop(avservice) { - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - @Override - public void success(ActionInvocation actioninvocation) { - super.success(actioninvocation); - displaySuccess(actioninvocation); - - } - - }; - upnpClient.getControlPoint().execute(stopAC); - myWait(20000l); - } - - public void testAVTransportActionNext() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getAVTransportService(device); - // Next - Log.d(getClass().getName(), "Action Next"); - actionFinished = false; - - ActionInvocation nextAI = new ActionInvocation( - avservice.getAction("Next")); - nextAI.setInput("InstanceID", new UnsignedIntegerFourBytes(0L)); - ActionCallback nextAC = new ActionCallback(nextAI) { - - public void failure(ActionInvocation actioninvocation) { - Log.d(getClass().getName(), "Failure actioninvocation: " - + actioninvocation); - } - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - @Override - public void success(ActionInvocation actioninvocation) { - displaySuccess(actioninvocation); - - } - }; - - upnpClient.getControlPoint().execute(nextAC); - myWait(20000l); - } - - public void testAVTransportActionGetTransportInfo() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getAVTransportService(device); - // GetTransportInfo - Log.d(getClass().getName(), "Action GetTransportInfo "); - actionFinished = false; - GetTransportInfo transportInfoAC = new GetTransportInfo(avservice) { - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - @Override - public void received(ActionInvocation actioninvocation, - TransportInfo transportinfo) { - Log.d(getClass().getName(), - "TransportInfo: " + transportinfo.getCurrentSpeed()); - Log.d(getClass().getName(), - "TransportInfo: " - + transportinfo.getCurrentTransportState()); - Log.d(getClass().getName(), - "TransportInfo: " - + transportinfo.getCurrentTransportStatus()); - - actionFinished = true; - - } - }; - upnpClient.getControlPoint().execute(transportInfoAC); - waitForActionComplete(); - - } - - public void testAVTransportActionSetAVTransportURI() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getAVTransportService(device); - - Log.d(getClass().getName(), "Action SetAVTransportURI "); - actionFinished = false; - SetAVTransportURI transportInfoAC = new SetAVTransportURI( - avservice, - "http://190.168.0.90/nas/Medien/Musik/A/ACDC/High%20Voltage/04%20-%20Live%20Wire.mp3") { - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getStatusMessage()); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getStatusCode()); - actionFinished = true; - - } - - @Override - public void success(ActionInvocation actioninvocation) { - super.success(actioninvocation); - displaySuccess(actioninvocation); - - } - - }; - upnpClient.getControlPoint().execute(transportInfoAC); - waitForActionComplete(); - - } - - public void testRenderingControlActionGetVolume() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE2); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE2); - Service avservice = getRenderingControlService(device); - // GetTransportInfo - Log.d(getClass().getName(), "Action GetVolume "); - actionFinished = false; - GetVolume actionCallback = new GetVolume(avservice) { - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - @Override - public void received(ActionInvocation actioninvocation, int i) { - Log.d(getClass().getName(), "Volume of Device: " + i); - actionFinished = true; - } - }; - - upnpClient.getControlPoint().execute(actionCallback); - waitForActionComplete(); - - } - - public void testRenderingControlActionSetVolume() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE2); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE2); - Service avservice = getRenderingControlService(device); - // GetTransportInfo - Log.d(getClass().getName(), "Action SetVolume "); - actionFinished = false; - SetVolume actionCallback = new SetVolume(avservice, 0) { - - @Override - public void success(ActionInvocation invocation) { - - super.success(invocation); - displaySuccess(invocation); - } - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - }; - - upnpClient.getControlPoint().execute(actionCallback); - waitForActionComplete(); - - } - - public void testRenderingControlActionGetMute() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE2); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE2); - Service avservice = getRenderingControlService(device); - // GetTransportInfo - Log.d(getClass().getName(), "Action GetMute "); - actionFinished = false; - GetMute actionCallback = new GetMute(avservice) { - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - public void received(ActionInvocation actioninvocation, boolean flag) { - Log.d(getClass().getName(), "Mute of Device: " + flag); - actionFinished = true; - } - }; - - upnpClient.getControlPoint().execute(actionCallback); - waitForActionComplete(); - - } - - public void testRenderingControlActionSetMute() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE2); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE2); - Service avservice = getRenderingControlService(device); - // GetTransportInfo - Log.d(getClass().getName(), "Action SetMute "); - actionFinished = false; - SetMute actionCallback = new SetMute(avservice, true) { - - @Override - public void success(ActionInvocation invocation) { - - super.success(invocation); - displaySuccess(invocation); - } - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - }; - - upnpClient.getControlPoint().execute(actionCallback); - waitForActionComplete(); - - } - - - public void testRenderingControlActionListPresets() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE2); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE2); - Service avservice = getRenderingControlService(device); - // - Log.d(getClass().getName(), "Action ListPresets "); - actionFinished = false; - - ActionInvocation actionInvocation = new ActionInvocation( - avservice.getAction("ListPresets")); - actionInvocation.setInput("InstanceID", new UnsignedIntegerFourBytes(0L)); - ActionCallback actionCallback = new ActionCallback(actionInvocation) { - - public void failure(ActionInvocation actioninvocation) { - Log.d(getClass().getName(), "Failure actioninvocation: " - + actioninvocation); - } - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - @Override - public void success(ActionInvocation actioninvocation) { - displaySuccess(actioninvocation); - actionFinished = true; - - } - }; - - upnpClient.getControlPoint().execute(actionCallback); - waitForActionComplete(); - - } - - public void testConnectionManagerActionGetProtocolInfoMS() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_TABLET); - Device device = upnpClient - .getDevice(OPENBIT_TABLET); - Service avservice = getConnectionManagerService(device); - Log.d(getClass().getName(), "Action GetProtocolInfo "); - actionFinished = false; - ActionCallback actionCallback = new GetProtocolInfo(avservice) { - - @Override - public void success(ActionInvocation invocation) { - - super.success(invocation); - displaySuccess(invocation); - } - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - upnpresponse != null? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), - "s: " + s); - actionFinished = true; - - } - - @Override - public void received(ActionInvocation arg0, ProtocolInfos arg1, - ProtocolInfos arg2) { - Log.d(getClass().getName(), "ProtocolInfos 1: " + arg1); - Log.d(getClass().getName(), "ProtocolInfos 2: " + arg2); - - } - - }; - - upnpClient.getControlPoint().execute(actionCallback); - waitForActionComplete(); - - } - - public void testConnectionManagerActionGetProtocolInfo() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getConnectionManagerService(device); - Log.d(getClass().getName(), "Action GetProtocolInfo "); - actionFinished = false; - ActionCallback actionCallback = new GetProtocolInfo(avservice) { - - @Override - public void success(ActionInvocation invocation) { - - super.success(invocation); - displaySuccess(invocation); - } - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - upnpresponse != null? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), - "s: " + s); - actionFinished = true; - - } - - @Override - public void received(ActionInvocation arg0, ProtocolInfos arg1, - ProtocolInfos arg2) { - Log.d(getClass().getName(), "ProtocolInfos 1: " + arg1); - Log.d(getClass().getName(), "ProtocolInfos 2: " + arg2); - - } - - }; - - upnpClient.getControlPoint().execute(actionCallback); - waitForActionComplete(); - - } - - public void testConnectionManagerActionGetCurrentConnectionIDs() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getConnectionManagerService(device); - Log.d(getClass().getName(), "Action GetCurrentConnectionIDs "); - actionFinished = false; - ActionInvocation actionInvocation = new ActionInvocation( - avservice.getAction("GetCurrentConnectionIDs")); - ActionCallback actionCallback = new ActionCallback(actionInvocation) { - - @Override - public void success(ActionInvocation invocation) { - - - displaySuccess(invocation); - } - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - - - }; - - upnpClient.getControlPoint().execute(actionCallback); - waitForActionComplete(); - - } - - - public void testConnectionManagerActionGetCurrentConnectionInfo() { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_AVTRANSPORT_DEVICE); - Device device = upnpClient - .getDevice(OPENBIT_AVTRANSPORT_DEVICE); - Service avservice = getConnectionManagerService(device); - Log.d(getClass().getName(), "Action GetCurrentConnectionInfo "); - actionFinished = false; - ActionInvocation actionInvocation = new ActionInvocation( - avservice.getAction("GetCurrentConnectionInfo")); - actionInvocation.setInput("ConnectionID", "0"); - ActionCallback actionCallback = new ActionCallback(actionInvocation) { - - @Override - public void success(ActionInvocation invocation) { - - - displaySuccess(invocation); - } - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " - + upnpresponse); - Log.d(getClass().getName(), - "UpnpResponse: " + upnpresponse.getResponseDetails()); - actionFinished = true; - - } - - - - }; - - upnpClient.getControlPoint().execute(actionCallback); - waitForActionComplete(); - - } - - public void testAnalyzeOpenbitMediaServer() throws Exception { - UpnpClient upnpClient = getInitializedUpnpClientWithYaaccUpnpServer(); - Device device = upnpClient.getDevice(OPENBIT_MEDIA_SERVER); - - ContentDirectoryBrowseResult result = upnpClient.browseSync(device,"0"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - // - result = upnpClient.browseSync(device,"1455"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - result = upnpClient.browseSync(device,"1458"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - result = upnpClient.browseSync(device,"380082"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - result = upnpClient.browseSync(device,"10"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - result = upnpClient.browseSync(device,"22"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - result = upnpClient.browseSync(device,"339532"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); -// assertNotNull(result.getResult().getItems()); -// assertNotNull(result.getResult().getItems().get(0)); -// upnpClient.playLocal(result.getResult().getItems().get(0)); -// myWait(120000L); - } - - public void testAnalyzeOpenbitTablet() throws Exception { - UpnpClient upnpClient = getInitializedUpnpClientWithDevice(OPENBIT_TABLET); - Device device = upnpClient.getDevice(OPENBIT_TABLET); - - ContentDirectoryBrowseResult result = upnpClient.browseSync(device,"0"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - // - result = upnpClient.browseSync(device,"1"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - result = upnpClient.browseSync(device,"2"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - result = upnpClient.browseSync(device,"101"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - result = upnpClient.browseSync(device,"102"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - result = upnpClient.browseSync(device,"201"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - result = upnpClient.browseSync(device,"202"); - assertNotNull(result); - assertNotNull(result.getResult()); - Log.d(getClass().getName(), "DidlContent: " + new DIDLParser().generate(result.getResult())); - - } -} diff --git a/test/src/de/yaacc/upnp/UpnpClientTest.java b/test/src/de/yaacc/upnp/UpnpClientTest.java deleted file mode 100644 index 3a21efc6..00000000 --- a/test/src/de/yaacc/upnp/UpnpClientTest.java +++ /dev/null @@ -1,915 +0,0 @@ -/* - * Copyright (C) 2013 www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp; - -import java.text.ParseException; -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; -import java.util.Timer; -import java.util.TimerTask; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionArgumentValue; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.model.meta.Action; -import org.fourthline.cling.model.meta.ActionArgument; -import org.fourthline.cling.model.meta.Device; -import org.fourthline.cling.model.meta.DeviceDetails; -import org.fourthline.cling.model.meta.DeviceIdentity; -import org.fourthline.cling.model.meta.LocalDevice; -import org.fourthline.cling.model.meta.LocalService; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.meta.StateVariable; -import org.fourthline.cling.model.meta.StateVariableTypeDetails; -import org.fourthline.cling.model.types.ServiceId; -import org.fourthline.cling.model.types.ServiceType; -import org.fourthline.cling.model.types.StringDatatype; -import org.fourthline.cling.model.types.UDADeviceType; -import org.fourthline.cling.model.types.UDAServiceId; -import org.fourthline.cling.model.types.UDN; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.support.avtransport.callback.GetCurrentTransportActions; -import org.fourthline.cling.support.avtransport.callback.GetMediaInfo; -import org.fourthline.cling.support.contentdirectory.callback.Browse.Status; -import org.fourthline.cling.support.model.BrowseFlag; -import org.fourthline.cling.support.model.MediaInfo; -import org.fourthline.cling.support.model.Res; -import org.fourthline.cling.support.model.TransportAction; -import org.fourthline.cling.support.model.container.Container; -import org.fourthline.cling.support.model.item.Item; - -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences.Editor; -import android.net.Uri; -import android.preference.PreferenceManager; -import android.test.ServiceTestCase; -import android.util.Log; -import android.webkit.MimeTypeMap; -import de.yaacc.R; -import de.yaacc.imageviewer.ImageViewerActivity; -import de.yaacc.musicplayer.BackgroundMusicService; -import de.yaacc.player.Player; -import de.yaacc.upnp.callback.contentdirectory.ContentDirectoryBrowseResult; -import de.yaacc.upnp.server.LocalUpnpServer; -import de.yaacc.upnp.server.YaaccUpnpServerService; - - -/** - * Testcase for UpnpClient-service class. - * - * @author Tobias Schöne (openbit) - * - */ -public class UpnpClientTest extends ServiceTestCase { - - private static final int MAX_DEPTH = 1; - protected Boolean flag = false; - private LocalUpnpServer localUpnpServer; - - public UpnpClientTest() { - super(UpnpRegistryService.class); - // TODO Auto-generated constructor stub - } - - /* - * (non-Javadoc) - * - * @see android.test.ServiceTestCase#setUp() - */ - @Override - protected void setUp() throws Exception { - super.setUp(); - - localUpnpServer = LocalUpnpServer.setup(getContext()); - //Start upnpserver service for avtransport - Intent svc = new Intent(getContext(), - YaaccUpnpServerService.class); - getContext().startService(svc); - } - - protected UpnpClient getInitializedUpnpClientWithLocalServer() { - return getInitializedUpnpClientWithDevice(LocalUpnpServer.UDN_ID); - } - - protected UpnpClient getInitializedUpnpClientWithYaaccUpnpServer() { - return getInitializedUpnpClientWithDevice(YaaccUpnpServerService.MEDIA_SERVER_UDN_ID); - } - - protected UpnpClient getInitializedUpnpClientWithDevice(String deviceId) { - UpnpClient upnpClient = new UpnpClient(); - upnpClient.initialize(getContext()); - flag = false; - new Timer().schedule(new TimerTask() { - - @Override - public void run() { - flag = true; - } - }, 120000l); // 120sec. Watchdog - - while (upnpClient.getDevice(deviceId) == null && !flag) { - // wait for local device is connected - } - assertFalse("Watchdog timeout No Device found!", flag); - return upnpClient; - } - - public void testUseCaseBrowseAsync() { - - UpnpClient upnpClient = new UpnpClient(); - upnpClient.initialize(getContext()); - flag = false; - new Timer().schedule(new TimerTask() { - - @Override - public void run() { - flag = true; - } - }, 30000l); // 30sec. Watchdog - - while (upnpClient.getDevice(LocalUpnpServer.UDN_ID) == null && !flag) { - // wait for local device is connected - } - - assertFalse("Watchdog timeout No Device found!", flag); - Device device = upnpClient.getDevice(LocalUpnpServer.UDN_ID); - ContentDirectoryBrowseResult result = upnpClient.browseAsync(device, - "1", BrowseFlag.DIRECT_CHILDREN, "", 0, 999l, null); - while (result.getStatus() != Status.OK - && result.getUpnpFailure() == null) { - // Do something very interesting while the asynchronous browse is - // running - try { - Thread.sleep(500); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - if (result != null && result.getResult() != null) { - for (Container container : result.getResult().getContainers()) { - Log.d(getClass().getName(), - "Container: " + container.getTitle() + " (" - + container.getChildCount() + ")"); - } - for (Item item : result.getResult().getItems()) { - Log.d(getClass().getName(), "Item: " - + item.getTitle() - + " (" - + item.getFirstResource().getProtocolInfo() - .getContentFormat() + ")"); - } - assertEquals(3, result.getResult().getItems().size()); - } - - } - - public void testScan() throws Exception { - - Context ctx = getContext(); - UpnpClient upnpClient = new UpnpClient(); - assertTrue(upnpClient.initialize(ctx)); - upnpClient.addUpnpClientListener(new UpnpClientListener() { - - @Override - public void deviceUpdated(Device device) { - Log.d(getClass().getName(), - "Device updated:" + device.getDisplayString()); - - } - - @Override - public void deviceRemoved(Device device) { - Log.d(getClass().getName(), - "Device removed:" + device.getDisplayString()); - - } - - @Override - public void deviceAdded(Device device) { - Log.d(getClass().getName(), - "Device added:" + device.getDisplayString()); - Log.d(getClass().getName(), "Identifier added:" - + device.getIdentity().getUdn().getIdentifierString()); - - } - }); - LocalDevice device = new LocalDevice(new DeviceIdentity(new UDN( - "de.yaacc.test.Dev1")), new UDADeviceType("SomeDevice", 1), - new DeviceDetails("Some Device"), new LocalService( - new ServiceType("de.yaacc.test", "Erna"), - new ServiceId("de.yaacc.test", "Erna1"), - new Action[] { new Action("action1", null) }, - new StateVariable[] { new StateVariable("state1", - new StateVariableTypeDetails( - new StringDatatype())) })); - while (!upnpClient.isInitialized()) - ; - assertNotNull(upnpClient.getRegistry()); - upnpClient.getRegistry().addDevice(device); - int size = upnpClient.getDevices().size(); - assertTrue(size > 0); - upnpClient.getRegistry().removeDevice(device); - assertEquals(size - 1, upnpClient.getDevices().size()); - - } - - public void testLookupServices() { - UpnpClient upnpClient = new UpnpClient(); - - final List> devices = searchDevices(upnpClient); - Log.d(getClass().getName(), - "DeviceCount: " + devices.size()); - for (Device device : devices) { - Log.d(getClass().getName(), - "#####Device: " + device.getDisplayString()); - Log.d(getClass().getName(), "#####Device Identifier:" - + device.getIdentity().getUdn().getIdentifierString()); - Service[] services = device.getServices(); - for (Service service : services) { - Log.d(getClass().getName(), "####Service: " + service); - Log.d(getClass().getName(), "####ServiceNamespace: " - + service.getServiceId().getNamespace()); - Action[] actions = service.getActions(); - for (Action action : actions) { - Log.d(getClass().getName(), "###Action: " + action); - ActionArgument[] inputArguments = action - .getInputArguments(); - for (ActionArgument actionArgument : inputArguments) { - Log.d(getClass().getName(), "##InputArgument: " - + actionArgument); - } - inputArguments = action.getOutputArguments(); - for (ActionArgument actionArgument : inputArguments) { - Log.d(getClass().getName(), "#OutputArgument: " - + actionArgument); - } - } - } - } - - } - - public void testRetrieveContentDirectoryContent() throws Exception { - UpnpClient upnpClient = new UpnpClient(); - final List> devices = searchDevices(upnpClient); - ContentDirectoryBrowser browse = null; - for (Device device : devices) { - Log.d(getClass().getName(), - "#####Device: " + device.getDisplayString()); - Service service = device.findService(new UDAServiceId( - "ContentDirectory")); - if (service != null) { - browse = new ContentDirectoryBrowser(service, "0", - BrowseFlag.DIRECT_CHILDREN); - upnpClient.getUpnpService().getControlPoint().execute(browse); - while (browse != null && browse.getStatus() != Status.OK) - ; - browseContainer(upnpClient, browse.getContainers(), service, 0); - } - - } - - } - - protected void browseContainer(UpnpClient upnpClient, - List containers, Service service, int depth) { - if (depth == MAX_DEPTH) - return; - if (containers == null) - return; - for (Container container : containers) { - ContentDirectoryBrowser dirBrowser = new ContentDirectoryBrowser( - service, container.getId(), BrowseFlag.DIRECT_CHILDREN); - upnpClient.getUpnpService().getControlPoint().execute(dirBrowser); - while (dirBrowser != null && dirBrowser.getStatus() != Status.OK) - ; - browseContainer(upnpClient, dirBrowser.getContainers(), service, - depth + 1); - } - } - - public void testConnectionInfos() throws Exception { - UpnpClient upnpClient = new UpnpClient(); - final List> deviceHolder = searchDevices(upnpClient); - getConnectionInfos(upnpClient, deviceHolder); - } - - @SuppressWarnings({ "rawtypes", "unchecked" }) - private void getConnectionInfos(UpnpClient upnpClient, - final List> devices) throws Exception { - for (Device device : devices) { - Service service = device.findService(new UDAServiceId( - "ConnectionManager")); - if (service != null) { - Action getCurrentConnectionIds = service - .getAction("GetCurrentConnectionIDs"); - assertNotNull(getCurrentConnectionIds); - ActionInvocation getCurrentConnectionIdsInvocation = new ActionInvocation( - getCurrentConnectionIds); - ActionCallback getCurrentConnectionCallback = new ActionCallback( - getCurrentConnectionIdsInvocation) { - - @Override - public void success(ActionInvocation invocation) { - ActionArgumentValue[] connectionIds = invocation - .getOutput(); - for (ActionArgumentValue connectionId : connectionIds) { - Log.d(getClass().getName(), connectionId.getValue().toString()); - - } - } - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(),"Failure:" + upnpresponse); - - } - }; - - upnpClient.getUpnpService().getControlPoint() - .execute(getCurrentConnectionCallback); - - } - } - myWait(); - } - - // Not implemented by MediaTomb - public void testGetMediaInfo() throws Exception { - UpnpClient upnpClient = new UpnpClient(); - final List> devices = searchDevices(upnpClient); - GetMediaInfo getMediaInfo = null; - for (Device device : devices) { - Log.d(getClass().getName(), "#####Device: " + device); - Service service = device.findService(new UDAServiceId( - "GetMediaInfo")); - if (service != null) { - Log.d(getClass().getName(), - "#####Service found: " + service.getServiceId() - + " Type: " + service.getServiceType()); - getMediaInfo = new GetMediaInfo(new UnsignedIntegerFourBytes( - "85778"), service) { - - @Override - public void received(ActionInvocation actioninvocation, - MediaInfo mediainfo) { - Log.d(getClass().getName(), "Mediainfo:" + mediainfo); - - } - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - System.err.println("Failure:" + upnpresponse); - - } - - }; - upnpClient.getUpnpService().getControlPoint() - .execute(getMediaInfo); - myWait(); - } - - } - - } - - // Not implemented by MediaTomb - public void testCurrentTransportActions() throws Exception { - UpnpClient upnpClient = new UpnpClient(); - final List> devices = searchDevices(upnpClient); - GetCurrentTransportActions getCurrentTransportActions = null; - for (Device device : devices) { - Log.d(getClass().getName(), "#####Device: " + device); - Service service = device.findService(new UDAServiceId( - "GetCurrentTransportActions")); - if (service != null) { - Log.d(getClass().getName(), - "#####Service found: " + service.getServiceId() - + " Type: " + service.getServiceType()); - getCurrentTransportActions = new GetCurrentTransportActions( - service) { - - @Override - public void failure(ActionInvocation actioninvocation, - UpnpResponse upnpresponse, String s) { - System.err.println("Failure:" + upnpresponse); - - } - - @Override - public void received(ActionInvocation actioninvocation, - TransportAction[] atransportaction) { - - Log.d(getClass().getName(), - "received TransportActions:"); - for (TransportAction action : atransportaction) { - Log.d(getClass().getName(), "TransportAction: " - + action); - } - - } - }; - - upnpClient.getUpnpService().getControlPoint() - .execute(getCurrentTransportActions); - myWait(); - } - - } - - } - - public void testStreamMP3() throws Exception { - streamMp3("101", LocalUpnpServer.UDN_ID); - - } - - protected void streamMp3(String instanceId, String upnpServerid) { - UpnpClient upnpClient = new UpnpClient(); - Device device = lookupDevice(upnpClient, upnpServerid); - ContentDirectoryBrowseResult browseResult = null; - if (device != null) { - Log.d(getClass().getName(), "#####Device: " + device); - browseResult = upnpClient.browseSync(device, instanceId); - List items = browseResult.getResult().getItems(); - for (Item item : items) { - Log.d(getClass().getName(), "ParentId: " + item.getParentID()); - Log.d(getClass().getName(), "ItemId: " + item.getId()); - Res resource = item.getFirstResource(); - if (resource == null) - break; - Log.d(getClass().getName(), - "ImportUri: " + resource.getImportUri()); - Log.d(getClass().getName(), - "Duration: " + resource.getDuration()); - Log.d(getClass().getName(), - "ProtocolInfo: " + resource.getProtocolInfo()); - Log.d(getClass().getName(), "ContentFormat: " - + resource.getProtocolInfo().getContentFormat()); - Log.d(getClass().getName(), "Value: " + resource.getValue()); - intentView(resource.getProtocolInfo().getContentFormat(), - Uri.parse(resource.getValue())); - } - - } - } - - public void testInfoInstance() throws Exception { - UpnpClient upnpClient = new UpnpClient(); - final List> devices = searchDevices(upnpClient); - ContentDirectoryBrowseResult browseResult = null; - for (Device device : devices) { - Log.d(getClass().getName(), - "#####Device: " + device.getDisplayString()); - browseResult = upnpClient.browseSync(device, "202"); - List items = new ArrayList(); - if( browseResult.getResult() != null) { - items = browseResult.getResult().getItems(); - } - - for (Item item : items) { - Log.d(getClass().getName(), "ParentId: " + item.getParentID()); - Log.d(getClass().getName(), "ItemId: " + item.getId()); - Res resource = item.getFirstResource(); - if (resource == null) - break; - Log.d(getClass().getName(), - "ImportUri: " + resource.getImportUri()); - Log.d(getClass().getName(), - "Duration: " + resource.getDuration()); - Log.d(getClass().getName(), - "ProtocolInfo: " + resource.getProtocolInfo()); - Log.d(getClass().getName(), "ContentFormat: " - + resource.getProtocolInfo().getContentFormat()); - Log.d(getClass().getName(), "Value: " + resource.getValue()); - - } - - } - - } - - public void testStreamMP3Album() throws Exception { - streamMP3Album("1", LocalUpnpServer.UDN_ID); - - } - - protected void streamMP3Album(String instanceId, String upnpServerid) { - UpnpClient upnpClient = new UpnpClient(); - Device device = lookupDevice(upnpClient, upnpServerid); - if (device != null) { - Log.d(getClass().getName(), "#####Device: " + device); - startMusicPlay(upnpClient, device, instanceId); - - } - } - - protected void startMusicPlay(UpnpClient upnpClient, - Device device, String instanceId) { - startMusicPlay(upnpClient, device, false, instanceId); - } - - protected void startMusicPlay(UpnpClient upnpClient, - Device device, boolean background, String instanceId) { - ContentDirectoryBrowseResult browseResult; - browseResult = upnpClient.browseSync(device, instanceId); - List items = browseResult.getResult().getItems(); - for (Item item : items) { - - Log.d(getClass().getName(), "ParentId: " + item.getParentID()); - Log.d(getClass().getName(), "ItemId: " + item.getId()); - Res resource = item.getFirstResource(); - if (resource == null) - break; - Log.d(getClass().getName(), "ImportUri: " + resource.getImportUri()); - Log.d(getClass().getName(), "Duration: " + resource.getDuration()); - Log.d(getClass().getName(), - "ProtocolInfo: " + resource.getProtocolInfo()); - Log.d(getClass().getName(), "ContentFormat: " - + resource.getProtocolInfo().getContentFormat()); - Log.d(getClass().getName(), "Value: " + resource.getValue()); - SimpleDateFormat dateFormat = new SimpleDateFormat("hh:mm:ss"); - - // just for a test - int millis = 0; - try { - Date date = dateFormat.parse(resource.getDuration()); - millis = date.getHours() * 60 * 60 * 1000; - millis += date.getMinutes() * 60 * 1000; - millis += date.getSeconds() * 1000; - assertEquals(date.getTime(), millis); - Log.d(getClass().getName(), - "HappyHappy Joy Joy Duration in Millis=" + millis); - Log.d(getClass().getName(), "Playing: " + item.getTitle()); - if (background) { - Log.d(getClass().getName(), - "Starting Background service... "); - Intent svc = new Intent(getContext(), - BackgroundMusicService.class); - svc.setData(Uri.parse(resource.getValue())); - getContext().startService(svc); - } else { - intentView(resource.getProtocolInfo().getContentFormat(), - Uri.parse(resource.getValue())); - } - } catch (ParseException e) { - Log.d(getClass().getName(), "bad duration format"); - ; - } - myWait(millis); - - } - } - - public void testStreamPictureWithMusicShow() throws Exception { - streamMusicWithPhotoShow("1", "2", LocalUpnpServer.UDN_ID); - - } - - protected void streamMusicWithPhotoShow(final String musicAlbumId, - String photoAlbumid, String deviceId) { - final UpnpClient upnpClient = new UpnpClient(); - final Device device = lookupDevice(upnpClient, deviceId); - if (device != null) { - Log.d(getClass().getName(), "#####Device: " + device); - new Thread(new Runnable() { - - @Override - public void run() { - startMusicPlay(upnpClient, device, true, musicAlbumId); - - } - }).start(); - - startPhotoShow(upnpClient, device, 10000l, photoAlbumid); - - } - - } - - public void testMimetypeDiscovery() { - Log.d(getClass().getName(), "jpg: " - + MimeTypeMap.getSingleton().getMimeTypeFromExtension("jpg")); - } - - public void testStreamPhotoShow() throws Exception { - streamPhotoShow("2", LocalUpnpServer.UDN_ID); - - } - - protected void streamPhotoShow(String instanceId, String upnpServerId) { - UpnpClient upnpClient = new UpnpClient(); - Device device = lookupDevice(upnpClient, upnpServerId); - if (device != null) { - Log.d(getClass().getName(), "#####Device: " + device); - startPhotoShow(upnpClient, device, 5000l, instanceId); - } - } - - protected void startPhotoShow(UpnpClient upnpClient, - Device device, long durationInMillis, String instanceId) { - ContentDirectoryBrowseResult browseResult; - browseResult = upnpClient.browseSync(device, instanceId); - List items = browseResult.getResult().getItems(); - for (Item item : items) { - - Log.d(getClass().getName(), "ParentId: " + item.getParentID()); - Log.d(getClass().getName(), "ItemId: " + item.getId()); - Res resource = item.getFirstResource(); - if (resource == null) - break; - Log.d(getClass().getName(), "ImportUri: " + resource.getImportUri()); - Log.d(getClass().getName(), - "ProtocolInfo: " + resource.getProtocolInfo()); - Log.d(getClass().getName(), "ContentFormat: " - + resource.getProtocolInfo().getContentFormat()); - Log.d(getClass().getName(), "Value: " + resource.getValue()); - Log.d(getClass().getName(), "Picture: " + item.getTitle()); - intentView(resource.getProtocolInfo().getContentFormat(), - Uri.parse(resource.getValue()), ImageViewerActivity.class); - myWait(durationInMillis); // Wait a bit between photo switch - } - } - - protected void intentView(String mime, Uri uri) { - intentView(mime, uri, null); - } - - protected void intentView(String mime, Uri uri, Class activityClazz) { - Intent intent = new Intent(Intent.ACTION_VIEW); - if (activityClazz != null) { - intent = new Intent(getContext(), activityClazz); - } - - intent.setDataAndType(uri, mime); - - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - - getContext().startActivity(intent); - // myWait(60000l); - } - - protected Device lookupDevice(UpnpClient upnpClient, - String deviceId) { - Device result = null; - List> devices = searchDevices(upnpClient); - for (Device device : devices) { - if (deviceId.equals(device.getIdentity().getUdn() - .getIdentifierString())) { - result = device; - break; - } - } - return result; - } - - protected List> searchDevices(UpnpClient upnpClient) { - Context ctx = getContext(); - - assertTrue(upnpClient.initialize(ctx)); - myWait(); - upnpClient.addUpnpClientListener(new UpnpClientListener() { - - @Override - public void deviceUpdated(Device device) { - Log.d(getClass().getName(), "Device updated:" + device); - - } - - @Override - public void deviceRemoved(Device device) { - Log.d(getClass().getName(), "Device removed:" + device); - - } - - @Override - public void deviceAdded(Device device) { - Log.d(getClass().getName(), - "Device added:" + device.getDisplayString()); - Log.d(getClass().getName(), "Identifier added:" - + device.getIdentity().getUdn().getIdentifierString()); - } - }); - while (!upnpClient.isInitialized()) - ; - upnpClient.searchDevices(); - myWait(); - - return new ArrayList(upnpClient.getDevices()); - } - - protected void myWait() { - myWait(30000l); - } - - protected void myWait(final long millis) { - - try { - Thread.sleep(millis); - } catch (InterruptedException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - } - - public void testUseCaseBrowse() { - - UpnpClient upnpClient = getInitializedUpnpClientWithLocalServer(); - - Device device = upnpClient.getDevice(LocalUpnpServer.UDN_ID); - ContentDirectoryBrowseResult result = upnpClient.browseSync(device, - "1", BrowseFlag.DIRECT_CHILDREN, "", 0, 999l, null); - if (result != null && result.getResult() != null) { - for (Container container : result.getResult().getContainers()) { - Log.d(getClass().getName(), - "Container: " + container.getTitle() + " (" - + container.getChildCount() + ")"); - } - for (Item item : result.getResult().getItems()) { - Log.d(getClass().getName(), "Item: " - + item.getTitle() - + " (" - + item.getFirstResource().getProtocolInfo() - .getContentFormat() + ")"); - } - assertEquals(3, result.getResult().getItems().size()); - } - - } - - public void testUseCasePlayLocalMusic() { - UpnpClient upnpClient = getInitializedUpnpClientWithLocalServer(); - Device device = upnpClient.getDevice(LocalUpnpServer.UDN_ID); - ContentDirectoryBrowseResult result = upnpClient.browseSync(device,"101"); - //MusicTrack - assertNotNull(result); - assertNotNull(result.getResult()); - assertNotNull(result.getResult().getItems()); - assertNotNull(result.getResult().getItems().get(0)); - List players = upnpClient.initializePlayers(result.getResult().getItems().get(0)); - - for (Player player : players) { - player.play(); - } - - } - - public void testUseCasePlayRemoteMusic() { - UpnpClient upnpClient = getInitializedUpnpClientWithYaaccUpnpServer(); - Device device = upnpClient.getDevice(LocalUpnpServer.UDN_ID); - ContentDirectoryBrowseResult result = upnpClient.browseSync(device,"101"); - //MusicTrack - assertNotNull(result); - assertNotNull(result.getResult()); - assertNotNull(result.getResult().getItems()); - assertNotNull(result.getResult().getItems().get(0)); - Editor editor = PreferenceManager.getDefaultSharedPreferences(upnpClient.getContext()).edit(); - editor.putString( - upnpClient.getContext().getString(R.string.settings_selected_receivers_title), - YaaccUpnpServerService.MEDIA_SERVER_UDN_ID); - editor.commit(); - List players = upnpClient.initializePlayers(result.getResult().getItems().get(0)); - - for (Player player : players) { - player.play(); - } - myWait(120000L); - } - - - public void testUseCasePlayLocalMusicFromYaaccUpnpServer() { - UpnpClient upnpClient = getInitializedUpnpClientWithYaaccUpnpServer(); - Device device = upnpClient.getDevice(YaaccUpnpServerService.MEDIA_SERVER_UDN_ID); - ContentDirectoryBrowseResult result = upnpClient.browseSync(device,"102"); - //MusicTrack - assertNotNull(result); - assertNotNull(result.getResult()); - assertNotNull(result.getResult().getItems()); - assertNotNull(result.getResult().getItems().get(0)); - Editor editor = PreferenceManager.getDefaultSharedPreferences(upnpClient.getContext()).edit(); - editor.putString( - upnpClient.getContext().getString(R.string.settings_selected_receivers_title), - UpnpClient.LOCAL_UID); - editor.commit(); - List players = upnpClient.initializePlayers(result.getResult().getItems().get(0)); - - for (Player player : players) { - player.play(); - } - myWait(120000L); - } - - public void testUseCasePlayLocalImage() { - UpnpClient upnpClient = getInitializedUpnpClientWithLocalServer(); - Device device = upnpClient.getDevice(LocalUpnpServer.UDN_ID); - ContentDirectoryBrowseResult result = upnpClient.browseSync(device,"202"); - //Image - assertNotNull(result); - assertNotNull(result.getResult()); - assertNotNull(result.getResult().getItems()); - assertNotNull(result.getResult().getItems().get(0)); - Editor editor = PreferenceManager.getDefaultSharedPreferences(upnpClient.getContext()).edit(); - editor.putString( - upnpClient.getContext().getString(R.string.settings_selected_receivers_title), - UpnpClient.LOCAL_UID); - editor.commit(); - List players = upnpClient.initializePlayers(result.getResult().getItems().get(0)); - - for (Player player : players) { - player.play(); - } - myWait(); - } - - public void testUseCasePlayLocalMusicAlbum() { - UpnpClient upnpClient = getInitializedUpnpClientWithLocalServer(); - Device device = upnpClient.getDevice(LocalUpnpServer.UDN_ID); - ContentDirectoryBrowseResult result = upnpClient.browseSync(device,"0"); - //MusicTrack - assertNotNull(result); - assertNotNull(result.getResult()); - assertNotNull(result.getResult().getContainers()); - assertNotNull(result.getResult().getContainers().get(0)); - Editor editor = PreferenceManager.getDefaultSharedPreferences(upnpClient.getContext()).edit(); - editor.putString( - upnpClient.getContext().getString(R.string.settings_selected_receivers_title), - UpnpClient.LOCAL_UID); - editor.commit(); - List players = upnpClient.initializePlayers(result.getResult().getItems().get(0)); - - for (Player player : players) { - player.play(); - } - - } - - public void testUseCasePlayLocalPhotoShow() { - UpnpClient upnpClient = getInitializedUpnpClientWithLocalServer(); - Device device = upnpClient.getDevice(LocalUpnpServer.UDN_ID); - ContentDirectoryBrowseResult result = upnpClient.browseSync(device,"0"); - //MusicTrack - assertNotNull(result); - assertNotNull(result.getResult()); - assertNotNull(result.getResult().getContainers()); - assertNotNull(result.getResult().getContainers().get(1)); - Editor editor = PreferenceManager.getDefaultSharedPreferences(upnpClient.getContext()).edit(); - editor.putString( - upnpClient.getContext().getString(R.string.settings_selected_receivers_title), - UpnpClient.LOCAL_UID); - editor.commit(); - List players = upnpClient.initializePlayers(result.getResult().getItems().get(0)); - - for (Player player : players) { - player.play(); - } - - } -// TODO must be implemented in another way -// public void testUseCasePlayLocalPhotoShowWithMusic() { -// UpnpClient upnpClient = getInitializedUpnpClientWithLocalServer(); -// Device device = upnpClient.getDevice(LocalUpnpServer.UDN_ID); -// ContentDirectoryBrowseResult result = upnpClient.browseSync(device,"0"); -// //MusicTrack -// assertNotNull(result); -// assertNotNull(result.getResult()); -// assertNotNull(result.getResult().getContainers()); -// assertNotNull(result.getResult().getContainers().get(0)); -// assertNotNull(result.getResult().getContainers().get(1)); -// upnpClient.playLocal(result.getResult().getContainers().get(1),result.getResult().getContainers().get(0)); -// -// } -} - -// TODO -// ArrayList imageUris = new ArrayList(); -// imageUris.add(imageUri1); // Add your image URIs here -// imageUris.add(imageUri2); -// -// Intent shareIntent = new Intent(); -// shareIntent.setAction(Intent.ACTION_SEND_MULTIPLE); -// shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, imageUris); -// shareIntent.setType("image/*"); -// startActivity(Intent.createChooser(shareIntent, "Share images to..")); \ No newline at end of file diff --git a/test/src/de/yaacc/upnp/YaaccUpnpServerServiceTest.java b/test/src/de/yaacc/upnp/YaaccUpnpServerServiceTest.java deleted file mode 100644 index 2636b271..00000000 --- a/test/src/de/yaacc/upnp/YaaccUpnpServerServiceTest.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (C) 2013 www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp; - -import java.util.Timer; -import java.util.TimerTask; - -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Action; -import org.fourthline.cling.model.meta.LocalDevice; -import org.fourthline.cling.model.meta.LocalService; -import org.fourthline.cling.model.types.ServiceId; -import org.fourthline.cling.model.types.UDAServiceId; -import org.fourthline.cling.model.types.UDN; - -import android.content.Intent; -import android.test.ServiceTestCase; -import de.yaacc.upnp.server.YaaccUpnpServerService; - -/** - * - * - * @author Tobias Schoene (openbit) - * - */ -public class YaaccUpnpServerServiceTest extends ServiceTestCase { - - boolean flag = false; - - public YaaccUpnpServerServiceTest() { - super(YaaccUpnpServerService.class); - - } - - /* - * (non-Javadoc) - * - * @see android.test.ServiceTestCase#setUp() - */ - @Override - protected void setUp() throws Exception { - super.setUp(); - Intent svc = new Intent(getContext(), - YaaccUpnpServerService.class); - startService(svc); - } - - private void waitForService() { - flag = false; - new Timer().schedule(new TimerTask() { - - @Override - public void run() { - flag = true; - } - }, 120000l); // 120sec. Watchdog - while ( (getService() == null || !getService().isInitialized()|| getService().getUpnpClient() == null || getService().getUpnpClient().getRegistry() == null) && !flag) { - // wait for local device is connected - } - assertFalse("Watchdog timeout upnpClient not initialized!", flag); - } - - public void testRendererGetProtocolInfo(){ - - waitForService(); - LocalDevice rendererDevice = getService().getUpnpClient().getRegistry().getLocalDevice(new UDN(YaaccUpnpServerService.MEDIA_RENDERER_UDN_ID),false); - LocalService connectionService = rendererDevice.findService(new ServiceId(UDAServiceId.DEFAULT_NAMESPACE,"ConnectionManager")); - Action action = connectionService.getAction("GetProtocolInfo"); - ActionInvocation actionInvocation = new ActionInvocation(action); - connectionService.getExecutor(action).execute(actionInvocation); - if(actionInvocation.getFailure() != null){ - throw new RuntimeException(actionInvocation.getFailure().fillInStackTrace()); - } - - } - - public void testServerGetProtocolInfo(){ - - waitForService(); - LocalDevice serverDevice = getService().getUpnpClient().getRegistry().getLocalDevice(new UDN(YaaccUpnpServerService.MEDIA_SERVER_UDN_ID),false); - LocalService connectionService = serverDevice.findService(new ServiceId(UDAServiceId.DEFAULT_NAMESPACE,"ConnectionManager")); - Action action = connectionService.getAction("GetProtocolInfo"); - ActionInvocation actionInvocation = new ActionInvocation(action); - connectionService.getExecutor(action).execute(actionInvocation); - if(actionInvocation.getFailure() != null){ - throw new RuntimeException(actionInvocation.getFailure().fillInStackTrace()); - } - - } - -} diff --git a/test/src/de/yaacc/upnp/model/types/SyncOffsetTest.java b/test/src/de/yaacc/upnp/model/types/SyncOffsetTest.java deleted file mode 100644 index 5d8025bd..00000000 --- a/test/src/de/yaacc/upnp/model/types/SyncOffsetTest.java +++ /dev/null @@ -1,202 +0,0 @@ -package de.yaacc.upnp.model.types; -/* -* Copyright (C) 2014 www.yaacc.de -* -* This program is free software; you can redistribute it and/or -* modify it under the terms of the GNU General Public License -* as published by the Free Software Foundation; either version 3 -* of the License, or (at your option) any later version. -* -* This program is distributed in the hope that it will be useful, -* but WITHOUT ANY WARRANTY; without even the implied warranty of -* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -* GNU General Public License for more details. -* -* You should have received a copy of the GNU General Public License -* along with this program; if not, write to the Free Software -* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. -*/ - -import android.test.AndroidTestCase; - -import junit.framework.TestCase; - -/** - * @author Tobias Schoene (TheOpenBit) - */ -public class SyncOffsetTest extends AndroidTestCase { - - public void testOffsetFormat(){ - SyncOffset syncOffset = new SyncOffset(true, 0, 0, 0, 0, 0, 0); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - assertEquals(syncOffset, new SyncOffset(syncOffset.toString())); - syncOffset = new SyncOffset(true, 10, 0, 0, 0, 0, 0); - assertEquals("P10:00:00.000 000 000", syncOffset.toString()); - assertEquals(syncOffset, new SyncOffset(syncOffset.toString())); - syncOffset = new SyncOffset(false, 10, 0, 0, 0, 0, 0); - assertEquals("-P10:00:00.000 000 000", syncOffset.toString()); - assertEquals(syncOffset, new SyncOffset(syncOffset.toString())); - syncOffset = new SyncOffset(true, 0, 21, 0, 0, 0, 0); - assertEquals("P00:21:00.000 000 000", syncOffset.toString()); - assertEquals(syncOffset, new SyncOffset(syncOffset.toString())); - syncOffset = new SyncOffset(true, 0, 0, 45, 0, 0, 0); - assertEquals("P00:00:45.000 000 000", syncOffset.toString()); - assertEquals(syncOffset, new SyncOffset(syncOffset.toString())); - syncOffset = new SyncOffset(true, 0, 0, 0, 600, 0, 0); - assertEquals("P00:00:00.600 000 000", syncOffset.toString()); - assertEquals(syncOffset, new SyncOffset(syncOffset.toString())); - syncOffset = new SyncOffset(true, 0, 0, 0, 0, 600, 0); - assertEquals("P00:00:00.000 600 000", syncOffset.toString()); - assertEquals(syncOffset, new SyncOffset(syncOffset.toString())); - syncOffset = new SyncOffset(true, 0, 0, 0, 0, 0, 800); - assertEquals("P00:00:00.000 000 800", syncOffset.toString()); - assertEquals(syncOffset, new SyncOffset(syncOffset.toString())); - } - - public void testOffsetWrongFormat(){ - //Parsingerror mus produce zero offset - try{ - new SyncOffset(true, 100, 0, 0, 0, 0, 0); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - SyncOffset syncOffset = new SyncOffset("P100:00:00.000 000 000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - try{ - new SyncOffset(true, 0, 1000, 0, 0, 0, 0); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - syncOffset = new SyncOffset("P00:1000:00.000 000 000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - try{ - new SyncOffset(false, 0, 0, 1000, 0, 0, 0); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - syncOffset = new SyncOffset("P00:00:1000.000 000 000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - - - try{ - new SyncOffset(true, 0, 0, 0, 10000, 0, 0); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - syncOffset = new SyncOffset("P00:00:00.10000 000 000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - - try{ - new SyncOffset(true, 0, 0, 0, 0, 10000, 0); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - syncOffset = new SyncOffset("P00:00:00.000 10000 000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - - try{ - new SyncOffset(true, 0, 0, 0, 0, 0, 10000); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - syncOffset = new SyncOffset("P00:00:00.000 000 10000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - - try{ - new SyncOffset(true, -100, 0, 0, 0, 0, 0); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - syncOffset = new SyncOffset("P-100:00:00.000 000 000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - try{ - new SyncOffset(true, 0, -1000, 0, 0, 0, 0); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - syncOffset = new SyncOffset("P00:-1000:00.000 000 000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - try{ - new SyncOffset(false, 0, 0, -1000, 0, 0, 0); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - syncOffset = new SyncOffset("P00:00:-1000.000 000 000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - - try{ - new SyncOffset(true, 0, 0, 0, -10000, 0, 0); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - syncOffset = new SyncOffset("P00:00:00.-10000 000 000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - - try{ - new SyncOffset(true, 0, 0, 0, 0, -10000, 0); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - syncOffset = new SyncOffset("P00:00:00.000 -10000 000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - - try{ - new SyncOffset(true, 0, 0, 0, 0, 0, -10000); - fail("No exception thrown"); - }catch(IllegalArgumentException ex){ - //expected - } - syncOffset = new SyncOffset("P00:00:00.000 000 -10000"); - assertEquals("P00:00:00.000 000 000", syncOffset.toString()); - - } - - - public void testAdjustOffset(){ - SyncOffset syncOffset = new SyncOffset("P10:00:00.000 000 000"); - syncOffset = syncOffset.add(new SyncOffset("P10:00:00.000 999 000")); - assertEquals("P20:00:00.000 999 000", syncOffset.toString()); - syncOffset = new SyncOffset("P10:00:00.000 000 000"); - syncOffset = syncOffset.add(new SyncOffset("P10:00:00.000 999 999")); - assertEquals("P20:00:00.000 999 999", syncOffset.toString()); - syncOffset = new SyncOffset("P10:00:00.000 001 001"); - syncOffset = syncOffset.add(new SyncOffset("P10:00:00.000 999 999")); - assertEquals("P20:00:00.001 001 000", syncOffset.toString()); - syncOffset = new SyncOffset("P10:00:00.999 000 001"); - syncOffset = syncOffset.add(new SyncOffset("P10:00:00.000 999 999")); - assertEquals("P20:00:01.000 000 000", syncOffset.toString()); - syncOffset = new SyncOffset("P10:00:00.999 000 001"); - syncOffset = syncOffset.add(new SyncOffset("P10:59:59.000 999 999")); - assertEquals("P21:00:00.000 000 000", syncOffset.toString()); - - syncOffset = new SyncOffset("P20:00:00.000 999 000"); - syncOffset = syncOffset.add(new SyncOffset("-P10:00:00.000 999 000")); - assertEquals("P10:00:00.000 000 000", syncOffset.toString()); - syncOffset = new SyncOffset("P20:00:00.000 999 999"); - syncOffset = syncOffset.add(new SyncOffset("-P10:00:00.000 999 999")); - assertEquals("P10:00:00.000 000 000", syncOffset.toString()); - syncOffset = new SyncOffset("P20:00:00.001 001 000"); - syncOffset = syncOffset.add(new SyncOffset("-P10:00:00.000 999 999")); - assertEquals("P10:00:00.000 001 001", syncOffset.toString()); - syncOffset = new SyncOffset("P20:00:01.000 000 000"); - syncOffset = syncOffset.add(new SyncOffset("-P10:00:00.000 999 999")); - assertEquals("P10:00:00.999 000 001", syncOffset.toString()); - syncOffset = new SyncOffset("P21:00:00.000 000 000"); - syncOffset = syncOffset.add(new SyncOffset("-P10:59:59.000 999 999")); - assertEquals("P10:00:00.999 000 001", syncOffset.toString()); - } - - - -} diff --git a/test/src/de/yaacc/upnp/server/ContentDirectory.java b/test/src/de/yaacc/upnp/server/ContentDirectory.java deleted file mode 100644 index bfc60ccd..00000000 --- a/test/src/de/yaacc/upnp/server/ContentDirectory.java +++ /dev/null @@ -1,175 +0,0 @@ -/* - * - * Copyright (C) 2013 www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp.server; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.fourthline.cling.support.contentdirectory.AbstractContentDirectoryService; -import org.fourthline.cling.support.contentdirectory.ContentDirectoryException; -import org.fourthline.cling.support.contentdirectory.DIDLParser; -import org.fourthline.cling.support.model.BrowseFlag; -import org.fourthline.cling.support.model.BrowseResult; -import org.fourthline.cling.support.model.DIDLContent; -import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.PersonWithRole; -import org.fourthline.cling.support.model.Res; -import org.fourthline.cling.support.model.SortCriterion; -import org.fourthline.cling.support.model.container.Container; -import org.fourthline.cling.support.model.container.MusicAlbum; -import org.fourthline.cling.support.model.container.PhotoAlbum; -import org.fourthline.cling.support.model.container.StorageFolder; -import org.fourthline.cling.support.model.item.Item; -import org.fourthline.cling.support.model.item.MusicTrack; -import org.fourthline.cling.support.model.item.Photo; -import org.seamless.util.MimeType; - -/** - * a simple local content directory for test purpose. - * - * @author Tobias Schöne (openbit) - * - */ -public class ContentDirectory extends AbstractContentDirectoryService { - - private Map content = new HashMap(); - - public ContentDirectory(){ - StorageFolder rootContainer = new StorageFolder("0","-1","Root","yaacc",2,907000l); - rootContainer.setSearchable(true); - rootContainer.setRestricted(false); - content.put(rootContainer.getId(),rootContainer); - List musicTracks = createMusicTracks("1"); - MusicAlbum musicAlbum = new MusicAlbum("1", rootContainer, "Music", "yaacc",musicTracks.size(),musicTracks); - musicAlbum.setSearchable(true); - musicAlbum.setRestricted(false); - rootContainer.addContainer(musicAlbum); - content.put(musicAlbum.getId(),musicAlbum); - List photos = createPhotos("2"); - PhotoAlbum photoAlbum = new PhotoAlbum("2", rootContainer, "Photos", "yaacc", photos.size(),photos); - photoAlbum.setSearchable(true); - photoAlbum.setRestricted(false); - rootContainer.addContainer(photoAlbum); - content.put(photoAlbum.getId(),photoAlbum); - - } - - private List createMusicTracks(String parentId) { - String album = ("Voice Mail"); - String creator = "Dr. Athur"; - PersonWithRole artist = new PersonWithRole(creator, "special"); - MimeType mimeType = new MimeType("audio", "mpeg"); - List result = new ArrayList(); - MusicTrack musicTrack = new MusicTrack("101", parentId, - "Bluey Shoey", creator, album, artist, new Res( - mimeType, 123456l, "00:02:33", 8192l, - "http://api.jamendo.com/get2/stream/track/redirect/?id=310355&streamencoding=mp31")); - content.put(musicTrack.getId(),musicTrack); - result.add(musicTrack); - musicTrack = new MusicTrack("102", parentId, - "8-Bit", creator, album, artist, new Res( - mimeType, 123456l, "00:02:01", 8192l, - "http://api.jamendo.com/get2/stream/track/redirect/?id=310370&streamencoding=mp31")); - content.put(musicTrack.getId(),musicTrack); - result.add(musicTrack); - musicTrack = new MusicTrack("103", parentId, - "Spooky Number 3", creator, album, artist, new Res( - mimeType, 123456l, "00:02:18", 8192l, - "http://api.jamendo.com/get2/stream/track/redirect/?id=310371&streamencoding=mp31")); - content.put(musicTrack.getId(),musicTrack); - result.add(musicTrack); - return result; - } - - private List createPhotos(String parentId) { - - - String album = ("kde-look.org"); - String creator = "http://kde-look.org/CONTENT/content-files/156304-DSC_0089-2-1600.jpg"; - MimeType mimeType = new MimeType("image", "jpeg"); - List result = new ArrayList(); - - String url = "http://kde-look.org/CONTENT/content-files/156304-DSC_0089-2-1600.jpg"; - creator = url; - Photo photo = new Photo("201",parentId,url,creator,album,new Res(mimeType,123456l,url)); - content.put(photo.getId(), photo); - result.add(photo); - url = "http://kde-look.org/CONTENT/content-files/156246-DSC_0021-1600.jpg"; - creator = url; - photo=new Photo("202",parentId,url,creator,album,new Res(mimeType,123456l,url)); - content.put(photo.getId(), photo); - result.add(photo); - url = "http://kde-look.org/CONTENT/content-files/156225-raining-bolt-1920x1200.JPG"; - creator = url; - content.put(photo.getId(), photo); - photo=new Photo("203",parentId,url,creator,album,new Res(mimeType,123456l,url)); - result.add(photo); - url = "http://kde-look.org/CONTENT/content-files/156223-kungsleden1900x1200.JPG"; - creator = url; - photo=new Photo("204",parentId,url,creator,album,new Res(mimeType,123456l,url)); - content.put(photo.getId(), photo); - result.add(photo); - url = "http://kde-look.org/CONTENT/content-files/156218-DSC_0012-1600.jpg"; - creator = url; - photo= new Photo("204",parentId,url,creator,album,new Res(mimeType,123456l,url)); - content.put(photo.getId(), photo); - result.add(photo); - return result; - } - - @Override - public BrowseResult browse(String objectID, BrowseFlag browseFlag, - String filter, long firstResult, long maxResults, - SortCriterion[] orderby) throws ContentDirectoryException { - - - int childCount=0; - DIDLObject didlObject = content.get(objectID); - DIDLContent didl = new DIDLContent(); - if(didlObject instanceof Container){ - Container container = (Container) didlObject; - childCount = container.getChildCount(); - for (Item item : container.getItems()) { - didl.addItem(item); - } - for (Container cont : container.getContainers()) { - didl.addContainer(cont); - } - } - if(didlObject instanceof Item){ - didl.addItem((Item) didlObject); - childCount = 1; - } - BrowseResult result = null; - try { - //Generate output with nested items - result = new BrowseResult(new DIDLParser().generate(didl,true), childCount, 1); - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return result; - - } - - - -} diff --git a/test/src/de/yaacc/upnp/server/LocalUpnpServer.java b/test/src/de/yaacc/upnp/server/LocalUpnpServer.java deleted file mode 100644 index e67e5b15..00000000 --- a/test/src/de/yaacc/upnp/server/LocalUpnpServer.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * - * Copyright (C) 2013 www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp.server; - -import java.util.ArrayList; -import java.util.List; - -import org.fourthline.cling.android.AndroidUpnpService; -import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder; -import org.fourthline.cling.model.DefaultServiceManager; -import org.fourthline.cling.model.ValidationException; -import org.fourthline.cling.model.meta.DeviceDetails; -import org.fourthline.cling.model.meta.DeviceIdentity; -import org.fourthline.cling.model.meta.LocalDevice; -import org.fourthline.cling.model.meta.LocalService; -import org.fourthline.cling.model.meta.ManufacturerDetails; -import org.fourthline.cling.model.types.UDADeviceType; -import org.fourthline.cling.model.types.UDN; -import org.fourthline.cling.support.contentdirectory.AbstractContentDirectoryService; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.os.IBinder; -import de.yaacc.upnp.UpnpRegistryService; -import de.yaacc.upnp.server.contentdirectory.YaaccContentDirectory; - - -/** - * A simple local mediaserver implementation. This class encapsulate the - * creation and registration of local upnp services. - * - * @author Tobias Schöne (openbit) - * - */ -public class LocalUpnpServer implements ServiceConnection{ - - public static final String UDN_ID = "YAACC-TEST-SEVER1"; - private AndroidUpnpService androidUpnpService; - private LocalDevice localDevice; - private Context context; - - public LocalUpnpServer(Context ctx) { - context = ctx; - } - - - public static LocalUpnpServer setup(Context ctx ) { - LocalUpnpServer upnpServer = new LocalUpnpServer(ctx); - ctx.bindService(new Intent(ctx, UpnpRegistryService.class), - upnpServer, Context.BIND_AUTO_CREATE); - return upnpServer; - - - - } - - - private LocalDevice createDevice() { - LocalDevice device; - try { - device = new LocalDevice( - new DeviceIdentity(new UDN(UDN_ID)), - new UDADeviceType("MediaServer"), - new DeviceDetails("YAACC-LocalMediaServer", new ManufacturerDetails("YAACC")), - createServices() - ); - - return device; - } catch (ValidationException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - return null; - } - - - private LocalService[] createServices() { - List> services = new ArrayList>(); - services.add(createContentDirectoryService()); - - return services.toArray(new LocalService[]{}); - } - - - private LocalService createContentDirectoryService() { - LocalService contentDirectoryService = new AnnotationLocalServiceBinder() - .read(YaaccContentDirectory.class); - contentDirectoryService.setManager(new DefaultServiceManager( - contentDirectoryService, null) { - - - @Override - protected YaaccContentDirectory createServiceInstance() - throws Exception { - return new YaaccContentDirectory(context); - } - }); - return contentDirectoryService; - } - - - - - //Implementation of ServiceConnectionInterface - @Override - public void onServiceConnected(ComponentName componentName, IBinder binder) { - if(binder instanceof AndroidUpnpService){ - androidUpnpService = (AndroidUpnpService)binder; - localDevice = createDevice(); - androidUpnpService.getRegistry().addDevice(localDevice); - } - - } - - @Override - public void onServiceDisconnected(ComponentName componentName) { - androidUpnpService.getRegistry().removeDevice(localDevice); - - } -} diff --git a/uitest/build.xml b/uitest/build.xml deleted file mode 100644 index b7de2bb2..00000000 --- a/uitest/build.xml +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/uitest/src/de/yaacc/UITestCase.java b/uitest/src/de/yaacc/UITestCase.java deleted file mode 100644 index 74f7f875..00000000 --- a/uitest/src/de/yaacc/UITestCase.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (C) 2013 www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc; - -import com.android.uiautomator.core.UiObject; -import com.android.uiautomator.core.UiScrollable; -import com.android.uiautomator.core.UiSelector; -import com.android.uiautomator.testrunner.UiAutomatorTestCase; - -/** - * UI Regressiontests for yaacc - * - * @author Tobias Schne openbit@schoenesnetz.de - * - */ -public class UITestCase extends UiAutomatorTestCase { - - /** - * Open Yaacc from the home screen - * @throws Exception - */ - public void testOpenYaacc() throws Exception { - - openYaacc(); - - // Validate that the package name is the expected one - UiObject settingsValidation = new UiObject( - new UiSelector().packageName("de.yaacc")); - assertTrue("Unable to detect Yaacc", settingsValidation.exists()); - closeBrowseActivity(); - - } - - - /** - * Select start local server with test content in the settings menu. - * @throws Exception - */ - public void testStartLocalServerUsingTestContent() - throws Exception { - openYaacc(); - getUiDevice().pressMenu(); // open option menu - UiScrollable settingsMenu = new UiScrollable( - new UiSelector().className("android.widget.ListView")); - UiObject settingsItem = settingsMenu.getChild(new UiSelector().className("android.widget.TextView") - .text("Settings")); - settingsItem.clickAndWaitForNewWindow(); - - UiObject validation = new UiObject(new UiSelector().text("Settings").className( - android.widget.TextView.class)); - assertTrue("Unable to open Yaacc settings ", validation.exists()); - UiScrollable settingsList = new UiScrollable(new UiSelector().className("android.widget.ListView").scrollable(true)); - assertTrue("Unbale to find list view for settings ", settingsList.exists()); - //Scroll to server settings - settingsList.getChildByText(new UiSelector().className(android.widget.TextView.class), "Local server configuration"); - int itemCount = settingsList.getChildCount(new UiSelector().className(android.widget.LinearLayout.class)); - assertTrue("itemCount > 0 (" + itemCount + ")", itemCount > 0); - for (int i = 0; i < itemCount; i++){ - UiObject item = settingsList.getChildByInstance(new UiSelector().className(android.widget.LinearLayout.class), i); - UiObject textView = item.getChild(new UiSelector().textStartsWith("local server").className( - android.widget.TextView.class)); - if(textView.exists()){ - UiObject checkBox = item.getChild(new UiSelector().className( - android.widget.CheckBox.class)); - assertTrue("CheckBox not found", checkBox.exists()); - if(!checkBox.isChecked()){ - checkBox.click(); - } - } - - } - - - - //Back to BrowseActivity - getUiDevice().pressBack(); - closeBrowseActivity(); - - } - - - private void openYaacc() throws Exception { - // Simulate a short press on the HOME button. - getUiDevice().pressHome(); - - // Were now in the home screen. Next, we want to simulate - // a user bringing up the All Apps screen. - // If you use the uiautomatorviewer tool to capture a snapshot - // of the Home screen, notice that the All Apps buttons - // content-description property has the value Apps. We can - // use this property to create a UiSelector to find the button. - UiObject allAppsButton = new UiObject( - new UiSelector().description("Apps")); - - // Simulate a click to bring up the All Apps screen. - allAppsButton.clickAndWaitForNewWindow(); - - // In the All Apps screen, the Settings app is located in - // the Apps tab. To simulate the user bringing up the Apps tab, - // we create a UiSelector to find a tab with the text - // label Apps. - UiObject appsTab = new UiObject(new UiSelector().text("Apps")); - - // Simulate a click to enter the Apps tab. - appsTab.click(); - - // Next, in the apps tabs, we can simulate a user swiping until - // they come to the Settings app icon. Since the container view - // is scrollable, we can use a UiScrollable object. - UiScrollable appViews = new UiScrollable( - new UiSelector().scrollable(true)); - - // Set the swiping mode to horizontal (the default is vertical) - appViews.setAsHorizontalList(); - - // Create a UiSelector to find the Yaacc app and simulate - // a user click to launch the app. - UiObject yaaccApp = appViews.getChildByText(new UiSelector() - .className(android.widget.TextView.class.getName()), "YAACC"); - yaaccApp.clickAndWaitForNewWindow(); - - } - - /** - * push back until the BrowsActivity closes. - */ - private void closeBrowseActivity() { - UiObject validation; - validation = new UiObject(new UiSelector().text("YAACC").className( - android.widget.TextView.class)); - // Stop Yaacc - while (validation.exists()) { - getUiDevice().pressBack(); - validation = new UiObject(new UiSelector().text("YAACC").className( - android.widget.TextView.class)); - } - } - -} diff --git a/yaacc/build.gradle b/yaacc/build.gradle index 7357cf29..edd0e730 100644 --- a/yaacc/build.gradle +++ b/yaacc/build.gradle @@ -1,7 +1,7 @@ buildscript { dependencies { - classpath 'com.android.tools.build:gradle:8.13.0' + classpath 'com.android.tools.build:gradle:8.13.2' } } @@ -11,7 +11,16 @@ apply plugin: 'com.android.application' dependencies { + configurations.all { + resolutionStrategy { + force 'org.jetbrains.kotlin:kotlin-stdlib:1.8.10' + force 'org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.10' + force 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.10' + } + } + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:4.11.0' testImplementation 'com.tngtech.junit.dataprovider:junit4-dataprovider:2.10' testImplementation 'xmlpull:xmlpull:1.1.3.1' testImplementation 'net.sf.kxml:kxml2:2.3.0' @@ -22,14 +31,27 @@ dependencies { implementation 'org.apache.httpcomponents.core5:httpcore5:5.2' implementation 'androidx.appcompat:appcompat:1.5.1' implementation 'androidx.preference:preference:1.1.0' + implementation 'androidx.media:media:1.6.0' + implementation 'androidx.media3:media3-session:1.2.0' + implementation 'androidx.media3:media3-exoplayer:1.2.0' + implementation 'androidx.media3:media3-ui:1.2.0' + implementation('androidx.mediarouter:mediarouter:1.6.0') { + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7' + exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8' + } implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.4.1' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1' -//https://developer.android.com/jetpack/androidx/migrate/artifact-mappings + implementation 'androidx.lifecycle:lifecycle-runtime:2.8.0' + implementation 'androidx.lifecycle:lifecycle-viewmodel:2.8.0' + implementation 'androidx.lifecycle:lifecycle-livedata:2.8.0' + implementation 'androidx.viewpager2:viewpager2:1.1.0' + implementation 'org.mp4parser:isoparser:1.9.56' + implementation 'com.github.chrisbanes:PhotoView:2.3.0' } android { - compileSdk 33 + compileSdk 34 sourceSets { diff --git a/yaacc/src/main/AndroidManifest.xml b/yaacc/src/main/AndroidManifest.xml index 2d48edeb..c0a75e7a 100644 --- a/yaacc/src/main/AndroidManifest.xml +++ b/yaacc/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="50000" + android:versionName="5.0.0"> @@ -31,7 +31,7 @@ - + - - + android:exported="false" + android:foregroundServiceType="mediaPlayback"> + + + + + android:enabled="true" + android:foregroundServiceType="mediaProjection" /> diff --git a/yaacc/src/main/java/de/yaacc/Yaacc.java b/yaacc/src/main/java/de/yaacc/Yaacc.java index 2675a486..243a8f6a 100644 --- a/yaacc/src/main/java/de/yaacc/Yaacc.java +++ b/yaacc/src/main/java/de/yaacc/Yaacc.java @@ -30,7 +30,6 @@ import android.os.BatteryManager; import android.os.CountDownTimer; import android.os.PowerManager; -import android.util.Log; import androidx.appcompat.app.AppCompatDelegate; import androidx.core.app.NotificationCompat; @@ -43,14 +42,14 @@ import java.util.stream.Collectors; import de.yaacc.browser.TabBrowserActivity; -import de.yaacc.musicplayer.BackgroundMusicService; import de.yaacc.player.PlayerService; import de.yaacc.upnp.UpnpClient; -import de.yaacc.upnp.UpnpRegistryService; -import de.yaacc.upnp.server.YaaccAudioRenderingControlService; import de.yaacc.upnp.server.YaaccUpnpServerService; +import de.yaacc.util.SAFCacheManager; import de.yaacc.util.NotificationId; +import de.yaacc.util.SafPermissionManager; import de.yaacc.util.ShutdownTimerListener; +import de.yaacc.util.YaaccLogger; /** * application which holds the global state @@ -70,7 +69,7 @@ public class Yaacc extends Application { @Override public void onCreate() { super.onCreate(); - upnpClient = new UpnpClient(this); + YaaccLogger.initialize(this); createNotificationChannel(); SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); boolean darkMode = preferences.getBoolean(getString(R.string.settings_dark_mode_key), true); @@ -81,13 +80,26 @@ public void onCreate() { } int numThreads = Integer.parseInt(preferences.getString(getString(R.string.settings_browse_load_threads_key), "10")); - Log.d(getClass().getName(), "Number of Threads used for content loading: " + numThreads); + YaaccLogger.d(getClass().getName(), "Number of Threads used for content loading: " + numThreads); if (numThreads <= 0) { - Log.d(getClass().getName(), "Number of Threads invalid using 10 threads instead: " + numThreads); + YaaccLogger.d(getClass().getName(), "Number of Threads invalid using 10 threads instead: " + numThreads); numThreads = 10; } contentLoadThreadPool = Executors.newFixedThreadPool(numThreads); + // Always start with streaming disabled + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + preferences.edit() + .putBoolean(getString(R.string.settings_local_server_serve_system_audio_chkbx), false) + .putBoolean(getString(R.string.settings_local_server_serve_screen_cast_chkbx), false) + .apply(); + } + + // Validate and cleanup SAF permissions on app startup + SafPermissionManager.validateAndCleanupPermissions(this); + startService(new Intent(this, YaaccUpnpServerService.class)); + upnpClient = new UpnpClient(this); + } public Executor getContentLoadExecutor() { @@ -110,7 +122,7 @@ public boolean isUnplugged() { } public void exit() { - Log.d(getClass().getName(), "Start shutdown and close"); + YaaccLogger.d(getClass().getName(), "Start shutdown and close"); upnpClient.shutdown(); //clear proxy links from preferences SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); @@ -119,10 +131,8 @@ public void exit() { SharedPreferences.Editor editor = preferences.edit(); proxyLinks.forEach(k -> editor.remove(k).commit()); stopService(new Intent(this, PlayerService.class)); - stopService(new Intent(this, BackgroundMusicService.class)); - stopService(new Intent(this, YaaccAudioRenderingControlService.class)); stopService(new Intent(this, YaaccUpnpServerService.class)); - stopService(new Intent(this, UpnpRegistryService.class)); + //FIXME work around to be fixed with new ui NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); @@ -131,9 +141,15 @@ public void exit() { mNotificationManager.cancel(NotificationId.YAACC.getId()); ActivityManager am = (ActivityManager) getApplicationContext().getSystemService(Context.ACTIVITY_SERVICE); am.getAppTasks().stream().forEach(t -> t.finishAndRemoveTask()); + clearCache(); Runtime.getRuntime().exit(0); } + private void clearCache() { + // Trim cache to recommended size using LRU + SAFCacheManager.getInstance(this).trimCache(); + } + public void createNotificationChannel() { CharSequence name = getString(R.string.channel_name); @@ -141,6 +157,7 @@ public void createNotificationChannel() { int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, name, importance); channel.setDescription(description); + channel.setSound(null, null); // Register the channel with the system; you can't change the importance @@ -179,7 +196,7 @@ public void startShutdownTimer(long duration) { shutdownTimer = new CountDownTimer(duration, 1000L) { @Override public void onTick(long millisUntilFinished) { - Log.d(getClass().getName(), "Shutdown in: " + millisUntilFinished + " millis"); + YaaccLogger.d(getClass().getName(), "Shutdown in: " + millisUntilFinished + " millis"); if (getShutdownTimerListener() != null) { getShutdownTimerListener().onTick(millisUntilFinished); } @@ -188,7 +205,7 @@ public void onTick(long millisUntilFinished) { @Override public void onFinish() { - Log.v(getClass().getName(), "Shutdown timer finished shutting down now!"); + YaaccLogger.v(getClass().getName(), "Shutdown timer finished shutting down now!"); exit(); } }; diff --git a/yaacc/src/main/java/de/yaacc/browser/BrowseContentItemAdapter.java b/yaacc/src/main/java/de/yaacc/browser/BrowseContentItemAdapter.java index 53f0cc4e..12d2b447 100644 --- a/yaacc/src/main/java/de/yaacc/browser/BrowseContentItemAdapter.java +++ b/yaacc/src/main/java/de/yaacc/browser/BrowseContentItemAdapter.java @@ -21,12 +21,12 @@ import android.content.SharedPreferences; import android.net.Uri; import android.os.AsyncTask; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; import android.widget.ImageView; +import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; @@ -38,7 +38,6 @@ import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.item.AudioItem; import org.fourthline.cling.support.model.item.ImageItem; -import org.fourthline.cling.support.model.item.Item; import org.fourthline.cling.support.model.item.PlaylistItem; import org.fourthline.cling.support.model.item.TextItem; import org.fourthline.cling.support.model.item.VideoItem; @@ -54,6 +53,7 @@ import de.yaacc.Yaacc; import de.yaacc.upnp.UpnpClient; import de.yaacc.util.ThemeHelper; +import de.yaacc.util.YaaccLogger; import de.yaacc.util.image.IconDownloadTask; /** @@ -62,9 +62,6 @@ * @author Christoph Haehnel (eyeless) */ public class BrowseContentItemAdapter extends RecyclerView.Adapter { - public static final Item LOAD_MORE_FAKE_ITEM = new Item("LoadMoreFakeItem", (String) null, "...", "", (DIDLObject.Class) null); - - private static final Item LOADING_FAKE_ITEM = new Item("LoadingFakeItem", (String) null, "Loading...", "", (DIDLObject.Class) null); private boolean loading = false; @@ -75,15 +72,22 @@ public class BrowseContentItemAdapter extends RecyclerView.Adapter(); allItemsFetched = false; this.upnpClient = upnpClient; + // Cache SharedPreferences lookup + this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context); + this.showThumbnails = sharedPreferences.getBoolean(context.getString(R.string.settings_thumbnails_chkbx), true); } @Override @@ -110,12 +114,14 @@ public Context getContext() { } public void setLoading(boolean loading) { - if (loading) { - addLoadingItem(); + this.loading = loading; + // Show/hide progress bar + if (progressBar != null) { + progressBar.setVisibility(loading ? View.VISIBLE : View.GONE); + YaaccLogger.d("BrowseContentItemAdapter", "Progress bar visibility set to: " + (loading ? "VISIBLE" : "GONE")); } else { - removeLoadingItem(); + YaaccLogger.e("BrowseContentItemAdapter", "Progress bar is null!"); } - this.loading = loading; } @Override @@ -123,21 +129,16 @@ public int getItemCount() { if (objects == null) { return 0; } - int result = objects.size(); - if (objects.contains(LOAD_MORE_FAKE_ITEM)) { - result--; - } - if (objects.contains(LOADING_FAKE_ITEM)) { - result--; - } - return result; + return objects.size(); } public void addAll(Collection newObjects) { - Log.d(getClass().getName(), "added objects; " + newObjects); - int start = objects.size() - 1; - objects.addAll(newObjects.stream().filter(it -> !objects.contains(it)).collect(Collectors.toList())); - notifyItemRangeInserted(start, objects.size()); + YaaccLogger.d(getClass().getName(), "added objects; " + newObjects); + int start = objects.size(); + List filteredObjects = newObjects.stream().filter(it -> !objects.contains(it)).collect(Collectors.toList()); + YaaccLogger.d(getClass().getName(), "Adding " + filteredObjects.size() + " new objects (filtered from " + newObjects.size() + " total)"); + objects.addAll(filteredObjects); + notifyItemRangeInserted(start, filteredObjects.size()); } public void clear() { @@ -213,23 +214,21 @@ public BrowseContentItemAdapter.ViewHolder onCreateViewHolder(ViewGroup parent, @Override public void onBindViewHolder(final BrowseContentItemAdapter.ViewHolder holder, final int listPosition) { - SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(context); - DIDLObject currentObject = (DIDLObject) getItem(listPosition); holder.name.setText(currentObject.getTitle()); + IconDownloadTask iconDownloadTask = new IconDownloadTask(holder.icon, this); asyncTasks.add(iconDownloadTask); holder.playAll.setOnClickListener((v) -> { - new ContentItemPlayTask(contentListFragment, currentObject).execute(ContentItemPlayTask.PLAY_ALL); + new ContentItemPlayTask(contentListFragment, currentObject, null).execute(ContentItemPlayTask.PLAY_ALL); }); holder.play.setOnClickListener((v) -> { - new ContentItemPlayTask(contentListFragment, currentObject).execute(ContentItemPlayTask.PLAY_CURRENT); + new ContentItemPlayTask(contentListFragment, currentObject, null).execute(ContentItemPlayTask.PLAY_CURRENT); }); holder.playlistAdd.setOnClickListener((v) -> { - new ContentItemPlayTask(contentListFragment, currentObject).execute(ContentItemPlayTask.ADD_TO_PLAYLIST); + new ContentItemPlayTask(contentListFragment, currentObject, null).execute(ContentItemPlayTask.ADD_TO_PLAYLIST); Toast toast = Toast.makeText(contentListFragment.getActivity(), R.string.add_to_playlist, Toast.LENGTH_SHORT); toast.show(); }); @@ -256,9 +255,7 @@ public void onBindViewHolder(final BrowseContentItemAdapter.ViewHolder holder, f holder.play.setVisibility(View.VISIBLE); holder.download.setVisibility(View.VISIBLE); holder.playlistAdd.setVisibility(View.VISIBLE); - if (preferences.getBoolean( - context.getString(R.string.settings_thumbnails_chkbx), - true)) { + if (showThumbnails) { DIDLObject.Property albumArtProperties = ((AudioItem) currentObject) .getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class); if (null != albumArtProperties) { @@ -273,11 +270,9 @@ public void onBindViewHolder(final BrowseContentItemAdapter.ViewHolder holder, f holder.play.setVisibility(View.VISIBLE); holder.download.setVisibility(View.VISIBLE); holder.playlistAdd.setVisibility(View.GONE); - if (preferences.getBoolean( - context.getString(R.string.settings_thumbnails_chkbx), - true)) + if (showThumbnails) iconDownloadTask.executeOnExecutor(((Yaacc) getContext().getApplicationContext()).getContentLoadExecutor(), - Uri.parse(((ImageItem) currentObject) + Uri.parse(currentObject .getFirstResource().getValue())); } else if (currentObject instanceof VideoItem) { holder.icon.setImageDrawable(ThemeHelper.tintDrawable(getContext().getResources().getDrawable(R.drawable.ic_baseline_movie_48, getContext().getTheme()), getContext().getTheme())); @@ -285,9 +280,7 @@ public void onBindViewHolder(final BrowseContentItemAdapter.ViewHolder holder, f holder.play.setVisibility(View.VISIBLE); holder.download.setVisibility(View.VISIBLE); holder.playlistAdd.setVisibility(View.VISIBLE); - if (preferences.getBoolean( - context.getString(R.string.settings_thumbnails_chkbx), - true)) { + if (showThumbnails) { DIDLObject.Property albumArtProperties = ((VideoItem) currentObject) .getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class); if (null != albumArtProperties) { @@ -308,19 +301,8 @@ public void onBindViewHolder(final BrowseContentItemAdapter.ViewHolder holder, f holder.play.setVisibility(View.GONE); holder.download.setVisibility(View.GONE); holder.playlistAdd.setVisibility(View.GONE); - } else if (currentObject == LOAD_MORE_FAKE_ITEM) { - holder.icon.setImageDrawable(ThemeHelper.tintDrawable(getContext().getResources().getDrawable(R.drawable.ic_baseline_refresh_48, getContext().getTheme()), getContext().getTheme())); - holder.playAll.setVisibility(View.GONE); - holder.play.setVisibility(View.GONE); - holder.download.setVisibility(View.GONE); - holder.playlistAdd.setVisibility(View.GONE); - } else if (currentObject == LOADING_FAKE_ITEM) { - holder.icon.setImageDrawable(ThemeHelper.tintDrawable(getContext().getResources().getDrawable(R.drawable.ic_baseline_download_48, getContext().getTheme()), getContext().getTheme())); - holder.playAll.setVisibility(View.GONE); - holder.play.setVisibility(View.GONE); - holder.download.setVisibility(View.GONE); - holder.playlistAdd.setVisibility(View.GONE); } else { + holder.icon.clearAnimation(); // Clear any previous animation holder.icon.setImageDrawable(ThemeHelper.tintDrawable(getContext().getResources().getDrawable(R.drawable.ic_baseline_question_mark_48, getContext().getTheme()), getContext().getTheme())); holder.playAll.setVisibility(View.GONE); holder.play.setVisibility(View.GONE); @@ -345,38 +327,6 @@ public void removeTask(AsyncTask task) { } } - public void addLoadMoreItem() { - if (!objects.contains(LOAD_MORE_FAKE_ITEM)) { - objects.add(LOAD_MORE_FAKE_ITEM); - notifyItemInserted(objects.size() - 1); - } - - } - - public void addLoadingItem() { - if (!objects.contains(LOADING_FAKE_ITEM)) { - objects.add(LOADING_FAKE_ITEM); - notifyItemInserted(objects.size() - 1); - } - - } - - public void removeLoadMoreItem() { - int idx = objects.indexOf(LOAD_MORE_FAKE_ITEM); - if (idx > -1) { - objects.remove(LOAD_MORE_FAKE_ITEM); - notifyItemRemoved(idx); - } - } - - public void removeLoadingItem() { - int idx = objects.indexOf(LOADING_FAKE_ITEM); - if (idx > -1) { - objects.remove(LOADING_FAKE_ITEM); - notifyItemRemoved(idx); - } - } - public DIDLObject getFolder(int position) { if (objects == null) { return null; @@ -396,7 +346,7 @@ public void loadMore(Long itemsToLoad, Integer scrollToPositionId) { setLoading(true); Long from = (long) getItemCount(); - Log.d(getClass().getName(), "loadMore from: " + from); + YaaccLogger.d(getClass().getName(), "loadMore from: " + from); BrowseItemLoadTask browseItemLoadTask = new BrowseItemLoadTask(this, itemsToLoad, scrollToPositionId); asyncTasks.add(browseItemLoadTask); diff --git a/yaacc/src/main/java/de/yaacc/browser/BrowseDeviceAdapter.java b/yaacc/src/main/java/de/yaacc/browser/BrowseDeviceAdapter.java index ce7f50e1..1f9e5b77 100644 --- a/yaacc/src/main/java/de/yaacc/browser/BrowseDeviceAdapter.java +++ b/yaacc/src/main/java/de/yaacc/browser/BrowseDeviceAdapter.java @@ -22,7 +22,6 @@ import android.content.ContextWrapper; import android.content.Intent; import android.net.Uri; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -46,9 +45,10 @@ import de.yaacc.R; import de.yaacc.upnp.UpnpClient; -import de.yaacc.upnp.server.YaaccUpnpServerControlActivity; +import de.yaacc.upnp.server.configuration.YaaccUpnpServerControlActivity; import de.yaacc.util.MediaStoreScanner; import de.yaacc.util.ThemeHelper; +import de.yaacc.util.YaaccLogger; import de.yaacc.util.image.IconDownloadTask; /** @@ -61,6 +61,47 @@ public class BrowseDeviceAdapter extends RecyclerView.Adapter> devices) { super(); @@ -75,6 +116,10 @@ public BrowseDeviceAdapter(Context ctx, RecyclerView deviceList, UpnpClient upnp notifyDataSetChanged(); } + public void setPermissionCallback(StreamPermissionCallback callback) { + this.permissionCallback = callback; + } + @Override public int getItemCount() { return devices.size(); @@ -114,24 +159,23 @@ public ViewHolder onCreateViewHolder(ViewGroup parent, } return false; }); - return new ViewHolder(view, context); + return new ViewHolder(view, context, this); } @Override public void onBindViewHolder(final ViewHolder holder, final int listPosition) { Device device = getItem(listPosition); if (device instanceof RemoteDevice) { - holder.scanButton.setVisibility(View.GONE); - holder.scanButtonLabel.setVisibility(View.GONE); holder.configButton.setVisibility(View.GONE); - holder.scanButton.setFocusable(false); + holder.streamAudioButton.setVisibility(View.GONE); + holder.streamVideoButton.setVisibility(View.GONE); if (device.hasIcons()) { Icon[] icons = device.getIcons(); for (Icon icon : icons) { if (48 == icon.getHeight() && 48 == icon.getWidth() && "image/png".equals(icon.getMimeType().toString())) { URL iconUri = ((RemoteDevice) device).normalizeURI(icon.getUri()); if (iconUri != null) { - Log.d(getClass().getName(), "Device icon uri:" + iconUri); + YaaccLogger.d(getClass().getName(), "Device icon uri:" + iconUri); new IconDownloadTask(holder.icon).execute(Uri.parse(iconUri.toString())); break; } @@ -142,14 +186,33 @@ public void onBindViewHolder(final ViewHolder holder, final int listPosition) { } } else if (device instanceof LocalDevice) { //We know our icon - holder.scanButton.setVisibility(View.VISIBLE); - holder.scanButton.setFocusable(true); - holder.scanButton.setImageDrawable(ThemeHelper.tintDrawable(context.getResources().getDrawable(R.drawable.ic_baseline_refresh_48, context.getTheme()), context.getTheme())); - holder.scanButtonLabel.setVisibility(View.VISIBLE); holder.icon.setImageResource(R.drawable.yaacc48_24_png); holder.configButton.setVisibility(View.VISIBLE); holder.configButton.setFocusable(true); holder.configButton.setImageDrawable(ThemeHelper.tintDrawable(context.getResources().getDrawable(R.drawable.ic_baseline_settings_32, context.getTheme()), context.getTheme())); + + // Show stream buttons only on Android 10+ + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + holder.streamAudioButton.setVisibility(View.VISIBLE); + holder.streamVideoButton.setVisibility(View.VISIBLE); + // Apply theme tinting like other buttons + holder.streamAudioButton.setImageDrawable(ThemeHelper.tintDrawable( + context.getResources().getDrawable(R.drawable.ic_live_audio_stream, context.getTheme()), + context.getTheme())); + holder.streamVideoButton.setImageDrawable(ThemeHelper.tintDrawable( + context.getResources().getDrawable(R.drawable.ic_live_video_stream, context.getTheme()), + context.getTheme())); + // Restore state from preferences + android.content.SharedPreferences prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(context); + isAudioStreaming = prefs.getBoolean(context.getString(R.string.settings_local_server_serve_system_audio_chkbx), false); + isVideoStreaming = prefs.getBoolean(context.getString(R.string.settings_local_server_serve_screen_cast_chkbx), false); + // Update button states + holder.updateStreamButtonState(holder.streamAudioButton, isAudioStreaming); + holder.updateStreamButtonState(holder.streamVideoButton, isVideoStreaming); + } else { + holder.streamAudioButton.setVisibility(View.GONE); + holder.streamVideoButton.setVisibility(View.GONE); + } } holder.name.setText(device.getDetails().getFriendlyName()); @@ -168,34 +231,110 @@ public void setDevices(List> devices) { static class ViewHolder extends RecyclerView.ViewHolder { ImageView icon; TextView name; - ImageButton scanButton; - TextView scanButtonLabel; ImageButton configButton; + ImageButton streamAudioButton; + ImageButton streamVideoButton; Context context; + BrowseDeviceAdapter adapter; private Timer timer; - public ViewHolder(View itemView, Context context) { + public ViewHolder(View itemView, Context context, BrowseDeviceAdapter adapter) { super(itemView); this.context = context; + this.adapter = adapter; timer = new Timer(); this.icon = itemView.findViewById(R.id.browseDeviceItemIcon); this.name = itemView.findViewById(R.id.browseDeviceItemName); - this.scanButtonLabel = itemView.findViewById(R.id.browseDeviceItemMediaStoreScanLabel); - this.scanButton = itemView.findViewById(R.id.browseDeviceItemRescan); - scanButton.setOnClickListener((v) -> { - timer.schedule(new TimerTask() { - @Override - public void run() { - new MediaStoreScanner().scanMediaFiles(getActivity(v.getContext())); - } - }, 10L); - - }); this.configButton = itemView.findViewById(R.id.browseDeviceItemConfig); configButton.setOnClickListener((v) -> { ViewHolder.this.context.startActivity(new Intent(ViewHolder.this.context, YaaccUpnpServerControlActivity.class)); }); + + this.streamAudioButton = itemView.findViewById(R.id.browseDeviceItemStreamAudio); + this.streamVideoButton = itemView.findViewById(R.id.browseDeviceItemStreamVideo); + + streamAudioButton.setOnClickListener((v) -> { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q) { + return; + } + + if (!isAudioStreaming) { + // Turning ON - check permission + if (!de.yaacc.upnp.server.media.MediaProjectionHelper.hasPermission()) { + // Request permission - mark that audio button requested it + pendingAudioRequest = true; + pendingVideoRequest = false; + if (adapter.permissionCallback != null) { + adapter.permissionCallback.requestMediaProjectionPermission(); + } + return; + } + } + + // Toggle state + isAudioStreaming = !isAudioStreaming; + updateStreamButtonState(streamAudioButton, isAudioStreaming); + + // Save to preferences (hidden from UI) + android.content.SharedPreferences prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(context); + prefs.edit().putBoolean(context.getString(R.string.settings_local_server_serve_system_audio_chkbx), isAudioStreaming).apply(); + + // If both disabled, clear permission + if (!isAudioStreaming && !isVideoStreaming) { + de.yaacc.upnp.server.media.MediaProjectionHelper.clearPermission(); + } + + // TODO: Start/stop audio capture service + YaaccLogger.i(getClass().getName(), "Audio streaming: " + isAudioStreaming); + }); + + streamVideoButton.setOnClickListener((v) -> { + if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.Q) { + return; + } + + if (!isVideoStreaming) { + // Turning ON - check permission + if (!de.yaacc.upnp.server.media.MediaProjectionHelper.hasPermission()) { + // Request permission - mark that video button requested it + pendingAudioRequest = false; + pendingVideoRequest = true; + if (adapter.permissionCallback != null) { + adapter.permissionCallback.requestMediaProjectionPermission(); + } + return; + } + } + + // Toggle state + isVideoStreaming = !isVideoStreaming; + updateStreamButtonState(streamVideoButton, isVideoStreaming); + + // Save to preferences (hidden from UI) + android.content.SharedPreferences prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(context); + prefs.edit().putBoolean(context.getString(R.string.settings_local_server_serve_screen_cast_chkbx), isVideoStreaming).apply(); + + // If both disabled, clear permission + if (!isAudioStreaming && !isVideoStreaming) { + de.yaacc.upnp.server.media.MediaProjectionHelper.clearPermission(); + } + + // TODO: Start/stop video capture service + YaaccLogger.i(getClass().getName(), "Video streaming: " + isVideoStreaming); + }); + } + + private void updateStreamButtonState(ImageButton button, boolean isActive) { + android.util.TypedValue typedValue = new android.util.TypedValue(); + if (isActive) { + // Use accent/primary color for active state + context.getTheme().resolveAttribute(com.google.android.material.R.attr.colorPrimary, typedValue, true); + button.setColorFilter(typedValue.data); + } else { + // Clear color filter to use default theme color + button.clearColorFilter(); + } } private Activity getActivity(Context ctx) { diff --git a/yaacc/src/main/java/de/yaacc/browser/BrowseItemLoadTask.java b/yaacc/src/main/java/de/yaacc/browser/BrowseItemLoadTask.java index 7cfc31c6..f97c110a 100644 --- a/yaacc/src/main/java/de/yaacc/browser/BrowseItemLoadTask.java +++ b/yaacc/src/main/java/de/yaacc/browser/BrowseItemLoadTask.java @@ -18,7 +18,7 @@ package de.yaacc.browser; import android.os.AsyncTask; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLContent; @@ -46,17 +46,16 @@ protected ContentDirectoryBrowseResult doInBackground(Long... params) { } Long from = params[0]; - Log.d(getClass().getName(), "loading from:" + from + " chunkSize: " + chunkSize); + YaaccLogger.d(getClass().getName(), "loading from:" + from + " chunkSize: " + chunkSize); return ((Yaacc) itemAdapter.getContext().getApplicationContext()).getUpnpClient().browseSync(itemAdapter.getNavigator().getCurrentPosition(), from, this.chunkSize); } @Override protected void onPostExecute(ContentDirectoryBrowseResult result) { - Log.d(getClass().getName(), "Ended AsyncTask for loading:" + result); + YaaccLogger.d(getClass().getName(), "Ended AsyncTask for loading:" + result); if (result == null) return; - itemAdapter.removeLoadMoreItem(); int previousItemCount = itemAdapter.getItemCount(); DIDLContent content = result.getResult(); if (content != null) { @@ -65,10 +64,6 @@ protected void onPostExecute(ContentDirectoryBrowseResult result) { itemAdapter.addAll(content.getItems()); boolean allItemsFetched = chunkSize != (itemAdapter.getItemCount() - previousItemCount); itemAdapter.setAllItemsFetched(allItemsFetched); - if (!allItemsFetched) { - itemAdapter.addLoadMoreItem(); - } - } else { // If result is null it may be an empty result // only in case of an UpnpFailure in the result it is really an @@ -77,7 +72,7 @@ protected void onPostExecute(ContentDirectoryBrowseResult result) { if (result.getUpnpFailure() != null) { String text = itemAdapter.getContext().getString(R.string.error_upnp_specific) + " " + result.getUpnpFailure(); - Log.e("ResolveError", text + "(" + itemAdapter.getNavigator().getCurrentPosition().getObjectId() + ")"); + YaaccLogger.e("ResolveError", text + "(" + itemAdapter.getNavigator().getCurrentPosition().getObjectId() + ")"); } else { } itemAdapter.clear(); diff --git a/yaacc/src/main/java/de/yaacc/browser/BrowseReceiverDeviceAdapter.java b/yaacc/src/main/java/de/yaacc/browser/BrowseReceiverDeviceAdapter.java index 854bd24a..c89c4575 100644 --- a/yaacc/src/main/java/de/yaacc/browser/BrowseReceiverDeviceAdapter.java +++ b/yaacc/src/main/java/de/yaacc/browser/BrowseReceiverDeviceAdapter.java @@ -17,18 +17,26 @@ */ package de.yaacc.browser; +import static de.yaacc.browser.RendererStatus.State.PAUSED; +import static de.yaacc.browser.RendererStatus.State.PLAYING; +import static de.yaacc.browser.RendererStatus.State.STOPPED; + +import android.app.PendingIntent; import android.content.Context; +import android.content.Intent; import android.net.Uri; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.CheckBox; +import android.widget.ImageButton; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.media3.common.util.UnstableApi; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; @@ -40,23 +48,40 @@ import java.net.URL; import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import de.yaacc.R; +import de.yaacc.player.AVTransportPlayer; +import de.yaacc.player.Player; import de.yaacc.upnp.UpnpClient; +import de.yaacc.upnp.callback.avtransport.Pause; +import de.yaacc.upnp.callback.avtransport.Play; +import de.yaacc.upnp.callback.avtransport.Stop; +import de.yaacc.upnp.server.http.HttpRequestSender; import de.yaacc.util.ThemeHelper; +import de.yaacc.util.YaaccLogger; import de.yaacc.util.image.IconDownloadTask; /** * @author Christoph Hähnel (eyeless) */ public class BrowseReceiverDeviceAdapter extends RecyclerView.Adapter { + private static final String ACTION_PLAY = "Play"; + private static final String ACTION_PAUSE = "Pause"; + private static final String ACTION_STOP = "Stop"; + private final List> selectedDevices; private final Context context; private List> devices; private UpnpClient upnpClient; private RecyclerView devicesListView; + private final Map statusMap = new HashMap<>(); + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); public BrowseReceiverDeviceAdapter(Context ctx, UpnpClient upnpClient, RecyclerView devicesListView, Collection> devices, Collection> selectedDevices) { @@ -66,6 +91,22 @@ public BrowseReceiverDeviceAdapter(Context ctx, UpnpClient upnpClient, RecyclerV context = ctx; this.upnpClient = upnpClient; this.devicesListView = devicesListView; + sortDevices(); + } + + private void sortDevices() { + devices.sort((d1, d2) -> { + // 1. Local device first + boolean d1Local = d1.getIdentity().getUdn().getIdentifierString().equals(UpnpClient.LOCAL_UID); + boolean d2Local = d2.getIdentity().getUdn().getIdentifierString().equals(UpnpClient.LOCAL_UID); + + if (d1Local != d2Local) { + return d1Local ? -1 : 1; + } + + // 2. Then by name + return d1.getDetails().getFriendlyName().compareTo(d2.getDetails().getFriendlyName()); + }); } @Override @@ -88,6 +129,16 @@ public BrowseReceiverDeviceAdapter.ViewHolder onCreateViewHolder(ViewGroup paren int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.browse_receiver_device_item, parent, false); + + // Add click listener to open player activity + view.setOnClickListener(v -> { + int position = devicesListView.getChildAdapterPosition(v); + if (position != RecyclerView.NO_POSITION) { + Device device = getItem(position); + openPlayerActivity(v.getContext(), device); + } + }); + view.setOnKeyListener((v, keyCode, event) -> { if (event.getAction() != android.view.KeyEvent.ACTION_DOWN) return false; @@ -116,45 +167,166 @@ public BrowseReceiverDeviceAdapter.ViewHolder onCreateViewHolder(ViewGroup paren @Override public void onBindViewHolder(final BrowseReceiverDeviceAdapter.ViewHolder holder, final int listPosition) { Device device = getItem(listPosition); + + // Tag the icon with device identity to prevent wrong icon loading + holder.icon.setTag(device.getIdentity().getUdn().getIdentifierString()); + + // Check if there's an active player for this device + Player player = getPlayerForDevice(device); + + // Load album art as background if available + String deviceId = device.getIdentity().getUdn().getIdentifierString(); + if (player != null && player.getAlbumArt() != null) { + holder.albumArt.setTag(deviceId); // Tag to prevent wrong image on reorder + new IconDownloadTask(holder.albumArt, 512, 512).execute(Uri.parse(player.getAlbumArt().toString())); + } else { + // Clear album art for devices without active playback + holder.albumArt.setTag(null); + holder.albumArt.setImageDrawable(null); + holder.albumArt.setVisibility(View.GONE); + } + + // Show device icon if (device instanceof RemoteDevice && device.hasIcons()) { - if (device.hasIcons()) { - Icon[] icons = device.getIcons(); - for (Icon icon : icons) { - if (48 == icon.getHeight() && 48 == icon.getWidth() && "image/png".equals(icon.getMimeType().toString())) { - URL iconUri = ((RemoteDevice) device).normalizeURI(icon.getUri()); - if (iconUri != null) { - Log.d(getClass().getName(), "Device icon uri:" + iconUri); - new IconDownloadTask(holder.icon).execute(Uri.parse(iconUri.toString())); - break; - } + Icon[] icons = device.getIcons(); + for (Icon icon : icons) { + if (48 == icon.getHeight() && 48 == icon.getWidth() && "image/png".equals(icon.getMimeType().toString())) { + URL iconUri = ((RemoteDevice) device).normalizeURI(icon.getUri()); + if (iconUri != null) { + YaaccLogger.d(getClass().getName(), "Device icon uri:" + iconUri); + new IconDownloadTask(holder.icon, device.getIdentity().getUdn().getIdentifierString()).execute(Uri.parse(iconUri.toString())); + break; } } - } else { - holder.icon.setImageDrawable(ThemeHelper.tintDrawable(context.getResources().getDrawable(R.drawable.ic_baseline_devices_48, context.getTheme()), context.getTheme())); } } else if (device instanceof LocalDevice || device instanceof UpnpClient.LocalDummyDevice) { - //We know our icon holder.icon.setImageResource(R.drawable.yaacc48_24_png); + } else { + holder.icon.setImageDrawable(ThemeHelper.tintDrawable(context.getResources().getDrawable(R.drawable.ic_baseline_devices_48, context.getTheme()), context.getTheme())); } holder.name.setText(device.getDetails().getFriendlyName()); holder.checkBox.setOnClickListener((it) -> { if (!((CheckBox) it).isChecked()) { - Log.d(getClass().getName(), "isNotChecked:" + device.getDisplayString()); + YaaccLogger.d(getClass().getName(), "isNotChecked:" + device.getDisplayString()); removeSelectedDevice(device); upnpClient.removeReceiverDevice(device); } else { - Log.d(getClass().getName(), "isChecked:" + device.getDisplayString()); + YaaccLogger.d(getClass().getName(), "isChecked:" + device.getDisplayString()); addSelectedDevice(device); upnpClient.addReceiverDevice(device); } + // Re-sort after selection change + sortDevices(); + notifyDataSetChanged(); }); holder.checkBox.setChecked(selectedDevices.contains(device)); new DeviceVolumeStateLoadTask(holder.volume, upnpClient).execute(device); new DeviceMuteStateLoadTask(holder.mute, upnpClient).execute(device); + // Wire up playback controls + wireUpControls(holder, device); + + // Set device name (always shown) + holder.name.setText(device.getDetails().getFriendlyName()); + + // Update status display + RendererStatus status = statusMap.get(device); + if (status != null) { + // Highlight if playing + holder.itemContainer.setSelected(status.isPlaying()); + holder.statusBadge.setVisibility(View.VISIBLE); + holder.statusBadge.setText(getStatusBadge(status.getState())); + + // Show status text with track info + String statusText = getStatusText(status); + if (statusText != null && !statusText.isEmpty()) { + holder.statusText.setVisibility(View.VISIBLE); + holder.statusText.setText(statusText); + } else { + holder.statusText.setVisibility(View.GONE); + } + + // Update volume only if changed + int newVolume = status.getVolume(); + if (holder.volume.getProgress() != newVolume) { + holder.volume.setProgress(newVolume); + holder.volumeText.setText(newVolume + "%"); + } + } else { + // No status from monitor - check if local device has active player + holder.statusText.setVisibility(View.GONE); + + // For local device, create status from player + if (device.getIdentity().getUdn().getIdentifierString().equals(UpnpClient.LOCAL_UID) && player != null) { + RendererStatus.State state = player.isPlaying() ? RendererStatus.State.PLAYING : + player.isPaused() ? RendererStatus.State.PAUSED : + RendererStatus.State.STOPPED; + String trackTitle = player.getCurrentItemTitle(); + RendererStatus localStatus = new RendererStatus(device, state.name(), trackTitle, 50); // Default volume + + // Add to status map so sorting works + statusMap.put(device, localStatus); + + // Update UI + holder.itemContainer.setSelected(localStatus.isPlaying()); + if (localStatus.isPlaying()) { + holder.statusBadge.setVisibility(View.VISIBLE); + holder.statusBadge.setText(getStatusBadge(RendererStatus.State.PLAYING)); + } else { + holder.statusBadge.setVisibility(View.GONE); + } + + // Show track info + String statusText = getStatusText(localStatus); + if (statusText != null && !statusText.isEmpty()) { + holder.statusText.setVisibility(View.VISIBLE); + holder.statusText.setText(statusText); + } + } else if (player != null) { + holder.itemContainer.setSelected(true); + holder.statusBadge.setVisibility(View.VISIBLE); + if (player.isPlaying()) { + holder.statusBadge.setText(getStatusBadge(PLAYING)); + } else if (player.isPaused()) { + holder.statusBadge.setText(getStatusBadge(PAUSED)); + } else { + holder.statusBadge.setText(getStatusBadge(STOPPED)); + } + } else { + holder.itemContainer.setSelected(false); + holder.statusBadge.setVisibility(View.GONE); + } + } + } + + private String getStatusBadge(RendererStatus.State state) { + String result = ""; + switch (state) { + case PLAYING: + result = context.getString(R.string.playing); + break; + case PAUSED: + result = context.getString(R.string.paused); + break; + case STOPPED: + result = context.getString(R.string.stopped); + break; + case NO_MEDIA: + result = context.getString(R.string.no_media); + break; + } + return result; } + private String getStatusText(RendererStatus status) { + String text = ""; + if (status.getTrackTitle() != null) { + text = status.getTrackTitle(); + } + return text; + } + public void setDevices(List> devices) { final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DeviceDiffCallback(this.devices, devices)); this.devices.clear(); @@ -184,24 +356,186 @@ public void addSelectedDevice(Device device) { public void removeSelectedDevice(Device device) { this.selectedDevices.remove(device); + } + + public void updateStatus(RendererStatus status) { + RendererStatus oldStatus = statusMap.get(status.getDevice()); + boolean wasPlaying = oldStatus != null && oldStatus.isPlaying(); + statusMap.put(status.getDevice(), status); + + // Re-sort if playing state changed (affects sort order) + if (wasPlaying != status.isPlaying()) { + sortAndNotify(); + } else { + // Just update the item + for (int i = 0; i < devices.size(); i++) { + if (devices.get(i).equals(status.getDevice())) { + notifyItemChanged(i); + break; + } + } + } + } + + public void sortAndNotify() { + sortDevices(); + notifyDataSetChanged(); + } + + public void updateLocalDeviceStatus() { + // Find local device and update its status + for (Device device : devices) { + if (device.getIdentity().getUdn().getIdentifierString().equals(UpnpClient.LOCAL_UID)) { + Player player = getPlayerForDevice(device); + if (player != null) { + // Use UPnP state strings that will be parsed to State enum + String state = player.isPlaying() ? "PLAYING" : + player.isPaused() ? "PAUSED_PLAYBACK" : + "STOPPED"; + String trackTitle = player.getCurrentItemTitle(); + RendererStatus localStatus = new RendererStatus(device, state, trackTitle, 50); + updateStatus(localStatus); + } else { + statusMap.remove(device); + notifyDataSetChanged(); + } + break; + } + } + } + + @OptIn(markerClass = UnstableApi.class) + private void openPlayerActivity(Context context, Device device) { + // Find active player for this device + for (Player player : upnpClient.getCurrentPlayers()) { + if (player instanceof AVTransportPlayer) { + AVTransportPlayer avPlayer = (AVTransportPlayer) player; + if (device.getIdentity().getUdn().getIdentifierString().equals(avPlayer.getDeviceId())) { + // Found player - open its activity + if (player.getNotificationIntent() != null) { + try { + player.getNotificationIntent().send(context, 0, new Intent()); + } catch (PendingIntent.CanceledException e) { + YaaccLogger.e(getClass().getName(), "Failed to open player activity", e); + } + } + return; // Take first match + } + } + } + // No active player found - do nothing + } + + private Player getPlayerForDevice(Device device) { + String deviceId = device.getIdentity().getUdn().getIdentifierString(); + + for (Player player : upnpClient.getCurrentPlayers()) { + if (player instanceof AVTransportPlayer) { + AVTransportPlayer avPlayer = (AVTransportPlayer) player; + if (deviceId.equals(avPlayer.getDeviceId())) { + return player; + } + } else if (deviceId.equals(UpnpClient.LOCAL_UID)) { + // Local device - return any local player (LocalMediaSessionPlayer, LocalImagePlayer, etc.) + if (!(player instanceof AVTransportPlayer)) { + return player; + } + } + } + return null; + } + private void wireUpControls(ViewHolder holder, Device device) { + holder.btnPlay.setOnClickListener(v -> executeAction(device, ACTION_PLAY)); + holder.btnPause.setOnClickListener(v -> executeAction(device, ACTION_PAUSE)); + holder.btnStop.setOnClickListener(v -> executeAction(device, ACTION_STOP)); + } + + private void executeAction(Device device, String action) { + // Handle local device differently + if (device instanceof LocalDevice || device instanceof UpnpClient.LocalDummyDevice) { + Player player = getPlayerForDevice(device); + if (player == null) return; + + switch (action) { + case ACTION_PLAY: + player.play(); + break; + case ACTION_PAUSE: + player.pause(); + break; + case ACTION_STOP: + player.stop(); + break; + } + return; + } + + // Handle remote UPnP device + org.fourthline.cling.model.meta.Service avTransport = device.findService(new org.fourthline.cling.model.types.UDAServiceId("AVTransport")); + if (avTransport == null) return; + + HttpRequestSender httpRequestSender = new HttpRequestSender(); + + switch (action) { + case ACTION_PLAY: + executorService.execute(new Play(avTransport, httpRequestSender) { + @Override + public void failure(org.fourthline.cling.model.action.ActionInvocation invocation, org.fourthline.cling.model.message.UpnpResponse response, String msg) { + YaaccLogger.e(getClass().getName(), "Play failed: " + msg); + } + }); + break; + case ACTION_PAUSE: + executorService.execute(new Pause(avTransport, httpRequestSender) { + @Override + public void failure(org.fourthline.cling.model.action.ActionInvocation invocation, org.fourthline.cling.model.message.UpnpResponse response, String msg) { + YaaccLogger.e(getClass().getName(), "Pause failed: " + msg); + } + }); + break; + case ACTION_STOP: + executorService.execute(new Stop(avTransport, httpRequestSender) { + @Override + public void failure(org.fourthline.cling.model.action.ActionInvocation invocation, org.fourthline.cling.model.message.UpnpResponse response, String msg) { + YaaccLogger.e(getClass().getName(), "Stop failed: " + msg); + } + }); + break; + } } static class ViewHolder extends RecyclerView.ViewHolder { ImageView icon; + ImageView albumArt; TextView name; + TextView statusBadge; + TextView statusText; + TextView volumeText; CheckBox checkBox; CheckBox mute; SeekBar volume; + ImageButton btnPlay; + ImageButton btnPause; + ImageButton btnStop; + View itemContainer; public ViewHolder(@NonNull View itemView) { super(itemView); icon = itemView.findViewById(R.id.browseReceiverDeviceItemIcon); + albumArt = itemView.findViewById(R.id.browseReceiverDeviceItemAlbumArt); name = itemView.findViewById(R.id.browseReceiverDeviceItemName); + statusBadge = itemView.findViewById(R.id.status_badge); + statusText = itemView.findViewById(R.id.status_text); + volumeText = itemView.findViewById(R.id.volume_text); checkBox = itemView.findViewById(R.id.browseReceiverDeviceItemCheckbox); mute = itemView.findViewById(R.id.browseReceiverDeviceItemMute); volume = itemView.findViewById(R.id.browseReceiverDeviceItemMuteVolumeSeekBar); + btnPlay = itemView.findViewById(R.id.btn_play); + btnPause = itemView.findViewById(R.id.btn_pause); + btnStop = itemView.findViewById(R.id.btn_stop); + itemContainer = itemView.findViewById(R.id.item_container); volume.setMax(100); } } -} \ No newline at end of file +} \ No newline at end of file diff --git a/yaacc/src/main/java/de/yaacc/browser/BrowseReceiverDeviceClickListener.java b/yaacc/src/main/java/de/yaacc/browser/BrowseReceiverDeviceClickListener.java index e439d66a..a5ad91b0 100644 --- a/yaacc/src/main/java/de/yaacc/browser/BrowseReceiverDeviceClickListener.java +++ b/yaacc/src/main/java/de/yaacc/browser/BrowseReceiverDeviceClickListener.java @@ -17,7 +17,7 @@ */ package de.yaacc.browser; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.view.View; import android.widget.CheckBox; @@ -48,12 +48,12 @@ public void onClick(View itemView) { .findViewById(R.id.browseReceiverDeviceItemCheckbox); Device device = (Device) adapter.getItem(recyclerView.getChildAdapterPosition(itemView)); if (checkBox.isChecked()) { - Log.d(getClass().getName(), "isChecked:" + device.getDisplayString()); + YaaccLogger.d(getClass().getName(), "isChecked:" + device.getDisplayString()); adapter.removeSelectedDevice(device); upnpClient.removeReceiverDevice(device); checkBox.setChecked(false); } else { - Log.d(getClass().getName(), "isNotChecked:" + device.getDisplayString()); + YaaccLogger.d(getClass().getName(), "isNotChecked:" + device.getDisplayString()); adapter.addSelectedDevice(device); upnpClient.addReceiverDevice(device); checkBox.setChecked(true); diff --git a/yaacc/src/main/java/de/yaacc/browser/ContentItemPlayTask.java b/yaacc/src/main/java/de/yaacc/browser/ContentItemPlayTask.java index c5fdde25..86b9b927 100644 --- a/yaacc/src/main/java/de/yaacc/browser/ContentItemPlayTask.java +++ b/yaacc/src/main/java/de/yaacc/browser/ContentItemPlayTask.java @@ -28,11 +28,12 @@ public class ContentItemPlayTask extends AsyncTask { public final static int ADD_TO_PLAYLIST = 2; private final ContentListFragment parent; private final DIDLObject currentObject; + private final Runnable onComplete; - - public ContentItemPlayTask(ContentListFragment parent, DIDLObject currentObject) { + public ContentItemPlayTask(ContentListFragment parent, DIDLObject currentObject, Runnable onComplete) { this.parent = parent; this.currentObject = currentObject; + this.onComplete = onComplete; } @Override @@ -49,4 +50,11 @@ public Void doInBackground(Integer... integers) { } return null; } + + @Override + protected void onPostExecute(Void result) { + if (onComplete != null) { + onComplete.run(); + } + } } diff --git a/yaacc/src/main/java/de/yaacc/browser/ContentListClickListener.java b/yaacc/src/main/java/de/yaacc/browser/ContentListClickListener.java index 471e3c9e..0de927a1 100644 --- a/yaacc/src/main/java/de/yaacc/browser/ContentListClickListener.java +++ b/yaacc/src/main/java/de/yaacc/browser/ContentListClickListener.java @@ -72,20 +72,18 @@ public void onClick(View itemView) { navigator.pushPosition(new Position(position, newObjectId, upnpClient.getProviderDeviceId(), currentObject.getTitle())); contentListFragment.populateItemList(true); } else if (currentObject instanceof Item) { - if (currentObject == BrowseContentItemAdapter.LOAD_MORE_FAKE_ITEM) { - adapter.loadMore(); + PlayableItem playableItem = new PlayableItem((Item) currentObject, 0); + ContentItemPlayTask task = new ContentItemPlayTask(contentListFragment, currentObject, () -> { + // Hide loading indicator when done + contentListFragment.hideLoading(position); + }); + // Show loading indicator + contentListFragment.showLoading(position); + if (playableItem.getMimeType() != null && playableItem.getMimeType().startsWith("video")) { + task.execute(ContentItemPlayTask.PLAY_CURRENT); } else { - PlayableItem playableItem = new PlayableItem((Item) currentObject, 0); - ContentItemPlayTask task = new ContentItemPlayTask(contentListFragment, currentObject); - if (playableItem.getMimeType() != null && playableItem.getMimeType().startsWith("video")) { - task.execute(ContentItemPlayTask.PLAY_CURRENT); - } else { - task.execute(ContentItemPlayTask.PLAY_ALL); - } - + task.execute(ContentItemPlayTask.PLAY_ALL); } } } - - } \ No newline at end of file diff --git a/yaacc/src/main/java/de/yaacc/browser/ContentListFragment.java b/yaacc/src/main/java/de/yaacc/browser/ContentListFragment.java index b597f08d..277e6d2a 100644 --- a/yaacc/src/main/java/de/yaacc/browser/ContentListFragment.java +++ b/yaacc/src/main/java/de/yaacc/browser/ContentListFragment.java @@ -18,8 +18,9 @@ package de.yaacc.browser; import android.graphics.drawable.Drawable; +import android.widget.ProgressBar; import android.os.Bundle; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.view.LayoutInflater; import android.view.View; import android.view.View.OnClickListener; @@ -67,6 +68,7 @@ public class ContentListFragment extends Fragment implements OnClickListener, private TextView currentFolderNameView; private View topSeperator; private TextView currentProvider; + private ProgressBar progressBar; @Override @@ -77,7 +79,7 @@ public void onResume() { public void run() { if (upnpClient.getProviderDevice() != null) { currentProvider.setText(upnpClient.getProviderDevice().getDetails().getFriendlyName()); - if (navigator != null && navigator.getCurrentPosition().getDeviceId() != null && upnpClient.getProviderDevice().getIdentity().getUdn().getIdentifierString().equals(navigator.getCurrentPosition().getDeviceId())) { + if (navigator != null && navigator.getCurrentPosition() != null && navigator.getCurrentPosition().getDeviceId() != null && upnpClient.getProviderDevice().getIdentity().getUdn().getIdentifierString().equals(navigator.getCurrentPosition().getDeviceId())) { populateItemList(false); } else { showMainFolder(); @@ -103,6 +105,8 @@ private void init(Bundle savedInstanceState, View contentlistView) { currentProvider = contentlistView.findViewById(R.id.contentListCurrentProvider); topSeperator = contentlistView.findViewById(R.id.contentListTopSeperator); contentList = contentlistView.findViewById(R.id.contentList); + progressBar = contentlistView.findViewById(R.id.contentListProgressBar); + YaaccLogger.d("ContentListFragment", "ProgressBar found: " + (progressBar != null)); contentList.setLayoutManager(new LinearLayoutManager(getActivity())); contentList.setFocusable(true); contentList.setFocusableInTouchMode(false); // Good for D-Pad primary interaction @@ -182,7 +186,7 @@ public void onCreate(Bundle savedInstanceState) { */ public boolean onBackPressed() { - Log.d(ContentListFragment.class.getName(), "onBackPressed() CurrentPosition: " + navigator.getCurrentPosition()); + YaaccLogger.d(ContentListFragment.class.getName(), "onBackPressed() CurrentPosition: " + navigator.getCurrentPosition()); if (bItemAdapter != null) { bItemAdapter.cancelRunningTasks(); @@ -239,7 +243,7 @@ private void initBrowsItemAdapter(RecyclerView itemList) { if (getContext() == null) { return; } - bItemAdapter = new BrowseContentItemAdapter(this, itemList, upnpClient); + bItemAdapter = new BrowseContentItemAdapter(this, itemList, upnpClient, progressBar); itemList.setAdapter(bItemAdapter); itemList.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override @@ -249,7 +253,7 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { if (linearLayoutManager != null && linearLayoutManager.findLastCompletelyVisibleItemPosition() == bItemAdapter.getItemCount() - 1) { if (getActivity() != null) { getActivity().runOnUiThread(() -> { - Log.d(getClass().getName(), "scroll int dx, int dy" + dx + ", " + dy); + YaaccLogger.d(getClass().getName(), "scroll int dx, int dy" + dx + ", " + dy); bItemAdapter.loadMore(); }); } @@ -282,6 +286,24 @@ public void populateItemList(boolean clear) { }); } + public void showLoading(int position) { + requireActivity().runOnUiThread(() -> { + ProgressBar progressBar = requireView().findViewById(R.id.contentListProgressBar); + if (progressBar != null) { + progressBar.setVisibility(View.VISIBLE); + } + }); + } + + public void hideLoading(int position) { + requireActivity().runOnUiThread(() -> { + ProgressBar progressBar = requireView().findViewById(R.id.contentListProgressBar); + if (progressBar != null) { + progressBar.setVisibility(View.GONE); + } + }); + } + private void clearItemList() { requireActivity().runOnUiThread(() -> { navigator = new Navigator(); @@ -306,7 +328,7 @@ public void deviceAdded(Device device) { */ @Override public void deviceRemoved(Device device) { - Log.d(this.getClass().toString(), "device removal called"); + YaaccLogger.d(this.getClass().toString(), "device removal called"); if (device.equals(upnpClient.getProviderDevice())) { clearItemList(); } diff --git a/yaacc/src/main/java/de/yaacc/browser/Navigator.java b/yaacc/src/main/java/de/yaacc/browser/Navigator.java index 8d06e363..c338fdf4 100644 --- a/yaacc/src/main/java/de/yaacc/browser/Navigator.java +++ b/yaacc/src/main/java/de/yaacc/browser/Navigator.java @@ -17,7 +17,7 @@ */ package de.yaacc.browser; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import java.io.Serializable; import java.util.ArrayList; @@ -52,7 +52,7 @@ public Position getCurrentPosition() { } public void pushPosition(Position pos) { - Log.d(getClass().getName(), "pushNavigation: " + pos.getObjectId()); + YaaccLogger.d(getClass().getName(), "pushNavigation: " + pos.getObjectId()); navigationPath.add(pos); } @@ -66,7 +66,7 @@ public Position popPosition() { if (!navigationPath.isEmpty()) { result = navigationPath.removeLast(); } - Log.d(getClass().getName(), "popNavigation: " + Objects.requireNonNull(result).getObjectId()); + YaaccLogger.d(getClass().getName(), "popNavigation: " + Objects.requireNonNull(result).getObjectId()); return result; } diff --git a/yaacc/src/main/java/de/yaacc/browser/PlayerListFragment.java b/yaacc/src/main/java/de/yaacc/browser/PlayerListFragment.java index a1b5c37d..e9b863eb 100644 --- a/yaacc/src/main/java/de/yaacc/browser/PlayerListFragment.java +++ b/yaacc/src/main/java/de/yaacc/browser/PlayerListFragment.java @@ -18,7 +18,7 @@ package de.yaacc.browser; import android.os.Bundle; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -101,7 +101,7 @@ public void onCreate(Bundle savedInstanceState) { } public boolean onBackPressed() { - Log.d(PlayerListFragment.class.getName(), "onBackPressed() CurrentPosition"); + YaaccLogger.d(PlayerListFragment.class.getName(), "onBackPressed() CurrentPosition"); if (requireActivity().getParent() instanceof TabBrowserActivity) { ((TabBrowserActivity) requireActivity().getParent()).setCurrentTab(BrowserTabs.RECEIVER); } diff --git a/yaacc/src/main/java/de/yaacc/browser/PlayerListItemAdapter.java b/yaacc/src/main/java/de/yaacc/browser/PlayerListItemAdapter.java index 48a433e6..54a16d11 100644 --- a/yaacc/src/main/java/de/yaacc/browser/PlayerListItemAdapter.java +++ b/yaacc/src/main/java/de/yaacc/browser/PlayerListItemAdapter.java @@ -19,7 +19,7 @@ import android.content.res.Configuration; import android.graphics.Bitmap; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.util.TypedValue; import android.view.LayoutInflater; import android.view.View; @@ -74,7 +74,7 @@ public Player getItem(int position) { } public void setItems(Collection newObjects) { - Log.d(getClass().getName(), "set objects; " + newObjects); + YaaccLogger.d(getClass().getName(), "set objects; " + newObjects); players.clear(); players.addAll(newObjects); notifyDataSetChanged(); @@ -114,16 +114,25 @@ public void onBindViewHolder(final PlayerListItemAdapter.ViewHolder holder, fina } } + + // Load album art if available + if (player.getAlbumArt() != null) { + new de.yaacc.util.image.IconDownloadTask(holder.albumArt, 512, 512).execute(android.net.Uri.parse(player.getAlbumArt().toString())); + } else { + holder.albumArt.setImageDrawable(null); + } } } static class ViewHolder extends RecyclerView.ViewHolder { ImageView icon; + ImageView albumArt; TextView name; public ViewHolder(@NonNull View itemView) { super(itemView); icon = itemView.findViewById(R.id.browsePlayerItemIcon); + albumArt = itemView.findViewById(R.id.browsePlayerItemAlbumArt); name = itemView.findViewById(R.id.browsePlayerItemName); } } diff --git a/yaacc/src/main/java/de/yaacc/browser/PlayerListItemClickListener.java b/yaacc/src/main/java/de/yaacc/browser/PlayerListItemClickListener.java index 51a6a26c..a9a45949 100644 --- a/yaacc/src/main/java/de/yaacc/browser/PlayerListItemClickListener.java +++ b/yaacc/src/main/java/de/yaacc/browser/PlayerListItemClickListener.java @@ -20,7 +20,7 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.view.View; import androidx.recyclerview.widget.RecyclerView; @@ -56,7 +56,7 @@ private void openIntent(Context context, Player player) { } catch (PendingIntent.CanceledException e) { // the stack trace isn't very helpful here. Just log the exception message. - Log.e(this.getClass().getName(), "Sending contentIntent failed", e); + YaaccLogger.e(this.getClass().getName(), "Sending contentIntent failed", e); } } diff --git a/yaacc/src/main/java/de/yaacc/browser/ReceiverListFragment.java b/yaacc/src/main/java/de/yaacc/browser/ReceiverListFragment.java index 941e2012..224f091f 100644 --- a/yaacc/src/main/java/de/yaacc/browser/ReceiverListFragment.java +++ b/yaacc/src/main/java/de/yaacc/browser/ReceiverListFragment.java @@ -19,7 +19,9 @@ import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.util.Log; +import android.os.Handler; +import android.os.Looper; +import de.yaacc.util.YaaccLogger; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -51,6 +53,7 @@ public class ReceiverListFragment extends Fragment implements protected RecyclerView contentList; private UpnpClient upnpClient = null; private BrowseReceiverDeviceAdapter bDeviceAdapter; + private RendererStatusMonitor statusMonitor; @Override public void onResume() { @@ -63,6 +66,51 @@ public void run() { } }); thread.start(); + + // Start monitoring when fragment visible + if (statusMonitor != null) { + statusMonitor.startMonitoring(new LinkedList<>(upnpClient.getDevicesProvidingAvTransportService())); + // Re-sort after a short delay to allow status updates to arrive + if (bDeviceAdapter != null) { + new Handler(Looper.getMainLooper()).postDelayed(() -> { + if (bDeviceAdapter != null) { + bDeviceAdapter.sortAndNotify(); + } + }, 500); + } + } + + // Poll local device status every 2 seconds + startLocalDevicePolling(); + } + + @Override + public void onPause() { + super.onPause(); + // Stop monitoring when fragment not visible + if (statusMonitor != null) { + statusMonitor.stopMonitoring(); + } + stopLocalDevicePolling(); + } + + private Handler localDeviceHandler = new Handler(Looper.getMainLooper()); + private Runnable localDevicePoller = new Runnable() { + @Override + public void run() { + if (bDeviceAdapter != null) { + bDeviceAdapter.updateLocalDeviceStatus(); + } + localDeviceHandler.postDelayed(this, 2000); + } + }; + + private void startLocalDevicePolling() { + localDeviceHandler.postDelayed(localDevicePoller, 2000); + } + + private void stopLocalDevicePolling() { + localDeviceHandler.removeCallbacks(localDevicePoller); } @@ -71,10 +119,20 @@ private void init(Bundle savedInstanceState, View view) { upnpClient = ((Yaacc) getActivity().getApplicationContext()).getUpnpClient(); contentList = view.findViewById(R.id.receiverList); contentList.setLayoutManager(new LinearLayoutManager(getActivity())); + contentList.setItemAnimator(null); // Disable animations to prevent flashing contentList.setFocusable(true); contentList.setFocusableInTouchMode(false); // Good for D-Pad primary interaction contentList.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); upnpClient.addUpnpClientListener(this); + + // Initialize status monitor + statusMonitor = new RendererStatusMonitor(); + statusMonitor.setListener(status -> { + if (bDeviceAdapter != null) { + bDeviceAdapter.updateStatus(status); + } + }); + ImageButton refresh = view.findViewById(R.id.receiverListRefreshButton); Drawable icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_refresh_32, getContext().getTheme()), getContext().getTheme()); refresh.setImageDrawable(icon); @@ -100,7 +158,7 @@ public void onCreate(Bundle savedInstanceState) { public boolean onBackPressed() { - Log.d(ReceiverListFragment.class.getName(), "onBackPressed() CurrentPosition"); + YaaccLogger.d(ReceiverListFragment.class.getName(), "onBackPressed() CurrentPosition"); if (getActivity().getParent() instanceof TabBrowserActivity) { ((TabBrowserActivity) getActivity().getParent()).setCurrentTab(BrowserTabs.CONTENT); } diff --git a/yaacc/src/main/java/de/yaacc/browser/RendererStatus.java b/yaacc/src/main/java/de/yaacc/browser/RendererStatus.java new file mode 100644 index 00000000..5c3b98f9 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/browser/RendererStatus.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.browser; + +import org.fourthline.cling.model.meta.Device; + +/** + * Holds the current status of a UPnP renderer. + */ +public class RendererStatus { + public enum State { PLAYING, PAUSED, STOPPED, NO_MEDIA } + + private final Device device; + private State state; + private String trackTitle; + private int volume; + + public RendererStatus(Device device, String upnpState, String trackTitle, int volume) { + this.device = device; + this.state = parseState(upnpState); + this.trackTitle = trackTitle; + this.volume = volume; + } + + private State parseState(String upnpState) { + if ("PLAYING".equals(upnpState)) return State.PLAYING; + if ("PAUSED_PLAYBACK".equals(upnpState)) return State.PAUSED; + if ("STOPPED".equals(upnpState)) return State.STOPPED; + return State.NO_MEDIA; + } + + public Device getDevice() { + return device; + } + + public State getState() { + return state; + } + + public String getTrackTitle() { + return trackTitle; + } + + public int getVolume() { + return volume; + } + + public boolean isPlaying() { + return state == State.PLAYING; + } +} diff --git a/yaacc/src/main/java/de/yaacc/browser/RendererStatusMonitor.java b/yaacc/src/main/java/de/yaacc/browser/RendererStatusMonitor.java new file mode 100644 index 00000000..390f03a1 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/browser/RendererStatusMonitor.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.browser; + +import android.os.Handler; +import android.os.Looper; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Device; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UDAServiceId; +import org.fourthline.cling.support.model.DIDLContent; +import org.fourthline.cling.support.model.PositionInfo; +import org.fourthline.cling.support.model.TransportInfo; +import org.fourthline.cling.support.model.item.Item; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import de.yaacc.upnp.callback.avtransport.GetPositionInfo; +import de.yaacc.upnp.callback.avtransport.GetTransportInfo; +import de.yaacc.upnp.callback.renderingcontrol.GetVolume; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.YaaccLogger; + +/** + * Monitors UPnP renderer status by polling AVTransport and RenderingControl services. + */ +public class RendererStatusMonitor { + private static final int POLL_INTERVAL_MS = 10000; // Reduced frequency to 10 seconds + + private final Handler handler = new Handler(Looper.getMainLooper()); + private final Map statusMap = new HashMap<>(); + private final ExecutorService executorService = Executors.newSingleThreadExecutor(); + private final HttpRequestSender httpRequestSender = new HttpRequestSender(); + private StatusListener listener; + + public interface StatusListener { + void onStatusChanged(RendererStatus status); + } + + public void setListener(StatusListener listener) { + this.listener = listener; + } + + public void startMonitoring(List devices) { + for (Device device : devices) { + pollDevice(device); + } + } + + public void stopMonitoring() { + handler.removeCallbacksAndMessages(null); + } + + public RendererStatus getStatus(Device device) { + return statusMap.get(device); + } + + private void pollDevice(Device device) { + Service avTransport = device.findService(new UDAServiceId("AVTransport")); + if (avTransport == null) { + handler.postDelayed(() -> pollDevice(device), POLL_INTERVAL_MS); + return; + } + + executorService.execute(new GetTransportInfo(avTransport, httpRequestSender) { + @Override + public void received(ActionInvocation invocation, TransportInfo transportInfo) { + String state = transportInfo.getCurrentTransportState().getValue(); + getTrackTitleAndVolume(device, state); + } + + @Override + public void failure(ActionInvocation invocation, UpnpResponse response, String msg) { + YaaccLogger.e(getClass().getName(), "GetTransportInfo failed: " + msg); + handler.postDelayed(() -> pollDevice(device), POLL_INTERVAL_MS); + } + }); + } + + private void getTrackTitleAndVolume(Device device, String state) { + if ("PLAYING".equals(state) || "PAUSED_PLAYBACK".equals(state)) { + Service avTransport = device.findService(new UDAServiceId("AVTransport")); + executorService.execute(new GetPositionInfo(avTransport, httpRequestSender) { + @Override + public void received(ActionInvocation invocation, PositionInfo positionInfo) { + String title = parseTrackTitle(positionInfo.getTrackMetaData()); + getVolume(device, state, title); + } + + @Override + public void failure(ActionInvocation invocation, UpnpResponse response, String msg) { + getVolume(device, state, null); + } + }); + } else { + getVolume(device, state, null); + } + } + + private void getVolume(Device device, String state, String trackTitle) { + Service renderingControl = device.findService(new UDAServiceId("RenderingControl")); + if (renderingControl == null) { + updateStatus(device, state, trackTitle, 50); + return; + } + + executorService.execute(new GetVolume(renderingControl, httpRequestSender) { + @Override + public void received(ActionInvocation invocation, int currentVolume) { + updateStatus(device, state, trackTitle, currentVolume); + } + + @Override + public void failure(ActionInvocation invocation, UpnpResponse response, String msg) { + updateStatus(device, state, trackTitle, 50); + } + }); + } + + private void updateStatus(Device device, String state, String trackTitle, int volume) { + RendererStatus status = new RendererStatus(device, state, trackTitle, volume); + statusMap.put(device, status); + + if (listener != null) { + handler.post(() -> listener.onStatusChanged(status)); + } + + handler.postDelayed(() -> pollDevice(device), POLL_INTERVAL_MS); + } + + private String parseTrackTitle(String trackMetaData) { + if (trackMetaData == null || trackMetaData.isEmpty()) { + return null; + } + + try { + DIDLContent metadata = new org.fourthline.cling.support.contentdirectory.DIDLParser().parse(trackMetaData); + List items = metadata.getItems(); + if (!items.isEmpty()) { + return items.get(0).getTitle(); + } + } catch (Exception e) { + YaaccLogger.d(getClass().getName(), "Failed to parse track metadata"); + } + + return null; + } +} diff --git a/yaacc/src/main/java/de/yaacc/browser/ServerListFragment.java b/yaacc/src/main/java/de/yaacc/browser/ServerListFragment.java index bf9368c7..14ef02b1 100644 --- a/yaacc/src/main/java/de/yaacc/browser/ServerListFragment.java +++ b/yaacc/src/main/java/de/yaacc/browser/ServerListFragment.java @@ -22,7 +22,7 @@ import android.content.SharedPreferences; import android.graphics.drawable.Drawable; import android.os.Bundle; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -81,10 +81,35 @@ private SharedPreferences getPreferences() { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // Register callback for MediaProjection stop events + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + de.yaacc.upnp.server.media.MediaProjectionHelper.setStopCallback(() -> { + // MediaProjection stopped - disable streaming + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + android.content.SharedPreferences prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(requireContext()); + prefs.edit() + .putBoolean(getString(R.string.settings_local_server_serve_system_audio_chkbx), false) + .putBoolean(getString(R.string.settings_local_server_serve_screen_cast_chkbx), false) + .apply(); + + de.yaacc.browser.BrowseDeviceAdapter.setAudioStreaming(false); + de.yaacc.browser.BrowseDeviceAdapter.setVideoStreaming(false); + + if (bDeviceAdapter != null) { + bDeviceAdapter.notifyDataSetChanged(); + } + + YaaccLogger.i(getClass().getName(), "Streaming disabled - MediaProjection stopped"); + }); + } + }); + } } public boolean onBackPressed() { - Log.d(ServerListFragment.class.getName(), "onBackPressed()"); + YaaccLogger.d(ServerListFragment.class.getName(), "onBackPressed()"); ((Yaacc) requireActivity().getApplicationContext()).exit(); ServerListFragment.super.requireActivity().finish(); return true; @@ -102,6 +127,13 @@ private void populateDeviceList() { RecyclerView deviceList = contentList; if (deviceList.getAdapter() == null) { bDeviceAdapter = new BrowseDeviceAdapter(getActivity(), deviceList, upnpClient, new ArrayList<>(upnpClient.getDevicesProvidingContentDirectoryService())); + // Set permission callback for streaming buttons + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + bDeviceAdapter.setPermissionCallback(() -> { + android.content.Intent intent = de.yaacc.upnp.server.media.MediaProjectionHelper.createPermissionIntent(requireContext()); + startActivityForResult(intent, de.yaacc.upnp.server.media.MediaProjectionHelper.REQUEST_CODE_MEDIA_PROJECTION); + }); + } deviceList.setAdapter(bDeviceAdapter); } else { bDeviceAdapter.setDevices(new LinkedList<>(upnpClient.getDevicesProvidingContentDirectoryService())); @@ -122,7 +154,7 @@ public void deviceAdded(Device device) { try { requireActivity(); } catch (IllegalStateException iex) { - Log.d(getClass().getName(), "ignoring illegal state exception on device added", iex); + YaaccLogger.d(getClass().getName(), "ignoring illegal state exception on device added", iex); return; } if (requireActivity().getParent() instanceof TabBrowserActivity) { @@ -136,7 +168,7 @@ public void deviceAdded(Device device) { */ @Override public void deviceRemoved(Device device) { - Log.d(this.getClass().toString(), "device removal called"); + YaaccLogger.d(this.getClass().toString(), "device removal called"); populateDeviceList(); @@ -198,11 +230,6 @@ private void init(Bundle savedInstanceState, View view) { localServerEnabledSwitch.setOnClickListener((v -> { getPreferences().edit().putBoolean(v.getContext().getString(R.string.settings_local_server_chkbx), localServerEnabledSwitch.isChecked()).apply(); if (v.getContext() instanceof TabBrowserActivity) { - if (localServerEnabledSwitch.isChecked()) { - v.getContext().getApplicationContext().startForegroundService(((TabBrowserActivity) v.getContext()).getYaaccUpnpServerService()); - } else { - v.getContext().getApplicationContext().stopService(((TabBrowserActivity) v.getContext()).getYaaccUpnpServerService()); - } setLocalServerState(view); } })); @@ -244,7 +271,7 @@ private void init(Bundle savedInstanceState, View view) { picker.addOnPositiveButtonClickListener(dialog -> { long millis = (picker.getHour() * 3600L + picker.getMinute() * 60L) * 1000L; - Log.d(getClass().getName(), "time set: " + picker.getHour() + ":" + picker.getMinute() + " millis: " + millis); + YaaccLogger.d(getClass().getName(), "time set: " + picker.getHour() + ":" + picker.getMinute() + " millis: " + millis); getPreferences().edit().putLong(getContext().getString(R.string.settings_shutdown_timer), millis).apply(); if (shutdownTimerSwitch.isChecked()) { ((Yaacc) getContext().getApplicationContext()).stopShutdownTimer(); @@ -318,4 +345,59 @@ public void setShutdownTimerRemainingTime(String s) { public void onTick(long millisUntilFinished) { setShutdownTimerRemainingTime(FormatHelper.parseMillisToTimeStringTo(millisUntilFinished)); } + + @Override + public void onActivityResult(int requestCode, int resultCode, android.content.Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q && + requestCode == de.yaacc.upnp.server.media.MediaProjectionHelper.REQUEST_CODE_MEDIA_PROJECTION) { + + if (de.yaacc.upnp.server.media.MediaProjectionHelper.handlePermissionResult(requireContext(), resultCode, data)) { + // Permission granted - enable only the button that requested it + YaaccLogger.i(getClass().getName(), "MediaProjection permission granted"); + + android.content.SharedPreferences prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(requireContext()); + android.content.SharedPreferences.Editor editor = prefs.edit(); + + // Enable only the button that requested permission + if (de.yaacc.browser.BrowseDeviceAdapter.isPendingAudioRequest()) { + editor.putBoolean(getString(R.string.settings_local_server_serve_system_audio_chkbx), true); + de.yaacc.browser.BrowseDeviceAdapter.setAudioStreaming(true); + } + if (de.yaacc.browser.BrowseDeviceAdapter.isPendingVideoRequest()) { + editor.putBoolean(getString(R.string.settings_local_server_serve_screen_cast_chkbx), true); + de.yaacc.browser.BrowseDeviceAdapter.setVideoStreaming(true); + } + editor.apply(); + + // Clear pending flags + de.yaacc.browser.BrowseDeviceAdapter.clearPendingRequests(); + + // Refresh the adapter to update button states + if (bDeviceAdapter != null) { + bDeviceAdapter.notifyDataSetChanged(); + } + } else { + // Permission denied - disable both and clear pending + YaaccLogger.w(getClass().getName(), "MediaProjection permission denied"); + + de.yaacc.browser.BrowseDeviceAdapter.clearPendingRequests(); + + android.content.SharedPreferences prefs = androidx.preference.PreferenceManager.getDefaultSharedPreferences(requireContext()); + prefs.edit() + .putBoolean(getString(R.string.settings_local_server_serve_system_audio_chkbx), false) + .putBoolean(getString(R.string.settings_local_server_serve_screen_cast_chkbx), false) + .apply(); + + // Update adapter state + if (bDeviceAdapter != null) { + de.yaacc.browser.BrowseDeviceAdapter.setAudioStreaming(false); + de.yaacc.browser.BrowseDeviceAdapter.setVideoStreaming(false); + // Refresh the adapter to update button states + bDeviceAdapter.notifyDataSetChanged(); + } + } + } + } } \ No newline at end of file diff --git a/yaacc/src/main/java/de/yaacc/browser/TabBrowserActivity.java b/yaacc/src/main/java/de/yaacc/browser/TabBrowserActivity.java index acd6734f..1651cc51 100644 --- a/yaacc/src/main/java/de/yaacc/browser/TabBrowserActivity.java +++ b/yaacc/src/main/java/de/yaacc/browser/TabBrowserActivity.java @@ -31,7 +31,7 @@ import android.os.Looper; import android.os.PowerManager; import android.provider.Settings; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.util.TypedValue; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; @@ -96,7 +96,6 @@ public class TabBrowserActivity extends AppCompatActivity implements OnClickList private Intent serverService = null; - private Toast volumeToast = null; //https://developer.android.com/about/versions/14/changes/partial-photo-video-access private static String[] getPermissions() { @@ -114,7 +113,8 @@ private static String[] getPermissions() { Manifest.permission.READ_MEDIA_AUDIO, Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO, - Manifest.permission.MANAGE_EXTERNAL_STORAGE + Manifest.permission.MANAGE_EXTERNAL_STORAGE, + Manifest.permission.POST_NOTIFICATIONS }; } else if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { @@ -210,7 +210,7 @@ public void onPageSelected(int position) { if (!checkIfAlreadyhavePermission()) { requestForSpecificPermission(); } else { - Log.d(getClass().getName(), "All permissions granted"); + YaaccLogger.d(getClass().getName(), "All permissions granted"); } checkBatteryOptimizationEnabled(); @@ -218,7 +218,7 @@ public void onPageSelected(int position) { // local server startup upnpClient = ((Yaacc) getApplicationContext()).getUpnpClient(); if (upnpClient == null) { - Log.d(getClass().getName(), "Upnp client is null"); + YaaccLogger.d(getClass().getName(), "Upnp client is null"); return; } @@ -230,7 +230,7 @@ public void onPageSelected(int position) { } checkIfReceivedShareIntent(null); - Log.d(this.getClass().getName(), "on create took: " + (System.currentTimeMillis() - start)); + YaaccLogger.d(this.getClass().getName(), "on create took: " + (System.currentTimeMillis() - start)); } private void checkBatteryOptimizationEnabled() { @@ -244,7 +244,7 @@ private void checkBatteryOptimizationEnabled() { try { startActivity(intent); } catch (ActivityNotFoundException ex) { - Log.d(getClass().getName(), "Ignoring exception ActivityNotFoundException during check for battery optimization"); + YaaccLogger.d(getClass().getName(), "Ignoring exception ActivityNotFoundException during check for battery optimization"); } } @@ -357,7 +357,6 @@ public void onResume() { long start = System.currentTimeMillis(); super.onResume(); viewPager.setUserInputEnabled(getPreferences().getBoolean(getString(R.string.settings_swipe_chkbx), true)); - setVolumeControlStream(-1000); //use an invalid audio stream to block controlling default streams boolean serverOn = getPreferences().getBoolean( getString(R.string.settings_local_server_chkbx), false); if (serverOn) { @@ -366,13 +365,13 @@ public void onResume() { getApplicationContext().stopService(getYaaccUpnpServerService()); } getApplicationContext().startForegroundService(getYaaccUpnpServerService()); - Log.d(this.getClass().getName(), "Starting local service"); + YaaccLogger.d(this.getClass().getName(), "Starting local service"); } else { getApplicationContext().stopService(getYaaccUpnpServerService()); - Log.d(this.getClass().getName(), "Stopping local service"); + YaaccLogger.d(this.getClass().getName(), "Stopping local service"); } leftSettings = false; - Log.d(this.getClass().getName(), "on on resume took: " + (System.currentTimeMillis() - start)); + YaaccLogger.d(this.getClass().getName(), "on on resume took: " + (System.currentTimeMillis() - start)); } @@ -391,7 +390,7 @@ public Intent getYaaccUpnpServerService() { @Override public void onBackPressed() { - Log.d(TabBrowserActivity.class.getName(), "onBackPressed() "); + YaaccLogger.d(TabBrowserActivity.class.getName(), "onBackPressed() "); if (getSupportFragmentManager().getFragments().size() > tabLayout.getSelectedTabPosition()) { Fragment fragment = getSupportFragmentManager().getFragments().get(tabLayout.getSelectedTabPosition()); if (!(fragment instanceof OnBackPressedListener) || !((OnBackPressedListener) fragment).onBackPressed()) { @@ -445,63 +444,9 @@ public void onClick(View view) { } - private Toast createVolumeToast(Drawable icon) { - LayoutInflater inflater = getLayoutInflater(); - View layout = inflater.inflate(R.layout.custom_toast, findViewById(R.id.toast_custom)); - TypedValue typedValue = new TypedValue(); - getTheme().resolveAttribute(android.R.attr.colorBackground, typedValue, true); - layout.setBackgroundColor(typedValue.data); - ImageView imageView = layout.findViewById(R.id.customToastImageView); - imageView.setImageDrawable(icon); - TextView text = layout.findViewById(R.id.customToastTextView); - text.setText(""); - Toast toast = new Toast(getApplicationContext()); - toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0); - toast.setDuration(Toast.LENGTH_SHORT); - toast.setView(layout); - return toast; - } - @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - upnpClient.getReceiverDevices().forEach(d -> { - if (upnpClient.hasActionGetVolume(d)) - switch (keyCode) { - case KeyEvent.KEYCODE_VOLUME_UP: - if (upnpClient.getVolume(d) < 100) { - upnpClient.setVolume(d, upnpClient.getVolume(d) + 1); - } - break; - case KeyEvent.KEYCODE_VOLUME_DOWN: - if (upnpClient.getVolume(d) > 0) { - upnpClient.setVolume(d, upnpClient.getVolume(d) - 1); - } - break; - } - }); - if (!upnpClient.getReceiverDevices().isEmpty()) { - if (KeyEvent.KEYCODE_VOLUME_UP == keyCode || KeyEvent.KEYCODE_VOLUME_DOWN == keyCode) { - Drawable icon = keyCode == KeyEvent.KEYCODE_VOLUME_UP ? ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_volume_up_96, getTheme()), getTheme()) : ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_volume_down_96, getTheme()), getTheme()); - if (volumeToast != null) { - volumeToast.cancel(); - } - volumeToast = createVolumeToast(icon); - volumeToast.show(); - if (viewPager != null && tabLayout != null && tabLayout.getSelectedTabPosition() == BrowserTabs.RECEIVER.ordinal() && tabLayout.getTabAt(tabLayout.getSelectedTabPosition()).view != null) { - List fragments = getSupportFragmentManager().getFragments(); - if (fragments.size() > viewPager.getCurrentItem()) { - RecyclerView view = fragments.get(viewPager.getCurrentItem()).getView().findViewById(R.id.receiverList); - if (view != null && view.getAdapter() != null) { - view.getAdapter().notifyDataSetChanged(); - } - } - } - } - } - - + // Let system handle volume keys - shows standard Android volume UI return super.onKeyDown(keyCode, event); } - - } diff --git a/yaacc/src/main/java/de/yaacc/imageviewer/ImageFragment.java b/yaacc/src/main/java/de/yaacc/imageviewer/ImageFragment.java new file mode 100644 index 00000000..a68af525 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/imageviewer/ImageFragment.java @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.imageviewer; + +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ProgressBar; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleEventObserver; + +import de.yaacc.R; +import de.yaacc.Yaacc; +import de.yaacc.util.YaaccLogger; + +/** + * Fragment for displaying a single image in ViewPager2. + */ +public class ImageFragment extends Fragment { + private static final String ARG_URI = "uri"; + private Uri uri; + private ImageView imageView; + private ProgressBar progressBar; + + public static ImageFragment newInstance(Uri uri) { + ImageFragment fragment = new ImageFragment(); + Bundle args = new Bundle(); + args.putParcelable(ARG_URI, uri); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null) { + uri = getArguments().getParcelable(ARG_URI); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_image, container, false); + imageView = view.findViewById(R.id.imageView); + progressBar = view.findViewById(R.id.progressBar); + + // Add click listener to toggle controls + imageView.setOnClickListener(v -> { + if (getActivity() instanceof ImageViewerActivity) { + ((ImageViewerActivity) getActivity()).toggleControlsFromFragment(); + } + }); + + return view; + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (uri != null) { + loadImage(uri); + } + } + + public void loadImage(Uri imageUri) { + if (imageView == null || imageUri == null) return; + + YaaccLogger.d(getClass().getName(), "Loading image: " + imageUri); + + // Show loading indicator + if (progressBar != null) { + progressBar.setVisibility(View.VISIBLE); + } + imageView.setImageBitmap(null); + + // Use existing ContentLoadExecutor + Yaacc app = (Yaacc) requireActivity().getApplication(); + app.getContentLoadExecutor().execute(() -> { + Bitmap bitmap = loadBitmap(imageUri); + if (getActivity() != null) { + getActivity().runOnUiThread(() -> { + // Hide loading indicator + if (progressBar != null) { + progressBar.setVisibility(View.GONE); + } + if (imageView != null && bitmap != null) { + imageView.setImageBitmap(bitmap); + } else if (imageView != null) { + imageView.setImageResource(R.drawable.yaacc192_32); + } + }); + } + }); + } + + private Bitmap loadBitmap(Uri imageUri) { + try { + if (getActivity() != null) { + String scheme = imageUri.getScheme(); + if ("http".equals(scheme) || "https".equals(scheme)) { + // Download from HTTP + java.net.URL url = new java.net.URL(imageUri.toString()); + return android.graphics.BitmapFactory.decodeStream(url.openStream()); + } else { + // Local file via ContentResolver + return android.graphics.BitmapFactory.decodeStream( + getActivity().getContentResolver().openInputStream(imageUri)); + } + } + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to load image: " + imageUri, e); + } + return null; + } + + public void setImageUri(Uri uri) { + this.uri = uri; + if (imageView != null) { + loadImage(uri); + } + } +} diff --git a/yaacc/src/main/java/de/yaacc/imageviewer/ImagePagerAdapter.java b/yaacc/src/main/java/de/yaacc/imageviewer/ImagePagerAdapter.java new file mode 100644 index 00000000..98ef8c84 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/imageviewer/ImagePagerAdapter.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.imageviewer; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.viewpager2.adapter.FragmentStateAdapter; + +import java.util.List; + +import de.yaacc.util.YaaccLogger; + +/** + * ViewPager2 adapter for image slideshow. + */ +public class ImagePagerAdapter extends FragmentStateAdapter { + private List imageUris; + + public ImagePagerAdapter(@NonNull FragmentActivity fragmentActivity) { + super(fragmentActivity); + } + + public void setImageUris(List uris) { + this.imageUris = uris; + // Don't call notifyDataSetChanged() - ViewPager2 handles updates + } + + @NonNull + @Override + public Fragment createFragment(int position) { + if (imageUris != null && position < imageUris.size()) { + return ImageFragment.newInstance(imageUris.get(position)); + } + return new ImageFragment(); + } + + @Override + public int getItemCount() { + return imageUris != null ? imageUris.size() : 0; + } + + public Uri getItem(int position) { + if (imageUris != null && position < imageUris.size()) { + return imageUris.get(position); + } + return null; + } +} diff --git a/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerActivity.java b/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerActivity.java index 2120d35a..05e2f729 100644 --- a/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerActivity.java +++ b/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerActivity.java @@ -1,6 +1,6 @@ /* - * * Copyright (C) 2013 Tobias Schoene www.yaacc.de + * Copyright (C) 2026 Modernization * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -22,28 +22,21 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.AsyncTask.Status; import android.os.Bundle; import android.os.IBinder; -import android.util.Log; -import android.view.KeyEvent; -import android.view.Menu; -import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; -import android.view.WindowManager; import android.widget.ImageButton; -import android.widget.ImageView; -import android.widget.RelativeLayout; import android.widget.Toast; -import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.res.ResourcesCompat; -import androidx.preference.PreferenceManager; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsCompat; +import androidx.lifecycle.ViewModelProvider; +import androidx.viewpager2.widget.ViewPager2; import java.io.Serializable; import java.util.ArrayList; @@ -57,671 +50,443 @@ import de.yaacc.player.Player; import de.yaacc.player.PlayerService; import de.yaacc.settings.SettingsActivity; -import de.yaacc.util.AboutActivity; import de.yaacc.util.ActivitySwipeDetector; +import de.yaacc.util.AboutActivity; import de.yaacc.util.SwipeReceiver; import de.yaacc.util.ThemeHelper; import de.yaacc.util.YaaccLogActivity; +import de.yaacc.util.YaaccLogger; /** - * a simple ImageViewer based on the android ImageView component; - *

- * you are able to start the activity either by using intnet.setData(anUri) or - * by intent.putExtra(ImageViewerActivity.URIS, aList); in the later case - * the activity needed to be started with Intent.ACTION_SEND_MULTIPLE - *

- *

- * The image viewer retrieves all images in a background task - * (RetrieveImageTask). The images are written in a memory cache. The picture - * show is processed by the ImageViewerActivity using the images in the cache. - * - * @author Tobias Schoene (openbit) + * Modern ImageViewer with ViewPager2 slideshow. */ public class ImageViewerActivity extends AppCompatActivity implements SwipeReceiver, ServiceConnection { public static final String URIS = "URIS_PARAM"; public static final String AUTO_START_SHOW = "AUTO_START_SHOW"; - private ImageView imageView; - private RetrieveImageTask retrieveImageTask; - private List imageUris; // playlist - private int currentImageIndex = 0; - private boolean pictureShowActive = false; - private boolean isProcessingCommand = false; // indicates an command - private Timer pictureShowTimer; - private ImageViewerBroadcastReceiver imageViewerBroadcastReceiver; - private PlayerService playerService; - private Menu menu; - - public void onServiceConnected(ComponentName className, IBinder binder) { - if (binder instanceof PlayerService.PlayerServiceBinder) { - Log.d(getClass().getName(), "PlayerService connected"); - playerService = ((PlayerService.PlayerServiceBinder) binder).getService(); - initialize(); - } - } - - public void onServiceDisconnected(ComponentName className) { - Log.d(getClass().getName(), "PlayerService disconnected"); - playerService = null; - } - - protected void initialize() { - - } + private ImageViewerViewModel viewModel; + private ImagePagerAdapter pagerAdapter; + private ViewPager2 viewPager; + private PlayerService playerService; + private ImageViewerBroadcastReceiver broadcastReceiver; + private Timer controlHideTimer; @Override protected void onCreate(Bundle savedInstanceState) { - Log.d(this.getClass().getName(), "OnCreate"); + YaaccLogger.d(this.getClass().getName(), "OnCreate"); super.onCreate(savedInstanceState); - init(savedInstanceState, getIntent()); - this.bindService(new Intent(this, PlayerService.class), - this, Context.BIND_AUTO_CREATE); - } - - /* - * (non-Javadoc) - * - * @see android.app.Activity#onNewIntent(android.content.Intent) - */ - @Override - protected void onNewIntent(Intent intent) { - super.onNewIntent(intent); - init(null, intent); - } - private void init(Bundle savedInstanceState, Intent intent) { - menuBarsHide(); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); - getWindow().clearFlags( - WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON); + setupEdgeToEdge(); setContentView(R.layout.activity_image_viewer); - imageView = findViewById(R.id.imageView); - ActivitySwipeDetector activitySwipeDetector = new ActivitySwipeDetector( - this); - RelativeLayout layout = findViewById(R.id.layout); - layout.setOnTouchListener(activitySwipeDetector); - layout.setFocusable(true); - layout.setFocusableInTouchMode(false); - layout.setOnKeyListener((v, keyCode, event) -> { - if (event.getAction() != android.view.KeyEvent.ACTION_DOWN) return false; - switch (keyCode) { - case android.view.KeyEvent.KEYCODE_DPAD_CENTER: - case android.view.KeyEvent.KEYCODE_ENTER: - // Trigger normal click - runOnUiThread(this::menuBarsShow); - startMenuHideTimer(); - return true; - } - return false; - }); - layout.requestLayout(); - currentImageIndex = 0; - imageUris = new ArrayList<>(); - if (savedInstanceState != null) { - pictureShowActive = savedInstanceState - .getBoolean("pictureShowActive"); - currentImageIndex = savedInstanceState.getInt("currentImageIndex"); - imageUris = (List) savedInstanceState - .getSerializable("imageUris"); - } else { - Log.d(this.getClass().getName(), - "Received Action View! now setting items "); - Serializable urisData = intent.getSerializableExtra(URIS); - if (urisData != null) { - if (urisData instanceof List) { - currentImageIndex = 0; - imageUris = (List) urisData; - Log.d(this.getClass().getName(), - "imageUris" + imageUris.toString()); - } - } else { - if (intent.getData() != null) { - currentImageIndex = 0; - imageUris.add(intent.getData()); - Log.d(this.getClass().getName(), "imageUris.add(i.getData)" - + imageUris.toString()); - } - } - pictureShowActive = intent.getBooleanExtra(AUTO_START_SHOW, false); - } - if (imageUris != null && !imageUris.isEmpty()) { - loadImage(); - } else { - runOnUiThread(() -> { - Toast toast = Toast.makeText(ImageViewerActivity.this, - R.string.no_valid_uri_data_found_to_display, - Toast.LENGTH_LONG); - toast.show(); - menuBarsHide(); - }); - } - } - - @Override - protected void onDestroy() { - try { - unbindService(this); - } catch (IllegalArgumentException iae) { - Log.d(getClass().getName(), "Ignore exception on unbind service while activity destroy"); + initViewModel(); + initViews(); + initViewPager(); + loadSettingsDuration(); + loadIntentData(savedInstanceState, getIntent()); + restoreStateFromPrefs(); + + // Ensure ViewPager is at correct position after all data is loaded + ImageViewerState state = viewModel.getState().getValue(); + if (state != null && state.getCurrentIndex() >= 0) { + viewPager.setCurrentItem(state.getCurrentIndex(), false); } - super.onDestroy(); } - /* - * (non-Javadoc) - * - * @see android.app.Activity#onResume() - */ - @Override - protected void onResume() { - - imageViewerBroadcastReceiver = new ImageViewerBroadcastReceiver(this); - imageViewerBroadcastReceiver.registerReceiver(); - this.bindService(new Intent(this, PlayerService.class), - this, Context.BIND_AUTO_CREATE); - super.onResume(); + private void loadSettingsDuration() { + android.content.SharedPreferences prefs = + androidx.preference.PreferenceManager.getDefaultSharedPreferences(this); + int durationMs = Integer.parseInt(prefs.getString( + getString(R.string.image_viewer_settings_duration_key), "5000")); + viewModel.setDurationMs(durationMs); } - /* - * (non-Javadoc) - * - * @see android.app.Activity#onPause() - */ - @Override - protected void onPause() { - cancleTimer(); - if (retrieveImageTask != null) { - retrieveImageTask.cancel(true); - retrieveImageTask = null; + private void setupEdgeToEdge() { + WindowCompat.setDecorFitsSystemWindows(getWindow(), false); + // Remove title from action bar + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayShowTitleEnabled(false); } - unregisterReceiver(imageViewerBroadcastReceiver); - imageViewerBroadcastReceiver = null; - super.onPause(); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - getMenuInflater().inflate(R.menu.activity_image_viewer, menu); - this.menu = menu; - MenuItem pauseItem = menu.findItem(R.id.menu_pause); - ImageButton pauseButton = new ImageButton(this); - Drawable icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_pause_32, getTheme()), getTheme()); - pauseButton.setImageDrawable(icon); - pauseButton.setFocusable(true); - pauseButton.setFocusableInTouchMode(true); - pauseButton.setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - pause(); - } - return false; - }); - pauseButton.setOnClickListener(v -> pause()); - pauseButton.setOnKeyListener((v, keyCode, event) -> { - if (event.getAction() == KeyEvent.ACTION_DOWN && - (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER)) { - pause(); - return true; + private void initViewModel() { + viewModel = new ViewModelProvider(this).get(ImageViewerViewModel.class); + + viewModel.getState().observe(this, state -> { + if (state == null) return; + + // Update ViewPager adapter data (don't trigger notifyDataSetChanged) + if (pagerAdapter != null && state.getImageUris() != null && !state.getImageUris().isEmpty()) { + pagerAdapter.setImageUris(state.getImageUris()); + // Manually set current item after adapter data is updated + if (state.getCurrentIndex() < state.getTotalImages()) { + viewPager.post(() -> viewPager.setCurrentItem(state.getCurrentIndex(), false)); + } } - return false; - }); - pauseItem.setActionView(pauseButton); - MenuItem playItem = menu.findItem(R.id.menu_play); - ImageButton playButton = new ImageButton(this); - icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_play_arrow_32, getTheme()), getTheme()); - playButton.setImageDrawable(icon); - playButton.setFocusable(true); - playButton.setFocusableInTouchMode(true); - playButton.setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - play(); + + // Update action bar visibility (no bottom controls anymore) + if (getSupportActionBar() != null) { + if (state.isControlsVisible()) { + getSupportActionBar().show(); + } else { + getSupportActionBar().hide(); + } } - return false; - }); - playButton.setOnClickListener(v -> play()); - playButton.setOnKeyListener((v, keyCode, event) -> { - if (event.getAction() == KeyEvent.ACTION_DOWN && - (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER)) { - play(); - return true; + + // Handle playback state changes + if (state.isPlaying() && !viewPager.isUserInputEnabled()) { + viewPager.setUserInputEnabled(true); } - return false; }); - playItem.setActionView(playButton); - MenuItem stopItem = menu.findItem(R.id.menu_stop); - ImageButton stopButton = new ImageButton(this); - icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_stop_32, getTheme()), getTheme()); - stopButton.setImageDrawable(icon); - stopButton.setFocusable(true); - stopButton.setFocusableInTouchMode(true); - stopButton.setFocusableInTouchMode(true); - stopButton.setOnTouchListener((v, event) -> { + } + + private void initViews() { + viewPager = findViewById(R.id.viewPager); + View rootView = findViewById(R.id.rootLayout); + + // Add touch listener directly to ViewPager to toggle controls + viewPager.setOnTouchListener((v, event) -> { if (event.getAction() == MotionEvent.ACTION_DOWN) { - stop(); - } - return false; - }); - stopButton.setOnClickListener(v -> stop()); - stopButton.setOnKeyListener((v, keyCode, event) -> { - if (event.getAction() == KeyEvent.ACTION_DOWN && - (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER)) { - stop(); - return true; + viewModel.toggleControls(); + ImageViewerState state = viewModel.getState().getValue(); + if (state != null && state.isControlsVisible()) { + resetControlHideTimer(); + } } - return false; + return false; // Don't consume, let ViewPager handle swipes }); - stopItem.setActionView(stopButton); - MenuItem nextItem = menu.findItem(R.id.menu_next); - ImageButton nextButton = new ImageButton(this); - icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_skip_next_32, getTheme()), getTheme()); - nextButton.setImageDrawable(icon); - nextButton.setFocusable(true); - nextButton.setFocusableInTouchMode(true); - nextButton.setFocusableInTouchMode(true); - nextButton.setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - next(); - } - return false; + + // Setup window insets for edge-to-edge + ViewCompat.setOnApplyWindowInsetsListener(rootView, (v, windowInsets) -> { + Insets insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(insets.left, insets.top, insets.right, insets.bottom); + return WindowInsetsCompat.CONSUMED; }); - nextButton.setOnClickListener(v -> previous()); - nextButton.setOnKeyListener((v, keyCode, event) -> { - if (event.getAction() == KeyEvent.ACTION_DOWN && - (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER)) { - next(); - return true; + } + + private void initViewPager() { + pagerAdapter = new ImagePagerAdapter(this); + viewPager.setAdapter(pagerAdapter); + + viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + super.onPageSelected(position); + ImageViewerState state = viewModel.getState().getValue(); + if (state != null && position != state.getCurrentIndex()) { + viewModel.setCurrentIndex(position); + // Also update LocalImagePlayer's current index for reopening + if (playerService != null) { + Player player = playerService.getFirstCurrentPlayerOfType(LocalImagePlayer.class); + if (player instanceof LocalImagePlayer) { + ((LocalImagePlayer) player).setCurrentIndex(position); + } + } + } } - return false; }); - nextItem.setActionView(nextButton); - MenuItem previousItem = menu.findItem(R.id.menu_previous); - ImageButton previousButton = new ImageButton(this); - icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_skip_previous_32, getTheme()), getTheme()); - previousButton.setImageDrawable(icon); - previousButton.setFocusable(true); - previousButton.setFocusableInTouchMode(true); - previousButton.setFocusableInTouchMode(true); - previousButton.setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - previous(); + } + + @SuppressWarnings("unchecked") + private void loadIntentData(Bundle savedInstanceState, Intent intent) { + if (savedInstanceState != null) { + // Restore from saved state + boolean wasPlaying = savedInstanceState.getBoolean("pictureShowActive", false); + int currentIndex = savedInstanceState.getInt("currentImageIndex", 0); + ArrayList savedUris = savedInstanceState.getParcelableArrayList("imageUris"); + + viewModel.setImageUris(savedUris); + viewModel.setCurrentIndex(currentIndex); + + if (wasPlaying) { + viewModel.play(); } - return false; - }); - previousButton.setOnClickListener(v -> previous()); - previousButton.setOnKeyListener((v, keyCode, event) -> { - if (event.getAction() == KeyEvent.ACTION_DOWN && - (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER)) { - previous(); - return true; + } else { + // Load from intent + ArrayList uris = intent.getParcelableArrayListExtra(URIS); + if (uris != null) { + // Only set if we don't already have data from prefs + ImageViewerState existingState = viewModel.getState().getValue(); + if (existingState == null || existingState.getTotalImages() == 0) { + viewModel.setImageUris(uris); + } + } else if (intent.getData() != null) { + List singleUri = new ArrayList<>(); + singleUri.add(intent.getData()); + viewModel.setImageUris(singleUri); } - return false; - }); - previousItem.setActionView(previousButton); - MenuItem exitItem = menu.findItem(R.id.menu_exit); - ImageButton exitButton = new ImageButton(this); - icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_cancel_32, getTheme()), getTheme()); - exitButton.setImageDrawable(icon); - exitButton.setFocusable(true); - exitButton.setFocusableInTouchMode(true); - exitButton.setFocusableInTouchMode(true); - exitButton.setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - exit(); + + // Restore current index from intent if provided + int intentIndex = intent.getIntExtra("currentIndex", -1); + if (intentIndex >= 0) { + viewModel.setCurrentIndex(intentIndex); } - return false; - }); - exitButton.setOnClickListener(v -> exit()); - exitButton.setOnKeyListener((v, keyCode, event) -> { - if (event.getAction() == KeyEvent.ACTION_DOWN && - (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_DPAD_CENTER)) { - exit(); - return true; + + if (intent.getBooleanExtra(AUTO_START_SHOW, false)) { + viewModel.play(); } - return false; - }); - exitItem.setActionView(exitButton); - - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - Intent i; - if (item.getItemId() == R.id.menu_settings) { - - i = new Intent(this, SettingsActivity.class); - startActivity(i); - return true; - } - if (item.getItemId() == R.id.menu_next) { - next(); - return true; - } - if (item.getItemId() == R.id.menu_pause) { - pause(); - return true; - } - if (item.getItemId() == R.id.menu_play) { - play(); - return true; - } - if (item.getItemId() == R.id.menu_previous) { - previous(); - return true; - } - if (item.getItemId() == R.id.menu_stop) { - stop(); - return true; - } - if (item.getItemId() == R.id.yaacc_log) { - YaaccLogActivity.showLog(this); - return true; - } - if (item.getItemId() == R.id.yaacc_about) { - AboutActivity.showAbout(this); - return true; } - if (item.getItemId() == R.id.menu_exit) { - exit(); - return true; - } - return super.onOptionsItemSelected(item); - } - private void exit() { - Player player = playerService.getFirstCurrentPlayerOfType(LocalImagePlayer.class); - if (player != null) { - - player.exit(); - } + private void showNoValidUriError() { + Toast.makeText(this, R.string.no_valid_uri_data_found_to_display, Toast.LENGTH_LONG).show(); finish(); } - /** - * In case of device rotation the activity will be restarted. In this case - * the original intent which where used to start the activity won't change. - * So we only need to store the state of the activity. - */ - @Override - public void onSaveInstanceState(Bundle savedInstanceState) { - super.onSaveInstanceState(savedInstanceState); - savedInstanceState.putBoolean("pictureShowActive", pictureShowActive); - savedInstanceState.putInt("currentImageIndex", currentImageIndex); - if (!(imageUris instanceof ArrayList)) { - imageUris = new ArrayList<>(imageUris); + private void resetControlHideTimer() { + if (controlHideTimer != null) { + controlHideTimer.cancel(); } - savedInstanceState.putSerializable("imageUris", (ArrayList) imageUris); - } - - /** - * Create and start a timer for the next picture change. The timer runs only - * once. - */ - public void startTimer() { - pictureShowTimer = new Timer(); - pictureShowTimer.schedule(new TimerTask() { + controlHideTimer = new Timer(); + controlHideTimer.schedule(new TimerTask() { @Override public void run() { - Log.d(getClass().getName(), "TimerEvent" + this); - ImageViewerActivity.this.next(); + runOnUiThread(() -> viewModel.hideControls()); } - }, getDuration()); + }, 10000); } - /** - * Start playing the picture show. - */ - public void play() { - if (isProcessingCommand) - return; - isProcessingCommand = true; - if (currentImageIndex < imageUris.size()) { -// Start the pictureShow - pictureShowActive = true; - loadImage(); - isProcessingCommand = false; - } + @Override + protected void onStart() { + super.onStart(); + bindService(new Intent(this, PlayerService.class), this, Context.BIND_AUTO_CREATE); } - /** - * - */ - private void loadImage() { - if (retrieveImageTask != null - && retrieveImageTask.getStatus() == Status.RUNNING) { - return; - } - retrieveImageTask = new RetrieveImageTask(this); - Log.d(getClass().getName(), - "showImage(" + imageUris.get(currentImageIndex) + ")"); - retrieveImageTask.executeOnExecutor(((Yaacc) getApplicationContext()).getContentLoadExecutor(), imageUris.get(currentImageIndex)); + @Override + protected void onResume() { + super.onResume(); + broadcastReceiver = new ImageViewerBroadcastReceiver(this); + broadcastReceiver.registerReceiver(); + // Restore state when coming back from background + restoreStateFromPrefs(); } - /** - * Stop picture show timer and reset the current playlist index. Display - * default image; - */ - public void stop() { - if (isProcessingCommand) - return; - isProcessingCommand = true; - cancleTimer(); - currentImageIndex = 0; - showDefaultImage(); - pictureShowActive = false; - isProcessingCommand = false; - } - - /** - * - */ - private void cancleTimer() { - if (pictureShowTimer != null) { - pictureShowTimer.cancel(); + @Override + public void finish() { + // Clear saved state when activity is closed + android.content.SharedPreferences prefs = getSharedPreferences("imageviewer_state", MODE_PRIVATE); + prefs.edit().clear().apply(); + super.finish(); + } + + @Override + protected void onStop() { + super.onStop(); + try { + unbindService(this); + } catch (IllegalArgumentException e) { + YaaccLogger.d(getClass().getName(), "Ignore exception on unbind service"); } } - /** - * - */ - private void showDefaultImage() { - imageView.setImageDrawable(ResourcesCompat.getDrawable(getResources(), - R.drawable.yaacc192_32, getTheme())); + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + setIntent(intent); + loadIntentData(null, intent); } - /** - * Stop the timer. - */ - public void pause() { - if (isProcessingCommand) - return; - isProcessingCommand = true; - cancleTimer(); - pictureShowActive = false; - isProcessingCommand = false; + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + ImageViewerState state = viewModel.getState().getValue(); + if (state != null) { + outState.putBoolean("pictureShowActive", state.isPlaying()); + outState.putInt("currentImageIndex", state.getCurrentIndex()); + outState.putParcelableArrayList("imageUris", new ArrayList<>(state.getImageUris())); + + // Also save to SharedPreferences for persistence across activity restarts + saveStateToPrefs(state); + } } - /** - * show the previous image - */ - public void previous() { - if (isProcessingCommand) - return; - isProcessingCommand = true; - cancleTimer(); - currentImageIndex--; - if (currentImageIndex < 0) { - if (imageUris.size() > 0) { - currentImageIndex = imageUris.size() - 1; - } else { - currentImageIndex = 0; + private void saveStateToPrefs(ImageViewerState state) { + android.content.SharedPreferences prefs = getSharedPreferences("imageviewer_state", MODE_PRIVATE); + android.content.SharedPreferences.Editor editor = prefs.edit(); + editor.putInt("currentImageIndex", state.getCurrentIndex()); + editor.putBoolean("isPlaying", state.isPlaying()); + editor.putInt("imageCount", state.getTotalImages()); + // Save URIs as string set + if (state.getImageUris() != null && !state.getImageUris().isEmpty()) { + String[] uriStrings = new String[state.getImageUris().size()]; + for (int i = 0; i < state.getImageUris().size(); i++) { + uriStrings[i] = state.getImageUris().get(i).toString(); + } + editor.putStringSet("imageUris", new java.util.HashSet<>(java.util.Arrays.asList(uriStrings))); + } + editor.apply(); + } + + private void restoreStateFromPrefs() { + android.content.SharedPreferences prefs = getSharedPreferences("imageviewer_state", MODE_PRIVATE); + int index = prefs.getInt("currentImageIndex", -1); + java.util.Set uriSet = prefs.getStringSet("imageUris", null); + + if (uriSet != null && !uriSet.isEmpty()) { + // Restore URIs if not already loaded from intent + ImageViewerState state = viewModel.getState().getValue(); + if (state == null || state.getTotalImages() == 0) { + ArrayList uris = new ArrayList<>(); + for (String uriString : uriSet) { + uris.add(Uri.parse(uriString)); + } + viewModel.setImageUris(uris); } } - loadImage(); - isProcessingCommand = false; + + if (index >= 0) { + viewModel.setCurrentIndex(index); + viewPager.setCurrentItem(index, false); + } } - /** - * show the next image. - */ - public void next() { - if (isProcessingCommand) - return; - isProcessingCommand = true; - cancleTimer(); - currentImageIndex++; - if (currentImageIndex > imageUris.size() - 1) { - currentImageIndex = 0; -// pictureShowActive = false; restart after last image - } - loadImage(); - isProcessingCommand = false; - } - - /** - * Displays an image and start the picture show timer. - * - * @param image image - */ - public void showImage(final Drawable image) { - if (image == null) { - showDefaultImage(); - return; + // ServiceConnection + @Override + public void onServiceConnected(ComponentName name, IBinder binder) { + if (binder instanceof PlayerService.PlayerServiceBinder) { + YaaccLogger.d(getClass().getName(), "PlayerService connected"); + playerService = ((PlayerService.PlayerServiceBinder) binder).getService(); } - Log.d(this.getClass().getName(), "image bounds: " + image.getBounds()); - runOnUiThread(new Runnable() { - public void run() { - Log.d(getClass().getName(), - "Start set image: " + System.currentTimeMillis()); - imageView.setImageDrawable(image); - Log.d(getClass().getName(), - "End set image: " + System.currentTimeMillis()); - } - }); } - /** - * Return the configured slide stay duration - */ - private int getDuration() { - SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(this); - return Integer - .parseInt(preferences.getString( - getString(R.string.image_viewer_settings_duration_key), - "5000")); + @Override + public void onServiceDisconnected(ComponentName name) { + YaaccLogger.d(getClass().getName(), "PlayerService disconnected"); + playerService = null; } - // interface SwipeReceiver + // SwipeReceiver @Override public void onRightToLeftSwipe() { - if (imageUris.size() > 1) { - next(); + ImageViewerState state = viewModel.getState().getValue(); + if (state != null && state.hasMultipleImages()) { + viewModel.next(); } } @Override public void onLeftToRightSwipe() { - if (imageUris.size() > 1) { - previous(); + ImageViewerState state = viewModel.getState().getValue(); + if (state != null && state.hasMultipleImages()) { + viewModel.previous(); } } @Override - public void onTopToBottomSwipe() { -// do nothing - } + public void onTopToBottomSwipe() {} @Override - public void onBottomToTopSwipe() { -// do nothing - } + public void onBottomToTopSwipe() {} @Override public void beginOnTouchProcessing(View v, MotionEvent event) { - runOnUiThread(this::menuBarsShow); + viewModel.showControls(); + resetControlHideTimer(); } @Override public void endOnTouchProcessing(View v, MotionEvent event) { - startMenuHideTimer(); + resetControlHideTimer(); } - /** - * - */ - private void startMenuHideTimer() { - Timer menuHideTimer = new Timer(); - menuHideTimer.schedule(new TimerTask() { - @Override - public void run() { - runOnUiThread(() -> menuBarsHide()); - } - }, 10000); + // Called from ImageFragment when image is clicked + public void toggleControlsFromFragment() { + viewModel.toggleControls(); + ImageViewerState state = viewModel.getState().getValue(); + if (state != null && state.isControlsVisible()) { + resetControlHideTimer(); + } } - public boolean isPictureShowActive() { - return pictureShowActive && imageUris != null && imageUris.size() > 1; + // Public methods called by BroadcastReceiver + public void play() { + viewModel.play(); + Toast.makeText(this, R.string.play, Toast.LENGTH_SHORT).show(); } - private String getPositionString() { - return " (" + (currentImageIndex + 1) + "/" + imageUris.size() + ")"; + public void pause() { + viewModel.pause(); + Toast.makeText(this, R.string.pause, Toast.LENGTH_SHORT).show(); } - //FIXME https://stackoverflow.com/questions/26580117/android-how-to-create-overlay-drop-down-menu-similar-to-google-app - private void menuBarsHide() { - Log.d(getClass().getName(), "menuBarsHide"); - ActionBar actionBar = getSupportActionBar(); - if (actionBar == null) { - Log.d(getClass().getName(), "menuBarsHide ActionBar is null"); - return; - } + public void stop() { + viewModel.stop(); + Toast.makeText(this, R.string.stop, Toast.LENGTH_SHORT).show(); + } + + public void next() { + viewModel.next(); + } - actionBar.setDisplayShowTitleEnabled(false); - actionBar.setDisplayShowHomeEnabled(false); - getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - getWindow().clearFlags( - WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - getWindow().getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_LOW_PROFILE); - actionBar.hide(); // slides out - } - - private void menuBarsShow() { - Log.d(getClass().getName(), "menuBarsShow"); - ActionBar actionBar = getSupportActionBar(); - if (actionBar == null) { - Log.d(getClass().getName(), "menuBarsShow ActionBar is null"); - return; + public void previous() { + viewModel.previous(); + } + + public void exit() { + viewModel.stop(); + Player player = playerService != null ? + playerService.getFirstCurrentPlayerOfType(LocalImagePlayer.class) : null; + if (player != null) { + player.exit(); } - actionBar.setDisplayShowTitleEnabled(false); - actionBar.setDisplayShowHomeEnabled(false); - getWindow().addFlags( - WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); - getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); - getWindow().getDecorView().setSystemUiVisibility( - View.SYSTEM_UI_FLAG_VISIBLE); - actionBar.show(); - focusPauseButton(); + finish(); } @Override - public void onBackPressed() { - super.onBackPressed(); - exit(); + public boolean onCreateOptionsMenu(android.view.Menu menu) { + getMenuInflater().inflate(R.menu.activity_image_viewer, menu); + return true; } - private void focusPauseButton() { - if (menu != null) { - final MenuItem pauseItem = menu.findItem(R.id.menu_pause); + @Override + public boolean onOptionsItemSelected(android.view.MenuItem item) { + return handleMenuItem(item.getItemId()) || super.onOptionsItemSelected(item); + } - if (pauseItem != null) { - final View pauseView = findViewById(pauseItem.getItemId()); - if (pauseView != null) { - pauseView.requestFocus(); - } - } + // Menu handling + public boolean handleMenuItem(int itemId) { + if (itemId == R.id.menu_settings) { + startActivity(new Intent(this, SettingsActivity.class)); + return true; + } + if (itemId == R.id.menu_next) { + next(); + return true; } + if (itemId == R.id.menu_pause) { + pause(); + return true; + } + if (itemId == R.id.menu_play) { + play(); + return true; + } + if (itemId == R.id.menu_previous) { + previous(); + return true; + } + if (itemId == R.id.menu_stop) { + stop(); + return true; + } + if (itemId == R.id.yaacc_log) { + YaaccLogActivity.showLog(this); + return true; + } + if (itemId == R.id.yaacc_about) { + AboutActivity.showAbout(this); + return true; + } + if (itemId == R.id.menu_exit) { + exit(); + return true; + } + return false; + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + exit(); } -} \ No newline at end of file +} diff --git a/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerBroadcastReceiver.java b/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerBroadcastReceiver.java index 6db7e47b..053e58c3 100644 --- a/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerBroadcastReceiver.java +++ b/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerBroadcastReceiver.java @@ -21,7 +21,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.util.Log; +import de.yaacc.util.YaaccLogger; /** * @author Tobias Schoene (openbit) @@ -44,7 +44,7 @@ public ImageViewerBroadcastReceiver() { } public ImageViewerBroadcastReceiver(ImageViewerActivity imageViewer) { - Log.d(this.getClass().getName(), "Starting Broadcast Receiver..."); + YaaccLogger.d(this.getClass().getName(), "Starting Broadcast Receiver..."); assert (imageViewer != null); this.imageViewer = imageViewer; @@ -55,9 +55,9 @@ public ImageViewerBroadcastReceiver(ImageViewerActivity imageViewer) { */ @Override public void onReceive(Context context, Intent intent) { - Log.d(this.getClass().getName(), "Received Action: " + intent.getAction()); + YaaccLogger.d(this.getClass().getName(), "Received Action: " + intent.getAction()); if (imageViewer == null) return; - Log.d(this.getClass().getName(), "Execute Action on imageViewer: " + imageViewer); + YaaccLogger.d(this.getClass().getName(), "Execute Action on imageViewer: " + imageViewer); if (ACTION_PLAY.equals(intent.getAction())) { imageViewer.play(); } else if (ACTION_PAUSE.equals(intent.getAction())) { @@ -76,7 +76,7 @@ public void onReceive(Context context, Intent intent) { } public void registerReceiver() { - Log.d(this.getClass().getName(), "Register Receiver"); + YaaccLogger.d(this.getClass().getName(), "Register Receiver"); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_PLAY); intentFilter.addAction(ACTION_PAUSE); diff --git a/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerState.java b/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerState.java new file mode 100644 index 00000000..86d084e8 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerState.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.imageviewer; + +import android.net.Uri; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * State container for ImageViewerActivity. + */ +public class ImageViewerState implements Serializable { + private List imageUris = new ArrayList<>(); + private int currentIndex = 0; + private boolean isPlaying = false; + private long durationMs = 5000; + private boolean controlsVisible = false; + + public List getImageUris() { + return imageUris; + } + + public void setImageUris(List imageUris) { + this.imageUris = imageUris != null ? imageUris : new ArrayList<>(); + } + + public int getCurrentIndex() { + return currentIndex; + } + + public void setCurrentIndex(int currentIndex) { + this.currentIndex = currentIndex; + } + + public boolean isPlaying() { + return isPlaying; + } + + public void setPlaying(boolean playing) { + isPlaying = playing; + } + + public long getDurationMs() { + return durationMs; + } + + public void setDurationMs(long durationMs) { + this.durationMs = durationMs; + } + + public boolean isControlsVisible() { + return controlsVisible; + } + + public void setControlsVisible(boolean controlsVisible) { + this.controlsVisible = controlsVisible; + } + + public int getTotalImages() { + return imageUris != null ? imageUris.size() : 0; + } + + public boolean hasMultipleImages() { + return getTotalImages() > 1; + } +} diff --git a/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerViewModel.java b/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerViewModel.java new file mode 100644 index 00000000..ee7b6117 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/imageviewer/ImageViewerViewModel.java @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.imageviewer; + +import android.app.Application; +import de.yaacc.util.YaaccLogger; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +/** + * ViewModel for ImageViewerActivity. + */ +public class ImageViewerViewModel extends AndroidViewModel { + private final MutableLiveData _state = new MutableLiveData<>(new ImageViewerState()); + public LiveData getState() { return _state; } + + private Timer slideshowTimer; + + public ImageViewerViewModel(@NonNull Application application) { + super(application); + } + + public void setImageUris(List uris) { + ImageViewerState current = _state.getValue(); + if (current != null) { + current.setImageUris(uris); + current.setCurrentIndex(0); + current.setPlaying(false); + _state.setValue(current); + } + } + + public void setCurrentIndex(int currentIndex) { + ImageViewerState current = _state.getValue(); + if (current != null) { + current.setCurrentIndex(currentIndex); + _state.setValue(current); + } + } + + public void next() { + ImageViewerState current = _state.getValue(); + if (current == null) return; + + int nextIndex = current.getCurrentIndex() + 1; + if (nextIndex >= current.getTotalImages()) { + nextIndex = 0; + } + current.setCurrentIndex(nextIndex); + _state.postValue(current); // Use postValue for background thread safety + } + + public void previous() { + ImageViewerState current = _state.getValue(); + if (current == null) return; + + int prevIndex = current.getCurrentIndex() - 1; + if (prevIndex < 0) { + prevIndex = Math.max(0, current.getTotalImages() - 1); + } + current.setCurrentIndex(prevIndex); + _state.postValue(current); // Use postValue for background thread safety + } + + public void play() { + ImageViewerState current = _state.getValue(); + if (current == null || current.getTotalImages() == 0) return; + + current.setPlaying(true); + _state.setValue(current); + startSlideshowTimer(); + } + + public void pause() { + cancelSlideshowTimer(); + ImageViewerState current = _state.getValue(); + if (current != null) { + current.setPlaying(false); + _state.setValue(current); + } + } + + public void stop() { + cancelSlideshowTimer(); + ImageViewerState current = _state.getValue(); + if (current != null) { + current.setPlaying(false); + current.setCurrentIndex(0); + _state.setValue(current); + } + } + + public void toggleControls() { + ImageViewerState current = _state.getValue(); + if (current != null) { + current.setControlsVisible(!current.isControlsVisible()); + _state.setValue(current); + } + } + + public void showControls() { + ImageViewerState current = _state.getValue(); + if (current != null) { + current.setControlsVisible(true); + _state.setValue(current); + } + } + + public void hideControls() { + ImageViewerState current = _state.getValue(); + if (current != null) { + current.setControlsVisible(false); + _state.setValue(current); + } + } + + public void setDurationMs(long durationMs) { + ImageViewerState current = _state.getValue(); + if (current != null) { + current.setDurationMs(durationMs); + _state.setValue(current); + } + } + + private void startSlideshowTimer() { + cancelSlideshowTimer(); + ImageViewerState current = _state.getValue(); + if (current == null) return; + + slideshowTimer = new Timer(); + slideshowTimer.schedule(new TimerTask() { + @Override + public void run() { + if (_state.getValue() != null && _state.getValue().isPlaying()) { + // Post to main thread + _state.postValue(_state.getValue()); + next(); + } + } + }, current.getDurationMs(), current.getDurationMs()); + } + + private void cancelSlideshowTimer() { + if (slideshowTimer != null) { + slideshowTimer.cancel(); + slideshowTimer = null; + } + } + + @Override + protected void onCleared() { + super.onCleared(); + cancelSlideshowTimer(); + } +} diff --git a/yaacc/src/main/java/de/yaacc/imageviewer/RetrieveImageTask.java b/yaacc/src/main/java/de/yaacc/imageviewer/RetrieveImageTask.java deleted file mode 100644 index bb6cc022..00000000 --- a/yaacc/src/main/java/de/yaacc/imageviewer/RetrieveImageTask.java +++ /dev/null @@ -1,229 +0,0 @@ -/* - * - * Copyright (C) 2013 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.imageviewer; - -import android.app.Dialog; -import android.content.ContentResolver; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.AsyncTask; -import android.util.DisplayMetrics; -import android.util.Log; -import android.view.Window; -import android.widget.Toast; - -import java.io.FileNotFoundException; -import java.io.FilterInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.MalformedURLException; - -import de.yaacc.R; - -/** - * Background task for retrieving network images. - * - * @author Tobias Schoene (openbit) - */ -public class RetrieveImageTask extends AsyncTask { - - private final ImageViewerActivity imageViewerActivity; - private Dialog pd; - - public RetrieveImageTask(ImageViewerActivity imageViewerActivity) { - this.imageViewerActivity = imageViewerActivity; - } - - @Override - protected Void doInBackground(Uri... imageUris) { - if (imageUris == null || imageUris.length == 0) { - return null; - } - if (imageUris.length > 1) { - throw new IllegalStateException("more than one uri to be retrieved"); - } - retrieveImage(imageUris[0]); - // This async task has no result - return null; - } - - /* - * (non-Javadoc) - * - * @see android.os.AsyncTask#onPostExecute(java.lang.Object) - */ - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - if (pd != null) { - pd.dismiss(); - } - // Start Timer after new image is loaded - if (imageViewerActivity.isPictureShowActive()) { - imageViewerActivity.startTimer(); - } - } - - /* - * (non-Javadoc) - * - * @see android.os.AsyncTask#onPreExecute() - */ - @Override - protected void onPreExecute() { - super.onPreExecute(); - imageViewerActivity.runOnUiThread(() -> { - pd = new Dialog(imageViewerActivity); - pd.requestWindowFeature(Window.FEATURE_NO_TITLE); - pd.setContentView(R.layout.yaacc_progress_dialog); - pd.getWindow().setBackgroundDrawableResource(android.R.color.transparent); - pd.show(); - }); - - } - - /** - * retrieves an image an stores them in the image cache of the - * ImageViewerActivity. - * - * @param imageUri the ImageUri - */ - private void retrieveImage(Uri imageUri) { - { - Log.d(getClass().getName(), "Load imageUri: " + imageUri); - Drawable image = null; - - try { - if (imageUri != null) { - - int heightPixels = imageViewerActivity.getResources() - .getDisplayMetrics().heightPixels; - int widthPixels = imageViewerActivity.getResources() - .getDisplayMetrics().widthPixels; - Log.d(getClass().getName(), - "Decode image: " + System.currentTimeMillis()); - Log.d(getClass().getName(), "Size width,height: " - + widthPixels + "," + heightPixels); - Bitmap bitmap = decodeSampledBitmapFromStream(imageUri, - widthPixels, heightPixels); - image = new BitmapDrawable( - imageViewerActivity.getResources(), bitmap); - Log.d(getClass().getName(), - "Got image: " + System.currentTimeMillis()); - Log.d(getClass().getName(), "image: " + image); - } - } catch (final Exception e) { - image = Drawable.createFromPath("@drawable/ic_launcher"); - Log.d(getClass().getName(), "Error while processing image", e); - imageViewerActivity.runOnUiThread(() -> { - Toast toast = Toast.makeText(imageViewerActivity, - "Exception:" + e.getMessage(), - Toast.LENGTH_LONG); - toast.show(); - }); - - } - - final Drawable finalImage = image; - imageViewerActivity.runOnUiThread(new Runnable() { - public void run() { - Log.d(getClass().getName(), - "Start show image: " + System.currentTimeMillis()); - imageViewerActivity.showImage(finalImage); - Log.d(getClass().getName(), - "End show image: " + System.currentTimeMillis()); - } - }); - - } - } - - /** - * @param imageUri the image uri - * @return return image stream - * @throws FileNotFoundException file not found - * @throws IOException something went wrong - * @throws MalformedURLException wrong uri - */ - private InputStream getUriAsStream(Uri imageUri) - throws FileNotFoundException, IOException, MalformedURLException { - InputStream is; - Log.d(getClass().getName(), "Start load: " + System.currentTimeMillis()); - if (ContentResolver.SCHEME_CONTENT.equals(imageUri.getScheme())) { - is = imageViewerActivity.getContentResolver().openInputStream( - imageUri); - } else { - is = (InputStream) new java.net.URL(imageUri.toString()) - .getContent(); - } - Log.d(getClass().getName(), "Stop load: " + System.currentTimeMillis()); - Log.d(getClass().getName(), "InputStream: " + is); - return is; - } - - private Bitmap decodeSampledBitmapFromStream(Uri imageUri, int reqWidth, - int reqHeight) throws IOException { - InputStream is = getUriAsStream(imageUri); - final BitmapFactory.Options options = new BitmapFactory.Options(); - options.inJustDecodeBounds = false; - options.outHeight = reqHeight; - options.outWidth = reqWidth; - options.inDensity = DisplayMetrics.DENSITY_LOW; - options.inSampleSize = 2; - Log.d(this.getClass().getName(), - "displaying image size width, height, inSampleSize " - + options.outWidth + "," + options.outHeight + "," - + options.inSampleSize); - Log.d(this.getClass().getName(), "free meomory before image load: " - + Runtime.getRuntime().freeMemory()); - Bitmap bitmap = BitmapFactory.decodeStream(new FlushedInputStream(is), - null, options); - Log.d(this.getClass().getName(), "free meomory after image load: " - + Runtime.getRuntime().freeMemory()); - return bitmap; - } - - static class FlushedInputStream extends FilterInputStream { - public FlushedInputStream(InputStream inputStream) { - super(inputStream); - } - - @Override - public long skip(long n) throws IOException { - long totalBytesSkipped = 0L; - while (totalBytesSkipped < n) { - long bytesSkipped = in.skip(n - totalBytesSkipped); - if (bytesSkipped == 0L) { - int byte_ = read(); - if (byte_ < 0) { - break; // we reached EOF - } else { - bytesSkipped = 1; // we read one byte - } - } - totalBytesSkipped += bytesSkipped; - } - return totalBytesSkipped; - } - } - -} diff --git a/yaacc/src/main/java/de/yaacc/musicplayer/BackgoundMusicServiceListener.java b/yaacc/src/main/java/de/yaacc/musicplayer/BackgoundMusicServiceListener.java deleted file mode 100644 index fc151ef1..00000000 --- a/yaacc/src/main/java/de/yaacc/musicplayer/BackgoundMusicServiceListener.java +++ /dev/null @@ -1,5 +0,0 @@ -package de.yaacc.musicplayer; - -public interface BackgoundMusicServiceListener { - void onCompletion(); -} diff --git a/yaacc/src/main/java/de/yaacc/musicplayer/BackgroundMusicBroadcastReceiver.java b/yaacc/src/main/java/de/yaacc/musicplayer/BackgroundMusicBroadcastReceiver.java deleted file mode 100644 index fbc7a860..00000000 --- a/yaacc/src/main/java/de/yaacc/musicplayer/BackgroundMusicBroadcastReceiver.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright (C) 2013 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.musicplayer; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.net.Uri; -import android.util.Log; - -/** - * @author Tobias Schoene (openbit) - */ -public class BackgroundMusicBroadcastReceiver extends BroadcastReceiver { - - public static String ACTION_PLAY = "de.yaacc.musicplayer.ActionPlay"; - public static String ACTION_STOP = "de.yaacc.musicplayer.ActionStop"; - public static String ACTION_PAUSE = "de.yaacc.musicplayer.ActionPause"; - public static String ACTION_SET_DATA = "de.yaacc.musicplayer.ActionSetData"; - public static String ACTION_SET_DATA_URI_PARAM = "de.yaacc.musicplayer.ActionSetDataUriParam"; - public static String ACTION_SEEK_TO = "de.yaacc.musicplayer.ActionSeekTo"; - public static String ACTION_SEEK_TO_PARAM = "de.yaacc.musicplayer.ActionSeekToParam"; - - - private final BackgroundMusicService backgroundMusicService; - - - public BackgroundMusicBroadcastReceiver(BackgroundMusicService backgroundMusicService) { - Log.d(this.getClass().getName(), "Starting Broadcast Receiver..."); - assert (backgroundMusicService != null); - this.backgroundMusicService = backgroundMusicService; - - } - - /* (non-Javadoc) - * @see android.content.BroadcastReceiver#onReceive(android.content.Context, android.content.Intent) - */ - @Override - public void onReceive(Context context, Intent intent) { - Log.d(this.getClass().getName(), "Received Action: " + intent.getAction()); - if (backgroundMusicService == null) return; - Log.d(this.getClass().getName(), "Execute Action on backgroundMusicService: " + backgroundMusicService); - if (ACTION_PLAY.equals(intent.getAction())) { - backgroundMusicService.play(); - } else if (ACTION_PAUSE.equals(intent.getAction())) { - backgroundMusicService.pause(); - } else if (ACTION_STOP.equals(intent.getAction())) { - backgroundMusicService.stop(); - } else if (ACTION_SET_DATA.equals(intent.getAction())) { - backgroundMusicService.setMusicUri((Uri) intent.getParcelableExtra(ACTION_SET_DATA_URI_PARAM)); - } else if (ACTION_SEEK_TO.equals(intent.getAction())) { - backgroundMusicService.seekTo(intent.getIntExtra(ACTION_SEEK_TO_PARAM, 0)); - } - - - } - - public void registerReceiver() { - Log.d(this.getClass().getName(), "Register BackgroundMusicBroadcastReceiver"); - IntentFilter intentFilter = new IntentFilter(); - intentFilter.addAction(ACTION_PLAY); - intentFilter.addAction(ACTION_PAUSE); - intentFilter.addAction(ACTION_STOP); - - intentFilter.addAction(ACTION_SET_DATA); - backgroundMusicService.registerReceiver(this, intentFilter); - - - } - -} diff --git a/yaacc/src/main/java/de/yaacc/musicplayer/BackgroundMusicService.java b/yaacc/src/main/java/de/yaacc/musicplayer/BackgroundMusicService.java deleted file mode 100644 index 5126d27e..00000000 --- a/yaacc/src/main/java/de/yaacc/musicplayer/BackgroundMusicService.java +++ /dev/null @@ -1,308 +0,0 @@ -/* - * - * Copyright (C) 2013 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.musicplayer; - -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Intent; -import android.media.AudioAttributes; -import android.media.AudioManager; -import android.media.MediaPlayer; -import android.net.Uri; -import android.os.Binder; -import android.os.IBinder; -import android.os.PowerManager; -import android.util.Log; - -import androidx.core.app.NotificationCompat; - -import java.util.ArrayList; -import java.util.List; - -import de.yaacc.R; -import de.yaacc.Yaacc; -import de.yaacc.browser.TabBrowserActivity; -import de.yaacc.util.NotificationId; - -/** - * A simple service for playing music in background. - * - * @author Tobias Schoene (openbit) - */ -public class BackgroundMusicService extends Service { - public static final String URIS = "URIS_PARAM"; // String Intent parameter - private final BackgroundMusicServiceBinder binder = new BackgroundMusicServiceBinder(); - private MediaPlayer player; - private BackgroundMusicBroadcastReceiver backgroundMusicBroadcastReceiver; - private int duration = 0; - private List serviceListener = new ArrayList<>(); - - public BackgroundMusicService() { - super(); - } - - /* - * (non-Javadoc) - * - * @see android.app.Service#onCreate() - */ - @Override - public void onCreate() { - super.onCreate(); - Log.d(this.getClass().getName(), "On Create"); - - // Start foreground immediately with a basic notification - Notification minimalNotification = new NotificationCompat.Builder(this, Yaacc.NOTIFICATION_CHANNEL_ID) - .setContentTitle("Background Music Service") - .setContentText("Initializing...") - .setSmallIcon(R.drawable.ic_notification_default) - .setGroup(Yaacc.NOTIFICATION_GROUP_KEY) // Ensure group is set for consistency - .setSilent(true) // Keep it silent initially - .build(); - startForeground(NotificationId.BACKGROUND_MUSIC_SERVICE.getId(), minimalNotification); - - // Perform potentially long-running initializations - ((Yaacc) getApplicationContext()).createYaaccGroupNotification(); - - // Now create and set the final notification - Intent notificationIntent = new Intent(this, TabBrowserActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(this, - 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); - Notification notification = new NotificationCompat.Builder(this, Yaacc.NOTIFICATION_CHANNEL_ID) - .setContentTitle("Background Music Service") - .setSilent(true) - .setContentText("running") - .setSmallIcon(R.drawable.ic_notification_default) - .setContentIntent(pendingIntent) - .setGroup(Yaacc.NOTIFICATION_GROUP_KEY) - .build(); - // Update the notification by calling startForeground again - startForeground(NotificationId.BACKGROUND_MUSIC_SERVICE.getId(), notification); - - } - - /* - * (non-Javadoc) - * - * @see android.app.Service#onDestroy() - */ - @Override - public void onDestroy() { - Log.d(this.getClass().getName(), "On Destroy"); - if (player != null) { - player.stop(); - player.release(); - //remove player after releasing - player = null; - } - if (backgroundMusicBroadcastReceiver != null) { - unregisterReceiver(backgroundMusicBroadcastReceiver); - } - } - - /* - * (non-Javadoc) - * - * @see android.app.Service#onBind(android.content.Intent) - */ - @Override - public IBinder onBind(Intent intent) { - Log.d(this.getClass().getName(), "On Bind"); - return binder; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - Log.d(this.getClass().getName(), "Received start id " + startId + ": " + intent); - initialize(intent); - return START_STICKY; - } - - private void initialize(Intent intent) { - backgroundMusicBroadcastReceiver = new BackgroundMusicBroadcastReceiver(this); - backgroundMusicBroadcastReceiver.registerReceiver(); - if (player != null) { - player.stop(); - player.release(); - //remove player after releasing - player = null; - } - try { - if (intent != null && intent.getData() != null) { - setMusicUri(intent.getData()); - } - } catch (Exception e) { - Log.e(this.getClass().getName(), "Ignoring exception while changing datasource uri", e); - - - } - - } - - /** - * stop current music play - */ - public void stop() { - if (player != null) { - try { - player.stop(); - } catch (Exception ex) { - Log.d(getClass().getName(), "Ignoring exception on stop action: ", ex); - } - } - } - - /** - * start current music play - */ - public void play() { - //final ActionState actionState = new ActionState(); - if (player != null && !player.isPlaying()) { - try { - player.start(); - } catch (Exception ex) { - Log.d(getClass().getName(), "Ignoring exception on start action: ", ex); - } - - } - } - - /** - * pause current music play - */ - public void pause() { - if (player != null) { - try { - player.pause(); - } catch (Exception ex) { - Log.d(getClass().getName(), "Ignoring exception on pause action: ", ex); - } - } - } - - /** - * Seeks to position - * - * @param pos the position - */ - public void seekTo(long pos) { - if (player != null) { - try { - player.seekTo(Long.valueOf(pos).intValue()); - } catch (Exception ex) { - Log.d(getClass().getName(), "Ignoring exception on steekTo action: ", ex); - } - } - - } - - /** - * change music uri - * - * @param uri the uri to play - */ - public void setMusicUri(Uri uri) { - Log.d(this.getClass().getName(), "changing datasource uri to:" + uri.toString()); - if (player != null) { - player.stop(); - player.reset(); - } else { - player = new MediaPlayer(); - } - player.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK); - player.setOnErrorListener((MediaPlayer mediaPlayer, int what, int extra) -> { - Log.e(getClass().getName(), "Error in State " + what + " extra: " + extra); - return false; - }); - player.setOnCompletionListener((mp) -> { - serviceListener.stream().forEach(it -> it.onCompletion()); - }); - player.setVolume(100, 100); - - - player.setAudioAttributes( - new AudioAttributes.Builder() - .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC) - .setUsage(AudioAttributes.USAGE_MEDIA) - .setLegacyStreamType(AudioManager.STREAM_MUSIC) - .build()); - player.setOnPreparedListener(mediaPlayer -> - duration = mediaPlayer.getDuration()); - - - try { - player.setDataSource(uri.toString()); - } catch (Exception e) { - Log.e(this.getClass().getName(), "Ignoring exception while changing datasource uri", e); - } - - - try { - player.prepare(); - } catch (Exception e) { - Log.e(this.getClass().getName(), "Ignoring exception while preparing media player", e); - } - - } - - /** - * returns the duration of the current track - * - * @return the duration - */ - public int getDuration() { - - return duration; - } - - /** - * return the current position in the playing track - * - * @return the position - */ - public int getCurrentPosition() { - int currentPosition = 0; - if (player != null) { - try { - currentPosition = player.getCurrentPosition(); - } catch (Exception ex) { - Log.d(getClass().getName(), "Caught player exception", ex); - } - } - - return currentPosition; - } - - public void removeServiceListener(BackgoundMusicServiceListener listener) { - serviceListener.remove(listener); - } - - public void addServiceListener(BackgoundMusicServiceListener listener) { - serviceListener.add(listener); - } - - - public class BackgroundMusicServiceBinder extends Binder { - public BackgroundMusicService getService() { - return BackgroundMusicService.this; - } - } - -} diff --git a/yaacc/src/main/java/de/yaacc/player/AVTransportController.java b/yaacc/src/main/java/de/yaacc/player/AVTransportController.java index 3c3558ae..8337e974 100644 --- a/yaacc/src/main/java/de/yaacc/player/AVTransportController.java +++ b/yaacc/src/main/java/de/yaacc/player/AVTransportController.java @@ -20,15 +20,18 @@ import android.content.ComponentName; import android.os.IBinder; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.action.ActionInvocation; import org.fourthline.cling.model.message.UpnpResponse; import org.fourthline.cling.model.meta.Device; import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.support.avtransport.callback.Next; -import org.fourthline.cling.support.avtransport.callback.Play; -import org.fourthline.cling.support.avtransport.callback.Previous; +import de.yaacc.upnp.callback.avtransport.Next; +import de.yaacc.upnp.callback.avtransport.Play; +import de.yaacc.upnp.callback.avtransport.Previous; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import de.yaacc.R; import de.yaacc.upnp.ActionState; @@ -37,10 +40,12 @@ public class AVTransportController extends AVTransportPlayer { public static final String DEVICE_ID = "DEVICE_ID"; + private final ExecutorService executorService; public AVTransportController(UpnpClient upnpClient, Device receiverDevice) { super(upnpClient, receiverDevice, "", "", null); + executorService = Executors.newFixedThreadPool(20); String deviceName = receiverDevice.getDetails().getFriendlyName() + " - " + receiverDevice.getDisplayString(); deviceName = upnpClient.getContext() .getString(R.string.playerNameAvTransport) @@ -51,14 +56,14 @@ public AVTransportController(UpnpClient upnpClient, Device receiverDevi public void onServiceConnected(ComponentName className, IBinder binder) { if (binder instanceof PlayerService.PlayerServiceBinder) { - Log.d(getClass().getName(), "ignore service connected"); + YaaccLogger.d(getClass().getName(), "ignore service connected"); } } public void onServiceDisconnected(ComponentName className) { - Log.d(getClass().getName(), "ignore service disconnected"); + YaaccLogger.d(getClass().getName(), "ignore service disconnected"); } @@ -80,25 +85,25 @@ public void next() { return; Service service = getUpnpClient().getAVTransportService(getDevice()); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found on Device: " + getDevice().getDisplayString()); return; } - Log.d(getClass().getName(), "Action Next"); + YaaccLogger.d(getClass().getName(), "Action Next"); final ActionState actionState = new ActionState(); actionState.actionFinished = false; - Next actionCallback = new Next(service) { + Next actionCallback = new Next(service, getHttpRequestSender()) { @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), upnpresponse != null ? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); actionState.actionFinished = true; } @@ -108,7 +113,7 @@ public void success(ActionInvocation actioninvocation) { actionState.actionFinished = true; } }; - getUpnpClient().getControlPoint().execute(actionCallback); + executorService.execute(actionCallback); } @Override @@ -117,25 +122,25 @@ public void previous() { return; Service service = getUpnpClient().getAVTransportService(getDevice()); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found on Device: " + getDevice().getDisplayString()); return; } - Log.d(getClass().getName(), "Action Previous"); + YaaccLogger.d(getClass().getName(), "Action Previous"); final ActionState actionState = new ActionState(); actionState.actionFinished = false; - Previous actionCallback = new Previous(service) { + Previous actionCallback = new Previous(service, getHttpRequestSender()) { @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), upnpresponse != null ? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); actionState.actionFinished = true; } @@ -145,7 +150,7 @@ public void success(ActionInvocation actioninvocation) { actionState.actionFinished = true; } }; - getUpnpClient().getControlPoint().execute(actionCallback); + executorService.execute(actionCallback); } @Override @@ -154,25 +159,25 @@ public void play() { return; Service service = getUpnpClient().getAVTransportService(getDevice()); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found on Device: " + getDevice().getDisplayString()); return; } - Log.d(getClass().getName(), "Action Play"); + YaaccLogger.d(getClass().getName(), "Action Play"); final ActionState actionState = new ActionState(); actionState.actionFinished = false; - Play actionCallback = new Play(service) { + Play actionCallback = new Play(service, getHttpRequestSender()) { @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), upnpresponse != null ? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); actionState.actionFinished = true; } @@ -182,6 +187,6 @@ public void success(ActionInvocation actioninvocation) { actionState.actionFinished = true; } }; - getUpnpClient().getControlPoint().execute(actionCallback); + executorService.execute(actionCallback); } } diff --git a/yaacc/src/main/java/de/yaacc/player/AVTransportPlayer.java b/yaacc/src/main/java/de/yaacc/player/AVTransportPlayer.java index 5d5cbde1..4e86366a 100644 --- a/yaacc/src/main/java/de/yaacc/player/AVTransportPlayer.java +++ b/yaacc/src/main/java/de/yaacc/player/AVTransportPlayer.java @@ -17,10 +17,35 @@ */ package de.yaacc.player; +import android.app.Activity; import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.net.Uri; -import android.util.Log; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.preference.PreferenceManager; +import android.support.v4.media.session.MediaSessionCompat; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.media3.common.MediaItem; +import androidx.media3.common.Player; +import androidx.media3.common.util.BitmapLoader; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaSession; +import androidx.media3.session.SessionCommand; +import androidx.media3.session.SessionCommands; +import androidx.media3.ui.PlayerNotificationManager; + +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; import org.fourthline.cling.model.action.ActionInvocation; import org.fourthline.cling.model.message.UpnpResponse; @@ -28,30 +53,50 @@ import org.fourthline.cling.model.meta.Icon; import org.fourthline.cling.model.meta.RemoteDevice; import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.support.avtransport.callback.GetPositionInfo; -import org.fourthline.cling.support.avtransport.callback.Pause; -import org.fourthline.cling.support.avtransport.callback.Play; -import org.fourthline.cling.support.avtransport.callback.Seek; -import org.fourthline.cling.support.avtransport.callback.SetAVTransportURI; -import org.fourthline.cling.support.avtransport.callback.Stop; +import org.fourthline.cling.model.types.UDAServiceType; import org.fourthline.cling.support.contentdirectory.DIDLParser; import org.fourthline.cling.support.model.DIDLContent; import org.fourthline.cling.support.model.DIDLObject; import org.fourthline.cling.support.model.PositionInfo; +import org.fourthline.cling.support.model.ProtocolInfo; +import org.fourthline.cling.support.model.ProtocolInfos; +import org.fourthline.cling.support.model.Res; +import org.fourthline.cling.support.model.TransportInfo; +import org.fourthline.cling.support.model.TransportState; import org.fourthline.cling.support.model.item.Item; import java.net.URI; import java.net.URL; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.HashMap; +import java.util.Map; import java.util.TimeZone; import java.util.Timer; import java.util.TimerTask; import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import de.yaacc.R; +import de.yaacc.Yaacc; +import de.yaacc.settings.SettingsFragment; import de.yaacc.upnp.ActionState; import de.yaacc.upnp.UpnpClient; +import de.yaacc.upnp.callback.avtransport.GetPositionInfo; +import de.yaacc.upnp.callback.avtransport.GetTransportInfo; +import de.yaacc.upnp.callback.avtransport.Pause; +import de.yaacc.upnp.callback.avtransport.Play; +import de.yaacc.upnp.callback.avtransport.Seek; +import de.yaacc.upnp.callback.avtransport.SetAVTransportURI; +import de.yaacc.upnp.callback.avtransport.Stop; +import de.yaacc.upnp.callback.connectionmanager.GetProtocolInfo; +import de.yaacc.upnp.server.http.YaaccUpnpServerContentHttpHandler; +import de.yaacc.util.InterfaceResolutionHelper; +import de.yaacc.util.YaaccLogger; +import de.yaacc.util.image.IconDownloadCacheHandler; import de.yaacc.util.image.ImageDownloader; /** @@ -59,14 +104,25 @@ * * @author Tobias Schoene (openbit) */ +@UnstableApi public class AVTransportPlayer extends AbstractPlayer { + + private final ExecutorService executorService; private String deviceId = ""; private int id; private String contentType; private PositionInfo currentPositionInfo; private ActionState positionActionState = null; private URI albumArtUri; + private AVTransportPlayerWrapper playerWrapper; + private MediaSession media3Session; + private PlayerNotificationManager notificationManager; + private int consecutivePositionFailures = 0; + + // Retry tracking for critical commands + private static final int MAX_RETRIES = 30; + private final Map commandRetries = new HashMap<>(); /** @@ -79,8 +135,15 @@ public AVTransportPlayer(UpnpClient upnpClient, Device receiverDevice, setName(name); setShortName(shortName); this.contentType = contentType; - id = Math.abs(UUID.randomUUID().hashCode()); + // id already initialized in base constructor setDeviceIcon(receiverDevice); + + // Configure MediaSession for remote volume control now that device is set + new Handler(Looper.getMainLooper()).post(() -> { + if (getMediaSession() != null) { + configureMediaSession(getMediaSession()); + } + }); } /** @@ -88,6 +151,209 @@ public AVTransportPlayer(UpnpClient upnpClient, Device receiverDevice, */ public AVTransportPlayer(UpnpClient upnpClient) { super(upnpClient); + executorService = Executors.newFixedThreadPool(20); + + // Generate ID first (required for notification) + id = Math.abs(UUID.randomUUID().hashCode()); + + // Initialize Media3 Player wrapper + playerWrapper = new AVTransportPlayerWrapper(this, null); + BitmapLoader bitmapLoader = new BitmapLoader() { + @Override + public ListenableFuture decodeBitmap(byte[] data) { + SettableFuture future = SettableFuture.create(); + try { + Bitmap bitmap = android.graphics.BitmapFactory.decodeByteArray(data, 0, data.length); + future.set(bitmap); + } catch (Exception e) { + future.setException(e); + } + return future; + } + + @Override + public ListenableFuture loadBitmap(Uri uri) { + return loadBitmap(uri, null); + } + + @Override + public ListenableFuture loadBitmap(Uri uri, @Nullable BitmapFactory.Options options) { + YaaccLogger.e(getClass().getName(), "BitmapLoader.loadBitmap called with uri: " + uri); + + // Check cache first + IconDownloadCacheHandler cache = IconDownloadCacheHandler.getInstance(); + Bitmap cachedBitmap = cache.getBitmap(uri, 512, 512); + if (cachedBitmap != null) { + YaaccLogger.e(getClass().getName(), "Returning cached bitmap: " + cachedBitmap.getWidth() + "x" + cachedBitmap.getHeight()); + return Futures.immediateFuture(cachedBitmap); + } + + SettableFuture future = SettableFuture.create(); + // Load bitmap in background using ImageDownloader + ((Yaacc) getContext().getApplicationContext()).getContentLoadExecutor().execute(() -> { + try { + YaaccLogger.e(getClass().getName(), "Loading bitmap from: " + uri); + Bitmap bitmap = new ImageDownloader().retrieveImageWithCertainSize(uri, 512, 512); + if (bitmap != null) { + cache.addBitmap(uri, 512, 512, bitmap); + } + YaaccLogger.e(getClass().getName(), "Bitmap loaded: " + (bitmap != null ? bitmap.getWidth() + "x" + bitmap.getHeight() : "null")); + future.set(bitmap); + YaaccLogger.e(getClass().getName(), "Future.set() called"); + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to load bitmap", e); + future.setException(e); + } + }); + return future; + } + + + }; + // Create Media3 MediaSession for the wrapper + media3Session = new MediaSession.Builder(getContext(), playerWrapper) + .setId("avtransport_" + id) + // Don't set BitmapLoader - let notification manager handle it via getCurrentLargeIcon() + .setCallback(new MediaSession.Callback() { + @Override + public MediaSession.ConnectionResult onConnect(MediaSession session, + MediaSession.ControllerInfo controller) { + MediaSession.ConnectionResult result = MediaSession.Callback.super.onConnect(session, controller); + + // Enable device volume commands + SessionCommands.Builder commandsBuilder = result.availableSessionCommands.buildUpon(); + commandsBuilder.add(new SessionCommand(SessionCommand.COMMAND_CODE_SESSION_SET_RATING)); + + return MediaSession.ConnectionResult.accept( + commandsBuilder.build(), + result.availablePlayerCommands + ); + } + }) + .build(); + + // Add listener to wrapper so Media3 session gets updates + playerWrapper.addListener(new Player.Listener() { + @Override + public void onMediaItemTransition(MediaItem mediaItem, int reason) { + YaaccLogger.d(getClass().getName(), "Media3: onMediaItemTransition - " + + (mediaItem != null ? mediaItem.mediaMetadata.title : "null")); + } + + @Override + public void onIsPlayingChanged(boolean isPlaying) { + YaaccLogger.d(getClass().getName(), "Media3: onIsPlayingChanged - " + isPlaying); + } + }); + + // Create notification manager + notificationManager = new PlayerNotificationManager.Builder( + getContext(), + getNotificationId(), + Yaacc.NOTIFICATION_CHANNEL_ID) + .setMediaDescriptionAdapter(new PlayerNotificationManager.MediaDescriptionAdapter() { + @Override + public CharSequence getCurrentContentTitle(Player player) { + return getCurrentItemTitle(); + } + + @Override + public PendingIntent createCurrentContentIntent(Player player) { + return getNotificationIntent(); + } + + @Override + public CharSequence getCurrentContentText(Player player) { + return getName(); + } + + @Override + public Bitmap getCurrentLargeIcon(Player player, + PlayerNotificationManager.BitmapCallback callback) { + // Get album art URI from AVTransportPlayer (includes cover.jpg fallback) + URI albumArtJavaUri = getAlbumArt(); + YaaccLogger.e(getClass().getName(), "getCurrentLargeIcon called, albumArtUri: " + albumArtJavaUri); + + if (albumArtJavaUri != null) { + android.net.Uri artworkUri = android.net.Uri.parse(albumArtJavaUri.toString()); + + // Check cache first - return immediately if available + IconDownloadCacheHandler cache = IconDownloadCacheHandler.getInstance(); + Bitmap cachedBitmap = cache.getBitmap(artworkUri, 512, 512); + if (cachedBitmap != null) { + YaaccLogger.e(getClass().getName(), "Returning cached bitmap synchronously: " + cachedBitmap.getWidth() + "x" + cachedBitmap.getHeight()); + return cachedBitmap; + } + + // Load bitmap in background thread and use callback + ((Yaacc) getContext().getApplicationContext()).getContentLoadExecutor().execute(() -> { + try { + YaaccLogger.e(getClass().getName(), "Loading bitmap from: " + artworkUri); + Bitmap bitmap = new ImageDownloader().retrieveImageWithCertainSize(artworkUri, 512, 512); + if (bitmap != null) { + cache.addBitmap(artworkUri, 512, 512, bitmap); + YaaccLogger.e(getClass().getName(), "Bitmap loaded, calling callback: " + bitmap.getWidth() + "x" + bitmap.getHeight()); + callback.onBitmap(bitmap); + } else { + YaaccLogger.e(getClass().getName(), "Bitmap is null"); + } + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to load album art", e); + } + }); + } else { + YaaccLogger.e(getClass().getName(), "albumArtUri is null"); + } + return null; + } + }) + .build(); + + notificationManager.setUseNextAction(true); + notificationManager.setUsePreviousAction(true); + notificationManager.setUseNextActionInCompactView(true); + notificationManager.setUsePreviousActionInCompactView(true); + notificationManager.setSmallIcon(R.drawable.ic_notification_default); + notificationManager.setVisibility(androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC); + notificationManager.setPriority(androidx.core.app.NotificationCompat.PRIORITY_DEFAULT); + + YaaccLogger.d(getClass().getName(), "Setting up PlayerNotificationManager"); + + // setPlayer must be called on main thread + new Handler(Looper.getMainLooper()).post(() -> { + YaaccLogger.d(getClass().getName(), "Calling notificationManager.setPlayer()"); + notificationManager.setPlayer(playerWrapper); + notificationManager.setMediaSessionToken(media3Session.getSessionCompatToken()); + YaaccLogger.d(getClass().getName(), "PlayerNotificationManager setup complete"); + }); + } + + @Override + protected void configureMediaSession(MediaSessionCompat mediaSession) { + // Don't configure legacy MediaSession - we're using Media3 MediaSession instead + // Deactivate it so only Media3 session is active + mediaSession.setActive(false); + YaaccLogger.d(getClass().getName(), "Deactivated legacy MediaSession - using Media3"); + } + + @Override + public void onServiceConnected(ComponentName className, IBinder binder) { + super.onServiceConnected(className, binder); + + // Register Media3 MediaSession with PlayerService (if initialized) + if (media3Session != null && binder instanceof PlayerService.PlayerServiceBinder) { + PlayerService playerService = ((PlayerService.PlayerServiceBinder) binder).getService(); + playerService.registerMediaSession(media3Session); + YaaccLogger.d(getClass().getName(), "Media3 MediaSession registered with PlayerService"); + + // Trigger initial device info query to activate volume control + new Handler(Looper.getMainLooper()).post(() -> { + if (playerWrapper != null) { + playerWrapper.getDeviceInfo(); + playerWrapper.getDeviceVolume(); + } + }); + } } protected Device getDevice() { @@ -102,48 +368,93 @@ public String getContentType() { return contentType; } + /** + * Helper to track and check if command should be retried + * + * @param commandKey Unique key for the command (e.g., "play_123") + * @return true if should retry, false if max retries reached + */ + private boolean shouldRetry(String commandKey) { + int retries = commandRetries.getOrDefault(commandKey, 0); + if (retries < MAX_RETRIES) { + commandRetries.put(commandKey, retries + 1); + YaaccLogger.w(getClass().getName(), "Retrying " + commandKey + " (attempt " + (retries + 1) + "/" + MAX_RETRIES + ")"); + return true; + } + commandRetries.remove(commandKey); + YaaccLogger.e(getClass().getName(), "Max retries reached for " + commandKey); + return false; + } + + /** + * Reset retry counter for successful command + */ + private void resetRetry(String commandKey) { + commandRetries.remove(commandKey); + } + + protected de.yaacc.upnp.server.http.HttpRequestSender getHttpRequestSender() { + return getUpnpClient().getYaaccUpnpServerService().getNetworkDeviceListener().getHttpRequestSender(); + } + /* (non-Javadoc) * @see de.yaacc.player.AbstractPlayer#stopItem(de.yaacc.player.PlayableItem) */ @Override protected void stopItem(PlayableItem playableItem) { if (getDevice() == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No receiver device found: " + deviceId); return; } Service service = getUpnpClient().getAVTransportService(getDevice()); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found on Device: " + getDevice().getDisplayString()); return; } final ActionState actionState = new ActionState(); // Now start Stopping - Log.d(getClass().getName(), "Action Stop"); + YaaccLogger.d(getClass().getName(), "Action Stop"); + doStopWithRetry(service, "stop_" + System.currentTimeMillis()); + } + + private void doStopWithRetry(Service service, final String retryKey) { + final ActionState actionState = new ActionState(); actionState.actionFinished = false; - Stop actionCallback = new Stop(service) { + Stop actionCallback = new Stop(service, getHttpRequestSender()) { @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), upnpresponse != null ? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); actionState.actionFinished = true; + + // Retry on failure + if (shouldRetry(retryKey)) { + executeCommand(new TimerTask() { + @Override + public void run() { + doStopWithRetry(service, retryKey); + } + }, new Date(System.currentTimeMillis() + 1000)); + } } @Override public void success(ActionInvocation actioninvocation) { super.success(actioninvocation); + resetRetry(retryKey); actionState.actionFinished = true; } }; - getUpnpClient().getControlPoint().execute(actionCallback); + executorService.execute(actionCallback); } /* (non-Javadoc) @@ -158,80 +469,432 @@ protected Object loadItem(PlayableItem playableItem) { * @see de.yaacc.player.AbstractPlayer#startItem(de.yaacc.player.PlayableItem, java.lang.Object) */ @Override - protected void startItem(PlayableItem playableItem, Object loadedItem) { + protected void startItem(PlayableItem playableItem, Object loadedItem, int index) { if (playableItem == null || getDevice() == null) return; - Log.d(getClass().getName(), "Uri: " + playableItem.getUri()); - Log.d(getClass().getName(), "Duration: " + playableItem.getDuration()); - Log.d(getClass().getName(), - "MimeType: " + playableItem.getMimeType()); - Log.d(getClass().getName(), "Title: " + playableItem.getTitle()); + + // Request audio focus for lock screen volume control + if (media3Session != null) { + new Handler(Looper.getMainLooper()).post(() -> { + // Trigger a state update to make this session active + playerWrapper.notifyPlaybackStateChanged(); + }); + } + + // Try to select best resource for this device + PlayableItem deviceOptimizedItem = selectBestResourceForDevice(playableItem); + + YaaccLogger.d(getClass().getName(), "Uri: " + deviceOptimizedItem.getUri()); + YaaccLogger.d(getClass().getName(), "Duration: " + deviceOptimizedItem.getDuration()); + YaaccLogger.d(getClass().getName(), + "MimeType: " + deviceOptimizedItem.getMimeType()); + YaaccLogger.d(getClass().getName(), "Title: " + deviceOptimizedItem.getTitle()); Service service = getUpnpClient().getAVTransportService(getDevice()); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found on Device: " + getDevice().getDisplayString()); return; } - Log.d(getClass().getName(), "Action SetAVTransportURI "); + + // Check transport state first and handle accordingly + checkTransportStateForStart(deviceOptimizedItem, service); + } + + /** + * Select the best resource for this specific device based on supported protocols + */ + private PlayableItem selectBestResourceForDevice(PlayableItem playableItem) { + Item item = playableItem.getItem(); + if (item == null || item.getResources().isEmpty()) { + return playableItem; + } + + // Get device's supported protocols + Service cmService = getDevice().findService(new UDAServiceType("ConnectionManager")); + if (cmService == null) { + YaaccLogger.d(getClass().getName(), "No ConnectionManager service, using default resource"); + return playableItem; + } + + // Query supported protocols synchronously + final ProtocolInfos[] supportedProtocols = new ProtocolInfos[1]; + final CountDownLatch latch = new CountDownLatch(1); + + executorService.execute( + new GetProtocolInfo(cmService, getHttpRequestSender()) { + @Override + public void received(ActionInvocation actionInvocation, ProtocolInfos sinkProtocolInfos, ProtocolInfos sourceProtocolInfos) { + supportedProtocols[0] = sinkProtocolInfos; + latch.countDown(); + } + + @Override + public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { + YaaccLogger.d(AVTransportPlayer.class.getName(), "GetProtocolInfo failed: " + defaultMsg); + latch.countDown(); + } + } + ); + + try { + latch.await(2, TimeUnit.SECONDS); + } catch (InterruptedException e) { + YaaccLogger.d(getClass().getName(), "GetProtocolInfo timeout"); + return playableItem; + } + + if (supportedProtocols[0] == null || supportedProtocols[0].isEmpty()) { + YaaccLogger.d(getClass().getName(), "No supported protocols found, using default resource"); + return playableItem; + } + + // Find best matching resource + Res bestMatch = null; + long bestBitrate = 0; + + for (Res resource : item.getResources()) { + if (resource.getProtocolInfo() == null) continue; + + String contentFormat = resource.getProtocolInfo().getContentFormat(); + if (contentFormat == null || contentFormat.isEmpty()) continue; + + // Check if device supports this format + boolean supported = false; + for (ProtocolInfo deviceProtocol : supportedProtocols[0]) { + if (deviceProtocol.getContentFormat().equals(contentFormat) || + deviceProtocol.getContentFormat().equals("*") || + deviceProtocol.getContentFormat().startsWith(contentFormat.split("/")[0] + "/*")) { + supported = true; + break; + } + } + + if (!supported) { + YaaccLogger.d(getClass().getName(), "Device doesn't support: " + contentFormat); + continue; + } + + // Among supported formats, prefer higher bitrate + Long bitrate = resource.getBitrate(); + if (bitrate != null && bitrate > bestBitrate) { + bestBitrate = bitrate; + bestMatch = resource; + } else if (bestMatch == null) { + bestMatch = resource; + } + } + + if (bestMatch != null && !bestMatch.equals(item.getFirstResource())) { + YaaccLogger.d(getClass().getName(), "Selected device-optimized resource: " + + bestMatch.getProtocolInfo().getContentFormat() + " bitrate: " + bestMatch.getBitrate()); + // Create new PlayableItem with selected resource + Item optimizedItem = new Item(item); + optimizedItem.setResources(java.util.Collections.singletonList(bestMatch)); + return new PlayableItem(optimizedItem, (int) playableItem.getDuration()); + } + + return playableItem; + } + + private void checkTransportStateForStart(PlayableItem playableItem, Service service) { + // Check current transport state first + GetTransportInfo stateCheck = new GetTransportInfo(service, getHttpRequestSender()) { + @Override + public void received(ActionInvocation actioninvocation, TransportInfo transportInfo) { + TransportState state = transportInfo.getCurrentTransportState(); + YaaccLogger.d(getClass().getName(), "Current state before Play: " + state); + + // Only resume if paused AND on the same track + if (state == TransportState.PAUSED_PLAYBACK && isPaused()) { + YaaccLogger.d(getClass().getName(), "Resuming from pause on same track, sending Play only"); + // For paused content on same track, just send Play command + Play playCallback = new Play(service, getHttpRequestSender()) { + @Override + public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { + YaaccLogger.d(getClass().getName(), "Resume Play failed: " + s); + setProcessingCommand(false); + } + + @Override + public void success(ActionInvocation invocation) { + YaaccLogger.d(getClass().getName(), "Resume Play succeeded"); + setProcessingCommand(false); + setPlaying(true); + playerWrapper.notifyPlaybackStateChanged(); + } + }; + executorService.execute(playCallback); + } else { + // For stopped, playing, or paused on different track, do full restart + YaaccLogger.d(getClass().getName(), "Sending Stop command to ensure clean state"); + executeCommand(new TimerTask() { + @Override + public void run() { + stop(); + // Wait a bit then proceed with SetURI + executeCommand(new TimerTask() { + @Override + public void run() { + proceedWithSetURI(playableItem, service); + } + }, new Date(System.currentTimeMillis() + 200)); + } + }, new Date()); + } + } + + @Override + public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { + YaaccLogger.d(getClass().getName(), "GetTransportInfo failed, proceeding with full restart"); + // If we can't get state, do full restart + executeCommand(new TimerTask() { + @Override + public void run() { + stop(); + executeCommand(new TimerTask() { + @Override + public void run() { + proceedWithSetURI(playableItem, service); + } + }, new Date(System.currentTimeMillis() + 200)); + } + }, new Date()); + } + }; + executorService.execute(stateCheck); + } + + private void proceedWithSetURI(PlayableItem playableItem, Service service) { + YaaccLogger.d(getClass().getName(), "Action SetAVTransportURI "); final ActionState actionState = new ActionState(); actionState.actionFinished = false; Item item = playableItem.getItem(); + + // Check if this device needs server-side range management + String deviceId = getDevice().getIdentity().getUdn().getIdentifierString(); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String prefKey = SettingsFragment.MANAGE_EXTERNAL_SEEKING + deviceId; + boolean useServerSideManagement = preferences.getBoolean(prefKey, false); + + YaaccLogger.d(getClass().getName(), "Device ID: " + deviceId); + YaaccLogger.d(getClass().getName(), "Preference key: " + prefKey); + YaaccLogger.d(getClass().getName(), "Server-side management enabled: " + useServerSideManagement); + YaaccLogger.d(getClass().getName(), "Item is null: " + (item == null)); + + if (useServerSideManagement && item != null) { + // Make a copy of the item to avoid modifying the shared instance + try { + // Create a new item with the same properties but modifiable resources + Item itemCopy = new Item(item.getId(), item.getParentID(), item.getTitle(), + item.getCreator(), item.getClazz()); + + // Copy all properties + for (DIDLObject.Property property : item.getProperties()) { + itemCopy.addProperty(property); + } + + // Copy and modify resources + for (Res resource : item.getResources()) { + String originalUri = resource.getValue(); + String modifiedUri = modifyProxyUrlWithDeviceId(originalUri); + + Res newResource = new Res(resource.getProtocolInfo(), resource.getSize(), + resource.getDuration(), resource.getBitrate(), modifiedUri); + itemCopy.addResource(newResource); + + if (!originalUri.equals(modifiedUri)) { + YaaccLogger.d(getClass().getName(), "Modified copied item resource URI: " + modifiedUri); + } + } + + item = itemCopy; + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to copy/modify item: " + e.getMessage()); + item = playableItem.getItem(); // Fall back to original + } + } + String metadata; try { metadata = new DIDLParser().generate((item == null) ? new DIDLContent() : new DIDLContent().addItem(item), false); } catch (Exception e) { - Log.d(getClass().getName(), "Error while generating Didl-Item xml: " + e); + YaaccLogger.d(getClass().getName(), "Error while generating Didl-Item xml: " + e); metadata = ""; } DIDLObject.Property albumArtUriProperty = playableItem.getItem() == null ? null : playableItem.getItem().getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class); albumArtUri = (albumArtUriProperty == null) ? null : albumArtUriProperty.getValue(); + // Load album art and update notification + if (albumArtUri != null) { + updateMetadataInternal(); + // Load album art in background and update icon + executorService.execute(() -> { + try { + Bitmap albumArtBitmap = new ImageDownloader().retrieveImageWithCertainSize( + Uri.parse(albumArtUri.toString()), 512, 512); + if (albumArtBitmap != null) { + setIcon(albumArtBitmap); + // Refresh notification with new icon + showNotificationInternal(); + } + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to load album art for notification", e); + } + }); + } + InternalSetAVTransportURI setAVTransportURI = new InternalSetAVTransportURI( - service, playableItem.getUri().toString(), actionState, metadata); - getUpnpClient().getControlPoint().execute(setAVTransportURI); + service, modifyProxyUrlWithDeviceId(playableItem.getUri().toString()), actionState, metadata, + getHttpRequestSender()); + YaaccLogger.d(getClass().getName(), "Original URI: " + playableItem.getUri().toString()); + YaaccLogger.d(getClass().getName(), "Modified URI: " + modifyProxyUrlWithDeviceId(playableItem.getUri().toString())); + executorService.execute(setAVTransportURI); waitForActionComplete(actionState); int tries = 1; if (setAVTransportURI.hasFailures) { //another try - Log.d(getClass().getName(), "setAVTransportURI.hasFailures"); + YaaccLogger.d(getClass().getName(), "setAVTransportURI.hasFailures"); while (setAVTransportURI.hasFailures && tries < 4) { tries++; - Log.d(getClass().getName(), "setAVTransportURI.hasFailures retry:" + tries); + YaaccLogger.d(getClass().getName(), "setAVTransportURI.hasFailures retry:" + tries); setAVTransportURI.hasFailures = false; - getUpnpClient().getControlPoint().execute(setAVTransportURI); + executorService.execute(setAVTransportURI); waitForActionComplete(actionState); } } if (setAVTransportURI.hasFailures) { //another try - Log.d(getClass().getName(), "Can't set AVTransportURI. Giving up"); + YaaccLogger.d(getClass().getName(), "Can't set AVTransportURI. Giving up"); return; } // Now start Playing - Log.d(getClass().getName(), "Action Play"); + YaaccLogger.d(getClass().getName(), "Action Play"); + lastRemainingTime = -1; // Reset to ensure timer gets set for new track + playRetryCount = 0; // Reset retry counter for new track + + // Add small delay before Play command to let renderer process URI + executeCommand(new TimerTask() { + @Override + public void run() { + // Check current state before sending Play + GetTransportInfo stateCheck = new GetTransportInfo(service, getHttpRequestSender()) { + @Override + public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { + YaaccLogger.d(getClass().getName(), "Failed to get transport state, sending Play anyway"); + startPlayAction(service, actionState); + } + + @Override + public void received(ActionInvocation actioninvocation, TransportInfo transportInfo) { + TransportState state = transportInfo.getCurrentTransportState(); + YaaccLogger.d(getClass().getName(), "Current state before Play: " + state); + + if (state == TransportState.STOPPED) { + YaaccLogger.d(getClass().getName(), "Valid state for Play command, proceeding"); + startPlayAction(service, actionState); + } else if (state == TransportState.PAUSED_PLAYBACK) { + YaaccLogger.d(getClass().getName(), "Resuming from pause, sending Play only"); + // For paused content, just send Play command without SetAVTransportURI + Play playCallback = new Play(service, getHttpRequestSender()) { + @Override + public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { + YaaccLogger.d(getClass().getName(), "Resume Play failed: " + s); + setProcessingCommand(false); + } + + @Override + public void success(ActionInvocation invocation) { + YaaccLogger.d(getClass().getName(), "Resume Play succeeded"); + setProcessingCommand(false); + setPlaying(true); + playerWrapper.notifyPlaybackStateChanged(); + } + }; + executorService.execute(playCallback); + } else if (state == TransportState.PLAYING) { + YaaccLogger.d(getClass().getName(), "Already playing, sending Stop first then Play"); + Stop stopCallback = new Stop(service, getHttpRequestSender()) { + @Override + public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { + YaaccLogger.d(getClass().getName(), "Stop before Play failed: " + s); + startPlayAction(service, actionState); + } + + @Override + public void success(ActionInvocation invocation) { + YaaccLogger.d(getClass().getName(), "Stop succeeded, now sending Play"); + executeCommand(new TimerTask() { + @Override + public void run() { + startPlayAction(service, actionState); + } + }, new Date(System.currentTimeMillis() + 200)); + } + }; + executorService.execute(stopCallback); + } else { + YaaccLogger.d(getClass().getName(), "Unknown state: " + state + ", sending Play anyway"); + startPlayAction(service, actionState); + } + } + }; + executorService.execute(stateCheck); + } + }, new Date(System.currentTimeMillis() + 200)); + } + + private void startPlayAction(Service service, final ActionState actionState) { + startPlayAction(service, actionState, "play_" + System.currentTimeMillis()); + } + + private void startPlayAction(Service service, final ActionState actionState, final String retryKey) { actionState.actionFinished = false; - Play actionCallback = new Play(service) { + Play actionCallback = new Play(service, getHttpRequestSender()) { @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), upnpresponse != null ? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); actionState.actionFinished = true; + + // Retry on failure + if (shouldRetry(retryKey)) { + executeCommand(new TimerTask() { + @Override + public void run() { + startPlayAction(service, actionState, retryKey); + } + }, new Date(System.currentTimeMillis() + 1000)); + } else { + setProcessingCommand(false); + } } @Override public void success(ActionInvocation actioninvocation) { super.success(actioninvocation); + resetRetry(retryKey); actionState.actionFinished = true; + setPlaying(true); + playerWrapper.notifyPlaybackStateChanged(); + setProcessingCommand(false); + + // Check transport state after Play command + executeCommand(new TimerTask() { + @Override + public void run() { + getTransportInfo(); + } + }, new Date(System.currentTimeMillis() + 500)); } }; - getUpnpClient().getControlPoint().execute(actionCallback); + executorService.execute(actionCallback); } /** @@ -261,16 +924,16 @@ public void run() { //work around byte code optimization i++; if (i == 100000) { - Log.d(getClass().getName(), "wait for action finished "); + // YaaccLogger.d(getClass().getName(), "wait for action finished "); i = 0; } } } if (actionState.watchdogFlag) { - Log.d(getClass().getName(), "Watchdog timeout!"); + YaaccLogger.d(getClass().getName(), "Watchdog timeout!"); } if (actionState.actionFinished) { - Log.d(getClass().getName(), "Action completed!"); + YaaccLogger.d(getClass().getName(), "Action completed!"); } } @@ -282,7 +945,7 @@ public void run() { public PendingIntent getNotificationIntent() { Intent notificationIntent = new Intent(getContext(), AVTransportPlayerActivity.class); - Log.d(getClass().getName(), "Put id into intent: " + getId()); + YaaccLogger.d(getClass().getName(), "Put id into intent: " + getId()); notificationIntent.setData(Uri.parse("http://0.0.0.0/" + getId() + "")); //just for making the intents different http://stackoverflow.com/questions/10561419/scheduling-more-than-one-pendingintent-to-same-activity-using-alarmmanager notificationIntent.putExtra(PLAYER_ID, getId()); return PendingIntent.getActivity(getContext(), 0, @@ -299,46 +962,124 @@ protected int getNotificationId() { return id; } - @Override - public void pause() { - super.pause(); + protected void doPause() { + YaaccLogger.d(getClass().getName(), "doPause() called"); if (getDevice() == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No receiver device found: " + deviceId); return; } Service service = getUpnpClient().getAVTransportService(getDevice()); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found on Device: " + getDevice().getDisplayString()); return; } - Log.d(getClass().getName(), "Action Pause "); + YaaccLogger.d(getClass().getName(), "Action Pause "); + doPauseWithRetry(service, "pause_" + System.currentTimeMillis()); + } + + private void doPauseWithRetry(Service service, final String retryKey) { final ActionState actionState = new ActionState(); actionState.actionFinished = false; - Pause actionCallback = new Pause(service) { + Pause actionCallback = new Pause(service, getHttpRequestSender()) { @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + YaaccLogger.d(getClass().getName(), "Pause FAILED: " + s); + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), upnpresponse != null ? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); actionState.actionFinished = true; + + // Retry on failure + if (shouldRetry(retryKey)) { + executeCommand(new TimerTask() { + @Override + public void run() { + doPauseWithRetry(service, retryKey); + } + }, new Date(System.currentTimeMillis() + 1000)); + } } @Override public void success(ActionInvocation actioninvocation) { super.success(actioninvocation); + resetRetry(retryKey); + YaaccLogger.d(getClass().getName(), "Pause SUCCESS - setting isPlaying=false"); actionState.actionFinished = true; + setPlaying(false); + playerWrapper.notifyPlaybackStateChanged(); + YaaccLogger.d(getClass().getName(), "After pause: isPlaying=" + isPlaying()); } }; - getUpnpClient().getControlPoint().execute(actionCallback); + executorService.execute(actionCallback); + } + + @Override + public Bitmap getIcon() { + // Try to get album art from cache only (don't block on download) + if (albumArtUri != null) { + IconDownloadCacheHandler cache = IconDownloadCacheHandler.getInstance(); + Bitmap albumArt = cache.getBitmap(android.net.Uri.parse(albumArtUri.toString()), 512, 512); + if (albumArt != null) { + return albumArt; + } + + // Trigger async download for next notification update + android.net.Uri artworkUri = android.net.Uri.parse(albumArtUri.toString()); + ((Yaacc) getContext().getApplicationContext()).getContentLoadExecutor().execute(() -> { + try { + Bitmap bitmap = new ImageDownloader().retrieveImageWithCertainSize(artworkUri, 512, 512); + if (bitmap != null) { + cache.addBitmap(artworkUri, 512, 512, bitmap); + // Trigger notification update by updating metadata + updateMetadataInternal(); + } + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Failed to load album art", e); + } + }); + } + // Fall back to device icon + return super.getIcon(); + } + + @Override + protected void doResume() { + // For UPnP, just send Play command to resume from current position + if (getDevice() == null) { + YaaccLogger.d(getClass().getName(), "No receiver device found: " + deviceId); + return; + } + Service service = getUpnpClient().getAVTransportService(getDevice()); + if (service == null) { + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found on Device: " + getDevice().getDisplayString()); + return; + } + + YaaccLogger.d(getClass().getName(), "Resuming playback with Play command"); + Play playCallback = new Play(service, getHttpRequestSender()) { + @Override + public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { + YaaccLogger.d(getClass().getName(), "Resume failed: " + s); + } + + @Override + public void success(ActionInvocation invocation) { + YaaccLogger.d(getClass().getName(), "Resume succeeded"); + setPlaying(true); + playerWrapper.notifyPlaybackStateChanged(); + } + }; + executorService.execute(playCallback); } @Override @@ -348,7 +1089,7 @@ public URI getAlbumArt() { public boolean getMute() { if (getDevice() == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No receiver device found: " + deviceId); return false; @@ -358,7 +1099,7 @@ public boolean getMute() { public void setMute(boolean mute) { if (getDevice() == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No receiver device found: " + deviceId); return; @@ -368,7 +1109,7 @@ public void setMute(boolean mute) { public int getVolume() { if (getDevice() == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No receiver device found: " + deviceId); return 0; @@ -378,7 +1119,7 @@ public int getVolume() { public void setVolume(int volume) { if (getDevice() == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No receiver device found: " + deviceId); return; @@ -386,40 +1127,183 @@ public void setVolume(int volume) { getUpnpClient().setVolume(getDevice(), volume); } + private int playRetryCount = 0; + private static final int MAX_PLAY_RETRIES = 3; + + private void getTransportInfo() { + if (getDevice() == null) { + YaaccLogger.d(getClass().getName(), "No receiver device found for transport info: " + deviceId); + return; + } + Service service = getUpnpClient().getAVTransportService(getDevice()); + if (service == null) { + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found for transport info"); + return; + } + + YaaccLogger.d(getClass().getName(), "GetTransportInfo"); + GetTransportInfo actionCallback = new GetTransportInfo(service, getHttpRequestSender()) { + @Override + public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { + YaaccLogger.d(getClass().getName(), "GetTransportInfo failure: " + s); + } + + @Override + public void received(ActionInvocation actioninvocation, TransportInfo info) { + YaaccLogger.d(getClass().getName(), "Transport State: " + info.getCurrentTransportState()); + + // If device paused externally, update player state + if (info.getCurrentTransportState() == TransportState.PAUSED_PLAYBACK && isPlaying()) { + YaaccLogger.d(getClass().getName(), "Device paused externally, updating state"); + setPlaying(false); + setPaused(true); // Set paused flag so isPaused() returns true + playerWrapper.notifyPlaybackStateChanged(); + return; + } + + // If device stopped, check if track actually ended or device just reconnected + if (info.getCurrentTransportState() == TransportState.STOPPED && isPlaying()) { + // Get position to verify track ended (position should be at end or 0:00:00) + getPositionInfo(); + if (currentPositionInfo != null) { + String relTime = currentPositionInfo.getRelTime(); + String duration = currentPositionInfo.getTrackDuration(); + // Only advance if position is at start (track ended and reset) or near end + if ("0:00:00".equals(relTime) || "0:00:01".equals(relTime)) { + YaaccLogger.d(getClass().getName(), "Track ended (position at start), advancing to next"); + consecutivePositionFailures = 0; + next(); + return; + } else { + YaaccLogger.d(getClass().getName(), "Device stopped but position is " + relTime + ", not advancing (device may have reconnected)"); + // Device stopped mid-track - could be pause or reconnection + // Set paused flag to prevent auto-resume on next check + setPlaying(false); + setPaused(true); + playerWrapper.notifyPlaybackStateChanged(); + YaaccLogger.d(getClass().getName(), "Set player to paused state"); + return; + } + } else { + // No position info, assume track ended + YaaccLogger.d(getClass().getName(), "Device stopped, no position info, advancing to next"); + consecutivePositionFailures = 0; + next(); + return; + } + } + + // Only retry Play if we think we should be playing (not paused by user) + if (info.getCurrentTransportState() != TransportState.PLAYING && + isPlaying() && + playRetryCount < MAX_RETRIES) { + playRetryCount++; + YaaccLogger.d(getClass().getName(), "Renderer not playing, sending Play command again (attempt " + playRetryCount + ")"); + executeCommand(new TimerTask() { + @Override + public void run() { + sendPlayCommand(); + } + }, new Date(System.currentTimeMillis() + 500)); + } else { + YaaccLogger.d(getClass().getName(), "Checking position (retries: " + playRetryCount + ")"); + getPositionInfo(); + } + } + }; + executorService.execute(actionCallback); + } + + private void sendPlayCommand() { + if (getDevice() == null) { + YaaccLogger.d(getClass().getName(), "No receiver device found for Play command: " + deviceId); + return; + } + Service service = getUpnpClient().getAVTransportService(getDevice()); + if (service == null) { + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found for Play command"); + return; + } + + YaaccLogger.d(getClass().getName(), "Sending additional Play command"); + Play actionCallback = new Play(service, getHttpRequestSender()) { + @Override + public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { + YaaccLogger.d(getClass().getName(), "Additional Play command failed: " + s); + getPositionInfo(); // Check position anyway + } + + @Override + public void success(ActionInvocation actioninvocation) { + super.success(actioninvocation); + YaaccLogger.d(getClass().getName(), "Additional Play command succeeded"); + setPlaying(true); + playerWrapper.notifyPlaybackStateChanged(); + + // Check transport state again after second Play command + executeCommand(new TimerTask() { + @Override + public void run() { + getTransportInfo(); + } + }, new Date(System.currentTimeMillis() + 1000)); // Wait 1 second then check state + } + }; + executorService.execute(actionCallback); + } protected void getPositionInfo() { if (positionActionState != null && !positionActionState.actionFinished) { return; } - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "GetPositioninfo"); if (getDevice() == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No receiver device found: " + deviceId); + + // Track device-not-found as position failure + consecutivePositionFailures++; + if (consecutivePositionFailures >= MAX_RETRIES && isPlaying()) { + YaaccLogger.w(getClass().getName(), "Device lost, stopping playback"); + consecutivePositionFailures = 0; + stop(); + } return; } Service service = getUpnpClient().getAVTransportService(getDevice()); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found on Device: " + getDevice().getDisplayString()); return; } - Log.d(getClass().getName(), "Action get position info "); + YaaccLogger.d(getClass().getName(), "Action get position info "); positionActionState = new ActionState(); positionActionState.actionFinished = false; - GetPositionInfo actionCallback = new GetPositionInfo(service) { + GetPositionInfo actionCallback = new GetPositionInfo(service, getHttpRequestSender()) { @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), upnpresponse != null ? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); positionActionState.actionFinished = true; + + // Track consecutive failures + consecutivePositionFailures++; + YaaccLogger.w(getClass().getName(), "Position query failed " + consecutivePositionFailures + " times"); + + // After 3 consecutive failures, check device state to see if track ended + if (consecutivePositionFailures >= MAX_RETRIES && isPlaying()) { + YaaccLogger.w(getClass().getName(), "Position query failed " + MAX_RETRIES + "times, checking transport state"); + consecutivePositionFailures = 0; + getTransportInfo(); + } } @Override @@ -431,13 +1315,76 @@ public void success(ActionInvocation actioninvocation) { @Override public void received(ActionInvocation actionInvocation, PositionInfo positionInfo) { positionActionState.result = positionInfo; + PositionInfo previousPositionInfo = currentPositionInfo; currentPositionInfo = positionInfo; - Log.d(getClass().getName(), "received Positioninfo= RelTime: " + positionInfo.getRelTime() + " remaining time: " + positionInfo.getTrackRemainingSeconds()); + consecutivePositionFailures = 0; // Reset failure counter on success + YaaccLogger.d(getClass().getName(), "received Positioninfo= RelTime: " + positionInfo.getRelTime() + " remaining time: " + positionInfo.getTrackRemainingSeconds()); + + // Detect track end: position reset to 0:00:00 after being > 0 + // This means device auto-advanced to next track + if ("0:00:00".equals(positionInfo.getRelTime()) && + previousPositionInfo != null && + !"0:00:00".equals(previousPositionInfo.getRelTime()) && + isPlaying()) { + YaaccLogger.d(getClass().getName(), "Position reset to 0:00:00 after playing, checking transport state"); + getTransportInfo(); // Check if still playing or stopped + return; + } + + // Update MediaSession with current position for lock screen controls + if (isPlaying()) { + updatePlaybackStateInternal(android.support.v4.media.session.PlaybackStateCompat.STATE_PLAYING); + } + + long currentRemainingTime = positionInfo.getTrackRemainingSeconds(); + + // Update server-side position management only for external URLs on basic renderers + if (positionInfo.getRelTime() != null && !positionInfo.getRelTime().isEmpty()) { + int currentIndex = getCurrentItemIndex(); + if (currentIndex >= 0 && currentIndex < getItems().size()) { + PlayableItem currentPlayableItem = getItems().get(currentIndex); + String itemUri = currentPlayableItem.getUri().toString(); + boolean isExternalUrl = itemUri != null && itemUri.contains("/proxy/"); + + if (isExternalUrl) { + try { + String[] timeParts = positionInfo.getRelTime().split(":"); + if (timeParts.length >= 3) { + long hours = Long.parseLong(timeParts[0]); + long minutes = Long.parseLong(timeParts[1]); + long seconds = Long.parseLong(timeParts[2]); + long timeMs = (hours * 3600 + minutes * 60 + seconds) * 1000; + + // Extract content key from proxy URL for renderer state key + String contentKey = itemUri.substring(itemUri.lastIndexOf("/") + 1); + + // Check if position is stuck (paused) + boolean isPaused = (lastRemainingTime != -1 && currentRemainingTime == lastRemainingTime); + + YaaccUpnpServerContentHttpHandler.updateRendererPosition( + "test_renderer_" + contentKey, timeMs, isPaused); + } + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Failed to parse position time: " + positionInfo.getRelTime(), e); + } + } + } + } + // Set timer on first position info OR when remaining time changes significantly + if (lastRemainingTime == -1 && currentRemainingTime > 1) { + // First position check - set timer only if we have valid remaining time + lastRemainingTime = currentRemainingTime; + updateTimer(); + } else if (currentRemainingTime > 1 && Math.abs(currentRemainingTime - lastRemainingTime) > 5) { + // Subsequent updates - only if remaining time changed significantly + lastRemainingTime = currentRemainingTime; + updateTimer(); + } } }; - getUpnpClient().getControlPoint().execute(actionCallback); + executorService.execute(actionCallback); } @@ -448,12 +1395,19 @@ public int getIconResourceId() { } + private long lastPositionUpdate = 0; + private long lastRemainingTime = -1; + private static final long POSITION_UPDATE_INTERVAL = 1000; // 1 second for better track completion detection + + public long getCurrentPosition() { - if (currentPositionInfo == null) { + long currentTime = System.currentTimeMillis(); + if (currentPositionInfo == null || (currentTime - lastPositionUpdate) > POSITION_UPDATE_INTERVAL) { getPositionInfo(); + lastPositionUpdate = currentTime; } if (currentPositionInfo != null) { - Log.v(getClass().getName(), "Elapsed time: " + currentPositionInfo.getTrackElapsedSeconds() + " in millis: " + currentPositionInfo.getTrackRemainingSeconds() * 1000); + YaaccLogger.v(getClass().getName(), "Elapsed time: " + currentPositionInfo.getTrackElapsedSeconds() + " in millis: " + currentPositionInfo.getTrackRemainingSeconds() * 1000); return currentPositionInfo.getTrackElapsedSeconds() * 1000; } return -1; @@ -463,44 +1417,82 @@ public long getCurrentPosition() { @Override public void seekTo(long millisecondsFromStart) { if (getDevice() == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No receiver device found: " + deviceId); return; } Service service = getUpnpClient().getAVTransportService(getDevice()); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found on Device: " + getDevice().getDisplayString()); return; } - Log.d(getClass().getName(), "Action seek "); + // Check if the service supports seek action + if (service.getAction("Seek") == null) { + YaaccLogger.w(getClass().getName(), "Player does not support Seek action"); + Context context = getUpnpClient().getContext(); + if (context instanceof Activity) { + ((Activity) context).runOnUiThread(() -> { + Toast.makeText(context, "Seek not supported by this player", Toast.LENGTH_SHORT).show(); + }); + } + return; + } + + YaaccLogger.d(getClass().getName(), "Action seek "); final ActionState actionState = new ActionState(); actionState.actionFinished = false; SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); String relativeTimeTarget = dateFormat.format(millisecondsFromStart); - Seek seekAction = new Seek(service, relativeTimeTarget) { + Seek seekAction = new Seek(service, relativeTimeTarget, getHttpRequestSender()) { @Override public void success(ActionInvocation invocation) { //super.success(invocation); - Log.d(getClass().getName(), "success seek" + invocation); - executeCommand(new TimerTask() { - @Override - public void run() { - updateTimer(); + YaaccLogger.d(getClass().getName(), "success seek" + invocation); + + // Update server-side position for external URLs + int currentIndex = getCurrentItemIndex(); + if (currentIndex >= 0 && currentIndex < getItems().size()) { + PlayableItem currentPlayableItem = getItems().get(currentIndex); + String itemUri = currentPlayableItem.getUri().toString(); + boolean isExternalUrl = itemUri != null && itemUri.contains("/proxy/"); + + if (isExternalUrl) { + String contentKey = itemUri.substring(itemUri.lastIndexOf("/") + 1); + String deviceId = getDevice().getIdentity().getUdn().getIdentifierString(); + YaaccUpnpServerContentHttpHandler.updateRendererPosition( + deviceId + "_" + contentKey, millisecondsFromStart, false); + + // Also save to preferences for HTTP handler + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + preferences.edit().putLong("server_position_" + deviceId, millisecondsFromStart).apply(); + + YaaccLogger.d(getClass().getName(), "Updated server position after seek: " + millisecondsFromStart + "ms"); } - }, new Date(System.currentTimeMillis() + 2000)); //wait two seconds before reading time from renderer + } + // Don't schedule position check - let normal position polling handle it } @Override public void failure(ActionInvocation arg0, UpnpResponse arg1, String arg2) { - Log.d(getClass().getName(), "fail seek"); + YaaccLogger.w(getClass().getName(), "Seek failed - Player may not support seeking"); + YaaccLogger.w(getClass().getName(), "UpnpResponse: " + (arg1 != null ? arg1.getResponseDetails() : "null")); + YaaccLogger.w(getClass().getName(), "Error: " + arg2); + + // Some players don't support seeking, just log and continue + Context context = getUpnpClient().getContext(); + if (context instanceof Activity) { + ((Activity) context).runOnUiThread(() -> { + Toast.makeText(context, "Seek not supported by this player", Toast.LENGTH_SHORT).show(); + }); + } } }; - getUpnpClient().getControlPoint().execute(seekAction); + executorService.execute(seekAction); } @@ -511,12 +1503,47 @@ public long getRemainingTime() { getPositionInfo(); } if (currentPositionInfo != null) { - Log.v(getClass().getName(), "Remaining time: " + currentPositionInfo.getTrackRemainingSeconds() + " in millis: " + currentPositionInfo.getTrackRemainingSeconds() * 1000); + YaaccLogger.v(getClass().getName(), "Remaining time: " + currentPositionInfo.getTrackRemainingSeconds() + " in millis: " + currentPositionInfo.getTrackRemainingSeconds() * 1000); return currentPositionInfo.getTrackRemainingSeconds() * 1000; } return -1; } + private String modifyProxyUrlWithDeviceId(String originalUrl) { + // Check if it's a proxy URL from the same YAACC server + if (originalUrl != null && originalUrl.contains("/proxy/")) { + try { + // Check if URL is from this YAACC server by comparing IP + java.net.URL url = new java.net.URL(originalUrl); + String urlHost = url.getHost(); + + // Get local server IP using YAACC method + String[] ifAndIp = InterfaceResolutionHelper.getIfAndIpAddress(getUpnpClient().getContext()); + String localIP = ifAndIp != null && ifAndIp.length > 0 ? ifAndIp[0] : null; + + YaaccLogger.d(getClass().getName(), "URL host: '" + urlHost + "', Local IP: '" + localIP + "'"); + YaaccLogger.d(getClass().getName(), "IP comparison: urlHost.equals(localIP) = " + urlHost.equals(localIP)); + + if (urlHost.equals(localIP) || urlHost.equals("localhost") || urlHost.equals("127.0.0.1")) { + // Get device UUID and URL-encode it for safe URL usage + String deviceId = getDevice().getIdentity().getUdn().getIdentifierString(); + String encodedDeviceId = java.net.URLEncoder.encode(deviceId, "UTF-8"); + + // Replace /proxy/contentKey with /proxy/encodedDeviceId/contentKey + String[] parts = originalUrl.split("/proxy/"); + if (parts.length == 2) { + String modifiedUrl = parts[0] + "/proxy/" + encodedDeviceId + "/" + parts[1]; + YaaccLogger.d(getClass().getName(), "Modified proxy URL: " + originalUrl + " -> " + modifiedUrl); + return modifiedUrl; + } + } + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Failed to modify proxy URL", e); + } + } + return originalUrl; + } + @Override public String getDuration() { if (currentPositionInfo == null) { @@ -545,6 +1572,18 @@ public void startTimer(final long duration) { @Override public void onDestroy() { + // Release notification manager and Media3 session + if (notificationManager != null) { + new Handler(Looper.getMainLooper()).post(() -> { + if (notificationManager != null) { + notificationManager.setPlayer(null); + } + if (media3Session != null) { + media3Session.release(); + } + }); + } + doExit(); super.onDestroy(); } @@ -558,7 +1597,7 @@ private void doExit() { try { Thread.sleep(200); } catch (InterruptedException e) { - Log.w(getClass().getName(), e); + YaaccLogger.w(getClass().getName(), e); } }; waitForActionComplete(actionState, fn); @@ -579,7 +1618,7 @@ private void setDeviceIcon(Device device) { if (120 == icon.getHeight() && 120 == icon.getWidth() && "image/png".equals(icon.getMimeType().toString())) { URL iconUri = ((RemoteDevice) device).normalizeURI(icon.getUri()); if (iconUri != null) { - Log.d(getClass().getName(), "Device icon uri:" + iconUri); + YaaccLogger.d(getClass().getName(), "Device icon uri:" + iconUri); setIcon(new ImageDownloader().retrieveImageWithCertainSize(Uri.parse(iconUri.toString()), icon.getWidth(), icon.getHeight())); break; } @@ -596,25 +1635,26 @@ private static class InternalSetAVTransportURI extends SetAVTransportURI { ActionState actionState; private InternalSetAVTransportURI(Service service, String uri, - ActionState actionState, String metadata) { - super(service, uri, metadata); + ActionState actionState, String metadata, de.yaacc.upnp.server.http.HttpRequestSender httpRequestSender) { + super(service, uri, metadata, httpRequestSender); this.actionState = actionState; + YaaccLogger.d(getClass().getName(), "InternalSetAVTransportURI created with URI: " + uri); } @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); if (upnpresponse != null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "UpnpResponse: " + upnpresponse.getResponseDetails()); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "UpnpResponse: " + upnpresponse.getStatusMessage()); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "UpnpResponse: " + upnpresponse.getStatusCode()); } hasFailures = true; - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); actionState.actionFinished = true; } @@ -628,7 +1668,7 @@ public void success(ActionInvocation actioninvocation) { public boolean hasActionGetVolume() { if (getDevice() == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No receiver device found: " + deviceId); return false; @@ -638,7 +1678,7 @@ public boolean hasActionGetVolume() { public boolean hasActionGetMute() { if (getDevice() == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No receiver device found: " + deviceId); return false; diff --git a/yaacc/src/main/java/de/yaacc/player/AVTransportPlayerActivity.java b/yaacc/src/main/java/de/yaacc/player/AVTransportPlayerActivity.java index bbf1f458..323fc36b 100644 --- a/yaacc/src/main/java/de/yaacc/player/AVTransportPlayerActivity.java +++ b/yaacc/src/main/java/de/yaacc/player/AVTransportPlayerActivity.java @@ -21,25 +21,18 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; -import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; -import android.util.Log; -import android.util.TypedValue; -import android.view.Gravity; import android.view.KeyEvent; -import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.view.ViewGroup; import android.view.WindowManager; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.SeekBar; import android.widget.TextView; -import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; @@ -62,6 +55,7 @@ import de.yaacc.util.AboutActivity; import de.yaacc.util.ThemeHelper; import de.yaacc.util.YaaccLogActivity; +import de.yaacc.util.YaaccLogger; import de.yaacc.util.image.ImageDownloadTask; /** @@ -77,11 +71,10 @@ public class AVTransportPlayerActivity extends AppCompatActivity implements Serv private int playerId; private AVTransportController player; - private Toast volumeToast; public void onServiceConnected(ComponentName className, IBinder binder) { if (binder instanceof PlayerService.PlayerServiceBinder) { - Log.d(getClass().getName(), "PlayerService connected"); + YaaccLogger.d(getClass().getName(), "PlayerService connected"); playerService = ((PlayerService.PlayerServiceBinder) binder).getService(); initialize(); } @@ -89,7 +82,7 @@ public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceDisconnected(ComponentName className) { - Log.d(getClass().getName(), "PlayerService disconnected"); + YaaccLogger.d(getClass().getName(), "PlayerService disconnected"); playerService = null; } @@ -107,7 +100,7 @@ protected void onPause() { try { getPlayerService().unbindService(this); } catch (IllegalArgumentException iae) { - Log.d(getClass().getName(), "Ignore exception on unbind service while activity pause"); + YaaccLogger.d(getClass().getName(), "Ignore exception on unbind service while activity pause"); } } } @@ -123,7 +116,8 @@ protected void onRestart() { @Override protected void onResume() { super.onResume(); - setVolumeControlStream(-1000); //use an invalid audio stream to block controlling default streams + // Use music stream for volume control (works for both local and remote) + setVolumeControlStream(android.media.AudioManager.STREAM_MUSIC); this.bindService(new Intent(this, PlayerService.class), this, Context.BIND_AUTO_CREATE); updateTime = true; @@ -132,50 +126,10 @@ protected void onResume() { @Override public boolean onKeyDown(int keyCode, KeyEvent event) { - if (getPlayer() != null && getPlayer().hasActionGetVolume() && (KeyEvent.KEYCODE_VOLUME_UP == keyCode || KeyEvent.KEYCODE_VOLUME_DOWN == keyCode)) { - Drawable icon = null; - switch (keyCode) { - case KeyEvent.KEYCODE_VOLUME_UP: - if (getPlayer().getVolume() < 100) { - getPlayer().setVolume(getPlayer().getVolume() + 1); - } - icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_volume_up_96, getTheme()), getTheme()); - break; - case KeyEvent.KEYCODE_VOLUME_DOWN: - if (getPlayer().getVolume() > 0) { - getPlayer().setVolume(getPlayer().getVolume() - 1); - } - icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_volume_down_96, getTheme()), getTheme()); - break; - } - SeekBar volumeSeekBar = (SeekBar) findViewById(R.id.avtransportPlayerActivityControlVolumeSeekBar); - volumeSeekBar.setProgress(getPlayer().getVolume()); - if (volumeToast != null) { - volumeToast.cancel(); - } - volumeToast = createVolumeToast(icon, getPlayer().getVolume()); - volumeToast.show(); - } + // Let system handle all volume keys - shows standard volume UI return super.onKeyDown(keyCode, event); } - private Toast createVolumeToast(Drawable icon, int volume) { - LayoutInflater inflater = getLayoutInflater(); - View layout = inflater.inflate(R.layout.custom_toast, (ViewGroup) findViewById(R.id.toast_custom)); - TypedValue typedValue = new TypedValue(); - getTheme().resolveAttribute(android.R.attr.colorBackground, typedValue, true); - layout.setBackgroundColor(typedValue.data); - ImageView imageView = (ImageView) layout.findViewById(R.id.customToastImageView); - imageView.setImageDrawable(icon); - TextView text = (TextView) layout.findViewById(R.id.customToastTextView); - text.setText("" + volume); - Toast toast = new Toast(getApplicationContext()); - toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0); - toast.setDuration(Toast.LENGTH_SHORT); - toast.setView(layout); - return toast; - } - @Override protected void onDestroy() { super.onDestroy(); @@ -183,22 +137,22 @@ protected void onDestroy() { try { unbindService(this); } catch (IllegalArgumentException iae) { - Log.d(getClass().getName(), "Ignore exception on unbind service while activity destroy"); + YaaccLogger.d(getClass().getName(), "Ignore exception on unbind service while activity destroy"); } } protected void initialize() { Player player = getPlayer(); - ImageButton btnPrev = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlPrev); - ImageButton btnNext = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlNext); - ImageButton btnStop = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlStop); - ImageButton btnPlay = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlPlay); - ImageButton btnPause = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlPause); - ImageButton btnPlaylist = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlPlaylist); - ImageButton btnExit = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlExit); - ImageButton btnFf = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlFastForward); - ImageButton btnFr = (ImageButton) findViewById(R.id.avtransportPlayerActivityControlFastRewind); + ImageButton btnPrev = findViewById(R.id.avtransportPlayerActivityControlPrev); + ImageButton btnNext = findViewById(R.id.avtransportPlayerActivityControlNext); + ImageButton btnStop = findViewById(R.id.avtransportPlayerActivityControlStop); + ImageButton btnPlay = findViewById(R.id.avtransportPlayerActivityControlPlay); + ImageButton btnPause = findViewById(R.id.avtransportPlayerActivityControlPause); + ImageButton btnPlaylist = findViewById(R.id.avtransportPlayerActivityControlPlaylist); + ImageButton btnExit = findViewById(R.id.avtransportPlayerActivityControlExit); + ImageButton btnFf = findViewById(R.id.avtransportPlayerActivityControlFastForward); + ImageButton btnFr = findViewById(R.id.avtransportPlayerActivityControlFastRewind); if (player == null) { btnPrev.setActivated(false); btnNext.setActivated(false); @@ -298,7 +252,7 @@ protected void initialize() { SeekBar volumeSeekBar = (SeekBar) findViewById(R.id.avtransportPlayerActivityControlVolumeSeekBar); volumeSeekBar.setMax(100); if (getPlayer() != null && getPlayer().hasActionGetVolume()) { - Log.d(getClass().getName(), "Volume:" + getPlayer().getVolume()); + YaaccLogger.d(getClass().getName(), "Volume:" + getPlayer().getVolume()); volumeSeekBar.setEnabled(true); volumeSeekBar.setProgress(getPlayer().getVolume()); } else { @@ -346,10 +300,10 @@ public void onStopTrackingTouch(android.widget.SeekBar seekBar) { long durationTimeMillis = Objects.requireNonNull(dateFormat.parse(durationString)).getTime(); int targetPosition = Double.valueOf(durationTimeMillis * ((double) seekBar.getProgress() / 100)).intValue(); - Log.d(getClass().getName(), "TargetPosition" + targetPosition); + YaaccLogger.d(getClass().getName(), "TargetPosition" + targetPosition); getPlayer().seekTo(targetPosition); } catch (ParseException pex) { - Log.d(getClass().getName(), "Error while parsing time string", pex); + YaaccLogger.d(getClass().getName(), "Error while parsing time string", pex); } } @@ -358,8 +312,10 @@ public void onStopTrackingTouch(android.widget.SeekBar seekBar) { } private void exit() { + YaaccLogger.d(getClass().getName(), "Exit button pressed"); Player player = getPlayer(); if (player != null) { + YaaccLogger.d(getClass().getName(), "Calling player.exit() for player: " + player.getId()); player.exit(); } finish(); @@ -379,7 +335,7 @@ protected void onCreate(Bundle savedInstanceState) { this.bindService(new Intent(this, PlayerService.class), this, Context.BIND_AUTO_CREATE); } catch (Exception ex) { - Log.d(getClass().getName(), "ignore exception on service bind during onCreate"); + YaaccLogger.d(getClass().getName(), "ignore exception on service bind during onCreate"); } // initialize buttons playerId = getIntent().getIntExtra(AVTransportPlayer.PLAYER_ID, -1); @@ -399,7 +355,7 @@ protected void onCreate(Bundle savedInstanceState) { findViewById(R.id.avtransportPlayerActivitySeparator).setVisibility(View.INVISIBLE); } } - Log.d(getClass().getName(), "Got id from intent: " + playerId); + YaaccLogger.d(getClass().getName(), "Got id from intent: " + playerId); } @@ -464,7 +420,7 @@ private void doSetTrackInfo() { position.setText(getPlayer().getPositionString()); TextView next = findViewById(R.id.avtransportPlayerActivityNextItem); next.setText(getPlayer().getNextItemTitle()); - ImageView albumArtView = (ImageView) findViewById(R.id.avtransportPlayerActivityImageView); + ImageView albumArtView = findViewById(R.id.avtransportPlayerActivityImageView); URI albumArtUri = getPlayer().getAlbumArt(); if (null != albumArtUri) { @@ -493,7 +449,7 @@ private void doSetTrackInfo() { seekBar.setProgress(progress); } } catch (ParseException pex) { - Log.d(getClass().getName(), "Error while parsing time string", pex); + YaaccLogger.d(getClass().getName(), "Error while parsing time string", pex); } } diff --git a/yaacc/src/main/java/de/yaacc/player/AVTransportPlayerWrapper.java b/yaacc/src/main/java/de/yaacc/player/AVTransportPlayerWrapper.java new file mode 100644 index 00000000..e2f2e8ea --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/player/AVTransportPlayerWrapper.java @@ -0,0 +1,833 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.player; + +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.TextureView; + +import androidx.media3.common.AudioAttributes; +import androidx.media3.common.DeviceInfo; +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.PlaybackException; +import androidx.media3.common.PlaybackParameters; +import androidx.media3.common.Player; +import androidx.media3.common.Timeline; +import androidx.media3.common.TrackSelectionParameters; +import androidx.media3.common.Tracks; +import androidx.media3.common.VideoSize; +import androidx.media3.common.text.CueGroup; +import androidx.media3.common.util.Size; +import androidx.media3.common.util.UnstableApi; +import androidx.preference.PreferenceManager; + +import org.fourthline.cling.support.model.DIDLObject; + +import java.net.URI; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import de.yaacc.R; +import de.yaacc.util.YaaccLogger; + +/** + * Media3 Player wrapper for AVTransportPlayer. + * Allows using PlayerNotificationManager with remote UPnP playback. + */ +@UnstableApi +public class AVTransportPlayerWrapper implements Player { + + private final AVTransportPlayer avTransportPlayer; + private final List listeners = new CopyOnWriteArrayList<>(); + private SharedPreferences preferences; + private int state; + + public AVTransportPlayerWrapper(AVTransportPlayer avTransportPlayer, Listener listener) { + this.avTransportPlayer = avTransportPlayer; + if (listener != null) { + this.listeners.add(listener); + } + this.preferences = PreferenceManager.getDefaultSharedPreferences(avTransportPlayer.getContext()); + } + + public void notifyPlaybackStateChanged() { + new Handler(Looper.getMainLooper()).post(() -> { + for (Listener listener : listeners) { + listener.onIsPlayingChanged(avTransportPlayer.isPlaying()); + listener.onMediaItemTransition(getCurrentMediaItem(), Player.MEDIA_ITEM_TRANSITION_REASON_AUTO); + } + }); + } + + public void notifyVolumeChanged() { + new Handler(Looper.getMainLooper()).post(() -> { + for (Listener listener : listeners) { + listener.onVolumeChanged(getVolume()); + } + }); + } + + @Override + public Looper getApplicationLooper() { + return Looper.getMainLooper(); + } + + @Override + public void addListener(Listener listener) { + YaaccLogger.d("AVTransportPlayerWrapper", "addListener called: " + (listener != null ? listener.getClass().getSimpleName() : "null")); + if (listener != null && !listeners.contains(listener)) { + listeners.add(listener); + } + } + + @Override + public void removeListener(Listener listener) { + listeners.remove(listener); + } + + @Override + public void setMediaItems(List mediaItems) { + // Not needed - AVTransportPlayer manages its own playlist + } + + @Override + public void setMediaItems(List mediaItems, boolean resetPosition) { + // Not needed + } + + @Override + public void setMediaItems(List mediaItems, int startIndex, long startPositionMs) { + // Not needed + } + + @Override + public void setMediaItem(MediaItem mediaItem) { + // Not needed + } + + @Override + public void setMediaItem(MediaItem mediaItem, long startPositionMs) { + // Not needed + } + + @Override + public void setMediaItem(MediaItem mediaItem, boolean resetPosition) { + // Not needed + } + + @Override + public void addMediaItem(MediaItem mediaItem) { + // Not needed + } + + @Override + public void addMediaItem(int index, MediaItem mediaItem) { + // Not needed + } + + @Override + public void addMediaItems(List mediaItems) { + // Not needed + } + + @Override + public void addMediaItems(int index, List mediaItems) { + // Not needed + } + + @Override + public void moveMediaItem(int currentIndex, int newIndex) { + // Not needed + } + + @Override + public void moveMediaItems(int fromIndex, int toIndex, int newIndex) { + // Not needed + } + + @Override + public void replaceMediaItem(int index, MediaItem mediaItem) { + + } + + @Override + public void replaceMediaItems(int fromIndex, int toIndex, List mediaItems) { + + } + + @Override + public void removeMediaItem(int index) { + // Not needed + } + + @Override + public void removeMediaItems(int fromIndex, int toIndex) { + // Not needed + } + + @Override + public void clearMediaItems() { + // Not needed + } + + @Override + public boolean isCommandAvailable(int command) { + return true; + } + + @Override + public boolean canAdvertiseSession() { + return true; + } + + @Override + public Commands getAvailableCommands() { + return new Commands.Builder().addAllCommands().build(); + } + + @Override + public void prepare() { + // Not needed + } + + @Override + public int getPlaybackState() { + if (avTransportPlayer == null || avTransportPlayer.isProcessingCommand()) { + YaaccLogger.d("AVTransportPlayerWrapper", "getPlaybackState: IDLE (processing)"); + return Player.STATE_IDLE; + } + + boolean playing = avTransportPlayer.isPlaying(); + int state = playing ? Player.STATE_READY : Player.STATE_IDLE; + YaaccLogger.d("AVTransportPlayerWrapper", "getPlaybackState: " + state + " (playing=" + playing + ")"); + return state; + } + + @Override + public int getPlaybackSuppressionReason() { + return PLAYBACK_SUPPRESSION_REASON_NONE; + } + + @Override + public PlaybackException getPlayerError() { + return null; + } + + @Override + public void play() { + YaaccLogger.d("AVTransportPlayerWrapper", "play() called"); + avTransportPlayer.play(); + // Notify listeners that playback started + for (Listener listener : listeners) { + YaaccLogger.d("AVTransportPlayerWrapper", "Notifying listener: playWhenReady=true"); + listener.onPlayWhenReadyChanged(true, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + listener.onPlaybackStateChanged(Player.STATE_READY); + } + } + + @Override + public void pause() { + YaaccLogger.d("AVTransportPlayerWrapper", "pause() called"); + avTransportPlayer.pause(); + // Notify listeners that playback paused + for (Listener listener : listeners) { + YaaccLogger.d("AVTransportPlayerWrapper", "Notifying listener: playWhenReady=false"); + listener.onPlayWhenReadyChanged(false, Player.PLAY_WHEN_READY_CHANGE_REASON_USER_REQUEST); + } + } + + @Override + public void setPlayWhenReady(boolean playWhenReady) { + if (playWhenReady) { + play(); + } else { + pause(); + } + } + + @Override + public boolean getPlayWhenReady() { + boolean playing = avTransportPlayer.isPlaying(); + YaaccLogger.d("AVTransportPlayerWrapper", "getPlayWhenReady: " + playing); + return playing; + } + + @Override + public void setRepeatMode(int repeatMode) { + preferences.edit().putBoolean(avTransportPlayer.getContext().getString(R.string.settings_replay_playlist_chkbx), repeatMode == REPEAT_MODE_ALL).apply(); + + } + + @Override + public int getRepeatMode() { + if (preferences.getBoolean(avTransportPlayer.getContext().getString(R.string.settings_replay_playlist_chkbx), false)) { + return REPEAT_MODE_ALL; + } + ; + + return REPEAT_MODE_OFF; + } + + @Override + public void setShuffleModeEnabled(boolean shuffleModeEnabled) { + preferences.edit().putBoolean(avTransportPlayer.getContext().getString(R.string.settings_music_player_shuffle_chkbx), shuffleModeEnabled).apply(); + } + + @Override + public boolean getShuffleModeEnabled() { + return avTransportPlayer.isShufflePlay(); + } + + @Override + public boolean isLoading() { + return false; + } + + @Override + public void seekToDefaultPosition() { + seekTo(0); + } + + @Override + public void seekToDefaultPosition(int mediaItemIndex) { + seekTo(mediaItemIndex, 0); + } + + @Override + public void seekTo(long positionMs) { + avTransportPlayer.seekTo(positionMs); + } + + @Override + public void seekTo(int mediaItemIndex, long positionMs) { + avTransportPlayer.seekTo(positionMs); + } + + @Override + public long getSeekBackIncrement() { + return 10000; + } + + @Override + public void seekBack() { + seekTo(Math.max(0, getCurrentPosition() - getSeekBackIncrement())); + } + + @Override + public long getSeekForwardIncrement() { + return 10000; + } + + @Override + public void seekForward() { + seekTo(getCurrentPosition() + getSeekForwardIncrement()); + } + + @Override + public boolean hasPrevious() { + return avTransportPlayer.getCurrentItemIndex() > 0; + } + + @Override + public boolean hasPreviousWindow() { + return false; + } + + @Override + public boolean hasPreviousMediaItem() { + return avTransportPlayer.getCurrentItemIndex() > 0; + } + + @Override + public void previous() { + avTransportPlayer.previous(); + } + + @Override + public void seekToPreviousWindow() { + + } + + @Override + public boolean hasNextMediaItem() { + return avTransportPlayer.getItems() != null && + avTransportPlayer.getCurrentItemIndex() < avTransportPlayer.getItems().size() - 1; + } + + @Override + public void next() { + avTransportPlayer.next(); + } + + @Override + public void seekToNextWindow() { + + } + + @Override + public void seekToPreviousMediaItem() { + avTransportPlayer.previous(); + } + + @Override + public long getMaxSeekToPreviousPosition() { + return 0; + } + + @Override + public void seekToNextMediaItem() { + avTransportPlayer.next(); + } + + @Override + public void seekToPrevious() { + seekToPreviousMediaItem(); + } + + @Override + public boolean hasNext() { + return avTransportPlayer.getCurrentItemIndex() < avTransportPlayer.getItems().size() - 1; + } + + @Override + public boolean hasNextWindow() { + return false; + } + + @Override + public void seekToNext() { + seekToNextMediaItem(); + } + + @Override + public void setPlaybackParameters(PlaybackParameters playbackParameters) { + // Not supported + } + + @Override + public void setPlaybackSpeed(float speed) { + // Not supported + } + + @Override + public PlaybackParameters getPlaybackParameters() { + return PlaybackParameters.DEFAULT; + } + + @Override + public void stop() { + avTransportPlayer.stop(); + } + + @Override + public void release() { + // Handled by AVTransportPlayer + } + + @Override + public Timeline getCurrentTimeline() { + return Timeline.EMPTY; + } + + @Override + public int getCurrentPeriodIndex() { + return 0; + } + + @Override + public int getCurrentWindowIndex() { + return 0; + } + + @Override + public int getCurrentMediaItemIndex() { + return avTransportPlayer.getCurrentItemIndex(); + } + + @Override + public int getNextWindowIndex() { + return 0; + } + + @Override + public int getNextMediaItemIndex() { + return hasNextMediaItem() ? getCurrentMediaItemIndex() + 1 : -1; + } + + @Override + public int getPreviousWindowIndex() { + return 0; + } + + @Override + public int getPreviousMediaItemIndex() { + return hasPreviousMediaItem() ? getCurrentMediaItemIndex() - 1 : -1; + } + + @Override + public MediaItem getCurrentMediaItem() { + if (avTransportPlayer.getItems() != null && + avTransportPlayer.getCurrentItemIndex() >= 0 && + avTransportPlayer.getCurrentItemIndex() < avTransportPlayer.getItems().size()) { + PlayableItem item = avTransportPlayer.getItems().get(avTransportPlayer.getCurrentItemIndex()); + if (item != null && item.getItem() != null) { + // Use getAlbumArt() which includes cover.jpg fallback + URI albumArtJavaUri = avTransportPlayer.getAlbumArt(); + MediaItem mediaItem = new MediaItem.Builder() + .setUri(item.getUri()) + .setMediaMetadata(new MediaMetadata.Builder() + .setTitle(item.getTitle()) + .setArtworkUri(albumArtJavaUri != null ? Uri.parse(albumArtJavaUri.toString()) : null) + .build()) + .build(); + YaaccLogger.v(getClass().getName(), "getCurrentMediaItem: " + item.getTitle()); + return mediaItem; + } + } + YaaccLogger.d(getClass().getName(), "getCurrentMediaItem: null"); + return null; + } + + @Override + public int getMediaItemCount() { + return avTransportPlayer.getItems() != null ? avTransportPlayer.getItems().size() : 0; + } + + @Override + public MediaItem getMediaItemAt(int index) { + if (avTransportPlayer.getItems() != null && + index >= 0 && index < avTransportPlayer.getItems().size()) { + PlayableItem item = avTransportPlayer.getItems().get(index); + if (item != null && item.getItem() != null) { + DIDLObject.Property albumArtUriProperty = item.getItem().getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class); + URI albumArtUri = (albumArtUriProperty == null) ? null : albumArtUriProperty.getValue(); + return new MediaItem.Builder() + .setUri(item.getUri()) + .setMediaMetadata(new MediaMetadata.Builder() + .setTitle(item.getTitle()) + .setArtworkUri(albumArtUri != null ? Uri.parse(albumArtUri.toString()) : null) + .build()) + .build(); + } + } + return null; + } + + @Override + public long getDuration() { + // Parse duration string to milliseconds + String duration = avTransportPlayer.getDuration(); + if (duration == null || duration.equals("00:00:00")) { + return androidx.media3.common.C.TIME_UNSET; + } + // Format is "HH:MM:SS" + try { + String[] parts = duration.split(":"); + if (parts.length == 3) { + long hours = Long.parseLong(parts[0]); + long minutes = Long.parseLong(parts[1]); + long seconds = Long.parseLong(parts[2]); + long durationMs = (hours * 3600 + minutes * 60 + seconds) * 1000; + android.util.Log.d("AVTransportPlayerWrapper", "Duration: " + duration + " = " + durationMs + "ms"); + return durationMs; + } + } catch (Exception e) { + android.util.Log.e("AVTransportPlayerWrapper", "Failed to parse duration: " + duration, e); + } + return androidx.media3.common.C.TIME_UNSET; + } + + @Override + public long getCurrentPosition() { + return avTransportPlayer.getCurrentPosition(); + } + + @Override + public long getBufferedPosition() { + return getCurrentPosition(); + } + + @Override + public int getBufferedPercentage() { + return 0; + } + + @Override + public long getTotalBufferedDuration() { + return 0; + } + + @Override + public boolean isCurrentWindowDynamic() { + return false; + } + + @Override + public boolean isCurrentMediaItemDynamic() { + return false; + } + + @Override + public boolean isCurrentWindowLive() { + return false; + } + + @Override + public boolean isCurrentMediaItemLive() { + return false; + } + + @Override + public long getCurrentLiveOffset() { + return 0; + } + + @Override + public boolean isCurrentWindowSeekable() { + return false; + } + + @Override + public boolean isCurrentMediaItemSeekable() { + return true; + } + + @Override + public boolean isPlayingAd() { + return false; + } + + @Override + public int getCurrentAdGroupIndex() { + return -1; + } + + @Override + public int getCurrentAdIndexInAdGroup() { + return -1; + } + + @Override + public long getContentDuration() { + return getDuration(); + } + + @Override + public long getContentPosition() { + return getCurrentPosition(); + } + + @Override + public long getContentBufferedPosition() { + return getBufferedPosition(); + } + + @Override + public AudioAttributes getAudioAttributes() { + return AudioAttributes.DEFAULT; + } + + @Override + public void setAudioAttributes(AudioAttributes audioAttributes, boolean handleAudioFocus) { + // Not supported for remote playback + } + + @Override + public void setVolume(float volume) { + avTransportPlayer.setVolume((int) (volume * 100)); + } + + @Override + public float getVolume() { + return avTransportPlayer.getVolume() / 100f; + } + + @Override + public void clearVideoSurface() { + // Not supported + } + + @Override + public void clearVideoSurface(android.view.Surface surface) { + // Not supported + } + + @Override + public void setVideoSurface(android.view.Surface surface) { + // Not supported + } + + @Override + public void setVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + // Not supported + } + + @Override + public void clearVideoSurfaceHolder(SurfaceHolder surfaceHolder) { + // Not supported + } + + @Override + public void setVideoSurfaceView(SurfaceView surfaceView) { + // Not supported + } + + @Override + public void clearVideoSurfaceView(SurfaceView surfaceView) { + // Not supported + } + + @Override + public void setVideoTextureView(TextureView textureView) { + // Not supported + } + + @Override + public void clearVideoTextureView(TextureView textureView) { + // Not supported + } + + @Override + public VideoSize getVideoSize() { + return VideoSize.UNKNOWN; + } + + @Override + public Size getSurfaceSize() { + return Size.UNKNOWN; + } + + @Override + public CueGroup getCurrentCues() { + return CueGroup.EMPTY_TIME_ZERO; + } + + @Override + public DeviceInfo getDeviceInfo() { + int currentVolume = avTransportPlayer.getVolume(); + YaaccLogger.d("AVTransportPlayerWrapper", "getDeviceInfo() - PLAYBACK_TYPE_REMOTE, volume=" + currentVolume); + return new DeviceInfo.Builder(DeviceInfo.PLAYBACK_TYPE_REMOTE) + .setMaxVolume(100) + .setMinVolume(0) + .build(); + } + + @Override + public int getDeviceVolume() { + int volume = avTransportPlayer.getVolume(); + YaaccLogger.d("AVTransportPlayerWrapper", "getDeviceVolume() returning: " + volume); + return volume; + } + + @Override + public boolean isDeviceMuted() { + return avTransportPlayer.getMute(); + } + + @Override + public void setDeviceVolume(int volume) { + YaaccLogger.d("AVTransportPlayerWrapper", "setDeviceVolume(" + volume + ")"); + avTransportPlayer.setVolume(volume); + notifyVolumeChanged(); + } + + @Override + public void setDeviceVolume(int volume, int flags) { + setDeviceVolume(volume); + } + + @Override + public void increaseDeviceVolume() { + setDeviceVolume(Math.min(100, getDeviceVolume() + 10)); + } + + @Override + public void increaseDeviceVolume(int flags) { + increaseDeviceVolume(); + } + + @Override + public void decreaseDeviceVolume() { + setDeviceVolume(Math.max(0, getDeviceVolume() - 10)); + } + + @Override + public void decreaseDeviceVolume(int flags) { + decreaseDeviceVolume(); + } + + @Override + public void setDeviceMuted(boolean muted) { + avTransportPlayer.setMute(muted); + } + + @Override + public void setDeviceMuted(boolean muted, int flags) { + setDeviceMuted(muted); + } + + @Override + public Tracks getCurrentTracks() { + return Tracks.EMPTY; + } + + @Override + public TrackSelectionParameters getTrackSelectionParameters() { + return TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT; + } + + @Override + public void setTrackSelectionParameters(TrackSelectionParameters parameters) { + // Not supported + } + + @Override + public MediaMetadata getMediaMetadata() { + MediaItem item = getCurrentMediaItem(); + return item != null ? item.mediaMetadata : MediaMetadata.EMPTY; + } + + @Override + public MediaMetadata getPlaylistMetadata() { + return MediaMetadata.EMPTY; + } + + @Override + public void setPlaylistMetadata(MediaMetadata mediaMetadata) { + // Not supported + } + + @Override + public Object getCurrentManifest() { + return null; + } + + @Override + public boolean isPlaying() { + boolean playing = avTransportPlayer.isPlaying(); + YaaccLogger.d("AVTransportPlayerWrapper", "isPlaying() called - returning " + playing); + return playing; + } + + +} diff --git a/yaacc/src/main/java/de/yaacc/player/AbstractPlayer.java b/yaacc/src/main/java/de/yaacc/player/AbstractPlayer.java index 09f4b22a..112eb515 100644 --- a/yaacc/src/main/java/de/yaacc/player/AbstractPlayer.java +++ b/yaacc/src/main/java/de/yaacc/player/AbstractPlayer.java @@ -27,13 +27,22 @@ import android.content.ServiceConnection; import android.content.SharedPreferences; import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; import android.os.Build; +import android.os.Handler; import android.os.IBinder; -import android.util.Log; +import android.os.Looper; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; import android.widget.Toast; import androidx.core.app.NotificationCompat; import androidx.core.content.ContextCompat; +import androidx.core.graphics.drawable.IconCompat; +import androidx.media.app.NotificationCompat.MediaStyle; +import androidx.media.session.MediaButtonReceiver; import androidx.preference.PreferenceManager; import java.beans.PropertyChangeListener; @@ -50,6 +59,8 @@ import de.yaacc.R; import de.yaacc.Yaacc; import de.yaacc.upnp.UpnpClient; +import de.yaacc.util.ThemeHelper; +import de.yaacc.util.YaaccLogger; /** * @author Tobias Schoene (openbit) @@ -74,6 +85,7 @@ public abstract class AbstractPlayer implements Player, ServiceConnection { private Object loadedItem = null; private int currentLoadedIndex = -1; private Bitmap icon = null; + private MediaSessionCompat mediaSession; /** * @param upnpClient the upnpclient @@ -82,11 +94,66 @@ public AbstractPlayer(UpnpClient upnpClient) { super(); this.upnpClient = upnpClient; startService(); + // Initialize MediaSession on main thread + new Handler(Looper.getMainLooper()).post(this::initMediaSession); + } + + private void initMediaSession() { + mediaSession = new MediaSessionCompat(getContext(), "YaaccPlayer_" + getId()); + mediaSession.setCallback(new MediaSessionCompat.Callback() { + @Override + public void onPlay() { + YaaccLogger.d(getClass().getName(), "MediaSession callback: onPlay() - isPlaying=" + isPlaying()); + AbstractPlayer.this.play(); + } + + @Override + public void onPause() { + YaaccLogger.d(getClass().getName(), "MediaSession callback: onPause() - isPlaying=" + isPlaying()); + AbstractPlayer.this.pause(); + } + + @Override + public void onStop() { + YaaccLogger.d(getClass().getName(), "MediaSession callback: onStop()"); + AbstractPlayer.this.stop(); + } + + @Override + public void onSkipToNext() { + YaaccLogger.d(getClass().getName(), "MediaSession callback: onSkipToNext()"); + AbstractPlayer.this.next(); + } + + @Override + public void onSkipToPrevious() { + YaaccLogger.d(getClass().getName(), "MediaSession callback: onSkipToPrevious()"); + AbstractPlayer.this.previous(); + } + }); + mediaSession.setActive(true); + + // Allow subclasses to configure MediaSession (e.g., for remote volume) + configureMediaSession(mediaSession); + } + + /** + * Override this to configure MediaSession (e.g., set volume provider for remote playback). + */ + protected void configureMediaSession(MediaSessionCompat mediaSession) { + // Default: local playback, no special configuration + } + + /** + * Get the MediaSession for this player. + */ + public MediaSessionCompat getMediaSession() { + return mediaSession; } public void onServiceConnected(ComponentName className, IBinder binder) { if (binder instanceof PlayerService.PlayerServiceBinder) { - Log.d("ServiceConnection", "connected"); + YaaccLogger.d("ServiceConnection", "connected"); playerService = ((PlayerService.PlayerServiceBinder) binder).getService(); playerService.addPlayer(this); @@ -95,7 +162,7 @@ public void onServiceConnected(ComponentName className, IBinder binder) { public void onServiceDisconnected(ComponentName className) { - Log.d("ServiceConnection", "disconnected"); + YaaccLogger.d("ServiceConnection", "disconnected"); if (playerService != null) { playerService.removePlayer(this); } @@ -127,9 +194,9 @@ public UpnpClient getUpnpClient() { public void startService() { if (playerService == null) { - upnpClient.getContext().startForegroundService(new Intent(upnpClient.getContext(), PlayerService.class)); - upnpClient.getContext().bindService(new Intent(upnpClient.getContext(), PlayerService.class), - this, Context.BIND_AUTO_CREATE); + Intent intent = new Intent(upnpClient.getContext(), PlayerService.class); + upnpClient.getContext().startForegroundService(intent); + upnpClient.getContext().bindService(intent, this, Context.BIND_AUTO_CREATE); } } @@ -240,9 +307,12 @@ public void run() { toast.show(); }); } - isPlaying = false; + setPlaying(false); paused = true; doPause(); + updatePlaybackState(PlaybackStateCompat.STATE_PAUSED); + updateMetadata(); + showNotification(); setProcessingCommand(false); } }, new Date(System.currentTimeMillis())); @@ -259,12 +329,20 @@ public void play() { return; setProcessingCommand(true); int possibleNextIndex = currentIndex; - if (possibleNextIndex >= 0 && possibleNextIndex < items.size()) { - loadItem(possibleNextIndex); - } + executeCommand(new TimerTask() { @Override public void run() { + // Load item asynchronously in background thread + if (possibleNextIndex >= 0 && possibleNextIndex < items.size()) { + Object item = loadItem(possibleNextIndex); + if (item == null) { + // Item not ready yet, stop processing + setProcessingCommand(false); + return; + } + } + if (currentIndex < items.size()) { Context context = getUpnpClient().getContext(); if (context instanceof Activity) { @@ -275,12 +353,16 @@ public void run() { toast.show(); }); } - isPlaying = true; + setPlaying(true); if (paused) { + paused = false; // Clear paused flag when resuming doResume(); } else { loadItem(previousIndex, currentIndex); } + updatePlaybackState(PlaybackStateCompat.STATE_PLAYING); + updateMetadata(); + showNotification(); setProcessingCommand(false); } } @@ -317,8 +399,9 @@ public void run() { if (!items.isEmpty()) { stopItem(items.get(currentIndex)); } - isPlaying = false; + setPlaying(false); paused = false; + updatePlaybackState(PlaybackStateCompat.STATE_STOPPED); setProcessingCommand(false); } }, new Date(System.currentTimeMillis())); @@ -331,7 +414,9 @@ public void run() { */ protected boolean isShufflePlay() { - return false; + //FIXME need to be a property in each player + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return preferences.getBoolean(getContext().getString(R.string.settings_music_player_shuffle_chkbx), false); } /* @@ -359,15 +444,30 @@ public boolean isPlaying() { return isPlaying; } + public boolean isPaused() { + return paused; + } + + protected void setPaused(boolean paused) { + this.paused = paused; + } + + public boolean isStopped() { + return !(isPlaying || paused); + } + public void setPlaying(boolean isPlaying) { + boolean wasPlaying = this.isPlaying; this.isPlaying = isPlaying; - } - public int getCurrentIndex() { - return currentIndex; + // Notify service of state change for foreground management + if (wasPlaying != isPlaying) { + firePropertyChange("playing", wasPlaying, isPlaying); + } } - public void setCurrentIndex(int currentIndex) { + + protected void setCurrentIndex(int currentIndex) { this.currentIndex = currentIndex; } @@ -382,6 +482,9 @@ public List getItems() { */ @Override public void setItems(PlayableItem... playableItems) { + boolean wasPlaying = isPlaying(); + cancelTimer(); // Cancel any pending auto-advance timer + items.clear(); // Clear existing items before adding new ones List itemsList = Arrays.asList(playableItems); if (isShufflePlay()) { @@ -389,6 +492,11 @@ public void setItems(PlayableItem... playableItems) { } items.addAll(itemsList); showNotification(); + + // Restart timer if we were playing + if (wasPlaying && items.size() > 1) { + updateTimer(); + } } @Override @@ -440,13 +548,16 @@ public String getNextItemTitle() { protected Object loadItem(int toLoadIndex) { if (toLoadIndex == currentLoadedIndex && loadedItem != null) { - Log.d(getClass().getName(), "returning already loaded item"); + YaaccLogger.d(getClass().getName(), "returning already loaded item"); return loadedItem; } if (toLoadIndex >= 0 && toLoadIndex <= items.size()) { - Log.d(getClass().getName(), "loaded item"); + YaaccLogger.d(getClass().getName(), "loaded item"); currentLoadedIndex = toLoadIndex; - loadedItem = loadItem(items.get(toLoadIndex)); + + PlayableItem playableItem = items.get(toLoadIndex); + + loadedItem = loadItem(playableItem); return loadedItem; } return null; @@ -459,7 +570,7 @@ protected void loadItem(int previousIndex, int nextIndex) { Object loadedItem = loadItem(nextIndex); firePropertyChange(PROPERTY_ITEM, items.get(previousIndex), items.get(nextIndex)); - startItem(playableItem, loadedItem); + startItem(playableItem, loadedItem, nextIndex); doPostLoadItem(playableItem); } @@ -520,10 +631,10 @@ protected long parseTimeStringToMillis(String timeString) { } millis = millis * 1000; } catch (Exception e) { - Log.d(getClass().getName(), "ignoring error on parsing to millis of:" + timeString, e); + YaaccLogger.d(getClass().getName(), "ignoring error on parsing to millis of:" + timeString, e); } } - Log.v(getClass().getName(), "parsing time string" + timeString + " result millis:" + millis); + YaaccLogger.v(getClass().getName(), "parsing time string" + timeString + " result millis:" + millis); return millis; } @@ -538,7 +649,7 @@ public long getRemainingTime() { * @param duration in millis */ public void startTimer(final long duration) { - Log.d(getClass().getName(), "Start timer duration: " + duration); + YaaccLogger.d(getClass().getName(), "Start timer duration: " + duration); cancelTimer(); Intent intent = new Intent(); intent.setAction(PlayerServiceBroadcastReceiver.ACTION_NEXT); @@ -556,14 +667,14 @@ public void startTimer(final long duration) { System.currentTimeMillis() + duration, alarmIntent ); - Log.d(getClass().getName(), "ExactAndAllowWhileIdle alarm event in: " + (System.currentTimeMillis() + duration)); + YaaccLogger.d(getClass().getName(), "ExactAndAllowWhileIdle alarm event in: " + (System.currentTimeMillis() + duration)); } else { alarmManager.setAndAllowWhileIdle( AlarmManager.RTC_WAKEUP, System.currentTimeMillis() + duration, alarmIntent ); - Log.d(getClass().getName(), "AndAllowWhileIdle alarm event in: " + (System.currentTimeMillis() + duration)); + YaaccLogger.d(getClass().getName(), "AndAllowWhileIdle alarm event in: " + (System.currentTimeMillis() + duration)); } } else { alarmManager.setExact( @@ -571,7 +682,7 @@ public void startTimer(final long duration) { System.currentTimeMillis() + duration, alarmIntent ); - Log.d(getClass().getName(), "exact alarm event in: " + (System.currentTimeMillis() + duration)); + YaaccLogger.d(getClass().getName(), "exact alarm event in: " + (System.currentTimeMillis() + duration)); } }); @@ -614,18 +725,45 @@ public void setProcessingCommand(boolean isProcessingCommand) { */ @Override public void exit() { + YaaccLogger.d(getClass().getName(), "Player.exit() called for player: " + getId()); if (isPlaying()) { stop(); } playerService.shutdown(this); - } /** * Displays the notification. */ private void showNotification() { + showNotificationInternal(); + } + + protected void showNotificationInternal() { + // Run on background thread to avoid blocking UI + new Thread(() -> showNotificationWithRetry(3)).start(); + } + + private void showNotificationWithRetry(int retryCount) { + // If MediaSession not ready yet, retry after delay (max 10 times = 2 seconds) + if (mediaSession == null && retryCount < 10) { + try { + Thread.sleep(200); + } catch (InterruptedException e) { + // Ignore + } + showNotificationWithRetry(retryCount + 1); + return; + } + + if (mediaSession == null) { + YaaccLogger.w(getClass().getName(), "MediaSession not ready after retries, skipping notification"); + return; + } + ((Yaacc) getContext().getApplicationContext()).createYaaccGroupNotification(); + + // Create media style notification with controls NotificationCompat.Builder mBuilder = new NotificationCompat.Builder( getContext(), Yaacc.NOTIFICATION_CHANNEL_ID).setOngoing(false) .setGroup(Yaacc.NOTIFICATION_GROUP_KEY) @@ -634,6 +772,43 @@ private void showNotification() { .setLargeIcon(getIcon()) .setContentTitle("Yaacc player") .setContentText(getShortName() == null ? "" : getShortName()); + + // Add progress bar if duration is available + try { + String durationStr = getDuration(); + if (durationStr != null && !durationStr.isEmpty()) { + // Parse duration string (format: "HH:MM:SS" or "MM:SS") + String[] parts = durationStr.split(":"); + long durationMs = 0; + if (parts.length == 3) { + durationMs = (Long.parseLong(parts[0]) * 3600 + Long.parseLong(parts[1]) * 60 + Long.parseLong(parts[2])) * 1000; + } else if (parts.length == 2) { + durationMs = (Long.parseLong(parts[0]) * 60 + Long.parseLong(parts[1])) * 1000; + } + if (durationMs > 0) { + long position = getCurrentPosition(); + mBuilder.setProgress((int) durationMs, (int) position, false); + } + } + } catch (Exception e) { + // Ignore parsing errors, just don't show progress + } + + // Add media controls if MediaSession is ready + if (mediaSession != null) { + mBuilder.setStyle(new MediaStyle() + .setMediaSession(mediaSession.getSessionToken()) + .setShowActionsInCompactView(0, 1, 2)) + .addAction(createNotificationAction(R.drawable.ic_baseline_skip_previous_24, "Previous", + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS)) + .addAction(createNotificationAction( + isPlaying() ? R.drawable.ic_baseline_pause_24 : R.drawable.ic_baseline_play_arrow_24, + isPlaying() ? "Pause" : "Play", + isPlaying() ? PlaybackStateCompat.ACTION_PAUSE : PlaybackStateCompat.ACTION_PLAY)) + .addAction(createNotificationAction(R.drawable.ic_baseline_skip_next_24, "Next", + PlaybackStateCompat.ACTION_SKIP_TO_NEXT)); + } + PendingIntent contentIntent = getNotificationIntent(); if (contentIntent != null) { mBuilder.setContentIntent(contentIntent); @@ -651,10 +826,103 @@ private void cancelNotification() { NotificationManager mNotificationManager = (NotificationManager) getContext() .getSystemService(Context.NOTIFICATION_SERVICE); // mId allows you to update the notification later on. - Log.d(getClass().getName(), "Cancel Notification with ID: " + getNotificationId()); + YaaccLogger.d(getClass().getName(), "Cancel Notification with ID: " + getNotificationId()); mNotificationManager.cancel(getNotificationId()); ((Yaacc) getContext().getApplicationContext()).cancelYaaccGroupNotification(); + if (mediaSession != null) { + mediaSession.setActive(false); + mediaSession.release(); + mediaSession = null; + } + } + + private NotificationCompat.Action createNotificationAction(int iconRes, String title, long action) { + Drawable drawable = ContextCompat.getDrawable(getContext(), iconRes); + IconCompat icon; + + if (drawable != null) { + drawable = ThemeHelper.tintDrawable(drawable, getContext().getTheme()); + Bitmap bitmap = Bitmap.createBitmap( + drawable.getIntrinsicWidth(), + drawable.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + icon = IconCompat.createWithBitmap(bitmap); + } else { + icon = IconCompat.createWithResource(getContext(), iconRes); + } + + return new NotificationCompat.Action.Builder(icon, title, createMediaAction(action)).build(); + } + + private PendingIntent createMediaAction(long action) { + Intent intent = new Intent(getContext(), MediaButtonReceiver.class); + intent.putExtra(PLAYER_ID, getId()); + return PendingIntent.getBroadcast(getContext(), (int) action, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } + + private void updatePlaybackState(int state) { + new Handler(Looper.getMainLooper()).post(() -> this.updatePlaybackStateInternal(state)); + } + + protected void updatePlaybackStateInternal(int state) { + if (mediaSession == null) return; + + long position = getCurrentPosition(); + + PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder() + .setActions(PlaybackStateCompat.ACTION_PLAY | + PlaybackStateCompat.ACTION_PAUSE | + PlaybackStateCompat.ACTION_PLAY_PAUSE | + PlaybackStateCompat.ACTION_STOP | + PlaybackStateCompat.ACTION_SKIP_TO_NEXT | + PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) + .setState(state, position, 1.0f); + + if (mediaSession != null) { + mediaSession.setPlaybackState(stateBuilder.build()); + } + } + + private void updateMetadata() { + updateMetadataInternal(); + } + + protected void updateMetadataInternal() { + if (mediaSession == null) return; + + MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, getCurrentItemTitle() != null ? getCurrentItemTitle() : "") + .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, getName() != null ? getName() : ""); + + // Add duration if available + try { + String durationStr = getDuration(); + if (durationStr != null && !durationStr.isEmpty()) { + String[] parts = durationStr.split(":"); + long durationMs = 0; + if (parts.length == 3) { + durationMs = (Long.parseLong(parts[0]) * 3600 + Long.parseLong(parts[1]) * 60 + Long.parseLong(parts[2])) * 1000; + } else if (parts.length == 2) { + durationMs = (Long.parseLong(parts[0]) * 60 + Long.parseLong(parts[1])) * 1000; + } + if (durationMs > 0) { + metadataBuilder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, durationMs); + } + } + } catch (Exception e) { + // Ignore parsing errors + } + + if (getIcon() != null) { + metadataBuilder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, getIcon()); + } + + mediaSession.setMetadata(metadataBuilder.build()); } /** @@ -679,7 +947,7 @@ public PendingIntent getNotificationIntent() { protected abstract Object loadItem(PlayableItem playableItem); protected abstract void startItem(PlayableItem playableItem, - Object loadedItem); + Object loadedItem, int index); /* * (non-Javadoc) @@ -691,15 +959,23 @@ public void onDestroy() { stop(); cancelNotification(); items.clear(); + + // Remove player from service if (playerService != null) { + playerService.removePlayer(this); try { playerService.unbindService(this); } catch (IllegalArgumentException iex) { - Log.d(getClass().getName(), "Exception while unbind service"); + YaaccLogger.d(getClass().getName(), "Exception while unbind service"); } - } + // Release MediaSession + if (mediaSession != null) { + mediaSession.setActive(false); + mediaSession.release(); + mediaSession = null; + } } /* diff --git a/yaacc/src/main/java/de/yaacc/player/LocalBackgoundMusicPlayer.java b/yaacc/src/main/java/de/yaacc/player/LocalBackgoundMusicPlayer.java deleted file mode 100644 index 3dfae743..00000000 --- a/yaacc/src/main/java/de/yaacc/player/LocalBackgoundMusicPlayer.java +++ /dev/null @@ -1,349 +0,0 @@ -/* - * Copyright (C) 2013 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.player; - -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.SharedPreferences; -import android.net.Uri; -import android.os.IBinder; -import android.util.Log; - -import androidx.preference.PreferenceManager; - -import org.fourthline.cling.support.model.DIDLObject; - -import java.net.URI; -import java.util.Locale; -import java.util.Timer; -import java.util.TimerTask; - -import de.yaacc.R; -import de.yaacc.musicplayer.BackgoundMusicServiceListener; -import de.yaacc.musicplayer.BackgroundMusicBroadcastReceiver; -import de.yaacc.musicplayer.BackgroundMusicService; -import de.yaacc.musicplayer.BackgroundMusicService.BackgroundMusicServiceBinder; -import de.yaacc.upnp.UpnpClient; -import de.yaacc.util.NotificationId; - -/** - * A Player for local music playing in background - * - * @author Tobias Schoene (openbit) - */ -public class LocalBackgoundMusicPlayer extends AbstractPlayer implements ServiceConnection, BackgoundMusicServiceListener { - - private BackgroundMusicService backgroundMusicService; - private Timer commandExecutionTimer; - private URI albumArtUri; - - /** - * @param name playerName - */ - public LocalBackgoundMusicPlayer(UpnpClient upnpClient, String name, String shortName) { - this(upnpClient); - setName(name); - setShortName(shortName); - } - - - public LocalBackgoundMusicPlayer(UpnpClient upnpClient) { - super(upnpClient); - Log.d(getClass().getName(), "Starting background music service... "); - Context context = getUpnpClient().getContext(); - - context.startForegroundService(new Intent(context, BackgroundMusicService.class)); - - context.bindService(new Intent(context, BackgroundMusicService.class), LocalBackgoundMusicPlayer.this, Context.BIND_AUTO_CREATE); - - } - - - /* - * (non-Javadoc) - * - * @see de.yaacc.player.AbstractPlayer#onDestroy() - */ - @Override - public void onDestroy() { - super.onDestroy(); - if (backgroundMusicService != null) { - backgroundMusicService.stop(); - try { - backgroundMusicService.removeServiceListener(this); - backgroundMusicService.unbindService(this); - } catch (IllegalArgumentException iex) { - Log.d(getClass().getName(), "ignoring exception while unbind service"); - } - backgroundMusicService.stopForeground(true); - } - - } - - /* - * (non-Javadoc) - * - * @see de.yaacc.player.AbstractPlayer#pause() - */ - @Override - protected void doPause() { - - commandExecutionTimer = new Timer(); - commandExecutionTimer.schedule(new TimerTask() { - - @Override - public void run() { - Intent intent = new Intent(); - intent.setAction(BackgroundMusicBroadcastReceiver.ACTION_PAUSE); - getContext().sendBroadcast(intent); - - } - }, 600L); - } - - @Override - protected void doResume() { - commandExecutionTimer = new Timer(); - commandExecutionTimer.schedule(new TimerTask() { - - @Override - public void run() { - Intent intent = new Intent(); - intent.setAction(BackgroundMusicBroadcastReceiver.ACTION_PLAY); - getContext().sendBroadcast(intent); - - } - }, 600L); - int timeLeft = getBackgroundService().getDuration() - getBackgroundService().getCurrentPosition(); - Log.d(this.getClass().getName(), "TimeLeft after resume: " + timeLeft + " duration: " + getBackgroundService().getDuration() + " curPos: " + getBackgroundService().getCurrentPosition()); - startTimer(timeLeft + getSilenceDuration()); - } - - /* - * (non-Javadoc) - * - * @see - * de.yaacc.player.AbstractPlayer#stopItem(de.yaacc.player.PlayableItem) - */ - @Override - protected void stopItem(PlayableItem playableItem) { - - // Communicating with the activity is only possible after the activity - // is started - // if we send an broadcast event to early the activity won't be up - // because there is no known way to query the activity state - // we are sending the command delayed - commandExecutionTimer = new Timer(); - commandExecutionTimer.schedule(new TimerTask() { - - @Override - public void run() { - Intent intent = new Intent(); - intent.setAction(BackgroundMusicBroadcastReceiver.ACTION_STOP); - getContext().sendBroadcast(intent); - - } - }, 600L); - } - - /* - * (non-Javadoc) - * - * @see - * de.yaacc.player.AbstractPlayer#loadItem(de.yaacc.player.PlayableItem) - */ - @Override - protected Object loadItem(PlayableItem playableItem) { - final Uri uri = playableItem.getUri(); - // Communicating with the activity is only possible after the activity - // is started - // if we send an broadcast event to early the activity won't be up - // because there is no known way to query the activity state - // we are sending the command delayed - commandExecutionTimer = new Timer(); - commandExecutionTimer.schedule(new TimerTask() { - - @Override - public void run() { - Intent intent = new Intent(); - intent.setAction(BackgroundMusicBroadcastReceiver.ACTION_SET_DATA); - intent.putExtra(BackgroundMusicBroadcastReceiver.ACTION_SET_DATA_URI_PARAM, uri); - getContext().sendBroadcast(intent); - } - }, 500L); //Must be the first command - return uri; - } - - /* - * (non-Javadoc) - * - * @see - * de.yaacc.player.AbstractPlayer#startItem(de.yaacc.player.PlayableItem, - * java.lang.Object) - */ - @Override - protected void startItem(PlayableItem playableItem, Object loadedItem) { - // Communicating with the activity is only possible after the activity - // is started - // if we send an broadcast event to early the activity won't be up - // because there is no known way to query the activity state - // we are sending the command delayed - DIDLObject.Property albumArtUriProperty = playableItem.getItem() == null ? null : playableItem.getItem().getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class); - albumArtUri = (albumArtUriProperty == null) ? null : albumArtUriProperty.getValue(); - - commandExecutionTimer = new Timer(); - commandExecutionTimer.schedule(new TimerTask() { - - @Override - public void run() { - Intent intent = new Intent(); - intent.setAction(BackgroundMusicBroadcastReceiver.ACTION_PLAY); - getContext().sendBroadcast(intent); - } - }, 600L); - } - - - /* - * (non-Javadoc) - * - * @see de.yaacc.player.AbstractPlayer#getNotificationIntent() - */ - @Override - public PendingIntent getNotificationIntent() { - Intent notificationIntent = new Intent(getContext(), MusicPlayerActivity.class); - notificationIntent.putExtra(PLAYER_ID, getId()); - return PendingIntent.getActivity(getContext(), 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); - - } - - /* - * (non-Javadoc) - * - * @see de.yaacc.player.AbstractPlayer#getNotificationId() - */ - @Override - protected int getNotificationId() { - - return NotificationId.LOCAL_BACKGROUND_MUSIC_PLAYER.getId(); - } - - /** - * read the setting for music player shuffle play. - * - * @return true, if shuffle play is enabled - */ - @Override - protected boolean isShufflePlay() { - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - return preferences.getBoolean(getContext().getString(R.string.settings_music_player_shuffle_chkbx), false); - - } - - /** - * Returns the duration of the current track - * - * @return the duration - */ - public String getDuration() { - if (!isMusicServiceBound()) return ""; - return formatMillis(getBackgroundService().getDuration()); - - } - - public long getCurrentPosition() { - if (!isMusicServiceBound()) return 0; - return getBackgroundService().getCurrentPosition(); - } - - public String getElapsedTime() { - if (!isMusicServiceBound()) return ""; - return formatMillis(getCurrentPosition()); - } - - - @Override - public URI getAlbumArt() { - return albumArtUri; - } - - private String formatMillis(long millis) { - - - int hours = (int) (millis / (1000 * 60 * 60)); - int minutes = (int) ((millis % (1000 * 60 * 60)) / (1000 * 60)); - int seconds = (int) (((millis % (1000 * 60 * 60)) % (1000 * 60)) / 1000); - - return String.format(Locale.ENGLISH, "%02d:%02d:%02d", hours, minutes, seconds); - } - - @Override - public void onServiceConnected(ComponentName className, IBinder binder) { - Log.d(getClass().getName(), "onServiceConnected..." + className); - if (binder instanceof BackgroundMusicServiceBinder) { - backgroundMusicService = ((BackgroundMusicServiceBinder) binder).getService(); - backgroundMusicService.addServiceListener(this); - } else { - super.onServiceConnected(className, binder); - } - - } - - @Override - public void onServiceDisconnected(ComponentName className) { - Log.d(getClass().getName(), "onServiceDisconnected..."); - backgroundMusicService = null; - - } - - /** - * True if the player is initialized. - * - * @return true or false - */ - public boolean isMusicServiceBound() { - return backgroundMusicService != null; - } - - private BackgroundMusicService getBackgroundService() { - return backgroundMusicService; - } - - @Override - public int getIconResourceId() { - return R.drawable.ic_baseline_library_music_32; - } - - public void seekTo(long millisecondsFromStart) { - backgroundMusicService.seekTo(millisecondsFromStart); - - } - - @Override - public void onCompletion() { - startTimer(getSilenceDuration()); - } - - @Override - protected void doPostLoadItem(PlayableItem playableItem) { - //do nothing - } -} diff --git a/yaacc/src/main/java/de/yaacc/player/LocalImagePlayer.java b/yaacc/src/main/java/de/yaacc/player/LocalImagePlayer.java index 2929e273..d3a3c6f3 100644 --- a/yaacc/src/main/java/de/yaacc/player/LocalImagePlayer.java +++ b/yaacc/src/main/java/de/yaacc/player/LocalImagePlayer.java @@ -1,5 +1,6 @@ /* * Copyright (C) 2013 Tobias Schoene www.yaacc.de + * Copyright (C) 2026 Modernization * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -8,16 +9,15 @@ * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package de.yaacc.player; -import android.app.NotificationManager; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -26,80 +26,74 @@ import android.graphics.Bitmap; import android.net.Uri; import android.os.IBinder; -import android.util.Log; - -import androidx.core.app.NotificationCompat; +import android.support.v4.media.session.MediaSessionCompat; import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; import java.net.URI; import java.util.ArrayList; -import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Timer; import java.util.TimerTask; import de.yaacc.R; -import de.yaacc.Yaacc; import de.yaacc.imageviewer.ImageViewerActivity; import de.yaacc.imageviewer.ImageViewerBroadcastReceiver; import de.yaacc.upnp.UpnpClient; import de.yaacc.util.NotificationId; +import de.yaacc.util.YaaccLogger; /** - * Player for local image viewing activity + * Player for local image viewing activity. + * Simplified - no notification code, just Player interface + remote control. * * @author Tobias Schoene (openbit) */ public class LocalImagePlayer implements Player, ServiceConnection { - private final UpnpClient upnpClient; + private final PropertyChangeSupport pcs = new PropertyChangeSupport(this); private Timer commandExecutionTimer; private String name; private String shortName; - private PendingIntent notificationIntent; private PlayerService playerService; private boolean isPlaying; + private boolean isPaused; + private ArrayList imageUris; // Store URIs for reopening activity + private int currentIndex; // Store current position for resuming slideshow - - /** - * @param upnpClient upnpClient - * @param name playerName - */ public LocalImagePlayer(UpnpClient upnpClient, String name, String shortName) { this(upnpClient); setName(name); setShortName(shortName); startService(); - } - public LocalImagePlayer(UpnpClient upnpClient) { this.upnpClient = upnpClient; } public void startService() { if (playerService == null) { - upnpClient.getContext().startForegroundService(new Intent(upnpClient.getContext(), PlayerService.class)); - upnpClient.getContext().bindService(new Intent(upnpClient.getContext(), PlayerService.class), + upnpClient.getContext().startForegroundService( + new Intent(upnpClient.getContext(), PlayerService.class)); + upnpClient.getContext().bindService( + new Intent(upnpClient.getContext(), PlayerService.class), this, Context.BIND_AUTO_CREATE); } } public void onServiceConnected(ComponentName className, IBinder binder) { if (binder instanceof PlayerService.PlayerServiceBinder) { - Log.d("ServiceConnection", "connected"); - + YaaccLogger.d("ServiceConnection", "connected"); playerService = ((PlayerService.PlayerServiceBinder) binder).getService(); playerService.addPlayer(this); } } - public void onServiceDisconnected(ComponentName className) { - Log.d("ServiceConnection", "disconnected"); + YaaccLogger.d("ServiceConnection", "disconnected"); if (playerService != null) { playerService.removePlayer(this); } @@ -114,145 +108,79 @@ public void setPlaying(boolean isPlaying) { this.isPlaying = isPlaying; } - /* - * (non-Javadoc) - * - * @see de.yaacc.player.Player#next() - */ @Override public void next() { - // Communicating with the activity is only possible after the activity - // is started - // if we send an broadcast event to early the activity won't be up - // in order there is no known way to query the activity state - // we are sending the command delayed commandExecutionTimer = new Timer(); commandExecutionTimer.schedule(new TimerTask() { - @Override public void run() { Intent intent = new Intent(); intent.setAction(ImageViewerBroadcastReceiver.ACTION_NEXT); upnpClient.getContext().sendBroadcast(intent); - } }, 500L); - } - /* - * (non-Javadoc) - * - * @see de.yaacc.player.Player#previous() - */ @Override public void previous() { - // Communicating with the activity is only possible after the activity - // is started - // if we send an broadcast event to early the activity won't be up - // in order there is no known way to query the activity state - // we are sending the command delayed commandExecutionTimer = new Timer(); commandExecutionTimer.schedule(new TimerTask() { - @Override public void run() { Intent intent = new Intent(); intent.setAction(ImageViewerBroadcastReceiver.ACTION_PREVIOUS); upnpClient.getContext().sendBroadcast(intent); - } }, 500L); - } - /* - * (non-Javadoc) - * - * @see de.yaacc.player.Player#pause() - */ @Override public void pause() { - // Communicating with the activity is only possible after the activity - // is started - // if we send an broadcast event to early the activity won't be up - // in order there is no known way to query the activity state - // we are sending the command delayed commandExecutionTimer = new Timer(); commandExecutionTimer.schedule(new TimerTask() { - @Override public void run() { Intent intent = new Intent(); intent.setAction(ImageViewerBroadcastReceiver.ACTION_PAUSE); upnpClient.getContext().sendBroadcast(intent); setPlaying(false); - + setPaused(true); } }, new Date()); - } - /* - * (non-Javadoc) - * - * @see de.yaacc.player.Player#play() - */ + @Override public void play() { - // Communicating with the activity is only possible after the activity - // is started - // if we send an broadcast event to early the activity won't be up - // in order there is no known way to query the activity state - // we are sending the command delayed commandExecutionTimer = new Timer(); commandExecutionTimer.schedule(new TimerTask() { - @Override public void run() { - Log.d(this.getClass().getName(), "send play"); + YaaccLogger.d(this.getClass().getName(), "send play"); Intent intent = new Intent(); intent.setAction(ImageViewerBroadcastReceiver.ACTION_PLAY); upnpClient.getContext().sendBroadcast(intent); setPlaying(true); - + setPaused(false); } }, new Date()); - } - /* - * (non-Javadoc) - * - * @see de.yaacc.player.Player#stop() - */ @Override public void stop() { - // Communicating with the activity is only possible after the activity - // is started - // if we send an broadcast event to early the activity won't be up - // in order there is no known way to query the activity state - // we are sending the command delayed commandExecutionTimer = new Timer(); commandExecutionTimer.schedule(new TimerTask() { - @Override public void run() { Intent intent = new Intent(); intent.setAction(ImageViewerBroadcastReceiver.ACTION_STOP); upnpClient.getContext().sendBroadcast(intent); setPlaying(false); - + setPaused(false); } }, new Date()); - } - /* - * (non-Javadoc) - * - * @see de.yaacc.player.Player#setItems(de.yaacc.player.PlayableItem[]) - */ @Override public void setItems(PlayableItem... items) { Intent intent = new Intent(upnpClient.getContext(), ImageViewerActivity.class); @@ -262,31 +190,22 @@ public void setItems(PlayableItem... items) { for (PlayableItem item : items) { uris.add(item.getUri()); } - intent.putExtra(ImageViewerActivity.URIS, uris); + intent.putParcelableArrayListExtra(ImageViewerActivity.URIS, uris); + + // Store URIs for reopening activity + this.imageUris = uris; + upnpClient.getContext().startActivity(intent); - showNotification(uris); } - /* - * (non-Javadoc) - * - * @see de.yaacc.player.Player#getName() - */ @Override public String getName() { - return name; } - /* - * (non-Javadoc) - * - * @see de.yaacc.player.Player#setName(java.lang.String) - */ @Override public void setName(String name) { this.name = name; - } @Override @@ -296,175 +215,53 @@ public String getShortName() { @Override public void setShortName(String name) { - shortName = name; - + this.shortName = name; } - /* - * (non-Javadoc) - * - * @see de.yaacc.player.Player#exit() - */ @Override public void exit() { if (isPlaying()) { stop(); } - playerService.shutdown(this); - + if (playerService != null) { + playerService.shutdown(this); + } } - /* - * (non-Javadoc) - * - * @see de.yaacc.player.Player#clear() - */ @Override public void clear() { - // TODO Auto-generated method stub - + // Not implemented } - /* - * (non-Javadoc) - * - * @see de.yaacc.player.Player#onDestroy() - */ @Override public void onDestroy() { - cancleNotification(); - // Communicating with the activity is only possible after the activity - // is started - // if we send an broadcast event to early the activity won't be up - // in order there is no known way to query the activity state - // we are sending the command delayed commandExecutionTimer = new Timer(); commandExecutionTimer.schedule(new TimerTask() { - @Override public void run() { Intent intent = new Intent(); intent.setAction(ImageViewerBroadcastReceiver.ACTION_EXIT); upnpClient.getContext().sendBroadcast(intent); - } }, 500L); - - } - - /** - * Displays the notification. - * - * @param uris uris - */ - private void showNotification(ArrayList uris) { - - NotificationCompat.Builder mBuilder = new NotificationCompat.Builder( - upnpClient.getContext(), Yaacc.NOTIFICATION_CHANNEL_ID) - .setGroup(Yaacc.NOTIFICATION_GROUP_KEY) - .setOngoing(false) - .setSilent(true) - .setSmallIcon(R.drawable.ic_notification_default) - .setLargeIcon(getIcon()) - .setContentTitle( - "Yaacc player " + (getName() == null ? "" : getName())); - // .setContentText("Current Title"); - PendingIntent contentIntent = getNotificationIntent(uris); - if (contentIntent != null) { - mBuilder.setContentIntent(contentIntent); - } - NotificationManager mNotificationManager = (NotificationManager) upnpClient.getContext() - .getSystemService(Context.NOTIFICATION_SERVICE); - // mId allows you to update the notification later on. - mNotificationManager.notify(getNotificationId(), mBuilder.build()); - } - - /** - * Cancels the notification. - */ - private void cancleNotification() { - NotificationManager mNotificationManager = (NotificationManager) upnpClient.getContext() - .getSystemService(Context.NOTIFICATION_SERVICE); - // mId allows you to update the notification later on. - mNotificationManager.cancel(getNotificationId()); - - } - - /* - * (non-Javadoc) - * - * @see de.yaacc.player.AbstractPlayer#getNotificationIntent() - */ - private PendingIntent getNotificationIntent(ArrayList uris) { - Intent intent = new Intent(upnpClient.getContext(), - ImageViewerActivity.class); - intent.setData(Uri.parse("http://0.0.0.0/" + Arrays.hashCode(uris.toArray()) + "")); //just for making the intents different http://stackoverflow.com/questions/10561419/scheduling-more-than-one-pendingintent-to-same-activity-using-alarmmanager - intent.putExtra(ImageViewerActivity.URIS, uris); - notificationIntent = PendingIntent.getActivity(upnpClient.getContext(), 0, - intent, PendingIntent.FLAG_IMMUTABLE); - return notificationIntent; - } - - /* - * (non-Javadoc) - * - * @see de.yaacc.player.AbstractPlayer#getNotificationId() - */ - private int getNotificationId() { - - return NotificationId.LOCAL_IMAGE_PLAYER.getId(); - } - - /* (non-Javadoc) - * @see de.yaacc.player.Player#getId() - */ - @Override - public int getId() { - return getNotificationId(); } @Override - public void addPropertyChangeListener(PropertyChangeListener listener) { - throw new UnsupportedOperationException(); - - } - - @Override - public void removePropertyChangeListener(PropertyChangeListener listener) { - throw new UnsupportedOperationException(); - - } - - - /** - * returns the current item position in the playlist - * - * @return the position string - */ public String getPositionString() { return ""; } - /** - * returns the title of the current item - * - * @return the title - */ + @Override public String getCurrentItemTitle() { return ""; } + @Override public int getCurrentItemIndex() { - //not yet implemented return 0; } - - /** - * returns the title of the next current item - * - * @return the title - */ + @Override public String getNextItemTitle() { return ""; } @@ -496,16 +293,12 @@ public Bitmap getIcon() { @Override public void setIcon(Bitmap icon) { - } - - //TODO Refactor not every player has a volume control public boolean getMute() { return upnpClient.isMute(); } - public void setMute(boolean mute) { upnpClient.setMute(mute); } @@ -538,9 +331,41 @@ public String getDeviceId() { return UpnpClient.LOCAL_UID; } + @Override + public int getId() { + return NotificationId.LOCAL_IMAGE_PLAYER.getId(); + } + @Override public PendingIntent getNotificationIntent() { - return notificationIntent; + // Create intent to open ImageViewerActivity + Intent intent = new Intent(upnpClient.getContext(), ImageViewerActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); + + // Include stored URIs so activity can reopen with images + if (imageUris != null && !imageUris.isEmpty()) { + intent.putParcelableArrayListExtra(ImageViewerActivity.URIS, imageUris); + intent.putExtra("currentIndex", currentIndex); + } + + return PendingIntent.getActivity( + upnpClient.getContext(), + getId(), + intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + } + + public ArrayList getImageUris() { + return imageUris; + } + + public int getCurrentIndex() { + return currentIndex; + } + + public void setCurrentIndex(int index) { + this.currentIndex = index; } @Override @@ -550,7 +375,7 @@ public void seekTo(long millisecondsFromStart) { @Override public void addItems(List playableItemList) { - //Not yet implemented + // Not yet implemented } @Override @@ -560,11 +385,40 @@ public List getItems() { @Override public void fastForward(int i) { - //Not implemented + // Not implemented } @Override public void fastRewind(int i) { - //Not implemented + // Not implemented + } + + @Override + public MediaSessionCompat getMediaSession() { + // Image player doesn't use MediaSession + return null; + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener listener) { + pcs.addPropertyChangeListener(listener); + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener listener) { + pcs.removePropertyChangeListener(listener); + } + + + public boolean isPaused() { + return isPaused; + } + + public boolean isStopped() { + return !(isPlaying || isPaused); + } + + private void setPaused(boolean b) { + isPaused = b; } } diff --git a/yaacc/src/main/java/de/yaacc/player/LocalMediaSessionPlayer.java b/yaacc/src/main/java/de/yaacc/player/LocalMediaSessionPlayer.java new file mode 100644 index 00000000..870f4911 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/player/LocalMediaSessionPlayer.java @@ -0,0 +1,525 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.player; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Intent; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.support.v4.media.session.MediaSessionCompat; + +import androidx.media3.common.MediaItem; +import androidx.media3.common.MediaMetadata; +import androidx.media3.common.Player; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.ExoPlayer; +import androidx.media3.session.MediaSession; +import androidx.media3.ui.PlayerNotificationManager; + +import org.fourthline.cling.support.model.DIDLObject; + +import java.net.URI; + +import de.yaacc.R; +import de.yaacc.Yaacc; +import de.yaacc.upnp.UpnpClient; +import de.yaacc.util.YaaccLogger; +import jakarta.annotation.Nullable; + +/** + * Local music player using ExoPlayer from PlayerService. + */ +@UnstableApi +public class LocalMediaSessionPlayer extends AbstractPlayer { + private ExoPlayer exoPlayer; + private MediaSession mediaSession; + private PlayerNotificationManager notificationManager; + private PlayerService playerService; + private URI albumArtUri; + private PlayableItem pendingItem; // Queue item if service not ready + private int pendingIndex; + + public LocalMediaSessionPlayer(UpnpClient upnpClient, String name, String shortName) { + this(upnpClient); + setName(name); + setShortName(shortName); + } + + public LocalMediaSessionPlayer(UpnpClient upnpClient) { + super(upnpClient); + // Don't initialize ExoPlayer here - wait until service connected + } + + private void initializeExoPlayer() { + if (exoPlayer != null) { + return; // Already initialized + } + YaaccLogger.d(getClass().getName(), "Initializing ExoPlayer"); + // Create ExoPlayer with audio attributes + exoPlayer = new ExoPlayer.Builder(getContext()).build(); + + androidx.media3.common.AudioAttributes audioAttributes = + new androidx.media3.common.AudioAttributes.Builder() + .setContentType(androidx.media3.common.C.AUDIO_CONTENT_TYPE_MUSIC) + .setUsage(androidx.media3.common.C.USAGE_MEDIA) + .build(); + exoPlayer.setAudioAttributes(audioAttributes, true); + + // Enable repeat mode so next/previous buttons always show + exoPlayer.setRepeatMode(androidx.media3.common.Player.REPEAT_MODE_ALL); + + exoPlayer.addListener(new Player.Listener() { + @Override + public void onMediaItemTransition(@Nullable MediaItem mediaItem, int reason) { + if (mediaItem != null && reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK) { + if (getCurrentItemIndex() != exoPlayer.getCurrentMediaItemIndex()) { + + setCurrentIndex(exoPlayer.getCurrentMediaItemIndex()); + } + YaaccLogger.d(getClass().getName(), "Media item changed: " + mediaItem.mediaMetadata.title); + + } + } + }); + + // Create MediaSession with session activity for notification + PendingIntent sessionActivity = PendingIntent.getActivity( + getContext(), + 0, + new Intent(getContext(), MusicPlayerActivity.class) + .putExtra(PLAYER_ID, getId()), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + + mediaSession = new MediaSession.Builder(getContext(), exoPlayer) + .setId("local_audio_" + getId() + "_" + System.currentTimeMillis()) + .setSessionActivity(sessionActivity) + .build(); + + // Create Media3 notification manager + notificationManager = new PlayerNotificationManager.Builder( + getContext(), + getNotificationId(), + Yaacc.NOTIFICATION_CHANNEL_ID) + .setMediaDescriptionAdapter(new PlayerNotificationManager.MediaDescriptionAdapter() { + @Override + public CharSequence getCurrentContentTitle(Player player) { + return getCurrentItemTitle(); + } + + @Override + public PendingIntent createCurrentContentIntent(Player player) { + return getNotificationIntent(); + } + + @Override + public CharSequence getCurrentContentText(Player player) { + return getName(); + } + + @Override + public android.graphics.Bitmap getCurrentLargeIcon(Player player, + PlayerNotificationManager.BitmapCallback callback) { + return null; + } + }) + .build(); + + // Enable next/previous actions + notificationManager.setUseNextAction(true); + notificationManager.setUsePreviousAction(true); + notificationManager.setUseNextActionInCompactView(true); + notificationManager.setUsePreviousActionInCompactView(true); + + notificationManager.setPlayer(exoPlayer); + notificationManager.setMediaSessionToken(mediaSession.getSessionCompatToken()); + + YaaccLogger.d(getClass().getName(), "ExoPlayer, MediaSession and notification initialized"); + } + + /** + * Get the Media3 MediaSession (not the legacy MediaSessionCompat). + * This is registered with PlayerService for system integration. + */ + public MediaSession getMedia3Session() { + return mediaSession; + } + + @Override + public void onServiceConnected(ComponentName className, IBinder binder) { + super.onServiceConnected(className, binder); + + if (binder instanceof PlayerService.PlayerServiceBinder) { + playerService = ((PlayerService.PlayerServiceBinder) binder).getService(); + + // Initialize ExoPlayer now that we have proper ID + initializeExoPlayer(); + + // Register our MediaSession with the service + playerService.registerMediaSession(mediaSession); + YaaccLogger.d(getClass().getName(), "MediaSession registered with PlayerService"); + + // If there's a pending item, play it now + if (pendingItem != null) { + YaaccLogger.d(getClass().getName(), "Playing pending item: " + pendingItem.getTitle()); + PlayableItem item = pendingItem; + startItem(item, null, pendingIndex); + pendingItem = null; + pendingIndex = -1; + } + } + } + + @Override + public int getIconResourceId() { + return R.drawable.ic_baseline_library_music_32; + } + + @Override + public void setItems(PlayableItem... playableItems) { + super.setItems(playableItems); + if (exoPlayer == null) { + new Handler(Looper.getMainLooper()).post(this::initializeExoPlayer); + } + // Add all items to ExoPlayer playlist + if (exoPlayer != null && playableItems.length > 0) { + new Handler(Looper.getMainLooper()).post(() -> { + if (exoPlayer != null) { + exoPlayer.clearMediaItems(); + for (PlayableItem item : playableItems) { + MediaItem.Builder builder = new MediaItem.Builder() + .setUri(item.getUri()); + + if (item.getTitle() != null) { + MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder() + .setTitle(item.getTitle()); + + // Add album art if available + DIDLObject.Property albumArtProp = item.getItem() == null ? null : + item.getItem().getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class); + if (albumArtProp != null && albumArtProp.getValue() != null) { + metadataBuilder.setArtworkUri(android.net.Uri.parse(albumArtProp.getValue().toString())); + } + + builder.setMediaMetadata(metadataBuilder.build()); + } + + exoPlayer.addMediaItem(builder.build()); + } + if (exoPlayer != null) { + exoPlayer.prepare(); + } + YaaccLogger.d(getClass().getName(), "Added " + playableItems.length + " items to ExoPlayer"); + } + }); + } + } + + @Override + protected void startItem(PlayableItem playableItem, Object loadedItem, int index) { + YaaccLogger.d(getClass().getName(), "startItem called for: " + playableItem.getTitle()); + + if (exoPlayer == null) { + YaaccLogger.w(getClass().getName(), "ExoPlayer not ready, queuing item"); + pendingItem = playableItem; + pendingIndex = index; + return; + } + + DIDLObject.Property albumArtUriProperty = playableItem.getItem() == null ? null : + playableItem.getItem().getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class); + albumArtUri = (albumArtUriProperty == null) ? null : albumArtUriProperty.getValue(); + + // Notify listeners that track changed (for UI updates) + firePropertyChange(PROPERTY_ITEM, null, playableItem); + + // ExoPlayer must be called from main thread - move ALL ExoPlayer access inside Handler + new Handler(Looper.getMainLooper()).post(() -> { + ExoPlayer player = exoPlayer; // Store local reference to avoid race condition + if (player != null) { + boolean needsSetItems = player.getMediaItemCount() != getItems().size(); + if (needsSetItems) { + // Clear and rebuild playlist + player.clearMediaItems(); + for (PlayableItem item : getItems()) { + MediaItem.Builder builder = new MediaItem.Builder() + .setUri(item.getUri()); + if (item.getTitle() != null) { + MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder() + .setTitle(item.getTitle()); + + // Add album art if available + DIDLObject.Property albumArtProp = item.getItem() == null ? null : + item.getItem().getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class); + if (albumArtProp != null && albumArtProp.getValue() != null) { + metadataBuilder.setArtworkUri(android.net.Uri.parse(albumArtProp.getValue().toString())); + } + + builder.setMediaMetadata(metadataBuilder.build()); + } + player.addMediaItem(builder.build()); + } + player.prepare(); + } + // Now seek and play + player.seekTo(index, 0); + player.play(); + setPlaying(true); + showNotificationInternal(); + YaaccLogger.d(getClass().getName(), "Started playing: " + playableItem.getTitle() + " at index " + index); + } + }); + } + + @Override + protected Object loadItem(PlayableItem playableItem) { + YaaccLogger.d(getClass().getName(), "loadItem called for: " + playableItem.getTitle()); + return playableItem; // Return non-null to indicate ready + } + + @Override + protected void doPostLoadItem(PlayableItem playableItem) { + // ExoPlayer handles track changes automatically, don't start timer + YaaccLogger.d(getClass().getName(), "doPostLoadItem - ExoPlayer handles track changes"); + } + + @Override + public void next() { + // Let ExoPlayer handle next track + if (exoPlayer != null) { + new Handler(Looper.getMainLooper()).post(() -> { + if (exoPlayer != null && exoPlayer.hasNextMediaItem()) { + exoPlayer.seekToNextMediaItem(); + //done by listener setCurrentIndex(getCurrentItemIndex() + 1); + } + }); + } + } + + + @Override + public void previous() { + // Let ExoPlayer handle previous track + if (exoPlayer != null) { + new Handler(Looper.getMainLooper()).post(() -> { + if (exoPlayer != null && exoPlayer.hasPreviousMediaItem()) { + exoPlayer.seekToPreviousMediaItem(); + //done by listener setCurrentIndex(getCurrentItemIndex() - 1); + } + }); + } + } + + /** + * Sync ExoPlayer playlist with current items list after reordering. + * Only updates items after current position (since those can be reordered). + */ + public void syncPlaylistToExoPlayer() { + if (exoPlayer == null || getItems().isEmpty()) { + return; + } + + new Handler(Looper.getMainLooper()).post(() -> { + if (exoPlayer != null) { + int currentIndex = exoPlayer.getCurrentMediaItemIndex(); + + // Remove all items after current position + int itemCount = exoPlayer.getMediaItemCount(); + for (int i = itemCount - 1; i > currentIndex; i--) { + exoPlayer.removeMediaItem(i); + } + + // Add updated items from the list + for (int i = currentIndex + 1; i < getItems().size(); i++) { + PlayableItem item = getItems().get(i); + MediaItem.Builder builder = new MediaItem.Builder() + .setUri(item.getUri()); + + if (item.getTitle() != null) { + MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder() + .setTitle(item.getTitle()); + + // Add album art if available + DIDLObject.Property albumArtProp = item.getItem() == null ? null : + item.getItem().getFirstProperty(DIDLObject.Property.UPNP.ALBUM_ART_URI.class); + if (albumArtProp != null && albumArtProp.getValue() != null) { + metadataBuilder.setArtworkUri(android.net.Uri.parse(albumArtProp.getValue().toString())); + } + + builder.setMediaMetadata(metadataBuilder.build()); + } + + exoPlayer.addMediaItem(builder.build()); + } + + YaaccLogger.d(getClass().getName(), "Synced playlist to ExoPlayer (optimized)"); + } + }); + } + + @Override + protected void stopItem(PlayableItem playableItem) { + new Handler(Looper.getMainLooper()).post(() -> { + if (exoPlayer != null) { + exoPlayer.stop(); + } + setPlaying(false); + }); + } + + @Override + protected void doPause() { + new Handler(Looper.getMainLooper()).post(() -> { + if (exoPlayer != null) { + exoPlayer.pause(); + } + setPlaying(false); + }); + } + + @Override + protected void doResume() { + new Handler(Looper.getMainLooper()).post(() -> { + if (exoPlayer != null) { + exoPlayer.play(); + } + setPlaying(true); + }); + } + + @Override + public long getCurrentPosition() { + if (exoPlayer != null) { + return exoPlayer.getCurrentPosition(); + } + return 0; + } + + @Override + public void seekTo(long millisecondsFromStart) { + new Handler(Looper.getMainLooper()).post(() -> { + if (exoPlayer != null) { + exoPlayer.seekTo(millisecondsFromStart); + } + }); + } + + @Override + public String getDuration() { + if (exoPlayer != null) { + long duration = exoPlayer.getDuration(); + if (duration > 0) { + return formatTime(duration); + } + } + return "00:00:00"; + } + + @Override + public String getElapsedTime() { + if (exoPlayer != null) { + return formatTime(exoPlayer.getCurrentPosition()); + } + return "00:00:00"; + } + + private String formatTime(long millis) { + long seconds = millis / 1000; + long hours = seconds / 3600; + long minutes = (seconds % 3600) / 60; + long secs = seconds % 60; + return String.format("%02d:%02d:%02d", hours, minutes, secs); + } + + @Override + public URI getAlbumArt() { + return albumArtUri; + } + + @Override + protected int getNotificationId() { + return de.yaacc.util.NotificationId.LOCAL_BACKGROUND_MUSIC_PLAYER.getId(); + } + + @Override + public PendingIntent getNotificationIntent() { + android.content.Intent intent = new android.content.Intent(getContext(), MusicPlayerActivity.class); + intent.putExtra(PLAYER_ID, getId()); + return PendingIntent.getActivity(getContext(), 0, intent, + PendingIntent.FLAG_IMMUTABLE); + } + + @Override + public MediaSessionCompat getMediaSession() { + // Return null - we use Media3 MediaSession, not legacy MediaSessionCompat + // This prevents AbstractPlayer from trying to use MediaSessionCompat in notification + return null; + } + + @Override + protected void showNotificationInternal() { + // PlayerNotificationManager handles notification automatically + // No manual notification needed + YaaccLogger.d(getClass().getName(), "Notification handled by PlayerNotificationManager"); + } + + @Override + public void onDestroy() { + YaaccLogger.d(getClass().getName(), "onDestroy called"); + + // Release notification manager on main thread + if (notificationManager != null) { + new Handler(Looper.getMainLooper()).post(() -> { + if (notificationManager != null) { + notificationManager.setPlayer(null); + } + }); + } + + // Unregister MediaSession from service + if (playerService != null && mediaSession != null) { + playerService.unregisterMediaSession(mediaSession); + } + + // Stop and release ExoPlayer (capture references before nulling) + final ExoPlayer playerToRelease = exoPlayer; + final MediaSession sessionToRelease = mediaSession; + + if (playerToRelease != null) { + new Handler(Looper.getMainLooper()).post(() -> { + playerToRelease.stop(); + playerToRelease.clearMediaItems(); + playerToRelease.release(); + YaaccLogger.d(getClass().getName(), "ExoPlayer stopped and released"); + }); + } + + // Release MediaSession + if (sessionToRelease != null) { + sessionToRelease.release(); + YaaccLogger.d(getClass().getName(), "MediaSession released"); + } + + exoPlayer = null; + mediaSession = null; + notificationManager = null; + super.onDestroy(); + } +} diff --git a/yaacc/src/main/java/de/yaacc/player/MultiContentPlayer.java b/yaacc/src/main/java/de/yaacc/player/MultiContentPlayer.java index 8541bd24..77d16827 100644 --- a/yaacc/src/main/java/de/yaacc/player/MultiContentPlayer.java +++ b/yaacc/src/main/java/de/yaacc/player/MultiContentPlayer.java @@ -20,13 +20,13 @@ import android.app.PendingIntent; import android.content.ActivityNotFoundException; import android.content.Intent; -import android.util.Log; import java.net.URI; import de.yaacc.R; import de.yaacc.upnp.UpnpClient; import de.yaacc.util.NotificationId; +import de.yaacc.util.YaaccLogger; /** * @author Tobias Schoene (openbit) @@ -61,7 +61,7 @@ public MultiContentPlayer(UpnpClient upnpClient) { */ @Override protected void stopItem(PlayableItem playableItem) { - Log.d(getClass().getName(), "Stop not implemented for multi player"); + YaaccLogger.d(getClass().getName(), "Stop not implemented for multi player"); } @@ -73,8 +73,8 @@ protected void stopItem(PlayableItem playableItem) { */ @Override protected Object loadItem(PlayableItem playableItem) { - // DO nothing special - return null; + // Return non-null to indicate item is ready + return playableItem; } /* @@ -85,7 +85,7 @@ protected Object loadItem(PlayableItem playableItem) { * java.lang.Object) */ @Override - protected void startItem(PlayableItem playableItem, Object loadedItem) { + protected void startItem(PlayableItem playableItem, Object loadedItem, int index) { Intent intent = new Intent(Intent.ACTION_VIEW); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); @@ -93,7 +93,7 @@ protected void startItem(PlayableItem playableItem, Object loadedItem) { try { getContext().startActivity(intent); } catch (final ActivityNotFoundException anfe) { - Log.e(getClass().getName(), R.string.can_not_start_activity + YaaccLogger.e(getClass().getName(), R.string.can_not_start_activity + anfe.getMessage(), anfe); } @@ -101,7 +101,7 @@ protected void startItem(PlayableItem playableItem, Object loadedItem) { @Override public long getCurrentPosition() { - Log.d(getClass().getName(), "CurrentPosition not implemented"); + YaaccLogger.d(getClass().getName(), "CurrentPosition not implemented"); return 0; } @@ -139,7 +139,7 @@ protected int getNotificationId() { @Override public void seekTo(long millisecondsFromStart) { - Log.d(getClass().getName(), "SeekTo not implemented"); + YaaccLogger.d(getClass().getName(), "SeekTo not implemented"); } diff --git a/yaacc/src/main/java/de/yaacc/player/MultiContentPlayerActivity.java b/yaacc/src/main/java/de/yaacc/player/MultiContentPlayerActivity.java index df52698f..6adf4765 100644 --- a/yaacc/src/main/java/de/yaacc/player/MultiContentPlayerActivity.java +++ b/yaacc/src/main/java/de/yaacc/player/MultiContentPlayerActivity.java @@ -23,7 +23,7 @@ import android.content.ServiceConnection; import android.os.Bundle; import android.os.IBinder; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.view.Menu; import android.view.MenuItem; import android.widget.ImageButton; @@ -47,7 +47,7 @@ public class MultiContentPlayerActivity extends AppCompatActivity implements Ser public void onServiceConnected(ComponentName className, IBinder binder) { if (binder instanceof PlayerService.PlayerServiceBinder) { - Log.d(getClass().getName(), "PlayerService connected"); + YaaccLogger.d(getClass().getName(), "PlayerService connected"); playerService = ((PlayerService.PlayerServiceBinder) binder).getService(); initialize(); } @@ -55,7 +55,7 @@ public void onServiceConnected(ComponentName className, IBinder binder) { //binder comes from server to communicate with method's of public void onServiceDisconnected(ComponentName className) { - Log.d(getClass().getName(), "PlayerService disconnected"); + YaaccLogger.d(getClass().getName(), "PlayerService disconnected"); playerService = null; } diff --git a/yaacc/src/main/java/de/yaacc/player/MusicPlayerActivity.java b/yaacc/src/main/java/de/yaacc/player/MusicPlayerActivity.java index ef1d589d..9611e889 100644 --- a/yaacc/src/main/java/de/yaacc/player/MusicPlayerActivity.java +++ b/yaacc/src/main/java/de/yaacc/player/MusicPlayerActivity.java @@ -24,7 +24,8 @@ import android.net.Uri; import android.os.Bundle; import android.os.IBinder; -import android.util.Log; +import android.view.KeyEvent; +import de.yaacc.util.YaaccLogger; import android.view.Menu; import android.view.MenuItem; import android.widget.ImageButton; @@ -63,7 +64,7 @@ public class MusicPlayerActivity extends AppCompatActivity implements ServiceCon public void onServiceConnected(ComponentName className, IBinder binder) { if (binder instanceof PlayerService.PlayerServiceBinder) { - Log.d(getClass().getName(), "PlayerService connected"); + YaaccLogger.d(getClass().getName(), "PlayerService connected"); playerService = ((PlayerService.PlayerServiceBinder) binder).getService(); initialize(); setTrackInfo(); @@ -72,7 +73,7 @@ public void onServiceConnected(ComponentName className, IBinder binder) { //binder comes from server to communicate with method's of public void onServiceDisconnected(ComponentName className) { - Log.d(getClass().getName(), "PlayerService disconnected"); + YaaccLogger.d(getClass().getName(), "PlayerService disconnected"); playerService = null; } @@ -101,7 +102,7 @@ protected void initialize() { btnFr.setActivated(false); } else { player.addPropertyChangeListener(event -> { - if (LocalBackgoundMusicPlayer.PROPERTY_ITEM.equals(event.getPropertyName())) { + if (LocalMediaSessionPlayer.PROPERTY_ITEM.equals(event.getPropertyName())) { runOnUiThread(this::setTrackInfo); } @@ -191,10 +192,10 @@ public void onStopTrackingTouch(android.widget.SeekBar seekBar) { long durationTimeMillis = Objects.requireNonNull(dateFormat.parse(durationString)).getTime(); int targetPosition = Double.valueOf(durationTimeMillis * ((double) seekBar.getProgress() / 100)).intValue(); - Log.d(getClass().getName(), "TargetPosition" + targetPosition); + YaaccLogger.d(getClass().getName(), "TargetPosition" + targetPosition); getPlayer().seekTo(targetPosition); } catch (ParseException pex) { - Log.d(getClass().getName(), "Error while parsing time string", pex); + YaaccLogger.d(getClass().getName(), "Error while parsing time string", pex); } } @@ -216,8 +217,7 @@ protected void onPause() { @Override protected void onRestart() { super.onRestart(); - this.bindService(new Intent(this, PlayerService.class), - this, Context.BIND_AUTO_CREATE); + // Don't bind again - already bound in onCreate() updateTime = true; setTrackInfo(); } @@ -225,8 +225,9 @@ protected void onRestart() { @Override protected void onResume() { super.onResume(); - this.bindService(new Intent(this, PlayerService.class), - this, Context.BIND_AUTO_CREATE); + // For local playback, use music stream for volume control + setVolumeControlStream(android.media.AudioManager.STREAM_MUSIC); + // Don't bind again - already bound in onCreate() updateTime = true; setTrackInfo(); } @@ -234,11 +235,13 @@ protected void onResume() { @Override protected void onDestroy() { super.onDestroy(); + YaaccLogger.d(getClass().getName(), "Activity onDestroy - unbinding from service"); updateTime = false; try { unbindService(this); + YaaccLogger.d(getClass().getName(), "Successfully unbound from service"); } catch (IllegalArgumentException iae) { - Log.d(getClass().getName(), "Ignore exception on unbind service while activity destroy"); + YaaccLogger.d(getClass().getName(), "Ignore exception on unbind service while activity destroy"); } } @@ -252,7 +255,7 @@ protected void onCreate(Bundle savedInstanceState) { } private Player getPlayer() { - return playerService == null ? null : playerService.getFirstCurrentPlayerOfType(LocalBackgoundMusicPlayer.class); + return playerService == null ? null : playerService.getFirstCurrentPlayerOfType(LocalMediaSessionPlayer.class); } @Override @@ -288,8 +291,10 @@ public boolean onOptionsItemSelected(MenuItem item) { } private void exit() { + YaaccLogger.d(getClass().getName(), "Exit button pressed"); Player player = getPlayer(); if (player != null) { + YaaccLogger.d(getClass().getName(), "Calling player.exit() for player: " + player.getId()); player.stop(); player.exit(); } @@ -335,7 +340,7 @@ private void doSetTrackInfo() { seekBar.setProgress(progress); } } catch (ParseException pex) { - Log.d(getClass().getName(), "Error while parsing time string", pex); + YaaccLogger.d(getClass().getName(), "Error while parsing time string", pex); } @@ -357,4 +362,13 @@ public void run() { }, 1000L); } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_VOLUME_UP || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) { + // For local playback, use system volume (AudioManager handles it automatically) + return super.onKeyDown(keyCode, event); + } + return super.onKeyDown(keyCode, event); + } } diff --git a/yaacc/src/main/java/de/yaacc/player/PlayableItem.java b/yaacc/src/main/java/de/yaacc/player/PlayableItem.java index 8219e051..50037645 100644 --- a/yaacc/src/main/java/de/yaacc/player/PlayableItem.java +++ b/yaacc/src/main/java/de/yaacc/player/PlayableItem.java @@ -18,7 +18,7 @@ package de.yaacc.player; import android.net.Uri; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.webkit.MimeTypeMap; import org.fourthline.cling.support.model.Res; @@ -46,7 +46,7 @@ public PlayableItem(Item item, int defaultDuration) { this.item = item; id = UUID.randomUUID(); setTitle(item.getTitle()); - Res resource = item.getFirstResource(); + Res resource = selectBestResource(item); if (resource != null) { setUri(Uri.parse(resource.getValue())); String mimeType = resource.getProtocolInfo().getContentFormat(); @@ -59,7 +59,7 @@ public PlayableItem(Item item, int defaultDuration) { // calculate duration long millis = defaultDuration; - Log.v(getClass().getName(), "resource.getDuration(): " + resource.getDuration()); + YaaccLogger.v(getClass().getName(), "resource.getDuration(): " + resource.getDuration()); if (resource.getDuration() != null) { try { String[] tokens = resource.getDuration().split(":"); @@ -72,16 +72,16 @@ public PlayableItem(Item item, int defaultDuration) { if (tokens.length > 2) { String seconds = tokens[2]; if (tokens[2].contains(".")) { - Log.d(getClass().getName(), "tokens[2]: " + tokens[2] + "split: " + tokens[2].split("\\.").length); + YaaccLogger.d(getClass().getName(), "tokens[2]: " + tokens[2] + "split: " + tokens[2].split("\\.").length); seconds = tokens[2].split("\\.")[0]; } millis += Long.parseLong(seconds); } millis = millis * 1000; - Log.d(getClass().getName(), "resource.getDuration(): " + resource.getDuration() + " millis: " + millis); + YaaccLogger.d(getClass().getName(), "resource.getDuration(): " + resource.getDuration() + " millis: " + millis); } catch (Exception e) { - Log.d(getClass().getName(), "bad duration format", e); + YaaccLogger.d(getClass().getName(), "bad duration format", e); } } setDuration(millis); @@ -100,6 +100,52 @@ public PlayableItem() { id = UUID.randomUUID(); } + /** + * Select the best playable resource from an item. + * Filters out non-media types and prefers higher quality. + */ + private Res selectBestResource(Item item) { + if (item.getResources() == null || item.getResources().isEmpty()) { + return null; + } + + // Always use first resource as fallback + Res bestResource = item.getResources().get(0); + long bestBitrate = 0; + + for (Res resource : item.getResources()) { + String contentFormat = resource.getProtocolInfo().getContentFormat(); + if (contentFormat == null || contentFormat.isEmpty()) { + continue; + } + + // Only accept audio, video, image, or streaming playlist types + if (!contentFormat.startsWith("audio/") && + !contentFormat.startsWith("video/") && + !contentFormat.startsWith("image/") && + !contentFormat.equals("application/vnd.apple.mpegurl") && + !contentFormat.equals("application/x-mpegURL")) { + YaaccLogger.d(getClass().getName(), "Skipping non-media resource: " + contentFormat); + continue; + } + + // Prefer higher bitrate + Long bitrate = resource.getBitrate(); + if (bitrate != null && bitrate > bestBitrate) { + bestBitrate = bitrate; + bestResource = resource; + } + } + + if (bestResource != null) { + YaaccLogger.d(getClass().getName(), "Selected resource: " + + bestResource.getProtocolInfo().getContentFormat() + + " bitrate: " + bestResource.getBitrate()); + } + + return bestResource; + } + /** * @return the mimeType diff --git a/yaacc/src/main/java/de/yaacc/player/PlayableItemDiffCallback.java b/yaacc/src/main/java/de/yaacc/player/PlayableItemDiffCallback.java index e4026c0d..aa322778 100644 --- a/yaacc/src/main/java/de/yaacc/player/PlayableItemDiffCallback.java +++ b/yaacc/src/main/java/de/yaacc/player/PlayableItemDiffCallback.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ package de.yaacc.player; import androidx.annotation.Nullable; diff --git a/yaacc/src/main/java/de/yaacc/player/Player.java b/yaacc/src/main/java/de/yaacc/player/Player.java index cc90b806..b1aad6f1 100644 --- a/yaacc/src/main/java/de/yaacc/player/Player.java +++ b/yaacc/src/main/java/de/yaacc/player/Player.java @@ -19,6 +19,7 @@ import android.app.PendingIntent; import android.graphics.Bitmap; +import android.support.v4.media.session.MediaSessionCompat; import java.beans.PropertyChangeListener; import java.net.URI; @@ -192,7 +193,7 @@ public interface Player { void setIcon(Bitmap icon); - + boolean getMute(); @@ -235,4 +236,13 @@ public interface Player { * @param i seconds */ void fastRewind(int i); + + /** + * Get the MediaSession for this player (for volume control integration) + */ + MediaSessionCompat getMediaSession(); + + boolean isPaused(); + + boolean isStopped(); } diff --git a/yaacc/src/main/java/de/yaacc/player/PlayerService.java b/yaacc/src/main/java/de/yaacc/player/PlayerService.java index ef3207f9..6a2455d0 100644 --- a/yaacc/src/main/java/de/yaacc/player/PlayerService.java +++ b/yaacc/src/main/java/de/yaacc/player/PlayerService.java @@ -19,16 +19,18 @@ import android.app.Notification; import android.app.PendingIntent; -import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.HandlerThread; import android.os.IBinder; import android.os.PowerManager; -import android.util.Log; import android.widget.Toast; +import androidx.annotation.OptIn; import androidx.core.app.NotificationCompat; +import androidx.media3.common.util.UnstableApi; +import androidx.media3.session.MediaSession; +import androidx.media3.session.MediaSessionService; import org.fourthline.cling.model.meta.Device; @@ -45,11 +47,12 @@ import de.yaacc.browser.TabBrowserActivity; import de.yaacc.upnp.UpnpClient; import de.yaacc.util.NotificationId; +import de.yaacc.util.YaaccLogger; /** * @author Tobias Schoene (tobexyz) */ -public class PlayerService extends Service { +public class PlayerService extends MediaSessionService { private final IBinder binder = new PlayerServiceBinder(); private PlayerServiceBroadcastReceiver playerServiceBroadcastReceiver; @@ -58,33 +61,155 @@ public class PlayerService extends Service { private PowerManager.WakeLock wakeLock; + // Track active MediaSession from LocalMediaSessionPlayer + private MediaSession activeMediaSession; + public PlayerService() { } + @Override + public void onCreate() { + super.onCreate(); + YaaccLogger.d(getClass().getName(), "Service created"); + } + + /** + * Register MediaSession from LocalMediaSessionPlayer. + * Called when player connects to service. + */ + public void registerMediaSession(MediaSession mediaSession) { + this.activeMediaSession = mediaSession; + YaaccLogger.d(getClass().getName(), "MediaSession registered: " + mediaSession.getId()); + } + + /** + * Unregister MediaSession when player is destroyed. + */ + public void unregisterMediaSession(MediaSession mediaSession) { + if (this.activeMediaSession == mediaSession) { + this.activeMediaSession = null; + YaaccLogger.d(getClass().getName(), "MediaSession unregistered"); + } + } + + private PendingIntent createSessionActivity() { + Intent intent = new Intent(this, de.yaacc.browser.TabBrowserActivity.class); + return PendingIntent.getActivity( + this, + 0, + intent, + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + } + public void addPlayer(Player player) { if (player.getId() == 0) { return; } currentActivePlayer.put(player.getId(), player); + + // Listen for playing state changes + player.addPropertyChangeListener(evt -> { + if ("playing".equals(evt.getPropertyName())) { + updateForegroundState(); + } + }); + + updateForegroundState(); } public void removePlayer(Player player) { - currentActivePlayer.remove(player.getId()); + updateForegroundState(); + } + + private void updateForegroundState() { + boolean hasPlayingPlayer = false; + + // Check if any player is actively playing + for (Player player : currentActivePlayer.values()) { + if (player.isPlaying()) { + hasPlayingPlayer = true; + break; + } + } + + if (hasPlayingPlayer) { + // At least one player playing - MediaSessionService handles foreground automatically + YaaccLogger.d(getClass().getName(), "Player active - service foreground"); + updateServiceNotification(); + } else { + // No players playing + if (currentActivePlayer.isEmpty()) { + // No players at all - stop service completely + YaaccLogger.d(getClass().getName(), "No players - stopping service"); + stopForeground(STOP_FOREGROUND_REMOVE); + ((Yaacc) getApplicationContext()).cancelYaaccGroupNotification(); + stopSelf(); + } else { + // Players exist but paused + // Don't stop foreground - Media3 PlayerNotificationManager handles it + YaaccLogger.d(getClass().getName(), "Players paused - keeping foreground for notification"); + updateServiceNotification(); + } + } + } + + private void updateServiceNotification() { + Intent notificationIntent = new Intent(this, TabBrowserActivity.class); + PendingIntent pendingIntent = PendingIntent.getActivity(this, + 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); + + // Build status text + int totalPlayers = currentActivePlayer.size(); + int playingCount = 0; + for (Player player : currentActivePlayer.values()) { + if (player.isPlaying()) { + playingCount++; + } + } + + String statusText = totalPlayers == 0 ? "running" : + totalPlayers + " player" + (totalPlayers > 1 ? "s" : "") + + (playingCount > 0 ? " (" + playingCount + " playing)" : " (paused)"); + + Notification notification = new NotificationCompat.Builder(this, Yaacc.NOTIFICATION_CHANNEL_ID) + .setGroup(Yaacc.NOTIFICATION_GROUP_KEY) + .setContentTitle("Player Service") + .setSilent(true) + .setContentText(statusText) + .setSmallIcon(R.drawable.ic_notification_default) + .setContentIntent(pendingIntent) + .build(); + + startForeground(NotificationId.PLAYER_SERVICE.getId(), notification); } @Override public void onDestroy() { - Log.d(this.getClass().getName(), "On Destroy"); + YaaccLogger.d(this.getClass().getName(), "On Destroy"); + // Stop all players before service is destroyed + shutdown(); + // MediaSession is owned by players, they will release it + super.onDestroy(); } @Override public IBinder onBind(Intent intent) { - Log.d(this.getClass().getName(), "On Bind"); + YaaccLogger.d(this.getClass().getName(), "On Bind"); + // Always return our binder for service binding + // MediaSessionService will handle media controller connections separately return binder; } + @Override + public MediaSession onGetSession(MediaSession.ControllerInfo controllerInfo) { + // Return active MediaSession from LocalMediaSessionPlayer + // Returns null if no LocalMediaSessionPlayer active (other players use MediaSessionCompat) + return activeMediaSession; + } + public Collection getPlayer() { return currentActivePlayer.values(); } @@ -92,25 +217,13 @@ public Collection getPlayer() { @Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); - Log.d(this.getClass().getName(), "Received start id " + startId + ": " + intent); + YaaccLogger.d(this.getClass().getName(), "Received start id " + startId + ": " + intent); if (playerServiceBroadcastReceiver == null) { playerServiceBroadcastReceiver = new PlayerServiceBroadcastReceiver(this); playerServiceBroadcastReceiver.registerReceiver(); } ((Yaacc) getApplicationContext()).createYaaccGroupNotification(); - Intent notificationIntent = new Intent(this, TabBrowserActivity.class); - PendingIntent pendingIntent = PendingIntent.getActivity(this, - 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); - Notification notification = new NotificationCompat.Builder(this, Yaacc.NOTIFICATION_CHANNEL_ID) - .setGroup(Yaacc.NOTIFICATION_GROUP_KEY) - .setContentTitle("Player Service") - .setSilent(true) - .setContentText("running") - .setSmallIcon(R.drawable.ic_notification_default) - .setContentIntent(pendingIntent) - .build(); - - startForeground(NotificationId.PLAYER_SERVICE.getId(), notification); + updateServiceNotification(); initialize(); return START_STICKY; @@ -126,9 +239,8 @@ public HandlerThread getPlayerHandlerThread() { } public Player getPlayer(int playerId) { - Log.v(this.getClass().getName(), "Get Player for id " + playerId); if (currentActivePlayer.get(playerId) == null) { - Log.v(this.getClass().getName(), "Get Player not found"); + YaaccLogger.v(this.getClass().getName(), "Get Player not found"); } return currentActivePlayer.get(playerId); } @@ -143,7 +255,7 @@ public Player getPlayer(int playerId) { * @return the player */ public List createPlayer(UpnpClient upnpClient, List items) { - Log.d(getClass().getName(), "create player..."); + YaaccLogger.d(getClass().getName(), "create player..."); List resultList = new ArrayList<>(); if (items.isEmpty()) { return resultList; @@ -165,7 +277,18 @@ public List createPlayer(UpnpClient upnpClient, List items } } - Log.d(getClass().getName(), "video:" + video + " image: " + image + " audio:" + music); + YaaccLogger.d(getClass().getName(), "video:" + video + " image: " + image + " audio:" + music); + + // Check if any receiver device is selected + if (upnpClient.getReceiverDevices().isEmpty()) { + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> + Toast.makeText(upnpClient.getContext(), + R.string.error_no_receiver_selected, + Toast.LENGTH_LONG).show() + ); + return resultList; + } + for (Device device : upnpClient.getReceiverDevices()) { result = createPlayer(upnpClient, device, video, image, music); if (result != null) { @@ -177,6 +300,43 @@ public List createPlayer(UpnpClient upnpClient, List items return resultList; } + /** + * Creates a player for the local device only (used by renderer service) + */ + public List createPlayerForLocalDevice(UpnpClient upnpClient, List items) { + YaaccLogger.d(getClass().getName(), "create player for local device..."); + List resultList = new ArrayList<>(); + if (items.isEmpty()) { + return resultList; + } + + boolean video = false; + boolean image = false; + boolean music = false; + for (PlayableItem playableItem : items) { + if (playableItem.getMimeType() != null) { + image = image || playableItem.getMimeType().startsWith("image"); + video = video || playableItem.getMimeType().startsWith("video"); + music = music || playableItem.getMimeType().startsWith("audio"); + } else { + image = true; + music = true; + video = true; + } + } + + Device localDevice = upnpClient.getDevice(UpnpClient.LOCAL_UID); + if (localDevice != null) { + Player result = createPlayer(upnpClient, localDevice, video, image, music); + if (result != null) { + addPlayer(result); + result.setItems(items.toArray(new PlayableItem[0])); + resultList.add(result); + } + } + return resultList; + } + @Override public boolean onUnbind(Intent intent) { if (playerServiceBroadcastReceiver != null) { @@ -196,6 +356,7 @@ public boolean onUnbind(Intent intent) { * @param music true if music items * @return the player or null if no device is present */ + @OptIn(markerClass = UnstableApi.class) private Player createPlayer(UpnpClient upnpClient, Device receiverDevice, boolean video, boolean image, boolean music) { if (receiverDevice == null) { @@ -267,15 +428,13 @@ private Player createImagePlayer(UpnpClient upnpClient) { } private Player createMusicPlayer(UpnpClient upnpClient) { - Player result = getFirstCurrentPlayerOfType(LocalBackgoundMusicPlayer.class); + Player result = getFirstCurrentPlayerOfType(LocalMediaSessionPlayer.class); if (result != null) { shutdown(result); } - return new LocalBackgoundMusicPlayer(upnpClient, upnpClient + return new LocalMediaSessionPlayer(upnpClient, upnpClient .getContext().getString(R.string.playerNameMusic), upnpClient .getContext().getString(R.string.playerShortNameMusic)); - - } /** @@ -346,7 +505,7 @@ public Class getPlayerClassForMimeType(String mimeType) { result = LocalImagePlayer.class; } else if (!video && !image && music) { // use musicplayer - result = LocalBackgoundMusicPlayer.class; + result = LocalMediaSessionPlayer.class; } } return result; @@ -359,14 +518,10 @@ public Class getPlayerClassForMimeType(String mimeType) { */ public void shutdown(Player player) { assert (player != null); + YaaccLogger.d(getClass().getName(), "Shutting down player: " + player.getId()); currentActivePlayer.remove(player.getId()); player.onDestroy(); - if (currentActivePlayer.isEmpty()) { - stopForeground(true); - ((Yaacc) getApplicationContext()).cancelYaaccGroupNotification(); - } - - + updateForegroundState(); } /** diff --git a/yaacc/src/main/java/de/yaacc/player/PlayerServiceBroadcastReceiver.java b/yaacc/src/main/java/de/yaacc/player/PlayerServiceBroadcastReceiver.java index 569c6528..005516db 100644 --- a/yaacc/src/main/java/de/yaacc/player/PlayerServiceBroadcastReceiver.java +++ b/yaacc/src/main/java/de/yaacc/player/PlayerServiceBroadcastReceiver.java @@ -21,7 +21,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.util.Log; +import de.yaacc.util.YaaccLogger; /** * @author tobexyz @@ -34,7 +34,7 @@ public class PlayerServiceBroadcastReceiver extends BroadcastReceiver { public PlayerServiceBroadcastReceiver(PlayerService playerService) { - Log.d(this.getClass().getName(), "Starting Broadcast Receiver..."); + YaaccLogger.d(this.getClass().getName(), "Starting Broadcast Receiver..."); assert (playerService != null); this.playerService = playerService; @@ -42,23 +42,23 @@ public PlayerServiceBroadcastReceiver(PlayerService playerService) { @Override public void onReceive(Context context, Intent intent) { - Log.d(this.getClass().getName(), "Received Action: " + intent.getAction()); + YaaccLogger.d(this.getClass().getName(), "Received Action: " + intent.getAction()); if (playerService == null) return; - Log.d(this.getClass().getName(), "Execute Action on playerService: " + playerService); + YaaccLogger.d(this.getClass().getName(), "Execute Action on playerService: " + playerService); if (ACTION_NEXT.equals(intent.getAction())) { Integer playerId = intent.getIntExtra(AbstractPlayer.PLAYER_ID, -1); Player player = playerService.getCurrentPlayerById(playerId); if (player != null) { - Log.d(this.getClass().getName(), "Player of intent found: " + playerId + " Intent: " + intent); + YaaccLogger.d(this.getClass().getName(), "Player of intent found: " + playerId + " Intent: " + intent); player.next(); } else { - Log.d(this.getClass().getName(), "Player of intent not found: " + playerId + " Intent: " + intent); + YaaccLogger.d(this.getClass().getName(), "Player of intent not found: " + playerId + " Intent: " + intent); } } } public void registerReceiver() { - Log.d(this.getClass().getName(), "Register PlayerServiceBroadcastReceiver"); + YaaccLogger.d(this.getClass().getName(), "Register PlayerServiceBroadcastReceiver"); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(ACTION_NEXT); playerService.registerReceiver(this, intentFilter); diff --git a/yaacc/src/main/java/de/yaacc/player/PlaylistItemAdapter.java b/yaacc/src/main/java/de/yaacc/player/PlaylistItemAdapter.java index 23185bf5..cb643e9c 100644 --- a/yaacc/src/main/java/de/yaacc/player/PlaylistItemAdapter.java +++ b/yaacc/src/main/java/de/yaacc/player/PlaylistItemAdapter.java @@ -1,8 +1,24 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ package de.yaacc.player; import android.content.Context; import android.graphics.Typeface; -import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; @@ -20,6 +36,7 @@ import de.yaacc.R; import de.yaacc.util.ThemeHelper; +import de.yaacc.util.YaaccLogger; public class PlaylistItemAdapter extends RecyclerView.Adapter { @@ -84,7 +101,7 @@ public void onBindViewHolder(final PlaylistItemAdapter.ViewHolder holder, final if (listPosition == selectedForMovePosition) { // Highlight the selected item (e.g., change background color or add a border) - Log.d(getClass().getName(), "Item selected for keyboard move: " + item.getTitle()); + YaaccLogger.d(getClass().getName(), "Item selected for keyboard move: " + item.getTitle()); holder.itemView.setBackgroundColor(context.getResources().getColor(R.color.design_default_color_secondary)); } else { holder.itemView.setBackgroundColor(context.getResources().getColor(android.R.color.transparent)); @@ -140,6 +157,12 @@ public boolean moveItem(int fromPosition, int toPosition) { if (listView != null) { listView.scrollToPosition(toPosition); } + + // Sync playlist to ExoPlayer if it's LocalMediaSessionPlayer + if (player instanceof de.yaacc.player.LocalMediaSessionPlayer) { + ((de.yaacc.player.LocalMediaSessionPlayer) player).syncPlaylistToExoPlayer(); + } + return true; } @@ -147,6 +170,11 @@ public boolean moveItem(int fromPosition, int toPosition) { private void removeItem(int listPosition) { if (player.getItems().size() > listPosition && listPosition >= 0 && !(player.isPlaying() && listPosition <= player.getCurrentItemIndex())) { player.getItems().remove(listPosition); + + // Sync playlist to ExoPlayer if it's LocalMediaSessionPlayer + if (player instanceof de.yaacc.player.LocalMediaSessionPlayer) { + ((de.yaacc.player.LocalMediaSessionPlayer) player).syncPlaylistToExoPlayer(); + } // Update local items list to reflect removal before notifying adapter PlayableItem removedItem = items.remove(listPosition); notifyItemRemoved(listPosition); diff --git a/yaacc/src/main/java/de/yaacc/player/YaaccMediaRouteProvider.java b/yaacc/src/main/java/de/yaacc/player/YaaccMediaRouteProvider.java new file mode 100644 index 00000000..2a3d225f --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/player/YaaccMediaRouteProvider.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.player; + +import android.content.Context; +import android.content.IntentFilter; +import android.media.MediaRouter; +import android.os.Handler; +import android.os.Looper; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.mediarouter.media.MediaRouteDescriptor; +import androidx.mediarouter.media.MediaRouteProvider; +import androidx.mediarouter.media.MediaRouteProviderDescriptor; +import androidx.mediarouter.media.MediaRouter.ControlRequestCallback; + +import org.fourthline.cling.model.meta.Device; + +import java.util.ArrayList; +import java.util.List; + +import de.yaacc.upnp.UpnpClient; +import de.yaacc.util.YaaccLogger; + +/** + * MediaRouteProvider for UPnP/DLNA devices. + * Exposes UPnP renderers as Media Router routes for system-wide volume control. + */ +public class YaaccMediaRouteProvider extends MediaRouteProvider { + private final UpnpClient upnpClient; + private final List upnpDevices = new ArrayList<>(); + + public YaaccMediaRouteProvider(Context context, UpnpClient upnpClient) { + super(context); + this.upnpClient = upnpClient; + publishRoutes(); + } + + /** + * Publish available UPnP devices as routes. + */ + private void publishRoutes() { + MediaRouteProviderDescriptor.Builder builder = new MediaRouteProviderDescriptor.Builder(); + + // Get UPnP devices from registry + upnpDevices.clear(); + upnpDevices.addAll(upnpClient.getDevices()); + + for (Device device : upnpDevices) { + if (isMediaRenderer(device)) { + MediaRouteDescriptor route = createRouteForDevice(device); + builder.addRoute(route); + } + } + + setDescriptor(builder.build()); + YaaccLogger.d(getClass().getName(), "Published " + upnpDevices.size() + " UPnP routes"); + } + + /** + * Check if device is a media renderer. + */ + private boolean isMediaRenderer(Device device) { + return device.findService( + new org.fourthline.cling.model.types.UDAServiceType("AVTransport") + ) != null; + } + + /** + * Create MediaRouteDescriptor for UPnP device. + */ + private MediaRouteDescriptor createRouteForDevice(Device device) { + String routeId = device.getIdentity().getUdn().getIdentifierString(); + String name = device.getDetails().getFriendlyName(); + + return new MediaRouteDescriptor.Builder(routeId, name) + .setDescription(device.getDetails().getModelDetails().getModelDescription()) + .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE) + .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) + .setVolumeMax(100) + .setVolume(50) + .addControlFilter(new IntentFilter("android.media.action.PLAY")) + .build(); + } + + @Nullable + @Override + public RouteController onCreateRouteController(@NonNull String routeId) { + Device device = findDeviceById(routeId); + if (device != null) { + return new UpnpRouteController(device); + } + return null; + } + + private Device findDeviceById(String routeId) { + for (Device device : upnpDevices) { + if (device.getIdentity().getUdn().getIdentifierString().equals(routeId)) { + return device; + } + } + return null; + } + + /** + * Route controller for UPnP device. + */ + private class UpnpRouteController extends RouteController { + private final Device device; + private AVTransportPlayer player; + + UpnpRouteController(Device device) { + this.device = device; + } + + @Override + public void onSelect() { + YaaccLogger.d(getClass().getName(), "Route selected: " + device.getDetails().getFriendlyName()); + // Create player for this device + String name = device.getDetails().getFriendlyName(); + player = new AVTransportPlayer(upnpClient, device, name, name, "audio/*,video/*,image/*"); + } + + @Override + public void onUnselect() { + YaaccLogger.d(getClass().getName(), "Route unselected"); + if (player != null) { + player.exit(); + player = null; + } + } + + @Override + public void onRelease() { + YaaccLogger.d(getClass().getName(), "Route released"); + onUnselect(); + } + + @Override + public void onSetVolume(int volume) { + YaaccLogger.d(getClass().getName(), "Set volume: " + volume); + if (player != null) { + player.setVolume(volume); + } + } + + @Override + public void onUpdateVolume(int delta) { + YaaccLogger.d(getClass().getName(), "Update volume: " + delta); + if (player != null) { + int currentVolume = player.getVolume(); + player.setVolume(currentVolume + delta); + } + } + + @Override + public boolean onControlRequest(android.content.Intent intent, ControlRequestCallback callback) { + YaaccLogger.d(getClass().getName(), "Control request: " + intent.getAction()); + + if (player == null) { + return false; + } + + String action = intent.getAction(); + if ("android.media.action.PLAY".equals(action)) { + player.play(); + return true; + } else if ("android.media.action.PAUSE".equals(action)) { + player.pause(); + return true; + } else if ("android.media.action.STOP".equals(action)) { + player.stop(); + return true; + } + + return false; + } + } + + /** + * Refresh routes when UPnP devices change. + * Must be called on main thread. + */ + public void refreshRoutes() { + new Handler(Looper.getMainLooper()).post(this::publishRoutes); + } +} diff --git a/yaacc/src/main/java/de/yaacc/settings/SettingsFragment.java b/yaacc/src/main/java/de/yaacc/settings/SettingsFragment.java index 890c669d..76a3c811 100644 --- a/yaacc/src/main/java/de/yaacc/settings/SettingsFragment.java +++ b/yaacc/src/main/java/de/yaacc/settings/SettingsFragment.java @@ -17,20 +17,31 @@ */ package de.yaacc.settings; +import android.os.Build; import android.os.Bundle; import android.text.InputType; import androidx.appcompat.app.AppCompatDelegate; import androidx.preference.CheckBoxPreference; import androidx.preference.EditTextPreference; +import androidx.preference.PreferenceCategory; import androidx.preference.PreferenceFragmentCompat; +import org.fourthline.cling.model.meta.Device; +import org.fourthline.cling.model.meta.RemoteDevice; + +import java.util.Collection; + import de.yaacc.R; +import de.yaacc.upnp.UpnpClient; +import de.yaacc.util.YaaccLogger; /** * @author Christoph Hähnel (eyeless) */ public class SettingsFragment extends PreferenceFragmentCompat { + public static final String MANAGE_EXTERNAL_SEEKING = "manage_external_seeking_"; + @Override public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { setPreferencesFromResource(R.xml.preference, rootKey); @@ -57,6 +68,63 @@ public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { }); } + androidx.preference.ListPreference logLevelPreference = findPreference(getString(R.string.settings_log_level_key)); + if (logLevelPreference != null) { + logLevelPreference.setOnPreferenceChangeListener((preference, newValue) -> { + if (newValue instanceof String) { + YaaccLogger.setLogLevel((String) newValue); + } + return true; + }); + } + + // Populate renderer settings dynamically + populateRendererSettings(); + } + + private void populateRendererSettings() { + PreferenceCategory rendererCategory = findPreference("renderer_settings_category"); + if (rendererCategory == null) return; + + // Clear existing preferences + rendererCategory.removeAll(); + + // Get UPnP client and discovered devices + UpnpClient upnpClient = ((de.yaacc.Yaacc) requireActivity().getApplicationContext()).getUpnpClient(); + if (upnpClient != null) { + Collection> devices = upnpClient.getDevices(); + + for (Device device : devices) { + if (device instanceof RemoteDevice && device.hasServices()) { + // Check if device has AVTransport service (is a renderer) + if (device.findService(org.fourthline.cling.model.types.ServiceType.valueOf("urn:schemas-upnp-org:service:AVTransport:1")) != null) { + addRendererPreference(rendererCategory, device); + } + } + } + } + + if (rendererCategory.getPreferenceCount() == 0) { + // Add info message if no renderers found + androidx.preference.Preference infoPreference = new androidx.preference.Preference(getContext()); + infoPreference.setTitle("No UPnP renderers discovered"); + infoPreference.setSummary("Renderers will appear here when discovered on the network"); + infoPreference.setEnabled(false); + rendererCategory.addPreference(infoPreference); + } + } + + private void addRendererPreference(PreferenceCategory category, Device device) { + String deviceId = device.getIdentity().getUdn().getIdentifierString(); + String deviceName = device.getDetails().getFriendlyName(); + + CheckBoxPreference preference = new CheckBoxPreference(getContext()); + preference.setKey(MANAGE_EXTERNAL_SEEKING + deviceId); + preference.setTitle(deviceName); + preference.setSummary(getString(R.string.enable_server_side_seeking_for_external_urls)); + preference.setDefaultValue(false); + + category.addPreference(preference); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/UpnpClient.java b/yaacc/src/main/java/de/yaacc/upnp/UpnpClient.java index 4207f7ee..4f49cc20 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/UpnpClient.java +++ b/yaacc/src/main/java/de/yaacc/upnp/UpnpClient.java @@ -28,18 +28,18 @@ import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.IBinder; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.webkit.MimeTypeMap; import androidx.annotation.NonNull; import androidx.preference.PreferenceManager; -import org.fourthline.cling.UpnpService; -import org.fourthline.cling.controlpoint.ControlPoint; import org.fourthline.cling.model.Namespace; import org.fourthline.cling.model.ValidationException; import org.fourthline.cling.model.action.ActionInvocation; import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.message.header.MXHeader; +import org.fourthline.cling.model.message.header.STAllHeader; import org.fourthline.cling.model.meta.Action; import org.fourthline.cling.model.meta.Device; import org.fourthline.cling.model.meta.DeviceDetails; @@ -57,10 +57,10 @@ import org.fourthline.cling.model.types.UDAServiceId; import org.fourthline.cling.model.types.UDAServiceType; import org.fourthline.cling.model.types.UDN; -import org.fourthline.cling.registry.Registry; -import org.fourthline.cling.registry.RegistryListener; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.upnp.registry.RegistryListener; import org.fourthline.cling.support.contentdirectory.DIDLParser; -import org.fourthline.cling.support.contentdirectory.callback.Browse.Status; +import de.yaacc.upnp.callback.contentdirectory.Browse.Status; import org.fourthline.cling.support.model.BrowseFlag; import org.fourthline.cling.support.model.DIDLContent; import org.fourthline.cling.support.model.DIDLObject; @@ -73,10 +73,10 @@ import org.fourthline.cling.support.model.item.Item; import org.fourthline.cling.support.model.item.MusicTrack; import org.fourthline.cling.support.model.item.VideoItem; -import org.fourthline.cling.support.renderingcontrol.callback.GetMute; -import org.fourthline.cling.support.renderingcontrol.callback.GetVolume; -import org.fourthline.cling.support.renderingcontrol.callback.SetMute; -import org.fourthline.cling.support.renderingcontrol.callback.SetVolume; +import de.yaacc.upnp.callback.renderingcontrol.GetMute; +import de.yaacc.upnp.callback.renderingcontrol.GetVolume; +import de.yaacc.upnp.callback.renderingcontrol.SetMute; +import de.yaacc.upnp.callback.renderingcontrol.SetVolume; import org.seamless.util.MimeType; import java.io.IOException; @@ -93,6 +93,8 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -100,16 +102,18 @@ import de.yaacc.Yaacc; import de.yaacc.browser.Position; import de.yaacc.browser.TabBrowserActivity; -import de.yaacc.musicplayer.BackgroundMusicService; import de.yaacc.player.PlayableItem; import de.yaacc.player.Player; import de.yaacc.player.PlayerService; +import de.yaacc.player.YaaccMediaRouteProvider; import de.yaacc.upnp.callback.contentdirectory.ContentDirectoryBrowseActionCallback; import de.yaacc.upnp.callback.contentdirectory.ContentDirectoryBrowseResult; +import de.yaacc.upnp.protocol.async.SendingSearch; import de.yaacc.upnp.server.YaaccUpnpServerService; import de.yaacc.upnp.server.avtransport.AvTransport; import de.yaacc.util.FileDownloader; import de.yaacc.util.FormatHelper; +import de.yaacc.util.InterfaceResolutionHelper; import de.yaacc.util.Watchdog; /** @@ -121,18 +125,23 @@ public class UpnpClient implements RegistryListener, ServiceConnection { public static String LOCAL_UID = "LOCAL_UID"; private final List listeners = new ArrayList<>(); + private final ExecutorService executorService; SharedPreferences preferences; - private UpnpService upnpService; + private Context context; private PlayerService playerService; private Device localDummyDevice; + private YaaccUpnpServerService yaaccUpnpServerService; + private YaaccMediaRouteProvider mediaRouteProvider; public UpnpClient() { + executorService = Executors.newFixedThreadPool(20); } public UpnpClient(Context context) { + this(); initialize(context); } @@ -148,9 +157,15 @@ public boolean initialize(Context context) { if (context != null) { this.context = context; this.preferences = PreferenceManager.getDefaultSharedPreferences(context); + + // Initialize MediaRouteProvider for UPnP devices + mediaRouteProvider = new YaaccMediaRouteProvider(context, this); + androidx.mediarouter.media.MediaRouter.getInstance(context) + .addProvider(mediaRouteProvider); + // FIXME check if this is right: Context.BIND_AUTO_CREATE kills the // service after closing the activity - return context.bindService(new Intent(context, UpnpRegistryService.class), this, Context.BIND_AUTO_CREATE); + return context.bindService(new Intent(context, YaaccUpnpServerService.class), this, Context.BIND_AUTO_CREATE); } return false; } @@ -179,10 +194,18 @@ private void fireReceiverDeviceRemoved(Device device) { private void deviceAdded(@SuppressWarnings("rawtypes") final Device device) { fireDeviceAdded(device); + // Refresh MediaRouter routes when devices change + if (mediaRouteProvider != null) { + mediaRouteProvider.refreshRoutes(); + } } private void deviceRemoved(@SuppressWarnings("rawtypes") final Device device) { fireDeviceRemoved(device); + // Refresh MediaRouter routes when devices change + if (mediaRouteProvider != null) { + mediaRouteProvider.refreshRoutes(); + } } private void deviceUpdated(@SuppressWarnings("rawtypes") final Device device) { @@ -218,18 +241,25 @@ private void fireDeviceUpdated(Device device) { */ @Override public void onServiceConnected(ComponentName className, IBinder service) { - if (service instanceof UpnpRegistryService.UpnpRegistryServiceBinder) { - setUpnpService(((UpnpRegistryService.UpnpRegistryServiceBinder) service).getService().getUpnpService()); + if (service instanceof YaaccUpnpServerService.YaaccUpnpServerServiceBinder) { + yaaccUpnpServerService = ((YaaccUpnpServerService.YaaccUpnpServerServiceBinder) service).getService(); + if (yaaccUpnpServerService.isInitialized()) { + yaaccUpnpServerService.getRegistry().addListener(this); + } refreshUpnpDeviceCatalog(); } if (service instanceof PlayerService.PlayerServiceBinder) { playerService = ((PlayerService.PlayerServiceBinder) service).getService(); - Log.e(getClass().getName(), "player service bounded"); + YaaccLogger.e(getClass().getName(), "player service bounded"); } } + public YaaccUpnpServerService getYaaccUpnpServerService() { + return yaaccUpnpServerService; + } + /* * (non-Javadoc) * @@ -239,9 +269,9 @@ public void onServiceConnected(ComponentName className, IBinder service) { */ @Override public void onServiceDisconnected(ComponentName componentName) { - Log.d(getClass().getName(), "on Service disconnect: " + componentName); - if (UpnpRegistryService.class.getName().equals(componentName.getClassName())) { - setUpnpService(null); + YaaccLogger.d(getClass().getName(), "on Service disconnect: " + componentName); + if (YaaccUpnpServerService.class.getName().equals(componentName.getClassName())) { + yaaccUpnpServerService = null; } if (PlayerService.class.getName().equals(componentName.getClassName())) { playerService = null; @@ -265,7 +295,7 @@ public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice remoted */ @Override public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice remotedevice, Exception exception) { - Log.v(getClass().getName(), "remoteDeviceDiscoveryFailed: " + remotedevice.getDisplayString(), exception); + YaaccLogger.v(getClass().getName(), "remoteDeviceDiscoveryFailed: " + remotedevice.getDisplayString(), exception); } /* @@ -278,7 +308,7 @@ public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice remotede */ @Override public void remoteDeviceAdded(Registry registry, RemoteDevice remotedevice) { - Log.v(getClass().getName(), "remoteDeviceAdded: " + remotedevice.getDisplayString()); + YaaccLogger.v(getClass().getName(), "remoteDeviceAdded: " + remotedevice.getDisplayString()); deviceAdded(remotedevice); } @@ -292,7 +322,7 @@ public void remoteDeviceAdded(Registry registry, RemoteDevice remotedevice) { */ @Override public void remoteDeviceUpdated(Registry registry, RemoteDevice remotedevice) { - Log.v(getClass().getName(), "remoteDeviceUpdated: " + remotedevice.getDisplayString()); + YaaccLogger.v(getClass().getName(), "remoteDeviceUpdated: " + remotedevice.getDisplayString()); deviceUpdated(remotedevice); } @@ -306,7 +336,7 @@ public void remoteDeviceUpdated(Registry registry, RemoteDevice remotedevice) { */ @Override public void remoteDeviceRemoved(Registry registry, RemoteDevice remotedevice) { - Log.v(getClass().getName(), "remoteDeviceRemoved: " + remotedevice.getDisplayString()); + YaaccLogger.v(getClass().getName(), "remoteDeviceRemoved: " + remotedevice.getDisplayString()); deviceRemoved(remotedevice); } @@ -319,7 +349,7 @@ public void remoteDeviceRemoved(Registry registry, RemoteDevice remotedevice) { */ @Override public void localDeviceAdded(Registry registry, LocalDevice localdevice) { - Log.v(getClass().getName(), "localDeviceAdded: " + localdevice.getDisplayString()); + YaaccLogger.v(getClass().getName(), "localDeviceAdded: " + localdevice.getDisplayString()); this.getRegistry().addDevice(localdevice); this.deviceAdded(localdevice); } @@ -336,9 +366,8 @@ public void localDeviceAdded(Registry registry, LocalDevice localdevice) { public void localDeviceRemoved(Registry registry, LocalDevice localdevice) { Registry currentRegistry = this.getRegistry(); if (localdevice != null && currentRegistry != null) { - Log.v(getClass().getName(), "localDeviceRemoved: " + localdevice.getDisplayString()); + YaaccLogger.v(getClass().getName(), "localDeviceRemoved: " + localdevice.getDisplayString()); this.deviceRemoved(localdevice); - this.getRegistry().removeDevice(localdevice); } } @@ -351,7 +380,7 @@ public void localDeviceRemoved(Registry registry, LocalDevice localdevice) { */ @Override public void beforeShutdown(Registry registry) { - Log.v(getClass().getName(), "beforeShutdown: " + registry); + YaaccLogger.v(getClass().getName(), "beforeShutdown: " + registry); } /* @@ -361,7 +390,7 @@ public void beforeShutdown(Registry registry) { */ @Override public void afterShutdown() { - Log.v(getClass().getName(), "afterShutdown "); + YaaccLogger.v(getClass().getName(), "afterShutdown "); } // **************************************************** @@ -374,13 +403,13 @@ public void afterShutdown() { */ public Service getAVTransportService(Device device) { if (device == null) { - Log.d(getClass().getName(), "Device is null!"); + YaaccLogger.d(getClass().getName(), "Device is null!"); return null; } ServiceId serviceId = new UDAServiceId("AVTransport"); Service service = device.findService(serviceId); if (service != null) { - Log.d(getClass().getName(), "Service found: " + service.getServiceId() + " Type: " + service.getServiceType()); + YaaccLogger.d(getClass().getName(), "Service found: " + service.getServiceId() + " Type: " + service.getServiceType()); } return service; } @@ -393,13 +422,13 @@ public void afterShutdown() { */ public Service getRenderingControlService(Device device) { if (device == null) { - Log.d(getClass().getName(), "Device is null!"); + YaaccLogger.d(getClass().getName(), "Device is null!"); return null; } ServiceId serviceId = new UDAServiceId("RenderingControl"); Service service = device.findService(serviceId); if (service != null) { - Log.d(getClass().getName(), "Service found: " + service.getServiceId() + " Type: " + service.getServiceType()); + YaaccLogger.d(getClass().getName(), "Service found: " + service.getServiceId() + " Type: " + service.getServiceType()); } return service; } @@ -414,25 +443,6 @@ public void addUpnpClientListener(UpnpClientListener listener) { } - /** - * returns the AndroidUpnpService - * - * @return the service - */ - private UpnpService getUpnpService() { - return upnpService; - } - - /** - * Setting an new upnpRegistryService. If the service is not null, refresh - * the device list. - * - * @param upnpService upnpservice - */ - private void setUpnpService(UpnpService upnpService) { - this.upnpService = upnpService; - } - /** * Returns all registered UpnpDevices. * @@ -499,20 +509,9 @@ private void setUpnpService(UpnpService upnpService) { * @return true or false */ public boolean isInitialized() { - return getUpnpService() != null; + return yaaccUpnpServerService != null && yaaccUpnpServerService.isInitialized(); } - /** - * returns the upnp control point - * - * @return the control point - */ - public ControlPoint getControlPoint() { - if (!isInitialized()) { - return null; - } - return upnpService.getControlPoint(); - } /** * Returns the upnp registry @@ -523,7 +522,10 @@ public Registry getRegistry() { if (!isInitialized()) { return null; } - return upnpService.getRegistry(); + if (!yaaccUpnpServerService.getRegistry().getListeners().contains(this)) { + yaaccUpnpServerService.getRegistry().addListener(this); + } + return yaaccUpnpServerService.getRegistry(); } /** @@ -538,12 +540,12 @@ public Context getContext() { */ private void refreshUpnpDeviceCatalog() { if (isInitialized()) { - for (Device device : getUpnpService().getRegistry().getDevices()) { + for (Device device : getRegistry().getDevices()) { // FIXME: What about removed devices? this.deviceAdded(device); } // Getting ready for future device advertisements - getUpnpService().getRegistry().addListener(this); + getRegistry().addListener(this); searchDevices(); } } @@ -608,19 +610,19 @@ public ContentDirectoryBrowseResult browseSync(Device device, String ob Service service = device.findService(new UDAServiceId("ContentDirectory")); ContentDirectoryBrowseActionCallback actionCallback; if (service != null) { - Log.d(getClass().getName(), "#####Service found: " + service.getServiceId() + " Type: " + service.getServiceType()); - actionCallback = new ContentDirectoryBrowseActionCallback(service, objectID, flag, filter, firstResult, maxResults, result, orderBy); - getControlPoint().execute(actionCallback); + YaaccLogger.d(getClass().getName(), "#####Service found: " + service.getServiceId() + " Type: " + service.getServiceType()); + actionCallback = new ContentDirectoryBrowseActionCallback(service, objectID, flag, filter, firstResult, maxResults, result, yaaccUpnpServerService.getNetworkDeviceListener().getHttpRequestSender(), orderBy); + executorService.execute(actionCallback); while (actionCallback.getStatus() == Status.LOADING && actionCallback.getUpnpFailure() == null) { //FIXME implement maybe async model? try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { - Log.e(getClass().getName(), "InterruptedException", e); + YaaccLogger.e(getClass().getName(), "InterruptedException", e); } } if (actionCallback.getUpnpFailure() != null) { - Log.e(getClass().getName(), "UPnP failure: " + actionCallback.getUpnpFailure()); + YaaccLogger.e(getClass().getName(), "UPnP failure: " + actionCallback.getUpnpFailure()); } } @@ -684,7 +686,7 @@ private void enrichWithCover(ContentDirectoryBrowseResult callbackResult) { */ public void searchDevices() { if (isInitialized()) { - getUpnpService().getControlPoint().search(); + executorService.execute(new SendingSearch(yaaccUpnpServerService.getNetworkDeviceListener().getUdpTransiver(), new STAllHeader(), MXHeader.DEFAULT_VALUE)); } } @@ -732,12 +734,12 @@ private boolean waitForPlayerServiceComeUp() { //active wait i++; if (i == 100000) { - Log.d(getClass().getName(), "wait for player service start"); + YaaccLogger.d(getClass().getName(), "wait for player service start"); i = 0; } } if (watchdog.hasTimeout()) { - Log.d(getClass().getName(), "Timeout occurred"); + YaaccLogger.d(getClass().getName(), "Timeout occurred"); return false; } else { watchdog.cancel(); @@ -761,29 +763,30 @@ public List initializePlayers(AvTransport transport) { PlayableItem playableItem = new PlayableItem(); List items = new ArrayList<>(); if (transport == null) { - return playerService.createPlayer(this, items); + return playerService.createPlayerForLocalDevice(this, items); } - Log.d(getClass().getName(), "TransportId: " + transport.getInstanceId()); + YaaccLogger.d(getClass().getName(), "TransportId: " + transport.getInstanceId()); PositionInfo positionInfo = transport.getPositionInfo(); - Log.d(getClass().getName(), "positionInfo: " + positionInfo); + YaaccLogger.d(getClass().getName(), "positionInfo: " + positionInfo); if (positionInfo == null) { - return playerService.createPlayer(this, items); + return playerService.createPlayerForLocalDevice(this, items); } DIDLContent metadata = null; try { if (positionInfo.getTrackMetaData() != null && !positionInfo.getTrackMetaData().contains("NOT_IMPLEMENTED")) { metadata = new DIDLParser().parse(positionInfo.getTrackMetaData()); } else { - Log.d(getClass().getName(), "Warning unparsable TackMetaData: " + positionInfo.getTrackMetaData()); + YaaccLogger.d(getClass().getName(), "Warning unparsable TackMetaData: " + positionInfo.getTrackMetaData()); } } catch (Exception e) { - Log.d(getClass().getName(), "Exception while parsing metadata: ", e); + YaaccLogger.d(getClass().getName(), "Exception while parsing metadata: ", e); } String mimeType = ""; if (metadata != null) { List metadataItems = metadata.getItems(); for (Item item : metadataItems) { playableItem.setTitle(item.getTitle()); + playableItem.setItem(item); // Store the DIDL item for album art and other metadata List metadataResources = item.getResources(); for (Res res : metadataResources) { if (res.getProtocolInfo() != null) { @@ -797,19 +800,19 @@ public List initializePlayers(AvTransport transport) { playableItem.setTitle(positionInfo.getTrackURI()); String fileExtension = MimeTypeMap.getFileExtensionFromUrl(positionInfo.getTrackURI()); mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension); - Log.d(getClass().getName(), "fileextension from trackURI: " + fileExtension); + YaaccLogger.d(getClass().getName(), "fileextension from trackURI: " + fileExtension); } playableItem.setMimeType(mimeType); playableItem.setUri(Uri.parse(positionInfo.getTrackURI())); - Log.d(getClass().getName(), "positionInfo.getTrackURI(): " + positionInfo.getTrackURI()); + YaaccLogger.d(getClass().getName(), "positionInfo.getTrackURI(): " + positionInfo.getTrackURI()); // FIXME Duration not supported in receiver yet // playableItem.setDuration(duration) items.add(playableItem); - Log.d(getClass().getName(), "TransportUri: " + positionInfo.getTrackURI()); - Log.d(getClass().getName(), "Current duration: " + positionInfo.getTrackDuration()); - Log.d(getClass().getName(), "TrackMetaData: " + positionInfo.getTrackMetaData()); - Log.d(getClass().getName(), "MimeType: " + playableItem.getMimeType()); - return playerService.createPlayer(this, items); + YaaccLogger.d(getClass().getName(), "TransportUri: " + positionInfo.getTrackURI()); + YaaccLogger.d(getClass().getName(), "Current duration: " + positionInfo.getTrackDuration()); + YaaccLogger.d(getClass().getName(), "TrackMetaData: " + positionInfo.getTrackMetaData()); + YaaccLogger.d(getClass().getName(), "MimeType: " + playableItem.getMimeType()); + return playerService.createPlayerForLocalDevice(this, items); } /** @@ -839,7 +842,7 @@ public List getCurrentPlayers(AvTransport transport) { return Collections.emptyList(); } - Log.d(getClass().getName(), "TransportId: " + transport.getInstanceId()); + YaaccLogger.d(getClass().getName(), "TransportId: " + transport.getInstanceId()); PositionInfo positionInfo = transport.getPositionInfo(); if (positionInfo == null) { return Collections.emptyList(); @@ -850,7 +853,7 @@ public List getCurrentPlayers(AvTransport transport) { metadata = new DIDLParser().parse(positionInfo.getTrackMetaData()); } } catch (Exception e) { - Log.d(getClass().getName(), "Exception while parsing metadata: ", e); + YaaccLogger.d(getClass().getName(), "Exception while parsing metadata: ", e); } String mimeType = ""; PlayableItem playableItem = new PlayableItem(); @@ -876,7 +879,7 @@ public List getCurrentPlayers(AvTransport transport) { } playableItem.setMimeType(mimeType); playableItem.setUri(Uri.parse(positionInfo.getTrackURI())); - Log.d(getClass().getName(), "MimeType: " + playableItem.getMimeType()); + YaaccLogger.d(getClass().getName(), "MimeType: " + playableItem.getMimeType()); return playerService.getCurrentPlayersOfType(playerService.getPlayerClassForMimeType(mimeType)); } @@ -946,7 +949,7 @@ private List toItemList(DIDLObject didlObject, int currentResultSize) { private DIDLContent loadContainer(Container container) { ContentDirectoryBrowseResult result = browseSync(getProviderDevice(), container.getId()); if (result.getUpnpFailure() != null) { - Log.e(getClass().getName(), "Error while loading container:" + result.getUpnpFailure().getDefaultMsg()); + YaaccLogger.e(getClass().getName(), "Error while loading container:" + result.getUpnpFailure().getDefaultMsg()); return null; } return result.getResult(); @@ -1021,7 +1024,7 @@ public void setReceiverDevices(Collection> receiverDevices) { if (receiverDevices == null) return; HashSet receiverIds = new HashSet<>(); for (Device receiver : receiverDevices) { - Log.d(this.getClass().getName(), "Receiver: " + receiver); + YaaccLogger.d(this.getClass().getName(), "Receiver: " + receiver); receiverIds.add(receiver.getIdentity().getUdn().getIdentifierString()); } setReceiverDeviceIds(receiverIds); @@ -1083,19 +1086,12 @@ public void setProviderDevice(Device provider) { */ public void shutdown() { // shutdown UpnpRegistry - boolean result = getContext().stopService(new Intent(getContext(), UpnpRegistryService.class)); - Log.d(getClass().getName(), "Stopping UpnpRegistryService succsessful= " + result); - // shutdown yaacc server service - result = getContext().stopService(new Intent(getContext(), YaaccUpnpServerService.class)); - Log.d(getClass().getName(), "Stopping YaaccUpnpServerService succsessful= " + result); + boolean result = getContext().stopService(new Intent(getContext(), YaaccUpnpServerService.class)); + YaaccLogger.d(getClass().getName(), "Stopping YaaccUpnpServerService succsessful= " + result); // stop all players if (playerService != null) { playerService.shutdown(); } - - result = getContext().stopService(new Intent(getContext(), BackgroundMusicService.class)); - Log.d(getClass().getName(), "Stopping BackgroundMusicService succsessful= " + result); - } /** @@ -1124,7 +1120,7 @@ public int getSilenceDuration() { localDummyDevice = new LocalDummyDevice(context); } catch (ValidationException e) { // Ignore - Log.d(this.getClass().getName(), "Something wrong with the LocalDummyDevice...", e); + YaaccLogger.d(this.getClass().getName(), "Something wrong with the LocalDummyDevice...", e); } } return localDummyDevice; @@ -1217,30 +1213,30 @@ public boolean getMute(Device device) { } Service service = getRenderingControlService(device); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found on Device: " + device.getDisplayString()); return false; } if (!hasActionGetMute(service)) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No action get mute found on Device: " + device.getDisplayString()); return false; } - Log.d(getClass().getName(), "Action get Mute "); + YaaccLogger.d(getClass().getName(), "Action get Mute "); final ActionState actionState = new ActionState(); actionState.actionFinished = false; - GetMute actionCallback = new GetMute(service) { + GetMute actionCallback = new GetMute(service, yaaccUpnpServerService.getNetworkDeviceListener().getHttpRequestSender()) { @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), upnpresponse != null ? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); actionState.actionFinished = true; } @@ -1257,9 +1253,9 @@ public void received(ActionInvocation actionInvocation, boolean currentMute) { } }; try { - getControlPoint().execute(actionCallback).get(10000L, TimeUnit.MILLISECONDS); + executorService.submit(actionCallback).get(10000L, TimeUnit.MILLISECONDS); } catch (Exception ex) { - Log.d(getClass().getName(), "Timeout occurred", ex); + YaaccLogger.d(getClass().getName(), "Timeout occurred", ex); } return actionState.result != null && (Boolean) actionState.result; } @@ -1270,7 +1266,7 @@ public boolean hasActionGetMute(Device device) { } Service service = getRenderingControlService(device); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No RenderingControl-Service found on Device: " + device.getDisplayString()); return false; @@ -1292,28 +1288,28 @@ public void setMute(Device device, boolean mute) { } Service service = getRenderingControlService(device); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No AVTransport-Service found on Device: " + device.getDisplayString()); return; } if (!hasActionSetMute(service)) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No action set mute found on Device: " + device.getDisplayString()); return; } - Log.d(getClass().getName(), "Action set Mute "); - SetMute actionCallback = new SetMute(service, mute) { + YaaccLogger.d(getClass().getName(), "Action set Mute "); + SetMute actionCallback = new SetMute(service, mute, yaaccUpnpServerService.getNetworkDeviceListener().getHttpRequestSender()) { @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), upnpresponse != null ? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); } @Override @@ -1321,7 +1317,7 @@ public void success(ActionInvocation actioninvocation) { super.success(actioninvocation); } }; - getControlPoint().execute(actionCallback); + executorService.execute(actionCallback); } @@ -1339,30 +1335,30 @@ public int getVolume(Device device) { Service service = getRenderingControlService(device); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No RenderingControl-Service found on Device: " + device.getDisplayString()); return 0; } if (!hasActionGetVolume(service)) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No action get volume found on Device: " + device.getDisplayString()); return 0; } - Log.d(getClass().getName(), "Action get Volume "); + YaaccLogger.d(getClass().getName(), "Action get Volume "); final ActionState actionState = new ActionState(); actionState.actionFinished = false; - GetVolume actionCallback = new GetVolume(service) { + GetVolume actionCallback = new GetVolume(service, yaaccUpnpServerService.getNetworkDeviceListener().getHttpRequestSender()) { @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), upnpresponse != null ? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); actionState.actionFinished = true; } @@ -1379,9 +1375,9 @@ public void received(ActionInvocation actionInvocation, int currentVolume) { } }; try { - getControlPoint().execute(actionCallback).get(10000L, TimeUnit.MILLISECONDS); + executorService.submit(actionCallback).get(10000L, TimeUnit.MILLISECONDS); } catch (Exception ex) { - Log.d(getClass().getName(), "Timeout occurred", ex); + YaaccLogger.d(getClass().getName(), "Timeout occurred", ex); } return actionState.result == null ? 0 : (Integer) actionState.result; @@ -1393,7 +1389,7 @@ public boolean hasActionGetVolume(Device device) { } Service service = getRenderingControlService(device); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No RenderingControl-Service found on Device: " + device.getDisplayString()); return false; @@ -1415,28 +1411,28 @@ public void setVolume(Device device, int volume) { Service service = getRenderingControlService(device); if (service == null) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No RenderingControl-Service found on Device: " + device.getDisplayString()); return; } if (!hasActionSetVolume(service)) { - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "No action set volume found on Device: " + device.getDisplayString()); return; } - Log.d(getClass().getName(), "Action set Volume "); - SetVolume actionCallback = new SetVolume(service, volume) { + YaaccLogger.d(getClass().getName(), "Action set Volume "); + SetVolume actionCallback = new SetVolume(service, volume, yaaccUpnpServerService.getNetworkDeviceListener().getHttpRequestSender()) { @Override public void failure(ActionInvocation actioninvocation, UpnpResponse upnpresponse, String s) { - Log.d(getClass().getName(), "Failure UpnpResponse: " + YaaccLogger.d(getClass().getName(), "Failure UpnpResponse: " + upnpresponse); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), upnpresponse != null ? "UpnpResponse: " + upnpresponse.getResponseDetails() : ""); - Log.d(getClass().getName(), "s: " + s); + YaaccLogger.d(getClass().getName(), "s: " + s); } @Override @@ -1444,7 +1440,7 @@ public void success(ActionInvocation actioninvocation) { super.success(actioninvocation); } }; - getControlPoint().execute(actionCallback); + executorService.execute(actionCallback); } public boolean hasActionSetVolume(Service service) { @@ -1577,7 +1573,7 @@ public PlayableItem createPlayableItem(Uri uri) throws IOException { } } catch (RuntimeException e) { //no media file with duration - Log.d(getClass().getName(), "Can't retrieve duration of media url assume shared image", e); + YaaccLogger.d(getClass().getName(), "Can't retrieve duration of media url assume shared image", e); res = new Res(MimeType.valueOf("image/*"), 1L, ""); res.setValue(uriString); item.setMimeType("image/*"); @@ -1586,7 +1582,7 @@ public PlayableItem createPlayableItem(Uri uri) throws IOException { item.setUri(uri); if (this.getPreferences().getBoolean(getContext().getString(R.string.settings_local_server_proxy_chkbx), false)) { String contentKey = sha256(uriString); - String proxyUrl = "http://" + YaaccUpnpServerService.getIpAddress(getContext()) + ":" + YaaccUpnpServerService.PORT + "/" + YaaccUpnpServerService.PROXY_PATH + "/" + contentKey; + String proxyUrl = "http://" + InterfaceResolutionHelper.getIpAddress(getContext()) + ":" + YaaccUpnpServerService.PORT + "/" + YaaccUpnpServerService.PROXY_PATH + "/" + contentKey; this.getPreferences().edit().putString(YaaccUpnpServerService.PROXY_LINK_KEY_PREFIX + contentKey, uriString).apply(); this.getPreferences().edit().putString(YaaccUpnpServerService.PROXY_LINK_MIME_TYPE_KEY_PREFIX + contentKey, item.getMimeType()).apply(); item.setUri(Uri.parse(proxyUrl)); @@ -1612,7 +1608,7 @@ private static String sha256(String input) { } return hexString.toString(); } catch (NoSuchAlgorithmException ex) { - Log.e(TabBrowserActivity.class.getName(), "no sha256 found", ex); + YaaccLogger.e(TabBrowserActivity.class.getName(), "no sha256 found", ex); return input; } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/UpnpRegistryService.java b/yaacc/src/main/java/de/yaacc/upnp/UpnpRegistryService.java deleted file mode 100644 index 474a022b..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/UpnpRegistryService.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * - * Copyright (C) 2013 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp; - -import android.app.Service; -import android.content.Intent; -import android.os.IBinder; -import android.util.Log; - -import org.fourthline.cling.UpnpService; -import org.fourthline.cling.UpnpServiceImpl; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.registry.Registry; -import org.fourthline.cling.transport.Router; - -import de.yaacc.upnp.server.YaaccRouter; - -/** - * This is an android service to provide access to an upnp registry. - * - * @author Tobias Schöne (openbit) - */ -public class UpnpRegistryService extends Service { - - protected UpnpService upnpService; - protected IBinder binder = new UpnpRegistryServiceBinder(); - - - /** - * Starts the UPnP service. - */ - @Override - public void onCreate() { - long start = System.currentTimeMillis(); - super.onCreate(); - - upnpService = new UpnpServiceImpl(new YaaccUpnpServiceConfiguration(this)) { - - @Override - protected Router createRouter(ProtocolFactory protocolFactory, Registry registry) { - return new YaaccRouter(getConfiguration(), protocolFactory, UpnpRegistryService.this); - } - - @Override - public synchronized void shutdown() { - // Now we can concurrently run the Cling shutdown code, without occupying the - // Android main UI thread. This will complete probably after the main UI thread - // is done. - super.shutdown(true); - } - }; - - Log.d(this.getClass().getName(), "on start took: " + (System.currentTimeMillis() - start)); - } - - - public UpnpService getUpnpService() { - return upnpService; - } - - - @Override - public IBinder onBind(Intent intent) { - return binder; - } - - /** - * Stops the UPnP service, when the last Activity unbinds from this Service. - */ - @Override - public void onDestroy() { - upnpService.shutdown(); - super.onDestroy(); - } - - protected class UpnpRegistryServiceBinder extends android.os.Binder { - - public UpnpRegistryService getService() { - return UpnpRegistryService.this; - } - - } - - -} diff --git a/yaacc/src/main/java/de/yaacc/upnp/YaaccAsyncStreamServerConfigurationImpl.java b/yaacc/src/main/java/de/yaacc/upnp/YaaccAsyncStreamServerConfigurationImpl.java deleted file mode 100644 index 6f6f5547..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/YaaccAsyncStreamServerConfigurationImpl.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * - * Copyright (C) 2023 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp; - -import org.fourthline.cling.transport.spi.StreamServerConfiguration; - -public class YaaccAsyncStreamServerConfigurationImpl implements StreamServerConfiguration { - - protected int listenPort; - protected int asyncTimeoutSeconds = 60; - - - public YaaccAsyncStreamServerConfigurationImpl(int listenPort) { - this.listenPort = listenPort; - } - - - /** - * @return Defaults to 0. - */ - public int getListenPort() { - return listenPort; - } - - - /** - * The time in seconds this server wait for the {@link org.fourthline.cling.transport.Router} - * to execute a {@link org.fourthline.cling.transport.spi.UpnpStream}. - * - * @return The default of 60 seconds. - */ - public int getAsyncTimeoutSeconds() { - return asyncTimeoutSeconds; - } -} diff --git a/yaacc/src/main/java/de/yaacc/upnp/YaaccAsyncStreamServerImpl.java b/yaacc/src/main/java/de/yaacc/upnp/YaaccAsyncStreamServerImpl.java deleted file mode 100644 index f3d6440d..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/YaaccAsyncStreamServerImpl.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * - * Copyright (C) 2023 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp; - -import android.util.Log; - -import org.apache.hc.core5.http.URIScheme; -import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncServer; -import org.apache.hc.core5.http2.impl.nio.bootstrap.H2ServerBootstrap; -import org.apache.hc.core5.reactor.IOReactorConfig; -import org.apache.hc.core5.util.TimeValue; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.transport.Router; -import org.fourthline.cling.transport.spi.InitializationException; -import org.fourthline.cling.transport.spi.StreamServer; - -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.util.concurrent.TimeUnit; - -public class YaaccAsyncStreamServerImpl implements StreamServer { - - - final protected YaaccAsyncStreamServerConfigurationImpl configuration; - private final ProtocolFactory protocolFactory; - protected int localPort; - private HttpAsyncServer server; - - public YaaccAsyncStreamServerImpl(ProtocolFactory protocolFactory, YaaccAsyncStreamServerConfigurationImpl configuration) { - this.configuration = configuration; - this.localPort = configuration.getListenPort(); - this.protocolFactory = protocolFactory; - } - - public YaaccAsyncStreamServerConfigurationImpl getConfiguration() { - return configuration; - } - - synchronized public void init(InetAddress bindAddress, final Router router) throws InitializationException { - Thread thread = new Thread(new Runnable() { - - @Override - public void run() { - try { - try { - - Log.d(getClass().getName(), "Adding connector: " + bindAddress + ":" + getConfiguration().getListenPort()); - - IOReactorConfig config = IOReactorConfig.custom() - .setSoTimeout(getConfiguration().getAsyncTimeoutSeconds(), TimeUnit.SECONDS) - .setTcpNoDelay(true) - .build(); - server = H2ServerBootstrap.bootstrap() - .setCanonicalHostName(bindAddress.getHostAddress()) - .setIOReactorConfig(config) - .register(router.getConfiguration().getNamespace().getBasePath().getPath() + "/*", new YaaccAsyncStreamServerRequestHandler(protocolFactory)) - .create(); - server.start(); - server.listen(new InetSocketAddress(getConfiguration().getListenPort()), URIScheme.HTTP); - } catch (Exception ex) { - throw new InitializationException("Could not initialize " + getClass().getSimpleName() + ": " + ex, ex); - } - } catch (Exception e) { - throw new InitializationException("Could run init thread " + getClass().getSimpleName() + ": " + e, e); - } - } - }); - - thread.start(); - - } - - synchronized public int getPort() { - return this.localPort; - } - - synchronized public void stop() { - if (server == null) { - return; - } - server.initiateShutdown(); - try { - server.awaitShutdown(TimeValue.ofSeconds(3)); - } catch (InterruptedException e) { - Log.w(getClass().getName(), "got exception on stream server stop ", e); - } - } - - public void run() { - //do nothing all stuff done in init - } - -} \ No newline at end of file diff --git a/yaacc/src/main/java/de/yaacc/upnp/YaaccNetworkAddressFactoryImpl.java b/yaacc/src/main/java/de/yaacc/upnp/YaaccNetworkAddressFactoryImpl.java deleted file mode 100644 index e30170e5..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/YaaccNetworkAddressFactoryImpl.java +++ /dev/null @@ -1,142 +0,0 @@ -/* - * - * Copyright (C) 2024 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp; - -import android.content.Context; -import android.util.Log; - -import org.fourthline.cling.model.Constants; -import org.fourthline.cling.transport.spi.NetworkAddressFactory; - -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Enumeration; -import java.util.Iterator; -import java.util.List; - -import de.yaacc.upnp.server.YaaccUpnpServerService; - -public class YaaccNetworkAddressFactoryImpl implements NetworkAddressFactory { - - private final Context context; - private final int streamListenPort; - - public YaaccNetworkAddressFactoryImpl(int streamListenPort, Context context) { - this.streamListenPort = streamListenPort; - this.context = context; - } - - - @Override - public InetAddress getMulticastGroup() { - try { - return InetAddress.getByName(Constants.IPV4_UPNP_MULTICAST_GROUP); - } catch (UnknownHostException ex) { - throw new RuntimeException(ex); - } - } - - @Override - public int getMulticastPort() { - return Constants.UPNP_MULTICAST_PORT; - } - - @Override - public int getStreamListenPort() { - return streamListenPort; - } - - @Override - public Iterator getNetworkInterfaces() { - List ifList = new ArrayList<>(); - if (YaaccUpnpServerService.getIfName(context) != null) { - try { - ifList.add(NetworkInterface.getByName(YaaccUpnpServerService.getIfName(context))); - } catch ( - SocketException se) { - Log.d(getClass().getName(), - "Error while retrieving network interfaces", se); - } - } else { - Log.d(getClass().getName(), - "network interface name is null, maybe device is offline"); - } - return ifList.iterator(); - } - - - @Override - public Iterator getBindAddresses() { - List result = new ArrayList<>(); - if (YaaccUpnpServerService.getIfName(context) != null) { - try { - if (NetworkInterface.getByName(YaaccUpnpServerService.getIfName(context)) != null) { - Enumeration iter = NetworkInterface.getByName(YaaccUpnpServerService.getIfName(context)).getInetAddresses(); - while (iter.hasMoreElements()) { - result.add(iter.nextElement()); - } - } else { - Log.d(getClass().getName(), - "network interface not found by name, maybe device is offline"); - } - } catch ( - SocketException se) { - Log.d(getClass().getName(), - "Error while retrieving network interfaces", se); - } - } else { - Log.d(getClass().getName(), - "network interface name is null, maybe device is offline"); - } - return result.iterator(); - } - - @Override - public boolean hasUsableNetwork() { - return !"0.0.0.0".equals(YaaccUpnpServerService.getIpAddress(context)); - } - - @Override - public Short getAddressNetworkPrefixLength(InetAddress inetAddress) { - return 0; - } - - @Override - public byte[] getHardwareAddress(InetAddress inetAddress) { - return new byte[0]; - } - - @Override - public InetAddress getBroadcastAddress(InetAddress inetAddress) { - return getBindAddresses().next(); - } - - @Override - public InetAddress getLocalAddress(NetworkInterface networkInterface, boolean isIPv6, InetAddress remoteAddress) throws IllegalStateException { - return getBindAddresses().next(); - } - - @Override - public void logInterfaceInformation() { - - } -} diff --git a/yaacc/src/main/java/de/yaacc/upnp/YaaccStreamingClientConfigurationImpl.java b/yaacc/src/main/java/de/yaacc/upnp/YaaccStreamingClientConfigurationImpl.java deleted file mode 100644 index dc2350b4..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/YaaccStreamingClientConfigurationImpl.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * - * Copyright (C) 2023 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp; - -import android.os.Build; - -import org.fourthline.cling.model.ServerClientTokens; -import org.fourthline.cling.transport.spi.AbstractStreamClientConfiguration; - -import java.util.concurrent.ExecutorService; - -public class YaaccStreamingClientConfigurationImpl extends AbstractStreamClientConfiguration { - - public YaaccStreamingClientConfigurationImpl(ExecutorService timeoutExecutorService) { - super(timeoutExecutorService); - } - - public YaaccStreamingClientConfigurationImpl(ExecutorService requestExecutorService, int timeoutSeconds) { - super(requestExecutorService, timeoutSeconds); - } - - public YaaccStreamingClientConfigurationImpl(ExecutorService requestExecutorService, int timeoutSeconds, int logWarningSeconds) { - super(requestExecutorService, timeoutSeconds, logWarningSeconds); - } - - - @Override - public String getUserAgentValue(int majorVersion, int minorVersion) { - // TODO: UPNP VIOLATION: Synology NAS requires User-Agent to contain - // "Android" to return DLNA protocolInfo required to stream to Samsung TV - // see: http://two-play.com/forums/viewtopic.php?f=6&t=81 - ServerClientTokens tokens = new ServerClientTokens(majorVersion, minorVersion); - tokens.setOsName("Android"); - tokens.setOsVersion(Build.VERSION.RELEASE); - return tokens.toString(); - } -} - diff --git a/yaacc/src/main/java/de/yaacc/upnp/YaaccUpnpServiceConfiguration.java b/yaacc/src/main/java/de/yaacc/upnp/YaaccUpnpServiceConfiguration.java deleted file mode 100644 index 3c8ddafd..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/YaaccUpnpServiceConfiguration.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * - * Copyright (C) 2023 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp; - -import android.content.Context; - -import org.fourthline.cling.DefaultUpnpServiceConfiguration; -import org.fourthline.cling.binding.xml.DeviceDescriptorBinder; -import org.fourthline.cling.binding.xml.RecoveringUDA10DeviceDescriptorBinderImpl; -import org.fourthline.cling.binding.xml.ServiceDescriptorBinder; -import org.fourthline.cling.binding.xml.UDA10ServiceDescriptorBinderSAXImpl; -import org.fourthline.cling.model.Namespace; -import org.fourthline.cling.model.types.ServiceType; -import org.fourthline.cling.model.types.UDAServiceType; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.transport.impl.RecoveringGENAEventProcessorImpl; -import org.fourthline.cling.transport.impl.RecoveringSOAPActionProcessorImpl; -import org.fourthline.cling.transport.spi.GENAEventProcessor; -import org.fourthline.cling.transport.spi.NetworkAddressFactory; -import org.fourthline.cling.transport.spi.SOAPActionProcessor; -import org.fourthline.cling.transport.spi.StreamClient; -import org.fourthline.cling.transport.spi.StreamServer; - -public class YaaccUpnpServiceConfiguration extends DefaultUpnpServiceConfiguration { - - private static final int PORT = 49154; - private final Context context; - - - public YaaccUpnpServiceConfiguration(Context context) { - this(PORT, context); - } - - public YaaccUpnpServiceConfiguration(int streamListenPort, Context context) { - super(streamListenPort, false); - this.context = context; - - // This should be the default on Android 2.1 but it's not set by default - //FIXME really needed? System.setProperty("org.xml.sax.driver", "org.xmlpull.v1.sax2.Driver"); - } - - @Override - protected NetworkAddressFactory createNetworkAddressFactory(int streamListenPort) { - return new YaaccNetworkAddressFactoryImpl(streamListenPort, context); - } - - @Override - protected Namespace createNamespace() { - // Http context path - return new Namespace("/upnp"); - } - - - @Override - public ServiceType[] getExclusiveServiceTypes() { - - return new ServiceType[]{new UDAServiceType("AVTransport"), new UDAServiceType("ContentDirectory"), new UDAServiceType("ConnectionManager"), new UDAServiceType("RenderingControl"), new UDAServiceType("X_MS_MediaReceiverRegistrar")}; - } - - @Override - public StreamClient createStreamClient() { - return new YaaccStreamingClientImpl( - new YaaccStreamingClientConfigurationImpl( - getSyncProtocolExecutorService() - ) - ); - } - - @Override - public StreamServer createStreamServer(ProtocolFactory protocolFactory, NetworkAddressFactory networkAddressFactory) { - - return new YaaccAsyncStreamServerImpl(protocolFactory, - new YaaccAsyncStreamServerConfigurationImpl(networkAddressFactory.getStreamListenPort()) - ); - } - - @Override - protected DeviceDescriptorBinder createDeviceDescriptorBinderUDA10() { - return new RecoveringUDA10DeviceDescriptorBinderImpl(); - } - - @Override - protected ServiceDescriptorBinder createServiceDescriptorBinderUDA10() { - return new UDA10ServiceDescriptorBinderSAXImpl(); - } - - @Override - protected SOAPActionProcessor createSOAPActionProcessor() { - return new RecoveringSOAPActionProcessorImpl(); - } - - @Override - protected GENAEventProcessor createGENAEventProcessor() { - return new RecoveringGENAEventProcessorImpl(); - } - - @Override - public int getRegistryMaintenanceIntervalMillis() { - return 7000; // Preserve battery on Android, only run every 7 seconds - } - -} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/ActionCallback.java b/yaacc/src/main/java/de/yaacc/upnp/callback/ActionCallback.java new file mode 100644 index 00000000..7daf72be --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/ActionCallback.java @@ -0,0 +1,88 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.LocalService; +import org.fourthline.cling.model.meta.RemoteService; +import org.fourthline.cling.model.meta.Service; + +import java.net.URL; + +import de.yaacc.upnp.protocol.sync.SendingAction; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class ActionCallback implements Runnable { + + protected final ActionInvocation actionInvocation; + protected final HttpRequestSender httpRequestSender; + + protected ActionCallback(ActionInvocation actionInvocation, HttpRequestSender httpRequestSender) { + this.actionInvocation = actionInvocation; + this.httpRequestSender = httpRequestSender; + } + + public ActionInvocation getActionInvocation() { + return actionInvocation; + } + + public void run() { + Service service = actionInvocation.getAction().getService(); + + if (service instanceof LocalService) { + LocalService localService = (LocalService) service; + localService.getExecutor(actionInvocation.getAction()).execute(actionInvocation); + + if (actionInvocation.getFailure() != null) { + failure(actionInvocation, null); + } else { + success(actionInvocation); + } + + } else if (service instanceof RemoteService) { + RemoteService remoteService = (RemoteService) service; + + URL controlURL; + try { + controlURL = remoteService.getDevice().normalizeURI(remoteService.getControlURI()); + } catch (IllegalArgumentException e) { + failure(actionInvocation, null, "bad control URL: " + remoteService.getControlURI()); + return; + } + + SendingAction protocol = new SendingAction(httpRequestSender, actionInvocation, controlURL); + protocol.run(); + + if (actionInvocation.getFailure() != null) { + failure(actionInvocation, null); + } else { + success(actionInvocation); + } + } + } + + public abstract void success(ActionInvocation invocation); + + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); + + protected void failure(ActionInvocation invocation, UpnpResponse operation) { + failure(invocation, operation, null); + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/SubscriptionCallback.java b/yaacc/src/main/java/de/yaacc/upnp/callback/SubscriptionCallback.java new file mode 100644 index 00000000..83f5ea0d --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/SubscriptionCallback.java @@ -0,0 +1,251 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback; + +import android.content.Context; + +import org.fourthline.cling.model.UnsupportedDataException; +import org.fourthline.cling.model.UserConstants; +import org.fourthline.cling.model.gena.CancelReason; +import org.fourthline.cling.model.gena.GENASubscription; +import org.fourthline.cling.model.gena.LocalGENASubscription; +import org.fourthline.cling.model.gena.RemoteGENASubscription; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.LocalService; +import org.fourthline.cling.model.meta.RemoteService; +import org.fourthline.cling.model.meta.Service; + +import java.util.Collections; +import java.util.concurrent.ExecutorService; + +import de.yaacc.upnp.protocol.sync.SendingSubscribe; +import de.yaacc.upnp.protocol.sync.SendingUnsubscribe; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.Exceptions; +import de.yaacc.util.InterfaceResolutionHelper; +import de.yaacc.util.YaaccLogger; + +public abstract class SubscriptionCallback implements Runnable { + + protected final Service service; + protected final Integer requestedDurationSeconds; + protected final Registry registry; + protected final HttpRequestSender httpRequestSender; + protected final ExecutorService executorService; + protected final Context context; + + private GENASubscription subscription; + + protected SubscriptionCallback(Service service, Registry registry, HttpRequestSender httpRequestSender, ExecutorService executorService, Context context) { + this.service = service; + this.requestedDurationSeconds = UserConstants.DEFAULT_SUBSCRIPTION_DURATION_SECONDS; + this.registry = registry; + this.httpRequestSender = httpRequestSender; + this.executorService = executorService; + this.context = context; + } + + protected SubscriptionCallback(Service service, int requestedDurationSeconds, Registry registry, HttpRequestSender httpRequestSender, ExecutorService executorService, Context context) { + this.service = service; + this.requestedDurationSeconds = requestedDurationSeconds; + this.registry = registry; + this.httpRequestSender = httpRequestSender; + this.executorService = executorService; + this.context = context; + } + + public static String createDefaultFailureMessage(UpnpResponse responseStatus, Exception exception) { + String message = "Subscription failed: "; + if (responseStatus != null) { + message = message + " HTTP response was: " + responseStatus.getResponseDetails(); + } else if (exception != null) { + message = message + " Exception occured: " + exception; + } else { + message = message + " No response received."; + } + return message; + } + + public Service getService() { + return service; + } + + synchronized public GENASubscription getSubscription() { + return subscription; + } + + synchronized public void setSubscription(GENASubscription subscription) { + this.subscription = subscription; + } + + synchronized public void run() { + if (getService() instanceof LocalService) { + establishLocalSubscription((LocalService) service); + } else if (getService() instanceof RemoteService) { + establishRemoteSubscription((RemoteService) service); + } + } + + private void establishLocalSubscription(LocalService service) { + if (registry.getLocalDevice(service.getDevice().getIdentity().getUdn(), false) == null) { + YaaccLogger.v(getClass().getName(), "Local device service is currently not registered, failing subscription immediately"); + failed(null, null, new IllegalStateException("Local device is not registered")); + return; + } + + LocalGENASubscription localSubscription = null; + try { + localSubscription = new LocalGENASubscription(service, Integer.MAX_VALUE, Collections.EMPTY_LIST) { + public void failed(Exception ex) { + synchronized (SubscriptionCallback.this) { + SubscriptionCallback.this.setSubscription(null); + SubscriptionCallback.this.failed(null, null, ex); + } + } + + public void established() { + synchronized (SubscriptionCallback.this) { + SubscriptionCallback.this.setSubscription(this); + SubscriptionCallback.this.established(this); + } + } + + public void ended(CancelReason reason) { + synchronized (SubscriptionCallback.this) { + SubscriptionCallback.this.setSubscription(null); + SubscriptionCallback.this.ended(this, reason, null); + } + } + + public void eventReceived() { + synchronized (SubscriptionCallback.this) { + SubscriptionCallback.this.eventReceived(this); + incrementSequence(); + } + } + }; + + registry.addLocalSubscription(localSubscription); + localSubscription.establish(); + eventReceived(localSubscription); + localSubscription.incrementSequence(); + localSubscription.registerOnService(); + + } catch (Exception ex) { + YaaccLogger.v(getClass().getName(), "Local callback creation failed: " + ex.toString()); + YaaccLogger.v(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); + if (localSubscription != null) + registry.removeLocalSubscription(localSubscription); + failed(localSubscription, null, ex); + } + } + + private void establishRemoteSubscription(RemoteService service) { + RemoteGENASubscription remoteSubscription = new RemoteGENASubscription(service, requestedDurationSeconds) { + public void failed(UpnpResponse responseStatus) { + synchronized (SubscriptionCallback.this) { + SubscriptionCallback.this.setSubscription(null); + SubscriptionCallback.this.failed(this, responseStatus, null); + } + } + + public void established() { + synchronized (SubscriptionCallback.this) { + SubscriptionCallback.this.setSubscription(this); + SubscriptionCallback.this.established(this); + } + } + + public void ended(CancelReason reason, UpnpResponse responseStatus) { + synchronized (SubscriptionCallback.this) { + SubscriptionCallback.this.setSubscription(null); + SubscriptionCallback.this.ended(this, reason, responseStatus); + } + } + + public void eventReceived() { + synchronized (SubscriptionCallback.this) { + SubscriptionCallback.this.eventReceived(this); + } + } + + public void eventsMissed(int numberOfMissedEvents) { + synchronized (SubscriptionCallback.this) { + SubscriptionCallback.this.eventsMissed(this, numberOfMissedEvents); + } + } + + public void invalidMessage(UnsupportedDataException ex) { + synchronized (SubscriptionCallback.this) { + SubscriptionCallback.this.invalidMessage(this, ex); + } + } + }; + + SendingSubscribe protocol = new SendingSubscribe( + registry, + httpRequestSender, + remoteSubscription, + InterfaceResolutionHelper.getNetworkAddress(context) + ); + protocol.run(); + } + + synchronized public void end() { + if (subscription == null) return; + if (subscription instanceof LocalGENASubscription) { + endLocalSubscription((LocalGENASubscription) subscription); + } else if (subscription instanceof RemoteGENASubscription) { + endRemoteSubscription((RemoteGENASubscription) subscription); + } + } + + private void endLocalSubscription(LocalGENASubscription subscription) { + registry.removeLocalSubscription(subscription); + subscription.end(null); + } + + private void endRemoteSubscription(RemoteGENASubscription subscription) { + executorService.execute(new SendingUnsubscribe(registry, httpRequestSender, subscription)); + } + + protected void failed(GENASubscription subscription, UpnpResponse responseStatus, Exception exception) { + failed(subscription, responseStatus, exception, createDefaultFailureMessage(responseStatus, exception)); + } + + protected abstract void failed(GENASubscription subscription, UpnpResponse responseStatus, Exception exception, String defaultMsg); + + protected abstract void established(GENASubscription subscription); + + protected abstract void ended(GENASubscription subscription, CancelReason reason, UpnpResponse responseStatus); + + protected abstract void eventReceived(GENASubscription subscription); + + protected void eventsMissed(GENASubscription subscription, int numberOfMissedEvents) { + } + + protected void invalidMessage(GENASubscription subscription, UnsupportedDataException ex) { + } + + @Override + public String toString() { + return "(SubscriptionCallback) " + getService(); + } +} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetCurrentTransportActions.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetCurrentTransportActions.java similarity index 52% rename from yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetCurrentTransportActions.java rename to yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetCurrentTransportActions.java index 32bfaf3b..1329c356 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetCurrentTransportActions.java +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetCurrentTransportActions.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,9 +31,8 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.support.avtransport.callback; +package de.yaacc.upnp.callback.avtransport; -import org.fourthline.cling.controlpoint.ActionCallback; import org.fourthline.cling.model.action.ActionInvocation; import org.fourthline.cling.model.meta.Service; import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; @@ -23,6 +40,9 @@ import java.util.logging.Logger; +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + /** * @author Christian Bauer */ @@ -30,17 +50,17 @@ public abstract class GetCurrentTransportActions extends ActionCallback { private static Logger log = Logger.getLogger(GetCurrentTransportActions.class.getName()); - public GetCurrentTransportActions(Service service) { - this(new UnsignedIntegerFourBytes(0), service); + public GetCurrentTransportActions(HttpRequestSender httpRequestSender, Service service) { + this(httpRequestSender, new UnsignedIntegerFourBytes(0), service); } - public GetCurrentTransportActions(UnsignedIntegerFourBytes instanceId, Service service) { - super(new ActionInvocation(service.getAction("GetCurrentTransportActions"))); + public GetCurrentTransportActions(HttpRequestSender httpRequestSender, UnsignedIntegerFourBytes instanceId, Service service) { + super(new ActionInvocation(service.getAction("GetCurrentTransportActions")), httpRequestSender); getActionInvocation().setInput("InstanceID", instanceId); } public void success(ActionInvocation invocation) { - String actionsString = (String)invocation.getOutput("Actions").getValue(); + String actionsString = (String) invocation.getOutput("Actions").getValue(); received(invocation, TransportAction.valueOfCommaSeparatedList(actionsString)); } diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetDeviceCapabilities.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetDeviceCapabilities.java new file mode 100644 index 00000000..3891beaf --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetDeviceCapabilities.java @@ -0,0 +1,89 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +/* + * Copyright (C) 2013 4th Line GmbH, Switzerland + * + * The contents of this file are subject to the terms of either the GNU + * Lesser General Public License Version 2 or later ("LGPL") or the + * Common Development and Distribution License Version 1 or later + * ("CDDL") (collectively, the "License"). You may not use this file + * except in compliance with the License. See LICENSE.txt for more + * information. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ + +package de.yaacc.upnp.callback.avtransport; + +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; +import org.fourthline.cling.support.model.DeviceCapabilities; + +import java.util.logging.Logger; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +/** + * + * @author Christian Bauer + */ +public abstract class GetDeviceCapabilities extends ActionCallback { + + private static Logger log = Logger.getLogger(GetDeviceCapabilities.class.getName()); + + public GetDeviceCapabilities(HttpRequestSender httpRequestSender, Service service) { + this(httpRequestSender, new UnsignedIntegerFourBytes(0), service); + } + + public GetDeviceCapabilities(HttpRequestSender httpRequestSender, UnsignedIntegerFourBytes instanceId, Service service) { + super(new ActionInvocation(service.getAction("GetDeviceCapabilities")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + } + + public void success(ActionInvocation invocation) { + DeviceCapabilities caps = new DeviceCapabilities(invocation.getOutputMap()); + received(invocation, caps); + } + + public abstract void received(ActionInvocation actionInvocation, DeviceCapabilities caps); + +} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetMediaInfo.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetMediaInfo.java similarity index 53% rename from yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetMediaInfo.java rename to yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetMediaInfo.java index 69635ffb..0753e8a1 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetMediaInfo.java +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetMediaInfo.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,9 +31,8 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.support.avtransport.callback; +package de.yaacc.upnp.callback.avtransport; -import org.fourthline.cling.controlpoint.ActionCallback; import org.fourthline.cling.model.action.ActionInvocation; import org.fourthline.cling.model.meta.Service; import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; @@ -23,20 +40,22 @@ import java.util.logging.Logger; +import de.yaacc.upnp.server.http.HttpRequestSender; + /** * * @author Christian Bauer */ -public abstract class GetMediaInfo extends ActionCallback { +public abstract class GetMediaInfo extends de.yaacc.upnp.callback.ActionCallback { private static Logger log = Logger.getLogger(GetMediaInfo.class.getName()); - public GetMediaInfo(Service service) { - this(new UnsignedIntegerFourBytes(0), service); + public GetMediaInfo(HttpRequestSender httpRequestSender, Service service) { + this(httpRequestSender, new UnsignedIntegerFourBytes(0), service); } - public GetMediaInfo(UnsignedIntegerFourBytes instanceId, Service service) { - super(new ActionInvocation(service.getAction("GetMediaInfo"))); + public GetMediaInfo(HttpRequestSender httpRequestSender, UnsignedIntegerFourBytes instanceId, Service service) { + super(new ActionInvocation(service.getAction("GetMediaInfo")), httpRequestSender); getActionInvocation().setInput("InstanceID", instanceId); } diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetPositionInfo.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetPositionInfo.java new file mode 100644 index 00000000..ae3306b6 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetPositionInfo.java @@ -0,0 +1,51 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.avtransport; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; +import org.fourthline.cling.support.model.PositionInfo; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class GetPositionInfo extends ActionCallback { + + public GetPositionInfo(Service service, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, httpRequestSender); + } + + public GetPositionInfo(UnsignedIntegerFourBytes instanceId, Service service, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("GetPositionInfo")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + } + + @Override + public void success(ActionInvocation invocation) { + PositionInfo positionInfo = new PositionInfo(invocation.getOutputMap()); + received(invocation, positionInfo); + } + + public abstract void received(ActionInvocation invocation, PositionInfo positionInfo); + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetTransportInfo.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetTransportInfo.java new file mode 100644 index 00000000..f2272256 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/GetTransportInfo.java @@ -0,0 +1,51 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.avtransport; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; +import org.fourthline.cling.support.model.TransportInfo; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class GetTransportInfo extends ActionCallback { + + public GetTransportInfo(Service service, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, httpRequestSender); + } + + public GetTransportInfo(UnsignedIntegerFourBytes instanceId, Service service, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("GetTransportInfo")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + } + + @Override + public void success(ActionInvocation invocation) { + TransportInfo transportInfo = new TransportInfo(invocation.getOutputMap()); + received(invocation, transportInfo); + } + + public abstract void received(ActionInvocation invocation, TransportInfo transportInfo); + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Next.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Next.java new file mode 100644 index 00000000..33e6a061 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Next.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.avtransport; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class Next extends ActionCallback { + + public Next(Service service, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, httpRequestSender); + } + + public Next(UnsignedIntegerFourBytes instanceId, Service service, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("Next")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + } + + @Override + public void success(ActionInvocation invocation) { + } + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Pause.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Pause.java new file mode 100644 index 00000000..9207f68a --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Pause.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.avtransport; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class Pause extends ActionCallback { + + public Pause(Service service, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, httpRequestSender); + } + + public Pause(UnsignedIntegerFourBytes instanceId, Service service, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("Pause")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + } + + @Override + public void success(ActionInvocation invocation) { + } + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Play.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Play.java new file mode 100644 index 00000000..49b4039a --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Play.java @@ -0,0 +1,51 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.avtransport; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class Play extends ActionCallback { + + public Play(Service service, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, "1", httpRequestSender); + } + + public Play(UnsignedIntegerFourBytes instanceId, Service service, HttpRequestSender httpRequestSender) { + this(instanceId, service, "1", httpRequestSender); + } + + public Play(UnsignedIntegerFourBytes instanceId, Service service, String speed, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("Play")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + getActionInvocation().setInput("Speed", speed); + } + + @Override + public void success(ActionInvocation invocation) { + } + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Previous.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Previous.java new file mode 100644 index 00000000..fb29b056 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Previous.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.avtransport; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class Previous extends ActionCallback { + + public Previous(Service service, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, httpRequestSender); + } + + public Previous(UnsignedIntegerFourBytes instanceId, Service service, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("Previous")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + } + + @Override + public void success(ActionInvocation invocation) { + } + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Seek.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Seek.java new file mode 100644 index 00000000..30a8d79d --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Seek.java @@ -0,0 +1,57 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.avtransport; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; +import org.fourthline.cling.support.model.SeekMode; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class Seek extends ActionCallback { + + public Seek(Service service, String relativeTimeTarget, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, SeekMode.REL_TIME, relativeTimeTarget, httpRequestSender); + } + + public Seek(UnsignedIntegerFourBytes instanceId, Service service, String relativeTimeTarget, HttpRequestSender httpRequestSender) { + this(instanceId, service, SeekMode.REL_TIME, relativeTimeTarget, httpRequestSender); + } + + public Seek(Service service, SeekMode mode, String target, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, mode, target, httpRequestSender); + } + + public Seek(UnsignedIntegerFourBytes instanceId, Service service, SeekMode mode, String target, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("Seek")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + getActionInvocation().setInput("Unit", mode.name()); + getActionInvocation().setInput("Target", target); + } + + @Override + public void success(ActionInvocation invocation) { + } + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/SetAVTransportURI.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/SetAVTransportURI.java new file mode 100644 index 00000000..599ed14b --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/SetAVTransportURI.java @@ -0,0 +1,57 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.avtransport; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class SetAVTransportURI extends ActionCallback { + + public SetAVTransportURI(Service service, String uri, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, uri, null, httpRequestSender); + } + + public SetAVTransportURI(Service service, String uri, String metadata, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, uri, metadata, httpRequestSender); + } + + public SetAVTransportURI(UnsignedIntegerFourBytes instanceId, Service service, String uri, HttpRequestSender httpRequestSender) { + this(instanceId, service, uri, null, httpRequestSender); + } + + public SetAVTransportURI(UnsignedIntegerFourBytes instanceId, Service service, String uri, String metadata, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("SetAVTransportURI")), httpRequestSender); + + getActionInvocation().setInput("InstanceID", instanceId); + getActionInvocation().setInput("CurrentURI", uri); + getActionInvocation().setInput("CurrentURIMetaData", metadata); + } + + @Override + public void success(ActionInvocation invocation) { + } + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/SetPlayMode.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/SetPlayMode.java new file mode 100644 index 00000000..926183ea --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/SetPlayMode.java @@ -0,0 +1,65 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +/* + * Copyright (C) 2013 4th Line GmbH, Switzerland + * + * The contents of this file are subject to the terms of either the GNU + * Lesser General Public License Version 2 or later ("LGPL") or the + * Common Development and Distribution License Version 1 or later + * ("CDDL") (collectively, the "License"). You may not use this file + * except in compliance with the License. See LICENSE.txt for more + * information. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ + +package de.yaacc.upnp.callback.avtransport; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; +import org.fourthline.cling.support.model.PlayMode; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.YaaccLogger; + +/** + * @author Christian Bauer + */ +public abstract class SetPlayMode extends ActionCallback { + + + public SetPlayMode(HttpRequestSender httpRequestSender, Service service, PlayMode playMode) { + this(httpRequestSender, new UnsignedIntegerFourBytes(0), service, playMode); + } + + public SetPlayMode(HttpRequestSender httpRequestSender, UnsignedIntegerFourBytes instanceId, Service service, PlayMode playMode) { + super(new ActionInvocation(service.getAction("SetPlayMode")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + getActionInvocation().setInput("NewPlayMode", playMode.toString()); + } + + @Override + public void success(ActionInvocation invocation) { + YaaccLogger.v(getClass().getName(), "Execution successful"); + } +} \ No newline at end of file diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Stop.java b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Stop.java new file mode 100644 index 00000000..b125c84f --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/avtransport/Stop.java @@ -0,0 +1,46 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.avtransport; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class Stop extends ActionCallback { + + public Stop(Service service, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, httpRequestSender); + } + + public Stop(UnsignedIntegerFourBytes instanceId, Service service, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("Stop")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + } + + @Override + public void success(ActionInvocation invocation) { + } + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/ConnectionComplete.java b/yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/ConnectionComplete.java new file mode 100644 index 00000000..edc09e61 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/ConnectionComplete.java @@ -0,0 +1,33 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.connectionmanager; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.meta.Service; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class ConnectionComplete extends ActionCallback { + + public ConnectionComplete(Service service, HttpRequestSender httpRequestSender, int connectionID) { + super(new ActionInvocation(service.getAction("ConnectionComplete")), httpRequestSender); + getActionInvocation().setInput("ConnectionID", connectionID); + } +} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/GetCurrentConnectionInfo.java b/yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/GetCurrentConnectionInfo.java similarity index 54% rename from yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/GetCurrentConnectionInfo.java rename to yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/GetCurrentConnectionInfo.java index 36241655..413bb9ff 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/GetCurrentConnectionInfo.java +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/GetCurrentConnectionInfo.java @@ -1,22 +1,23 @@ /* - * Copyright (C) 2013 4th Line GmbH, Switzerland * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +package de.yaacc.upnp.callback.connectionmanager; -package org.fourthline.cling.support.connectionmanager.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; import org.fourthline.cling.model.ServiceReference; import org.fourthline.cling.model.action.ActionException; import org.fourthline.cling.model.action.ActionInvocation; @@ -25,32 +26,26 @@ import org.fourthline.cling.support.model.ConnectionInfo; import org.fourthline.cling.support.model.ProtocolInfo; -/** - * @author Alessio Gaeta - * @author Christian Bauer - */ +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + public abstract class GetCurrentConnectionInfo extends ActionCallback { - public GetCurrentConnectionInfo(Service service, int connectionID) { - this(service, null, connectionID); + public GetCurrentConnectionInfo(Service service, HttpRequestSender httpRequestSender, int connectionID) { + super(new ActionInvocation(service.getAction("GetCurrentConnectionInfo")), httpRequestSender); + getActionInvocation().setInput("ConnectionID", connectionID); } - protected GetCurrentConnectionInfo(Service service, ControlPoint controlPoint, int connectionID) { - super(new ActionInvocation(service.getAction("GetCurrentConnectionInfo")), controlPoint); - getActionInvocation().setInput("ConnectionID", connectionID); - } - @Override public void success(ActionInvocation invocation) { - try { ConnectionInfo info = new ConnectionInfo( - (Integer)invocation.getInput("ConnectionID").getValue(), - (Integer)invocation.getOutput("RcsID").getValue(), - (Integer)invocation.getOutput("AVTransportID").getValue(), + (Integer) invocation.getInput("ConnectionID").getValue(), + (Integer) invocation.getOutput("RcsID").getValue(), + (Integer) invocation.getOutput("AVTransportID").getValue(), new ProtocolInfo(invocation.getOutput("ProtocolInfo").toString()), new ServiceReference(invocation.getOutput("PeerConnectionManager").toString()), - (Integer)invocation.getOutput("PeerConnectionID").getValue(), + (Integer) invocation.getOutput("PeerConnectionID").getValue(), ConnectionInfo.Direction.valueOf(invocation.getOutput("Direction").toString()), ConnectionInfo.Status.valueOf(invocation.getOutput("Status").toString()) ); @@ -66,5 +61,4 @@ public void success(ActionInvocation invocation) { } public abstract void received(ActionInvocation invocation, ConnectionInfo connectionInfo); - } diff --git a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/GetProtocolInfo.java b/yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/GetProtocolInfo.java similarity index 62% rename from yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/GetProtocolInfo.java rename to yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/GetProtocolInfo.java index 940ca860..d6c9dddc 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/GetProtocolInfo.java +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/GetProtocolInfo.java @@ -1,22 +1,23 @@ /* - * Copyright (C) 2013 4th Line GmbH, Switzerland * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +package de.yaacc.upnp.callback.connectionmanager; -package org.fourthline.cling.support.connectionmanager.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; import org.fourthline.cling.model.action.ActionArgumentValue; import org.fourthline.cling.model.action.ActionException; import org.fourthline.cling.model.action.ActionInvocation; @@ -24,17 +25,13 @@ import org.fourthline.cling.model.types.ErrorCode; import org.fourthline.cling.support.model.ProtocolInfos; -/** - * @author Christian Bauer - */ -public abstract class GetProtocolInfo extends ActionCallback { +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; - public GetProtocolInfo(Service service) { - this(service, null); - } +public abstract class GetProtocolInfo extends ActionCallback { - protected GetProtocolInfo(Service service, ControlPoint controlPoint) { - super(new ActionInvocation(service.getAction("GetProtocolInfo")), controlPoint); + public GetProtocolInfo(Service service, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("GetProtocolInfo")), httpRequestSender); } @Override @@ -58,5 +55,4 @@ public void success(ActionInvocation invocation) { } public abstract void received(ActionInvocation actionInvocation, ProtocolInfos sinkProtocolInfos, ProtocolInfos sourceProtocolInfos); - -} \ No newline at end of file +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/PrepareForConnection.java b/yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/PrepareForConnection.java new file mode 100644 index 00000000..1b504cf8 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/connectionmanager/PrepareForConnection.java @@ -0,0 +1,54 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.connectionmanager; + +import org.fourthline.cling.model.ServiceReference; +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.support.model.ConnectionInfo; +import org.fourthline.cling.support.model.ProtocolInfo; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class PrepareForConnection extends ActionCallback { + + public PrepareForConnection(Service service, HttpRequestSender httpRequestSender, + ProtocolInfo remoteProtocolInfo, ServiceReference peerConnectionManager, + int peerConnectionID, ConnectionInfo.Direction direction) { + super(new ActionInvocation(service.getAction("PrepareForConnection")), httpRequestSender); + + getActionInvocation().setInput("RemoteProtocolInfo", remoteProtocolInfo.toString()); + getActionInvocation().setInput("PeerConnectionManager", peerConnectionManager.toString()); + getActionInvocation().setInput("PeerConnectionID", peerConnectionID); + getActionInvocation().setInput("Direction", direction.toString()); + } + + @Override + public void success(ActionInvocation invocation) { + received( + invocation, + (Integer) invocation.getOutput("ConnectionID").getValue(), + (Integer) invocation.getOutput("RcsID").getValue(), + (Integer) invocation.getOutput("AVTransportID").getValue() + ); + } + + public abstract void received(ActionInvocation invocation, int connectionID, int rcsID, int avTransportID); +} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/callback/Browse.java b/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/Browse.java similarity index 65% rename from yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/callback/Browse.java rename to yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/Browse.java index 79a4a9fd..1b41f4ca 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/callback/Browse.java +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/Browse.java @@ -1,23 +1,23 @@ /* - * Copyright (C) 2013 4th Line GmbH, Switzerland * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +package de.yaacc.upnp.callback.contentdirectory; -package org.fourthline.cling.support.contentdirectory.callback; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; import org.fourthline.cling.model.action.ActionException; import org.fourthline.cling.model.action.ActionInvocation; import org.fourthline.cling.model.meta.Service; @@ -29,11 +29,10 @@ import org.fourthline.cling.support.model.DIDLContent; import org.fourthline.cling.support.model.SortCriterion; -/** - * Invokes a "Browse" action, parses the result. - * - * @author Christian Bauer - */ +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.YaaccLogger; + public abstract class Browse extends ActionCallback { public static final String CAPS_WILDCARD = "*"; @@ -54,23 +53,16 @@ public String getDefaultMessage() { } } - - /** - * Browse with first result 0 and {@link #getDefaultMaxResults()}, filters with {@link #CAPS_WILDCARD}. - */ - public Browse(Service service, String containerId, BrowseFlag flag) { - this(service, containerId, flag, CAPS_WILDCARD, 0, null); + public Browse(Service service, String objectID, BrowseFlag flag, HttpRequestSender httpRequestSender) { + this(service, objectID, flag, CAPS_WILDCARD, 0, null, httpRequestSender); } - /** - * @param maxResults Can be null, then {@link #getDefaultMaxResults()} is used. - */ public Browse(Service service, String objectID, BrowseFlag flag, - String filter, long firstResult, Long maxResults, SortCriterion... orderBy) { + String filter, long firstResult, Long maxResults, HttpRequestSender httpRequestSender, SortCriterion... orderBy) { - super(new ActionInvocation(service.getAction("Browse"))); + super(new ActionInvocation(service.getAction("Browse")), httpRequestSender); - Log.v(getClass().getName(), "Creating browse action for object ID: " + objectID); + YaaccLogger.v(getClass().getName(), "Creating browse action for object ID: " + objectID); getActionInvocation().setInput("ObjectID", objectID); getActionInvocation().setInput("BrowseFlag", flag.toString()); @@ -88,8 +80,9 @@ public void run() { super.run(); } + @Override public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Successful browse action, reading output argument values"); + YaaccLogger.v(getClass().getName(), "Successful browse action, reading output argument values"); BrowseResult result = new BrowseResult( invocation.getOutput("Result").getValue().toString(), @@ -122,23 +115,11 @@ public void success(ActionInvocation invocation) { } } - /** - * Some media servers will crash if there is no limit on the maximum number of results. - * - * @return The default limit, 999. - */ public long getDefaultMaxResults() { return 999; } public boolean receivedRaw(ActionInvocation actionInvocation, BrowseResult browseResult) { - /* - if (log.isLoggable(Level.FINER)) { - log.finer("-------------------------------------------------------------------------------------"); - log.finer("\n" + XML.pretty(browseResult.getDidl())); - log.finer("-------------------------------------------------------------------------------------"); - } - */ return true; } diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/ContentDirectoryBrowseActionCallback.java b/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/ContentDirectoryBrowseActionCallback.java index 314d389f..a4cb433a 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/ContentDirectoryBrowseActionCallback.java +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/ContentDirectoryBrowseActionCallback.java @@ -18,18 +18,18 @@ */ package de.yaacc.upnp.callback.contentdirectory; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.action.ActionInvocation; import org.fourthline.cling.model.message.UpnpResponse; import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.support.contentdirectory.callback.Browse; import org.fourthline.cling.support.model.BrowseFlag; import org.fourthline.cling.support.model.BrowseResult; import org.fourthline.cling.support.model.DIDLContent; import org.fourthline.cling.support.model.SortCriterion; import de.yaacc.upnp.UpnpFailure; +import de.yaacc.upnp.server.http.HttpRequestSender; /** * ActionCallback for content directory browsing. @@ -44,8 +44,8 @@ public class ContentDirectoryBrowseActionCallback extends Browse { public ContentDirectoryBrowseActionCallback(Service service, String objectID, BrowseFlag flag, String filter, long firstResult, Long maxResults, ContentDirectoryBrowseResult browsingResult, - SortCriterion... orderBy) { - super(service, objectID, flag, filter, firstResult, maxResults, orderBy); + HttpRequestSender httpRequestSender, SortCriterion... orderBy) { + super(service, objectID, flag, filter, firstResult, maxResults, httpRequestSender, orderBy); this.browsingResult = browsingResult; } @@ -58,7 +58,7 @@ public ContentDirectoryBrowseActionCallback(Service service, String object public boolean receivedRaw(ActionInvocation actionInvocation, BrowseResult browseResult) { // TODO Auto-generated method stub - Log.d(this.getClass().getName(), "RAW-Result: " + browseResult.getResult()); + YaaccLogger.d(this.getClass().getName(), "RAW-Result: " + browseResult.getResult()); return super.receivedRaw(actionInvocation, browseResult); } diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/ContentDirectoryBrowseResult.java b/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/ContentDirectoryBrowseResult.java index c9ece336..aa9d1472 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/ContentDirectoryBrowseResult.java +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/ContentDirectoryBrowseResult.java @@ -18,7 +18,7 @@ */ package de.yaacc.upnp.callback.contentdirectory; -import org.fourthline.cling.support.contentdirectory.callback.Browse.Status; +import de.yaacc.upnp.callback.contentdirectory.Browse.Status; import org.fourthline.cling.support.model.DIDLContent; import de.yaacc.upnp.UpnpFailure; diff --git a/yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/callback/Search.java b/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/Search.java similarity index 73% rename from yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/callback/Search.java rename to yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/Search.java index 611d4f52..74c428b6 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/callback/Search.java +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/contentdirectory/Search.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,11 +31,8 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.support.contentdirectory.callback; +package de.yaacc.upnp.callback.contentdirectory; -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; import org.fourthline.cling.model.action.ActionException; import org.fourthline.cling.model.action.ActionInvocation; import org.fourthline.cling.model.meta.Service; @@ -28,6 +43,10 @@ import org.fourthline.cling.support.model.SearchResult; import org.fourthline.cling.support.model.SortCriterion; +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.YaaccLogger; + /** * Invokes a "Search" action, parses the result. * @@ -57,18 +76,18 @@ public String getDefaultMessage() { /** * Search with first result 0 and {@link #getDefaultMaxResults()}, filters with {@link #CAPS_WILDCARD}. */ - public Search(Service service, String containerId, String searchCriteria) { - this(service, containerId, searchCriteria, CAPS_WILDCARD, 0, null); + public Search(HttpRequestSender httpRequestSender, Service service, String containerId, String searchCriteria) { + this(httpRequestSender, service, containerId, searchCriteria, CAPS_WILDCARD, 0, null); } /** * @param maxResults Can be null, then {@link #getDefaultMaxResults()} is used. */ - public Search(Service service, String containerId, String searchCriteria, String filter, + public Search(HttpRequestSender httpRequestSender, Service service, String containerId, String searchCriteria, String filter, long firstResult, Long maxResults, SortCriterion... orderBy) { - super(new ActionInvocation(service.getAction("Search"))); + super(new ActionInvocation(service.getAction("Search")), httpRequestSender); - Log.v(getClass().getName(), "Creating browse action for container ID: " + containerId); + YaaccLogger.v(getClass().getName(), "Creating browse action for container ID: " + containerId); getActionInvocation().setInput("ContainerID", containerId); getActionInvocation().setInput("SearchCriteria", searchCriteria); @@ -89,7 +108,7 @@ public void run() { @Override public void success(ActionInvocation actionInvocation) { - Log.v(getClass().getName(), "Successful search action, reading output argument values"); + YaaccLogger.v(getClass().getName(), "Successful search action, reading output argument values"); SearchResult result = new SearchResult( actionInvocation.getOutput("Result").getValue().toString(), diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/GetMute.java b/yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/GetMute.java new file mode 100644 index 00000000..791c403f --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/GetMute.java @@ -0,0 +1,52 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.renderingcontrol; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; +import org.fourthline.cling.support.model.Channel; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class GetMute extends ActionCallback { + + public GetMute(Service service, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, httpRequestSender); + } + + public GetMute(UnsignedIntegerFourBytes instanceId, Service service, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("GetMute")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + getActionInvocation().setInput("Channel", Channel.Master.toString()); + } + + @Override + public void success(ActionInvocation invocation) { + boolean currentMute = (Boolean) invocation.getOutput("CurrentMute").getValue(); + received(invocation, currentMute); + } + + public abstract void received(ActionInvocation actionInvocation, boolean currentMute); + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/GetVolume.java b/yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/GetVolume.java similarity index 52% rename from yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/GetVolume.java rename to yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/GetVolume.java index 40590573..4f6c36f7 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/GetVolume.java +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/GetVolume.java @@ -1,56 +1,55 @@ /* - * Copyright (C) 2013 4th Line GmbH, Switzerland * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +package de.yaacc.upnp.callback.renderingcontrol; -package org.fourthline.cling.support.renderingcontrol.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; import org.fourthline.cling.model.action.ActionException; import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; import org.fourthline.cling.model.meta.Service; import org.fourthline.cling.model.types.ErrorCode; import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; import org.fourthline.cling.support.model.Channel; -import java.util.logging.Logger; +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; -/** - * - * @author Christian Bauer - */ public abstract class GetVolume extends ActionCallback { - private static Logger log = Logger.getLogger(GetVolume.class.getName()); - - public GetVolume(Service service) { - this(new UnsignedIntegerFourBytes(0), service); + public GetVolume(Service service, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, httpRequestSender); } - public GetVolume(UnsignedIntegerFourBytes instanceId, Service service) { - super(new ActionInvocation(service.getAction("GetVolume"))); + public GetVolume(UnsignedIntegerFourBytes instanceId, Service service, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("GetVolume")), httpRequestSender); getActionInvocation().setInput("InstanceID", instanceId); getActionInvocation().setInput("Channel", Channel.Master.toString()); } + @Override public void success(ActionInvocation invocation) { boolean ok = true; int currentVolume = 0; try { - currentVolume = Integer.valueOf(invocation.getOutput("CurrentVolume").getValue().toString()); // UnsignedIntegerTwoBytes... + currentVolume = Integer.valueOf(invocation.getOutput("CurrentVolume").getValue().toString()); } catch (Exception ex) { invocation.setFailure( - new ActionException(ErrorCode.ACTION_FAILED, "Can't parse ProtocolInfo response: " + ex, ex) + new ActionException(ErrorCode.ACTION_FAILED, "Can't parse volume response: " + ex, ex) ); failure(invocation, null); ok = false; @@ -60,4 +59,6 @@ public void success(ActionInvocation invocation) { public abstract void received(ActionInvocation actionInvocation, int currentVolume); -} \ No newline at end of file + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/SetMute.java b/yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/SetMute.java new file mode 100644 index 00000000..0fdccd6d --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/SetMute.java @@ -0,0 +1,48 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.renderingcontrol; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class SetMute extends ActionCallback { + + public SetMute(Service service, boolean desiredMute, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, desiredMute, httpRequestSender); + } + + public SetMute(UnsignedIntegerFourBytes instanceId, Service service, boolean desiredMute, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("SetMute")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + getActionInvocation().setInput("Channel", "Master"); + getActionInvocation().setInput("DesiredMute", desiredMute); + } + + @Override + public void success(ActionInvocation invocation) { + } + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/SetVolume.java b/yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/SetVolume.java new file mode 100644 index 00000000..94e6c021 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/callback/renderingcontrol/SetVolume.java @@ -0,0 +1,49 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.callback.renderingcontrol; + +import org.fourthline.cling.model.action.ActionInvocation; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.meta.Service; +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; +import org.fourthline.cling.model.types.UnsignedIntegerTwoBytes; + +import de.yaacc.upnp.callback.ActionCallback; +import de.yaacc.upnp.server.http.HttpRequestSender; + +public abstract class SetVolume extends ActionCallback { + + public SetVolume(Service service, long newVolume, HttpRequestSender httpRequestSender) { + this(new UnsignedIntegerFourBytes(0), service, newVolume, httpRequestSender); + } + + public SetVolume(UnsignedIntegerFourBytes instanceId, Service service, long newVolume, HttpRequestSender httpRequestSender) { + super(new ActionInvocation(service.getAction("SetVolume")), httpRequestSender); + getActionInvocation().setInput("InstanceID", instanceId); + getActionInvocation().setInput("Channel", "Master"); + getActionInvocation().setInput("DesiredVolume", new UnsignedIntegerTwoBytes(newVolume)); + } + + @Override + public void success(ActionInvocation invocation) { + } + + @Override + public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/model/YaaccItem.java b/yaacc/src/main/java/de/yaacc/upnp/model/YaaccItem.java new file mode 100644 index 00000000..2e70e60a --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/model/YaaccItem.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.model; + +import org.fourthline.cling.support.model.DIDLObject; +import org.fourthline.cling.support.model.Res; +import org.fourthline.cling.support.model.item.AudioItem; +import org.fourthline.cling.support.model.item.ImageItem; +import org.fourthline.cling.support.model.item.Item; +import org.fourthline.cling.support.model.item.VideoItem; + +import java.util.ArrayList; +import java.util.List; + +/** + * Lightweight Item wrapper that avoids Cling Property overhead. + * Converts to Cling Item only when needed for serialization. + */ +public class YaaccItem { + + private final String id; + private final String parentId; + private final String title; + private final String creator; + private final boolean restricted; + private final String clazz; // e.g., "object.item.audioItem" + protected List resources = new ArrayList<>(); + + public YaaccItem(String id, String parentId, String title, String creator, boolean restricted, String clazz) { + this.id = id; + this.parentId = parentId; + this.title = title; + this.creator = creator; + this.restricted = restricted; + this.clazz = clazz; + } + + public String getId() { + return id; + } + + public String getParentId() { + return parentId; + } + + public String getTitle() { + return title; + } + + public String getCreator() { + return creator; + } + + public boolean isRestricted() { + return restricted; + } + + public String getClazz() { + return clazz; + } + + public List getResources() { + return resources; + } + + public void addResource(YaaccRes res) { + resources.add(res); + } + + /** + * Convert to Cling Item for UPnP serialization. + */ + public Item toClingItem() { + Res[] clingResources = new Res[resources.size()]; + for (int i = 0; i < resources.size(); i++) { + clingResources[i] = resources.get(i).toClingRes(); + } + + Item item; + if (clazz.contains("audioItem")) { + item = new AudioItem(id, parentId, title, creator, clingResources); + } else if (clazz.contains("videoItem")) { + item = new VideoItem(id, parentId, title, creator, clingResources); + } else if (clazz.contains("imageItem")) { + item = new ImageItem(id, parentId, title, creator, clingResources); + } else { + item = new Item(id, parentId, title, creator, new org.fourthline.cling.support.model.DIDLObject.Class(clazz)); + } + + item.setRestricted(restricted); + return item; + } + + /** + * Create from Cling Item. + */ + public static YaaccItem fromClingItem(Item item) { + String itemClazz = item.getClazz() != null ? item.getClazz().getValue() : "object.item"; + YaaccItem yaaccItem = new YaaccItem( + item.getId(), + item.getParentID(), + item.getTitle(), + item.getCreator(), + item.isRestricted(), + itemClazz + ); + + for (org.fourthline.cling.support.model.Res res : item.getResources()) { + yaaccItem.addResource(YaaccRes.fromClingRes(res)); + } + + return yaaccItem; + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/model/YaaccMusicTrack.java b/yaacc/src/main/java/de/yaacc/upnp/model/YaaccMusicTrack.java new file mode 100644 index 00000000..2925326a --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/model/YaaccMusicTrack.java @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.model; + +import org.fourthline.cling.support.model.DIDLObject; +import org.fourthline.cling.support.model.PersonWithRole; +import org.fourthline.cling.support.model.Res; +import org.fourthline.cling.support.model.item.MusicTrack; + +import java.net.URI; +import java.util.ArrayList; +import java.util.List; + +/** + * Lightweight MusicTrack wrapper that avoids Cling Property overhead. + * Supports: artist, album, track number, genre, date, album art URI. + */ +public class YaaccMusicTrack extends YaaccItem { + + private String album; + private String artist; + private Integer trackNumber; + private String date; + private String[] genres; + private URI albumArtUri; + + public YaaccMusicTrack(String id, String parentId, String title, String creator, boolean restricted) { + super(id, parentId, title, creator, restricted, "object.item.audioItem.musicTrack"); + } + + public void setAlbum(String album) { + this.album = album; + } + + public void setArtist(String artist) { + this.artist = artist; + } + + public void setTrackNumber(Integer trackNumber) { + this.trackNumber = trackNumber; + } + + public void setDate(String date) { + this.date = date; + } + + public void setGenres(String[] genres) { + this.genres = genres; + } + + public void setAlbumArtUri(URI albumArtUri) { + this.albumArtUri = albumArtUri; + } + + public String getAlbum() { + return album; + } + + public String getArtist() { + return artist; + } + + public Integer getTrackNumber() { + return trackNumber; + } + + public String getDate() { + return date; + } + + public String[] getGenres() { + return genres; + } + + public URI getAlbumArtUri() { + return albumArtUri; + } + + /** + * Convert to Cling MusicTrack for UPnP serialization. + */ + @Override + public MusicTrack toClingItem() { + Res[] clingResources = new Res[resources.size()]; + for (int i = 0; i < resources.size(); i++) { + clingResources[i] = resources.get(i).toClingRes(); + } + + MusicTrack track = new MusicTrack( + getId(), + getParentId(), + getTitle() + (album != null ? " - (" + album + ")" : ""), + artist != null ? artist : getCreator(), + album, + artist != null ? artist : getCreator(), + clingResources + ); + + track.setRestricted(isRestricted()); + + if (albumArtUri != null) { + track.replaceFirstProperty(new DIDLObject.Property.UPNP.ALBUM_ART_URI(albumArtUri)); + } + + if (artist != null) { + track.setArtists(new PersonWithRole[]{new PersonWithRole(artist)}); + } + + if (trackNumber != null && trackNumber > 0) { + track.setOriginalTrackNumber(trackNumber); + } + + if (date != null && !date.isEmpty()) { + track.setDate(date); + } + + if (genres != null && genres.length > 0) { + track.setGenres(genres); + } + + return track; + } + + /** + * Create from Cling MusicTrack. + */ + public static YaaccMusicTrack fromClingMusicTrack(MusicTrack track) { + YaaccMusicTrack yaaccTrack = new YaaccMusicTrack( + track.getId(), + track.getParentID(), + track.getTitle(), + track.getCreator(), + track.isRestricted() + ); + + yaaccTrack.setAlbum(track.getAlbum()); + yaaccTrack.setArtist(track.getFirstArtist() != null ? track.getFirstArtist().getName() : null); + yaaccTrack.setTrackNumber(track.getOriginalTrackNumber()); + yaaccTrack.setDate(track.getDate()); + yaaccTrack.setGenres(track.getGenres()); + + URI albumArtUri = track.getFirstPropertyValue(DIDLObject.Property.UPNP.ALBUM_ART_URI.class); + if (albumArtUri != null) { + yaaccTrack.setAlbumArtUri(albumArtUri); + } + + for (org.fourthline.cling.support.model.Res res : track.getResources()) { + yaaccTrack.addResource(YaaccRes.fromClingRes(res)); + } + + return yaaccTrack; + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/model/YaaccPhoto.java b/yaacc/src/main/java/de/yaacc/upnp/model/YaaccPhoto.java new file mode 100644 index 00000000..63c8adfc --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/model/YaaccPhoto.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.model; + +import org.fourthline.cling.support.model.DIDLObject; +import org.fourthline.cling.support.model.Res; +import org.fourthline.cling.support.model.item.Photo; + +import java.net.URI; + +/** + * Lightweight Photo wrapper that avoids Cling Property overhead. + * Supports: album art URI. + */ +public class YaaccPhoto extends YaaccItem { + + private URI albumArtUri; + + public YaaccPhoto(String id, String parentId, String title, String creator, boolean restricted) { + super(id, parentId, title, creator, restricted, "object.item.imageItem.photo"); + } + + public void setAlbumArtUri(URI albumArtUri) { + this.albumArtUri = albumArtUri; + } + + public URI getAlbumArtUri() { + return albumArtUri; + } + + /** + * Convert to Cling Photo for UPnP serialization. + */ + @Override + public Photo toClingItem() { + Res[] clingResources = new Res[resources.size()]; + for (int i = 0; i < resources.size(); i++) { + clingResources[i] = resources.get(i).toClingRes(); + } + + Photo photo = new Photo( + getId(), + getParentId(), + getTitle(), + getCreator(), + "", + clingResources + ); + + photo.setRestricted(isRestricted()); + + if (albumArtUri != null) { + photo.replaceFirstProperty(new DIDLObject.Property.UPNP.ALBUM_ART_URI(albumArtUri)); + } + + return photo; + } + + /** + * Create from Cling Photo. + */ + public static YaaccPhoto fromClingPhoto(Photo photo) { + YaaccPhoto yaaccPhoto = new YaaccPhoto( + photo.getId(), + photo.getParentID(), + photo.getTitle(), + photo.getCreator(), + photo.isRestricted() + ); + + URI albumArtUri = photo.getFirstPropertyValue(DIDLObject.Property.UPNP.ALBUM_ART_URI.class); + if (albumArtUri != null) { + yaaccPhoto.setAlbumArtUri(albumArtUri); + } + + for (Res res : photo.getResources()) { + yaaccPhoto.addResource(YaaccRes.fromClingRes(res)); + } + + return yaaccPhoto; + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/model/YaaccRes.java b/yaacc/src/main/java/de/yaacc/upnp/model/YaaccRes.java new file mode 100644 index 00000000..73de5286 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/model/YaaccRes.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.model; + +import org.fourthline.cling.support.model.ProtocolInfo; +import org.fourthline.cling.support.model.Res; + +/** + * Lightweight Resource wrapper that avoids Cling overhead. + * Converts to Cling Res only when needed for serialization. + */ +public class YaaccRes { + + private final ProtocolInfo protocolInfo; + private final Long size; + private final String duration; + private final Long bitrate; + private final Long sampleFrequency; + private final Long nrAudioChannels; + private final Long bitsPerSample; + private final String resolution; + private final String value; + + public YaaccRes(ProtocolInfo protocolInfo, Long size, String duration, Long bitrate, String value) { + this.protocolInfo = protocolInfo; + this.size = size; + this.duration = duration; + this.bitrate = bitrate; + this.sampleFrequency = null; + this.nrAudioChannels = null; + this.bitsPerSample = null; + this.resolution = null; + this.value = value; + } + + public YaaccRes(ProtocolInfo protocolInfo, Long size, String duration, Long bitrate, + Long sampleFrequency, Long nrAudioChannels, Long bitsPerSample, String value) { + this.protocolInfo = protocolInfo; + this.size = size; + this.duration = duration; + this.bitrate = bitrate; + this.sampleFrequency = sampleFrequency; + this.nrAudioChannels = nrAudioChannels; + this.bitsPerSample = bitsPerSample; + this.resolution = null; + this.value = value; + } + + public YaaccRes(ProtocolInfo protocolInfo, Long size, String resolution, String value) { + this.protocolInfo = protocolInfo; + this.size = size; + this.duration = null; + this.bitrate = null; + this.sampleFrequency = null; + this.nrAudioChannels = null; + this.bitsPerSample = null; + this.resolution = resolution; + this.value = value; + } + + public ProtocolInfo getProtocolInfo() { + return protocolInfo; + } + + public Long getSize() { + return size; + } + + public String getDuration() { + return duration; + } + + public Long getBitrate() { + return bitrate; + } + + public Long getSampleFrequency() { + return sampleFrequency; + } + + public Long getNrAudioChannels() { + return nrAudioChannels; + } + + public Long getBitsPerSample() { + return bitsPerSample; + } + + public String getResolution() { + return resolution; + } + + public String getValue() { + return value; + } + + /** + * Convert to Cling Res for UPnP serialization. + */ + public Res toClingRes() { + Res res = new Res(protocolInfo, size, duration, bitrate, value); + if (sampleFrequency != null) res.setSampleFrequency(sampleFrequency); + if (nrAudioChannels != null) res.setNrAudioChannels(nrAudioChannels); + if (bitsPerSample != null) res.setBitsPerSample(bitsPerSample); + if (resolution != null) res.setResolution(resolution); + return res; + } + + /** + * Create from Cling Res. + */ + public static YaaccRes fromClingRes(Res res) { + YaaccRes yaaccRes = new YaaccRes( + res.getProtocolInfo(), + res.getSize(), + res.getDuration(), + res.getBitrate(), + res.getValue() + ); + // Note: sampleFrequency, nrAudioChannels, bitsPerSample, resolution not preserved + return yaaccRes; + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/model/message/header/ContentLengthHeader.java b/yaacc/src/main/java/de/yaacc/upnp/model/message/header/ContentLengthHeader.java index fb52b26d..a9a2d623 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/model/message/header/ContentLengthHeader.java +++ b/yaacc/src/main/java/de/yaacc/upnp/model/message/header/ContentLengthHeader.java @@ -1,4 +1,21 @@ - +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2023 Tobias Schoene www.yaacc.de * diff --git a/yaacc/src/main/java/de/yaacc/upnp/model/types/SyncOffset.java b/yaacc/src/main/java/de/yaacc/upnp/model/types/SyncOffset.java deleted file mode 100644 index ef724d5e..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/model/types/SyncOffset.java +++ /dev/null @@ -1,330 +0,0 @@ -/* - * Copyright (C) 2014 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp.model.types; - -import android.util.Log; - -import org.fourthline.cling.model.types.Datatype; -import org.fourthline.cling.model.types.InvalidValueException; - -import java.util.Locale; - -/** - * Representation of the upnp type SyncOffset. - * Format of the upnp type: - * duration ::= ['-']'P' time - * time::= HH ':' MM ':' SS'.' MilliS MicroS NanoS - * HH ::= 2DIGIT (* 00 - 23 *) - * MM ::= 2DIGIT (* 00 - 59 *) - * SS ::= 2DIGIT (* 00 - 59 *) - * MilliS ::= 3DIGIT - * MicroS ::= 3DIGIT - * NanoS ::= 3DIGIT - * - * @author Tobias Schoene (TheOpenBit) - */ -public class SyncOffset implements Datatype { - - - private int hour = 0; - private int minute = 0; - private int second = 0; - private int millis = 0; - private int micros = 0; - private int nanos = 0; - - - private boolean positive = true; - - public SyncOffset(boolean positive, int hour, int minute, int second, int millis, int micros, int nanos) { - if (!(hour >= 0 && hour <= 23)) { - throw new IllegalArgumentException("hour must fit interval 0-23, but was: " + hour); - } - if (!(minute >= 0 && minute <= 59)) { - throw new IllegalArgumentException("minute must fit interval 0-60, but was: " + minute); - } - - if (!(second >= 0 && second <= 59)) { - throw new IllegalArgumentException("second must fit interval 0-60, but was: " + second); - - } - - if (!(millis >= 0 && millis <= 999)) { - throw new IllegalArgumentException("millis must fit interval 0-60, but was: " + millis); - } - - if (!(micros >= 0 && micros <= 999)) { - throw new IllegalArgumentException("micros must fit interval 0-999, but was: " + micros); - } - - if (!(nanos >= 0 && nanos <= 999)) { - throw new IllegalArgumentException("nanos must fit interval 0-999, but was: " + nanos); - } - - this.positive = positive; - this.hour = hour; - this.minute = minute; - this.second = second; - this.millis = millis; - this.micros = micros; - this.nanos = nanos; - - - } - - public SyncOffset() { - - } - - public SyncOffset(long nanos) { - if (nanos < 0) { - positive = false; - } - hour = (int) (nanos / (60L * 60L * 1000L * 1000L * 1000L)); - long rest = nanos % (60L * 60L * 1000L * 1000L * 1000L); - minute = (int) (rest / (60L * 1000L * 1000L * 1000L)); - rest = rest % (60L * 1000L * 1000L * 1000L); - second = (int) (rest / (1000L * 1000L * 1000L)); - rest = rest % (1000L * 1000L * 1000L); - millis = (int) (rest / (1000L * 1000L)); - rest = rest % (1000L * 1000L); - micros = (int) (rest / 1000L); - this.nanos = (int) (rest % 1000L); - } - - public SyncOffset(String offset) { - if (offset == null || offset.equals("")) { - return; - } - positive = !offset.startsWith("-"); - String toBeParsed = offset; - if (positive && toBeParsed.startsWith("P")) { - toBeParsed = toBeParsed.substring(1); - } else if (toBeParsed.startsWith("-P")) { - toBeParsed = toBeParsed.substring(2); - } else { - Log.w(this.getClass().getName(), "Can't parse offset format: " + offset + " ignoring it"); - return; - } - //Minimum requiered format - if (toBeParsed.matches("^\\d{2}:\\d{2}:\\d{2}.*")) { - hour = Integer.parseInt(toBeParsed.substring(0, 2)); - if (hour < 0 || hour > 23) { - hour = 0; - Log.w(this.getClass().getName(), "Can't parse offset hour format: " + offset + " ignoring it"); - } - toBeParsed = toBeParsed.substring(3); - minute = Integer.parseInt(toBeParsed.substring(0, 2)); - if (minute < 0 || minute > 59) { - minute = 0; - Log.w(this.getClass().getName(), "Can't parse offset minute format: " + offset + " ignoring it"); - } - toBeParsed = toBeParsed.substring(3); - second = Integer.parseInt(toBeParsed.substring(0, 2)); - if (second < 0 || second > 59) { - second = 0; - Log.w(this.getClass().getName(), "Can't parse offset second format: " + offset + " ignoring it"); - } - toBeParsed = toBeParsed.substring(2); - if (toBeParsed.indexOf('.') > 0 || (toBeParsed.indexOf('.') == -1 && toBeParsed.length() > 0)) { - Log.w(this.getClass().getName(), "Can't parse offset second format: " + offset + " ignoring it"); - second = 0; - if (toBeParsed.indexOf('.') > 0) { - toBeParsed = toBeParsed.substring(toBeParsed.indexOf('.')); - } - } - } else { - Log.w(this.getClass().getName(), "Can't parse offset time format: " + offset + " ignoring it"); - return; - } - if (toBeParsed.matches("^[.]\\d{3}.*")) { - millis = Integer.parseInt(toBeParsed.substring(1, 4)); - if (millis < 0 || millis > 999) { - millis = 0; - Log.w(this.getClass().getName(), "Can't parse offset millis format: " + offset + " ignoring it"); - } - toBeParsed = toBeParsed.substring(4); - if (!toBeParsed.startsWith(" ") && toBeParsed.indexOf(' ') > -1) { - toBeParsed = toBeParsed.substring(toBeParsed.indexOf(' ')); - millis = 0; - Log.w(this.getClass().getName(), "Can't parse offset millis format: " + offset + " ignoring it"); - } - if (toBeParsed.matches("^\\s\\d{3}.*")) { - micros = Integer.parseInt(toBeParsed.substring(1, 4)); - if (micros < 0 || micros > 999) { - micros = 0; - Log.w(this.getClass().getName(), "Can't parse offset micros format: " + offset + " ignoring it"); - } - toBeParsed = toBeParsed.substring(4); - if (!toBeParsed.startsWith(" ") && toBeParsed.indexOf(' ') > -1) { - toBeParsed = toBeParsed.substring(toBeParsed.indexOf(' ')); - micros = 0; - Log.w(this.getClass().getName(), "Can't parse offset micros format: " + offset + " ignoring it"); - } - if (toBeParsed.matches("^\\s\\d{3}.*")) { - nanos = Integer.parseInt(toBeParsed.substring(1, 4)); - if (nanos < 0 || nanos > 999) { - nanos = 0; - Log.w(this.getClass().getName(), "Can't parse offset nanos format: " + offset + " ignoring it"); - } - toBeParsed = toBeParsed.substring(4); - if (toBeParsed.length() > 0) { - nanos = 0; - Log.w(this.getClass().getName(), "Can't parse offset nanos format: " + offset + " ignoring it"); - } - } else { - Log.w(this.getClass().getName(), "Can't parse offset nanos : " + offset + " ignoring it"); - } - } else { - Log.w(this.getClass().getName(), "Can't parse offset mircos : " + offset + " ignoring it"); - } - } else { - Log.w(this.getClass().getName(), "Can't parse offset sub second format: " + offset + " ignoring it"); - } - - } - - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append(positive ? "" : "-"); - sb.append("P"); - sb.append(String.format(Locale.ENGLISH, "%02d", hour)); - sb.append(":"); - sb.append(String.format(Locale.ENGLISH, "%02d", minute)); - sb.append(":"); - sb.append(String.format(Locale.ENGLISH, "%02d", second)); - sb.append("."); - sb.append(String.format(Locale.ENGLISH, "%03d", millis)); - sb.append(" "); - sb.append(String.format(Locale.ENGLISH, "%03d", micros)); - sb.append(" "); - sb.append(String.format(Locale.ENGLISH, "%03d", nanos)); - return sb.toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - SyncOffset that = (SyncOffset) o; - - if (hour != that.hour) return false; - if (positive != that.positive) return false; - if (micros != that.micros) return false; - if (millis != that.millis) return false; - if (minute != that.minute) return false; - if (nanos != that.nanos) return false; - if (second != that.second) return false; - - return true; - } - - @Override - public int hashCode() { - int result = hour; - result = 31 * result + minute; - result = 31 * result + second; - result = 31 * result + millis; - result = 31 * result + micros; - result = 31 * result + nanos; - result = 31 * result + (positive ? 1 : 0); - return result; - } - - public int getHour() { - return hour; - } - - public int getMinute() { - return minute; - } - - public int getSecond() { - return second; - } - - public int getMillis() { - return millis; - } - - public int getMicros() { - return micros; - } - - public int getNanos() { - return nanos; - } - - - public boolean isPositive() { - return positive; - } - - - public SyncOffset add(SyncOffset syncOffset) { - return new SyncOffset((isPositive() ? 1 : -1) * toNanos() + (syncOffset.isPositive() ? 1 : -1) * syncOffset.toNanos()); - } - - public long toNanos() { - return getNanos() + - getMicros() * 1000L + - getMillis() * 1000L * 1000L + - getSecond() * 1000L * 1000L * 1000L + - getMinute() * 60L * 1000L * 1000L * 1000L + - getHour() * 60L * 60L * 1000L * 1000L * 1000L; - } - - - @Override - public boolean isHandlingJavaType(Class aClass) { - if (aClass == null) { - return false; - } - return aClass.isAssignableFrom(getClass()); - } - - @Override - public Builtin getBuiltin() { - return null; - } - - @Override - public boolean isValid(SyncOffset o) { - return (o != null); - //TODO to be implemented - - } - - @Override - public String getString(SyncOffset o) throws InvalidValueException { - - return (o == null) ? "" : o.toString(); - } - - @Override - public SyncOffset valueOf(String s) throws InvalidValueException { - return new SyncOffset(s); - } - - @Override - public String getDisplayString() { - return toString(); - } -} - diff --git a/yaacc/src/main/java/de/yaacc/upnp/protocol/ProtocolCreationException.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/ProtocolCreationException.java new file mode 100644 index 00000000..89d1a9c8 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/ProtocolCreationException.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.protocol; + +public class ProtocolCreationException extends Exception { + + public ProtocolCreationException(String s) { + super(s); + } + + public ProtocolCreationException(String s, Throwable throwable) { + super(s, throwable); + } +} diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/ReceivingAsync.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/ReceivingAsync.java similarity index 66% rename from yaacc/src/main/java/org/fourthline/cling/protocol/ReceivingAsync.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/ReceivingAsync.java index 51f21183..8d9cbfa7 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/ReceivingAsync.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/ReceivingAsync.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,42 +31,31 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol; - -import android.util.Log; +package de.yaacc.upnp.protocol; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.message.UpnpMessage; import org.fourthline.cling.model.message.header.UpnpHeader; -import org.fourthline.cling.transport.RouterException; -import org.seamless.util.Exceptions; + +import java.io.IOException; + +import de.yaacc.util.Exceptions; +import de.yaacc.util.YaaccLogger; /** * Supertype for all asynchronously executing protocols, handling reception of UPnP messages. - *

- * After instantiation by the {@link ProtocolFactory}, this protocol run()s and - * calls its own {@link #waitBeforeExecution()} method. By default, the protocol does not wait - * before then proceeding with {@link #execute()}. - *

* * @param The type of UPnP message handled by this protocol. * @author Christian Bauer */ public abstract class ReceivingAsync implements Runnable { - private final UpnpService upnpService; private M inputMessage; - protected ReceivingAsync(UpnpService upnpService, M inputMessage) { - this.upnpService = upnpService; + protected ReceivingAsync(M inputMessage) { this.inputMessage = inputMessage; } - public UpnpService getUpnpService() { - return upnpService; - } - public M getInputMessage() { return inputMessage; } @@ -58,7 +65,7 @@ public void run() { try { proceed = waitBeforeExecution(); } catch (InterruptedException ex) { - Log.v(getClass().getName(), "Protocol wait before execution interrupted (on shutdown?): " + getClass().getSimpleName()); + YaaccLogger.v(getClass().getName(), "Protocol wait before execution interrupted (on shutdown?): " + getClass().getSimpleName()); proceed = false; } @@ -68,7 +75,7 @@ public void run() { } catch (Exception ex) { Throwable cause = Exceptions.unwrap(ex); if (cause instanceof InterruptedException) { - Log.v(getClass().getName(), "Interrupted protocol '" + getClass().getSimpleName() + "': " + ex, cause); + YaaccLogger.v(getClass().getName(), "Interrupted protocol '" + getClass().getSimpleName() + "': " + ex, cause); } else { throw new RuntimeException( "Fatal error while executing protocol '" + getClass().getSimpleName() + "': " + ex, ex @@ -89,7 +96,7 @@ protected boolean waitBeforeExecution() throws InterruptedException { return true; } - protected abstract void execute() throws RouterException; + protected abstract void execute() throws IOException; protected H getFirstHeader(UpnpHeader.Type headerType, Class subtype) { return getInputMessage().getHeaders().getFirstHeader(headerType, subtype); diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/ReceivingSync.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/ReceivingSync.java similarity index 68% rename from yaacc/src/main/java/org/fourthline/cling/protocol/ReceivingSync.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/ReceivingSync.java index 8a06fbb8..f6c20cfb 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/ReceivingSync.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/ReceivingSync.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,26 +31,21 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol; - -import android.util.Log; +package de.yaacc.upnp.protocol; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.message.StreamRequestMessage; import org.fourthline.cling.model.message.StreamResponseMessage; import org.fourthline.cling.model.profile.RemoteClientInfo; -import org.fourthline.cling.transport.RouterException; + +import java.io.IOException; + +import de.yaacc.util.YaaccLogger; /** * Supertype for all synchronously executing protocols, handling reception of UPnP messages and return a response. *

- * After instantiation by the {@link ProtocolFactory}, this protocol run()s and - * calls its own {@link #waitBeforeExecution()} method. By default, the protocol does not wait - * before then proceeding with {@link #executeSync()}. - *

- *

* The returned response will be available to the client of this protocol. The - * client will then call either {@link #responseSent(org.fourthline.cling.model.message.StreamResponseMessage)} + * client will then call either {@link #responseSent(StreamResponseMessage)} * or {@link #responseException(Throwable)}, depending on whether the response was successfully * delivered. The protocol can override these methods to decide if the whole procedure it is * implementing was successful or not, including not only creation but also delivery of the response. @@ -48,8 +61,9 @@ public abstract class ReceivingSync 0) { - Log.v(getClass().getName(), "Setting extra headers on response message: " + getRemoteClientInfo().getExtraResponseHeaders().size()); + YaaccLogger.v(getClass().getName(), "Setting extra headers on response message: " + getRemoteClientInfo().getExtraResponseHeaders().size()); outputMessage.getHeaders().putAll(getRemoteClientInfo().getExtraResponseHeaders()); } } - protected abstract OUT executeSync() throws RouterException; + protected abstract OUT executeSync() throws IOException; /** * Called by the client of this protocol after the returned response has been successfully delivered. diff --git a/yaacc/src/main/java/de/yaacc/upnp/protocol/RetrieveRemoteDescriptors.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/RetrieveRemoteDescriptors.java new file mode 100644 index 00000000..2d8f076b --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/RetrieveRemoteDescriptors.java @@ -0,0 +1,382 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +/* + * Copyright (C) 2013 4th Line GmbH, Switzerland + * + * The contents of this file are subject to the terms of either the GNU + * Lesser General Public License Version 2 or later ("LGPL") or the + * Common Development and Distribution License Version 1 or later + * ("CDDL") (collectively, the "License"). You may not use this file + * except in compliance with the License. See LICENSE.txt for more + * information. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ + +package de.yaacc.upnp.protocol; + +import org.fourthline.cling.binding.xml.DescriptorBindingException; +import org.fourthline.cling.binding.xml.UDA10DeviceDescriptorBinderImpl; +import org.fourthline.cling.binding.xml.UDA10ServiceDescriptorBinderImpl; +import org.fourthline.cling.model.ValidationError; +import org.fourthline.cling.model.ValidationException; +import org.fourthline.cling.model.message.StreamRequestMessage; +import org.fourthline.cling.model.message.StreamResponseMessage; +import org.fourthline.cling.model.message.UpnpRequest; +import org.fourthline.cling.model.meta.Icon; +import org.fourthline.cling.model.meta.RemoteDevice; +import org.fourthline.cling.model.meta.RemoteService; +import org.fourthline.cling.model.types.ServiceType; +import org.fourthline.cling.model.types.UDN; + +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +import de.yaacc.upnp.registry.RegistrationException; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.upnp.server.YaaccUpnpServerService; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.Exceptions; +import de.yaacc.util.YaaccLogger; + +/** + * Retrieves all remote device XML descriptors, parses them, creates an immutable device and service metadata graph. + *

+ * This implementation encapsulates all steps which are necessary to create a fully usable and populated + * device metadata graph of a particular UPnP device. It starts with an unhydrated and typically just + * discovered {@link RemoteDevice}, the only property that has to be available is + * its {@link org.fourthline.cling.model.meta.RemoteDeviceIdentity}. + *

+ *

+ * This protocol implementation will then retrieve the device's XML descriptor, parse it, and retrieve and + * parse all service descriptors until all device and service metadata has been retrieved. The fully + * hydrated device is then added to the {@link de.yaacc.upnp.registry.Registry}. + *

+ *

+ * Any descriptor retrieval, parsing, or validation error of the metadata will abort this protocol + * with a warning message in the log. + *

+ * + * @author Christian Bauer + */ +public class RetrieveRemoteDescriptors implements Runnable { + + + private final Registry registry; + private final HttpRequestSender httpRequestSender; + + private final UDA10DeviceDescriptorBinderImpl uda10DeviceDescriptorBinder = new UDA10DeviceDescriptorBinderImpl(); + private final UDA10ServiceDescriptorBinderImpl uda10ServiceDescriptorBinder = new UDA10ServiceDescriptorBinderImpl(); + private RemoteDevice rd; + + private static final Set activeRetrievals = new CopyOnWriteArraySet(); + protected List errorsAlreadyLogged = new ArrayList<>(); + + public RetrieveRemoteDescriptors(Registry registry, HttpRequestSender httpRequestSender, RemoteDevice rd) { + this.registry = registry; + this.httpRequestSender = httpRequestSender; + this.rd = rd; + } + + + public void run() { + + URL deviceURL = rd.getIdentity().getDescriptorURL(); + + // Performance optimization, try to avoid concurrent GET requests for device descriptor, + // if we retrieve it once, we have the hydrated device. There is no different outcome + // processing this several times concurrently. + + if (activeRetrievals.contains(deviceURL)) { + YaaccLogger.v(getClass().getName(), "Exiting early, active retrieval for URL already in progress: " + deviceURL); + return; + } + + // Exit if it has been discovered already, could be we have been waiting in the executor queue too long + if (registry.getRemoteDevice(rd.getIdentity().getUdn(), true) != null) { + YaaccLogger.v(getClass().getName(), "Exiting early, already discovered: " + deviceURL); + return; + } + + try { + activeRetrievals.add(deviceURL); + describe(); + } catch (IOException ex) { + YaaccLogger.w(getClass().getName(), + "Descriptor retrieval failed: " + deviceURL, + ex + ); + } finally { + activeRetrievals.remove(deviceURL); + } + } + + protected void describe() throws IOException { + + // All of the following is a very expensive and time consuming procedure, thanks to the + // braindead design of UPnP. Several GET requests, several descriptors, several XML parsing + // steps - all of this could be done with one and it wouldn't make a difference. So every + // call of this method has to be really necessary and rare. + + + StreamRequestMessage deviceDescRetrievalMsg; + StreamResponseMessage deviceDescMsg; + + try { + + deviceDescRetrievalMsg = + new StreamRequestMessage(UpnpRequest.Method.GET, rd.getIdentity().getDescriptorURL()); + + + YaaccLogger.v(getClass().getName(), "Sending device descriptor retrieval message: " + deviceDescRetrievalMsg); + deviceDescMsg = httpRequestSender.send(deviceDescRetrievalMsg); + + } catch (IllegalArgumentException | IOException ex) { + // UpnpRequest constructor can throw IllegalArgumentException on invalid URI + // IllegalArgumentException can also be thrown by Apache HttpClient on blank URI in send() + YaaccLogger.w(getClass().getName(), + "Device descriptor retrieval failed: " + + rd.getIdentity().getDescriptorURL() + + ", possibly invalid URL: ", ex); + return; + } + + if (deviceDescMsg == null) { + YaaccLogger.w(getClass().getName(), + "Device descriptor retrieval failed, no response: " + rd.getIdentity().getDescriptorURL() + ); + return; + } + + if (deviceDescMsg.getOperation().isFailed()) { + YaaccLogger.w(getClass().getName(), + "Device descriptor retrieval failed: " + + rd.getIdentity().getDescriptorURL() + + ", " + + deviceDescMsg.getOperation().getResponseDetails() + ); + return; + } + + if (!deviceDescMsg.isContentTypeTextUDA()) { + YaaccLogger.v(getClass().getName(), + "Received device descriptor without or with invalid Content-Type: " + + rd.getIdentity().getDescriptorURL()); + // We continue despite the invalid UPnP message because we can still hope to convert the content + } + + String descriptorContent = deviceDescMsg.getBodyString(); + if (descriptorContent == null || descriptorContent.length() == 0) { + YaaccLogger.w(getClass().getName(), "Received empty device descriptor:" + rd.getIdentity().getDescriptorURL()); + return; + } + + YaaccLogger.v(getClass().getName(), "Received root device descriptor: " + deviceDescMsg); + describe(descriptorContent); + } + + protected void describe(String descriptorXML) throws IOException { + + boolean notifiedStart = false; + RemoteDevice describedDevice = null; + try { + + describedDevice = uda10DeviceDescriptorBinder.describe( + rd, + descriptorXML + ); + + YaaccLogger.v(getClass().getName(), "Remote device described (without services) notifying listeners: " + describedDevice); + notifiedStart = registry.notifyDiscoveryStart(describedDevice); + + YaaccLogger.v(getClass().getName(), "Hydrating described device's services: " + describedDevice); + RemoteDevice hydratedDevice = describeServices(describedDevice); + if (hydratedDevice == null) { + if (!errorsAlreadyLogged.contains(rd.getIdentity().getUdn())) { + errorsAlreadyLogged.add(rd.getIdentity().getUdn()); + YaaccLogger.w(getClass().getName(), "Device service description failed: " + rd); + } + if (notifiedStart) + registry.notifyDiscoveryFailure( + describedDevice, + new DescriptorBindingException("Device service description failed: " + rd) + ); + return; + } else { + YaaccLogger.v(getClass().getName(), "Adding fully hydrated remote device to registry: " + hydratedDevice); + // The registry will do the right thing: A new root device is going to be added, if it's + // already present or we just received the descriptor again (because we got an embedded + // devices' notification), it will simply update the expiration timestamp of the root + // device. + registry.addDevice(hydratedDevice); + } + + } catch (ValidationException ex) { + // Avoid error log spam each time device is discovered, errors are logged once per device. + if (!errorsAlreadyLogged.contains(rd.getIdentity().getUdn())) { + errorsAlreadyLogged.add(rd.getIdentity().getUdn()); + YaaccLogger.w(getClass().getName(), "Could not validate device model: " + rd); + for (ValidationError validationError : ex.getErrors()) { + YaaccLogger.w(getClass().getName(), validationError.toString()); + } + if (describedDevice != null && notifiedStart) + registry.notifyDiscoveryFailure(describedDevice, ex); + } + + } catch (DescriptorBindingException ex) { + YaaccLogger.w(getClass().getName(), "Could not hydrate device or its services from descriptor: " + rd); + YaaccLogger.w(getClass().getName(), "Cause was: " + Exceptions.unwrap(ex)); + if (describedDevice != null && notifiedStart) + registry.notifyDiscoveryFailure(describedDevice, ex); + + } catch (RegistrationException ex) { + YaaccLogger.w(getClass().getName(), "Adding hydrated device to registry failed: " + rd); + YaaccLogger.w(getClass().getName(), "Cause was: " + ex.toString()); + if (describedDevice != null && notifiedStart) + registry.notifyDiscoveryFailure(describedDevice, ex); + } + } + + protected RemoteDevice describeServices(RemoteDevice currentDevice) + throws IOException, DescriptorBindingException, ValidationException { + + List describedServices = new ArrayList<>(); + if (currentDevice.hasServices()) { + List filteredServices = filterExclusiveServices(currentDevice.getServices()); + for (RemoteService service : filteredServices) { + RemoteService svc = describeService(service); + // Skip invalid services (yes, we can continue with only some services available) + if (svc != null) + describedServices.add(svc); + else + YaaccLogger.w(getClass().getName(), "Skipping invalid service '" + service + "' of: " + currentDevice); + } + } + + List describedEmbeddedDevices = new ArrayList<>(); + if (currentDevice.hasEmbeddedDevices()) { + for (RemoteDevice embeddedDevice : currentDevice.getEmbeddedDevices()) { + // Skip invalid embedded device + if (embeddedDevice == null) + continue; + RemoteDevice describedEmbeddedDevice = describeServices(embeddedDevice); + // Skip invalid embedded services + if (describedEmbeddedDevice != null) + describedEmbeddedDevices.add(describedEmbeddedDevice); + } + } + + Icon[] iconDupes = new Icon[currentDevice.getIcons().length]; + for (int i = 0; i < currentDevice.getIcons().length; i++) { + Icon icon = currentDevice.getIcons()[i]; + iconDupes[i] = icon.deepCopy(); + } + + // Yes, we create a completely new immutable graph here + return currentDevice.newInstance( + currentDevice.getIdentity().getUdn(), + currentDevice.getVersion(), + currentDevice.getType(), + currentDevice.getDetails(), + iconDupes, + currentDevice.toServiceArray(describedServices), + describedEmbeddedDevices + ); + } + + protected RemoteService describeService(RemoteService service) + throws IOException, DescriptorBindingException, ValidationException { + + URL descriptorURL; + try { + descriptorURL = service.getDevice().normalizeURI(service.getDescriptorURI()); + } catch (IllegalArgumentException e) { + YaaccLogger.w(getClass().getName(), "Could not normalize service descriptor URL: " + service.getDescriptorURI()); + return null; + } + + StreamRequestMessage serviceDescRetrievalMsg = new StreamRequestMessage(UpnpRequest.Method.GET, descriptorURL); + + + YaaccLogger.v(getClass().getName(), "Sending service descriptor retrieval message: " + serviceDescRetrievalMsg); + StreamResponseMessage serviceDescMsg = null; + try { + serviceDescMsg = httpRequestSender.send(serviceDescRetrievalMsg); + } catch (IOException e) { + YaaccLogger.w(getClass().getName(), "Could not retrieve service descriptor, got exception: " + service, e); + return null; + } + + if (serviceDescMsg == null) { + YaaccLogger.w(getClass().getName(), "Could not retrieve service descriptor, no response: " + service); + return null; + } + + if (serviceDescMsg.getOperation().isFailed()) { + YaaccLogger.w(getClass().getName(), "Service descriptor retrieval failed: " + + descriptorURL + + ", " + + serviceDescMsg.getOperation().getResponseDetails()); + return null; + } + + if (!serviceDescMsg.isContentTypeTextUDA()) { + YaaccLogger.v(getClass().getName(), "Received service descriptor without or with invalid Content-Type: " + descriptorURL); + // We continue despite the invalid UPnP message because we can still hope to convert the content + } + + String descriptorContent = serviceDescMsg.getBodyString(); + if (descriptorContent == null || descriptorContent.length() == 0) { + YaaccLogger.w(getClass().getName(), "Received empty service descriptor:" + descriptorURL); + return null; + } + + YaaccLogger.v(getClass().getName(), "Received service descriptor, hydrating service model: " + serviceDescMsg); + + return uda10ServiceDescriptorBinder.describe(service, descriptorContent); + } + + protected List filterExclusiveServices(RemoteService[] services) { + ServiceType[] exclusiveTypes = YaaccUpnpServerService.EXCLUSIVE_SERVER_TYPES; + + if (exclusiveTypes == null || exclusiveTypes.length == 0) + return Arrays.asList(services); + + List exclusiveServices = new ArrayList<>(); + for (RemoteService discoveredService : services) { + for (ServiceType exclusiveType : exclusiveTypes) { + if (discoveredService.getServiceType().implementsVersion(exclusiveType)) { + YaaccLogger.v(getClass().getName(), "Including exclusive service: " + discoveredService); + exclusiveServices.add(discoveredService); + } else { + YaaccLogger.v(getClass().getName(), "Excluding unwanted service: " + exclusiveType); + } + } + } + return exclusiveServices; + } + +} diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/SendingAsync.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/SendingAsync.java similarity index 50% rename from yaacc/src/main/java/org/fourthline/cling/protocol/SendingAsync.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/SendingAsync.java index 1b62bb64..3131ff70 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/SendingAsync.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/SendingAsync.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,22 +31,17 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol; +package de.yaacc.upnp.protocol; -import android.util.Log; +import java.io.IOException; -import org.fourthline.cling.UpnpService; -import org.fourthline.cling.transport.RouterException; -import org.seamless.util.Exceptions; +import de.yaacc.util.YaaccLogger; /** * Supertype for all synchronously executing protocols, sending UPnP messages. + * *

- * After instantiation by the {@link ProtocolFactory}, this protocol run()s and - * calls its {@link #execute()} method. - *

- *

- * A {@link RouterException} during execution will be wrapped in a fatal RuntimeException, + * A {@link IOException} during execution will be wrapped in a fatal RuntimeException, * unless its cause is an InterruptedException, in which case an INFO message will be logged. *

* @@ -37,23 +50,20 @@ public abstract class SendingAsync implements Runnable { - private final UpnpService upnpService; - - protected SendingAsync(UpnpService upnpService) { - this.upnpService = upnpService; + protected SendingAsync() { } - public UpnpService getUpnpService() { - return upnpService; - } public void run() { try { execute(); } catch (Exception ex) { - Throwable cause = Exceptions.unwrap(ex); + Throwable cause = ex; + for (Throwable current = ex; current != null; current = current.getCause()) { + cause = current; + } if (cause instanceof InterruptedException) { - Log.v(getClass().getName(), "Interrupted protocol '" + getClass().getSimpleName() + "': " + ex, cause); + YaaccLogger.v(getClass().getName(), "Interrupted protocol '" + getClass().getSimpleName() + "': " + ex, cause); } else { throw new RuntimeException( "Fatal error while executing protocol '" + getClass().getSimpleName() + "': " + ex, ex @@ -62,7 +72,7 @@ public void run() { } } - protected abstract void execute() throws RouterException; + protected abstract void execute() throws IOException; @Override public String toString() { diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/SendingSync.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/SendingSync.java similarity index 58% rename from yaacc/src/main/java/org/fourthline/cling/protocol/SendingSync.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/SendingSync.java index 251a3aab..11ef0f65 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/SendingSync.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/SendingSync.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,23 +31,19 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol; +package de.yaacc.upnp.protocol; + import org.fourthline.cling.model.message.StreamRequestMessage; import org.fourthline.cling.model.message.StreamResponseMessage; -import org.fourthline.cling.UpnpService; -import org.fourthline.cling.transport.RouterException; + +import java.io.IOException; /** * Supertype for all synchronously executing protocols, sending UPnP messages. - *

- * After instantiation by the {@link ProtocolFactory}, this protocol run()s and - * calls its {@link #executeSync()} method. - *

* - * @param The type of request UPnP message send by this protocol. + * @param The type of request UPnP message send by this protocol. * @param The type of response UPnP message expected by this protocol. - * * @author Christian Bauer */ public abstract class SendingSync extends SendingAsync { @@ -37,8 +51,7 @@ public abstract class SendingSync incomingRequest) { - return new ReceivingNotification(getUpnpService(), incomingRequest); + return new ReceivingNotification(registry, httpRequestSender, incomingRequest); } protected ReceivingAsync createReceivingSearch(IncomingDatagramMessage incomingRequest) { - return new ReceivingSearch(getUpnpService(), incomingRequest); + return new ReceivingSearch(context, registry, udpTransiver, incomingRequest); } protected ReceivingAsync createReceivingSearchResponse(IncomingDatagramMessage incomingResponse) { - return new ReceivingSearchResponse(getUpnpService(), incomingResponse); + return new ReceivingSearchResponse(registry, httpRequestSender, incomingResponse); } - // DO NOT USE THE PARSED/TYPED MSG HEADERS! THIS WOULD DEFEAT THE PURPOSE OF THIS OPTIMIZATION! protected boolean isByeBye(IncomingDatagramMessage message) { String ntsHeader = message.getHeaders().getFirstHeader(UpnpHeader.Type.NTS.getHttpName()); @@ -134,7 +134,7 @@ protected boolean isSsdpAlive(IncomingDatagramMessage message) { } protected boolean isSupportedServiceAdvertisement(IncomingDatagramMessage message) { - ServiceType[] exclusiveServiceTypes = getUpnpService().getConfiguration().getExclusiveServiceTypes(); + ServiceType[] exclusiveServiceTypes = YaaccUpnpServerService.EXCLUSIVE_SERVER_TYPES; if (exclusiveServiceTypes == null) return false; // Discovery is disabled if (exclusiveServiceTypes.length == 0) return true; // Any advertisement is fine @@ -148,25 +148,30 @@ protected boolean isSupportedServiceAdvertisement(IncomingDatagramMessage messag return true; } } catch (InvalidValueException ex) { - Log.v(getClass().getName(), "Not a named service type header value: " + usnHeader); + YaaccLogger.v(getClass().getName(), "Not a named service type header value: " + usnHeader); } - Log.v(getClass().getName(), "Service advertisement not supported, dropping it: " + usnHeader); + YaaccLogger.v(getClass().getName(), "Service advertisement not supported, dropping it: " + usnHeader); return false; } + + public Namespace getNamespace() { + return NAMESPACE; + } + public ReceivingSync createReceivingSync(StreamRequestMessage message) throws ProtocolCreationException { - Log.v(getClass().getName(), "Creating protocol for incoming synchronous: " + message); + YaaccLogger.v(getClass().getName(), "Creating protocol for incoming synchronous: " + message); if (message.getOperation().getMethod().equals(UpnpRequest.Method.GET)) { return createReceivingRetrieval(message); - } else if (getUpnpService().getConfiguration().getNamespace().isControlPath(message.getUri())) { + } else if (getNamespace().isControlPath(message.getUri())) { if (message.getOperation().getMethod().equals(UpnpRequest.Method.POST)) return createReceivingAction(message); - } else if (getUpnpService().getConfiguration().getNamespace().isEventSubscriptionPath(message.getUri())) { + } else if (getNamespace().isEventSubscriptionPath(message.getUri())) { if (message.getOperation().getMethod().equals(UpnpRequest.Method.SUBSCRIBE)) { return createReceivingSubscribe(message); @@ -174,7 +179,7 @@ public ReceivingSync createReceivingSync(StreamRequestMessage message) throws Pr return createReceivingUnsubscribe(message); } - } else if (getUpnpService().getConfiguration().getNamespace().isEventCallbackPath(message.getUri())) { + } else if (getNamespace().isEventCallbackPath(message.getUri())) { if (message.getOperation().getMethod().equals(UpnpRequest.Method.NOTIFY)) return createReceivingEvent(message); @@ -186,14 +191,14 @@ public ReceivingSync createReceivingSync(StreamRequestMessage message) throws Pr // TODO: UPNP VIOLATION: Yamaha does the same // /dev/9ab0c000-f668-11de-9976-00a0de870fd4/svc/upnp-org/RenderingControl/event/cb> activeStreamServers = - getUpnpService().getRouter().getActiveStreamServers( - subscription.getService().getDevice().getIdentity().getDiscoveredOnLocalAddress() - ); - return new SendingSubscribe(getUpnpService(), subscription, activeStreamServers); - } catch (RouterException ex) { - throw new ProtocolCreationException( - "Failed to obtain local stream servers (for event callback URL creation) from router", - ex - ); - } + return new SendingSubscribe(registry, httpRequestSender, subscription, InterfaceResolutionHelper.getNetworkAddress(context)); } + public SendingRenewal createSendingRenewal(RemoteGENASubscription subscription) { - return new SendingRenewal(getUpnpService(), subscription); + return new SendingRenewal(registry, httpRequestSender, subscription); } + public SendingUnsubscribe createSendingUnsubscribe(RemoteGENASubscription subscription) { - return new SendingUnsubscribe(getUpnpService(), subscription); + return new SendingUnsubscribe(registry, httpRequestSender, subscription); } + public SendingEvent createSendingEvent(LocalGENASubscription subscription) { - return new SendingEvent(getUpnpService(), subscription); + return new SendingEvent(httpRequestSender, subscription); } + protected ReceivingRetrieval createReceivingRetrieval(StreamRequestMessage message) { - return new ReceivingRetrieval(getUpnpService(), message); + return new ReceivingRetrieval(registry, message); } protected ReceivingAction createReceivingAction(StreamRequestMessage message) { - return new ReceivingAction(getUpnpService(), message); + return new ReceivingAction(registry, message); } protected ReceivingSubscribe createReceivingSubscribe(StreamRequestMessage message) { - return new ReceivingSubscribe(getUpnpService(), message); + return new ReceivingSubscribe(registry, httpRequestSender, message); } protected ReceivingUnsubscribe createReceivingUnsubscribe(StreamRequestMessage message) { - return new ReceivingUnsubscribe(getUpnpService(), message); + return new ReceivingUnsubscribe(registry, message); } protected ReceivingEvent createReceivingEvent(StreamRequestMessage message) { - return new ReceivingEvent(getUpnpService(), message); + return new ReceivingEvent(registry, message); } } diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/async/ReceivingNotification.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/ReceivingNotification.java similarity index 56% rename from yaacc/src/main/java/org/fourthline/cling/protocol/async/ReceivingNotification.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/async/ReceivingNotification.java index 8aea954b..9bf16070 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/async/ReceivingNotification.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/ReceivingNotification.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,11 +31,8 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.async; - -import android.util.Log; +package de.yaacc.upnp.protocol.async; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.ValidationError; import org.fourthline.cling.model.ValidationException; import org.fourthline.cling.model.message.IncomingDatagramMessage; @@ -26,9 +41,14 @@ import org.fourthline.cling.model.meta.RemoteDevice; import org.fourthline.cling.model.meta.RemoteDeviceIdentity; import org.fourthline.cling.model.types.UDN; -import org.fourthline.cling.protocol.ReceivingAsync; -import org.fourthline.cling.protocol.RetrieveRemoteDescriptors; -import org.fourthline.cling.transport.RouterException; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; + +import de.yaacc.upnp.protocol.ReceivingAsync; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.YaaccLogger; /** * Handles reception of notification messages. @@ -37,7 +57,7 @@ *

*

* If an ALIVE message has been received, a new background process will be started - * running {@link org.fourthline.cling.protocol.RetrieveRemoteDescriptors}. + * running {@link RetrieveRemoteDescriptors}. *

*

* If a BYEBYE message has been received, the device will be removed from the registry @@ -71,68 +91,75 @@ */ public class ReceivingNotification extends ReceivingAsync { + private final ExecutorService executorService; + private Registry registry; + private HttpRequestSender httpRequestSender; + - public ReceivingNotification(UpnpService upnpService, IncomingDatagramMessage inputMessage) { - super(upnpService, new IncomingNotificationRequest(inputMessage)); + public ReceivingNotification(Registry registry, HttpRequestSender httpRequestSender, IncomingDatagramMessage inputMessage) { + super(new IncomingNotificationRequest(inputMessage)); + this.registry = registry; + executorService = registry.getExecutorService(); + this.httpRequestSender = httpRequestSender; } - protected void execute() throws RouterException { + protected void execute() throws IOException { UDN udn = getInputMessage().getUDN(); if (udn == null) { - Log.v(getClass().getName(), "Ignoring notification message without UDN: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Ignoring notification message without UDN: " + getInputMessage()); return; } RemoteDeviceIdentity rdIdentity = new RemoteDeviceIdentity(getInputMessage()); - Log.v(getClass().getName(), "Received device notification: " + rdIdentity); + YaaccLogger.v(getClass().getName(), "Received device notification: " + rdIdentity); RemoteDevice rd; try { rd = new RemoteDevice(rdIdentity); } catch (ValidationException ex) { - Log.w(getClass().getName(), "Validation errors of device during discovery: " + rdIdentity); + YaaccLogger.w(getClass().getName(), "Validation errors of device during discovery: " + rdIdentity); for (ValidationError validationError : ex.getErrors()) { - Log.w(getClass().getName(), validationError.toString()); + YaaccLogger.w(getClass().getName(), validationError.toString()); } return; } if (getInputMessage().isAliveMessage()) { - Log.v(getClass().getName(), "Received device ALIVE advertisement, descriptor location is: " + rdIdentity.getDescriptorURL()); + YaaccLogger.v(getClass().getName(), "Received device ALIVE advertisement, descriptor location is: " + rdIdentity.getDescriptorURL()); if (rdIdentity.getDescriptorURL() == null) { - Log.v(getClass().getName(), "Ignoring message without location URL header: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Ignoring message without location URL header: " + getInputMessage()); return; } if (rdIdentity.getMaxAgeSeconds() == null) { - Log.v(getClass().getName(), "Ignoring message without max-age header: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Ignoring message without max-age header: " + getInputMessage()); return; } - if (getUpnpService().getRegistry().update(rdIdentity)) { - Log.v(getClass().getName(), "Remote device was already known: " + udn); + if (registry.update(rdIdentity)) { + YaaccLogger.v(getClass().getName(), "Remote device was already known: " + udn); return; } // Unfortunately, we always have to retrieve the descriptor because at this point we // have no idea if it's a root or embedded device - getUpnpService().getConfiguration().getAsyncProtocolExecutor().execute( - new RetrieveRemoteDescriptors(getUpnpService(), rd) + executorService.execute( + new RetrieveRemoteDescriptors(registry, httpRequestSender, rd) ); } else if (getInputMessage().isByeByeMessage()) { - Log.v(getClass().getName(), "Received device BYEBYE advertisement"); - boolean removed = getUpnpService().getRegistry().removeDevice(rd); + YaaccLogger.v(getClass().getName(), "Received device BYEBYE advertisement"); + boolean removed = registry.removeDevice(rd); if (removed) { - Log.v(getClass().getName(), "Removed remote device from registry: " + rd); + YaaccLogger.v(getClass().getName(), "Removed remote device from registry: " + rd); } } else { - Log.v(getClass().getName(), "Ignoring unknown notification message: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Ignoring unknown notification message: " + getInputMessage()); } } diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/async/ReceivingSearch.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/ReceivingSearch.java similarity index 61% rename from yaacc/src/main/java/org/fourthline/cling/protocol/async/ReceivingSearch.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/async/ReceivingSearch.java index 6f65d01a..3b2b3839 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/async/ReceivingSearch.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/ReceivingSearch.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,11 +31,10 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.async; +package de.yaacc.upnp.protocol.async; -import android.util.Log; +import android.content.Context; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.DiscoveryOptions; import org.fourthline.cling.model.Location; import org.fourthline.cling.model.NetworkAddress; @@ -41,14 +58,20 @@ import org.fourthline.cling.model.types.DeviceType; import org.fourthline.cling.model.types.ServiceType; import org.fourthline.cling.model.types.UDN; -import org.fourthline.cling.protocol.ReceivingAsync; -import org.fourthline.cling.transport.RouterException; +import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Random; +import de.yaacc.upnp.protocol.ReceivingAsync; +import de.yaacc.upnp.protocol.UpnpProtocolHandler; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.upnp.server.udp.UdpTransiver; +import de.yaacc.util.InterfaceResolutionHelper; +import de.yaacc.util.YaaccLogger; + /** * Handles reception of search requests, responds for local registered devices. *

@@ -66,42 +89,35 @@ public class ReceivingSearch extends ReceivingAsync { - final protected Random randomGenerator = new Random(); + private final Context context; + private final UdpTransiver udpTransiver; + Registry registry; + - public ReceivingSearch(UpnpService upnpService, IncomingDatagramMessage inputMessage) { - super(upnpService, new IncomingSearchRequest(inputMessage)); + public ReceivingSearch(Context context, Registry registry, UdpTransiver udpTransiver, + IncomingDatagramMessage inputMessage) { + super(new IncomingSearchRequest(inputMessage)); + this.registry = registry; + this.context = context; + this.udpTransiver = udpTransiver; } - protected void execute() throws RouterException { - Log.v(getClass().getName(), "execute receiving search"); - if (getUpnpService().getRouter() == null) { - // TODO: http://mailinglists.945824.n3.nabble.com/rare-NPE-on-start-tp3078213p3142767.html - Log.v(getClass().getName(), "Router hasn't completed initialization, ignoring received search message"); - return; - } + protected void execute() throws IOException { + YaaccLogger.v(getClass().getName(), "execute receiving search"); if (!getInputMessage().isMANSSDPDiscover()) { - Log.v(getClass().getName(), "Invalid search request, no or invalid MAN ssdp:discover header: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Invalid search request, no or invalid MAN ssdp:discover header: " + getInputMessage()); return; } UpnpHeader searchTarget = getInputMessage().getSearchTarget(); if (searchTarget == null) { - Log.v(getClass().getName(), "Invalid search request, did not contain ST header: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Invalid search request, did not contain ST header: " + getInputMessage()); return; } + sendResponses(searchTarget); - List activeStreamServers = - getUpnpService().getRouter().getActiveStreamServers(getInputMessage().getLocalAddress()); - if (activeStreamServers.size() == 0) { - Log.v(getClass().getName(), "Aborting search response, no active stream servers found (network disabled?)"); - return; - } - - for (NetworkAddress activeStreamServer : activeStreamServers) { - sendResponses(searchTarget, activeStreamServer); - } } @Override @@ -110,7 +126,7 @@ protected boolean waitBeforeExecution() throws InterruptedException { Integer mx = getInputMessage().getMX(); if (mx == null) { - Log.v(getClass().getName(), "Invalid search request, did not contain MX header: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Invalid search request, did not contain MX header: " + getInputMessage()); return false; } @@ -120,68 +136,69 @@ protected boolean waitBeforeExecution() throws InterruptedException { if (mx > 120 || mx <= 0) mx = MXHeader.DEFAULT_VALUE; // Only wait if there is something to wait for - if (getUpnpService().getRegistry().getLocalDevices().size() > 0) { - int sleepTime = randomGenerator.nextInt(mx * 1000); - Log.v(getClass().getName(), "Sleeping " + sleepTime + " milliseconds to avoid flooding with search responses"); + if (registry.getLocalDevices().size() > 0) { + int sleepTime = new Random().nextInt(mx * 1000); + YaaccLogger.v(getClass().getName(), "Sleeping " + sleepTime + " milliseconds to avoid flooding with search responses"); Thread.sleep(sleepTime); } return true; } - protected void sendResponses(UpnpHeader searchTarget, NetworkAddress activeStreamServer) throws RouterException { + protected void sendResponses(UpnpHeader searchTarget) throws IOException { + NetworkAddress currentNetworkAddress = InterfaceResolutionHelper.getNetworkAddress(context); if (searchTarget instanceof STAllHeader) { - sendSearchResponseAll(activeStreamServer); + sendSearchResponseAll(currentNetworkAddress); } else if (searchTarget instanceof RootDeviceHeader) { - sendSearchResponseRootDevices(activeStreamServer); + sendSearchResponseRootDevices(currentNetworkAddress); } else if (searchTarget instanceof UDNHeader) { - sendSearchResponseUDN((UDN) searchTarget.getValue(), activeStreamServer); + sendSearchResponseUDN((UDN) searchTarget.getValue(), currentNetworkAddress); } else if (searchTarget instanceof DeviceTypeHeader) { - sendSearchResponseDeviceType((DeviceType) searchTarget.getValue(), activeStreamServer); + sendSearchResponseDeviceType((DeviceType) searchTarget.getValue(), currentNetworkAddress); } else if (searchTarget instanceof ServiceTypeHeader) { - sendSearchResponseServiceType((ServiceType) searchTarget.getValue(), activeStreamServer); + sendSearchResponseServiceType((ServiceType) searchTarget.getValue(), currentNetworkAddress); } else { - Log.w(getClass().getName(), "Non-implemented search request target: " + searchTarget.getClass()); + YaaccLogger.w(getClass().getName(), "Non-implemented search request target: " + searchTarget.getClass()); } } - protected void sendSearchResponseAll(NetworkAddress activeStreamServer) throws RouterException { - Log.v(getClass().getName(), "Responding to 'all' search with advertisement messages for all local devices"); + protected void sendSearchResponseAll(NetworkAddress activeStreamServer) throws IOException { + YaaccLogger.v(getClass().getName(), "Responding to 'all' search with advertisement messages for all local devices"); - for (LocalDevice localDevice : getUpnpService().getRegistry().getLocalDevices()) { + for (LocalDevice localDevice : registry.getLocalDevices()) { if (isAdvertisementDisabled(localDevice)) continue; // We are re-using the regular notification messages here but override the NT with the ST header - Log.v(getClass().getName(), "Sending root device messages: " + localDevice); + YaaccLogger.v(getClass().getName(), "Sending root device messages: " + localDevice); List rootDeviceMsgs = createDeviceMessages(localDevice, activeStreamServer); for (OutgoingSearchResponse upnpMessage : rootDeviceMsgs) { - getUpnpService().getRouter().send(upnpMessage); + udpTransiver.send(upnpMessage); } if (localDevice.hasEmbeddedDevices()) { for (LocalDevice embeddedDevice : localDevice.findEmbeddedDevices()) { - Log.v(getClass().getName(), "Sending embedded device messages: " + embeddedDevice); + YaaccLogger.v(getClass().getName(), "Sending embedded device messages: " + embeddedDevice); List embeddedDeviceMsgs = createDeviceMessages(embeddedDevice, activeStreamServer); for (OutgoingSearchResponse upnpMessage : embeddedDeviceMsgs) { - getUpnpService().getRouter().send(upnpMessage); + udpTransiver.send(upnpMessage); } } } @@ -190,10 +207,10 @@ protected void sendSearchResponseAll(NetworkAddress activeStreamServer) throws R createServiceTypeMessages(localDevice, activeStreamServer); if (serviceTypeMsgs.size() > 0) { - Log.v(getClass().getName(), "Sending service type messages"); + YaaccLogger.v(getClass().getName(), "Sending service type messages"); for (OutgoingSearchResponse upnpMessage : serviceTypeMsgs) { - getUpnpService().getRouter().send(upnpMessage); + udpTransiver.send(upnpMessage); } } @@ -204,13 +221,20 @@ protected List createDeviceMessages(LocalDevice device, NetworkAddress activeStreamServer) { List msgs = new ArrayList<>(); + // Check if network is available + Location location = getDescriptorLocation(activeStreamServer, device); + if (location == null) { + YaaccLogger.w(getClass().getName(), "Network unavailable, cannot create device messages"); + return msgs; // Return empty list + } + // See the tables in UDA 1.0 section 1.1.2 if (device.isRoot()) { msgs.add( new OutgoingSearchResponseRootDevice( getInputMessage(), - getDescriptorLocation(activeStreamServer, device), + location, device ) ); @@ -219,7 +243,7 @@ protected List createDeviceMessages(LocalDevice device, msgs.add( new OutgoingSearchResponseUDN( getInputMessage(), - getDescriptorLocation(activeStreamServer, device), + location, device ) ); @@ -227,7 +251,7 @@ protected List createDeviceMessages(LocalDevice device, msgs.add( new OutgoingSearchResponseDeviceType( getInputMessage(), - getDescriptorLocation(activeStreamServer, device), + location, device ) ); @@ -242,11 +266,19 @@ protected List createDeviceMessages(LocalDevice device, protected List createServiceTypeMessages(LocalDevice device, NetworkAddress activeStreamServer) { List msgs = new ArrayList<>(); + + // Check if network is available + Location location = getDescriptorLocation(activeStreamServer, device); + if (location == null) { + YaaccLogger.w(getClass().getName(), "Network unavailable, cannot create service type messages"); + return msgs; // Return empty list + } + for (ServiceType serviceType : device.findServiceTypes()) { OutgoingSearchResponse message = new OutgoingSearchResponseServiceType( getInputMessage(), - getDescriptorLocation(activeStreamServer, device), + location, device, serviceType ); @@ -256,9 +288,9 @@ protected List createServiceTypeMessages(LocalDevice dev return msgs; } - protected void sendSearchResponseRootDevices(NetworkAddress activeStreamServer) throws RouterException { - Log.v(getClass().getName(), "Responding to root device search with advertisement messages for all local root devices"); - for (LocalDevice device : getUpnpService().getRegistry().getLocalDevices()) { + protected void sendSearchResponseRootDevices(NetworkAddress activeStreamServer) throws IOException { + YaaccLogger.v(getClass().getName(), "Responding to root device search with advertisement messages for all local root devices"); + for (LocalDevice device : registry.getLocalDevices()) { if (isAdvertisementDisabled(device)) continue; @@ -270,18 +302,18 @@ protected void sendSearchResponseRootDevices(NetworkAddress activeStreamServer) device ); prepareOutgoingSearchResponse(message); - getUpnpService().getRouter().send(message); + udpTransiver.send(message); } } - protected void sendSearchResponseUDN(UDN udn, NetworkAddress activeStreamServer) throws RouterException { - Device device = getUpnpService().getRegistry().getDevice(udn, false); + protected void sendSearchResponseUDN(UDN udn, NetworkAddress activeStreamServer) throws IOException { + Device device = registry.getDevice(udn, false); if (device != null && device instanceof LocalDevice) { if (isAdvertisementDisabled((LocalDevice) device)) return; - Log.v(getClass().getName(), "Responding to UDN device search: " + udn); + YaaccLogger.v(getClass().getName(), "Responding to UDN device search: " + udn); OutgoingSearchResponse message = new OutgoingSearchResponseUDN( getInputMessage(), @@ -289,42 +321,47 @@ protected void sendSearchResponseUDN(UDN udn, NetworkAddress activeStreamServer) (LocalDevice) device ); prepareOutgoingSearchResponse(message); - getUpnpService().getRouter().send(message); + udpTransiver.send(message); } } - protected void sendSearchResponseDeviceType(DeviceType deviceType, NetworkAddress activeStreamServer) throws RouterException { - Log.v(getClass().getName(), "Responding to device type search: " + deviceType); - Collection> devices = getUpnpService().getRegistry().getDevices(deviceType); + protected void sendSearchResponseDeviceType(DeviceType deviceType, NetworkAddress activeStreamServer) throws IOException { + YaaccLogger.v(getClass().getName(), "Responding to device type search: " + deviceType); + Collection> devices = registry.getDevices(deviceType); for (Device device : devices) { if (device instanceof LocalDevice) { if (isAdvertisementDisabled((LocalDevice) device)) continue; - Log.v(getClass().getName(), "Sending matching device type search result for: " + device); + YaaccLogger.v(getClass().getName(), "Sending matching device type search result for: " + device); + Location location = getDescriptorLocation(activeStreamServer, (LocalDevice) device); + if (location == null) { + YaaccLogger.w(getClass().getName(), "Skipping search response, network unavailable"); + continue; + } OutgoingSearchResponse message = new OutgoingSearchResponseDeviceType( getInputMessage(), - getDescriptorLocation(activeStreamServer, (LocalDevice) device), + location, (LocalDevice) device ); prepareOutgoingSearchResponse(message); - getUpnpService().getRouter().send(message); + udpTransiver.send(message); } } } - protected void sendSearchResponseServiceType(ServiceType serviceType, NetworkAddress activeStreamServer) throws RouterException { - Log.v(getClass().getName(), "Responding to service type search: " + serviceType); - Collection> devices = getUpnpService().getRegistry().getDevices(serviceType); + protected void sendSearchResponseServiceType(ServiceType serviceType, NetworkAddress activeStreamServer) throws IOException { + YaaccLogger.v(getClass().getName(), "Responding to service type search: " + serviceType); + Collection> devices = registry.getDevices(serviceType); for (Device device : devices) { if (device instanceof LocalDevice) { if (isAdvertisementDisabled((LocalDevice) device)) continue; - Log.v(getClass().getName(), "Sending matching service type search result: " + device); + YaaccLogger.v(getClass().getName(), "Sending matching service type search result: " + device); OutgoingSearchResponse message = new OutgoingSearchResponseServiceType( getInputMessage(), @@ -333,21 +370,25 @@ protected void sendSearchResponseServiceType(ServiceType serviceType, NetworkAdd serviceType ); prepareOutgoingSearchResponse(message); - getUpnpService().getRouter().send(message); + udpTransiver.send(message); } } } protected Location getDescriptorLocation(NetworkAddress activeStreamServer, LocalDevice device) { + if (activeStreamServer == null || activeStreamServer.getAddress() == null) { + YaaccLogger.w(getClass().getName(), "Network address unavailable, cannot create descriptor location"); + return null; + } return new Location( activeStreamServer, - getUpnpService().getConfiguration().getNamespace().getDescriptorPathString(device) + UpnpProtocolHandler.NAMESPACE.getDescriptorPathString(device) ); } protected boolean isAdvertisementDisabled(LocalDevice device) { DiscoveryOptions options = - getUpnpService().getRegistry().getDiscoveryOptions(device.getIdentity().getUdn()); + registry.getDiscoveryOptions(device.getIdentity().getUdn()); return options != null && !options.isAdvertised(); } diff --git a/yaacc/src/main/java/de/yaacc/upnp/protocol/async/ReceivingSearchResponse.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/ReceivingSearchResponse.java new file mode 100644 index 00000000..eab6c039 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/ReceivingSearchResponse.java @@ -0,0 +1,133 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +/* + * Copyright (C) 2013 4th Line GmbH, Switzerland + * + * The contents of this file are subject to the terms of either the GNU + * Lesser General Public License Version 2 or later ("LGPL") or the + * Common Development and Distribution License Version 1 or later + * ("CDDL") (collectively, the "License"). You may not use this file + * except in compliance with the License. See LICENSE.txt for more + * information. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ + +package de.yaacc.upnp.protocol.async; + +import org.fourthline.cling.model.ValidationError; +import org.fourthline.cling.model.ValidationException; +import org.fourthline.cling.model.message.IncomingDatagramMessage; +import org.fourthline.cling.model.message.UpnpResponse; +import org.fourthline.cling.model.message.discovery.IncomingSearchResponse; +import org.fourthline.cling.model.meta.RemoteDevice; +import org.fourthline.cling.model.meta.RemoteDeviceIdentity; +import org.fourthline.cling.model.types.UDN; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; + +import de.yaacc.upnp.protocol.ReceivingAsync; +import de.yaacc.upnp.protocol.RetrieveRemoteDescriptors; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.YaaccLogger; + +/** + * Handles reception of search response messages. + *

+ * This protocol implementation is basically the same as + * the {@link ReceivingNotification} protocol for + * an ALIVE message. + *

+ * + * @author Christian Bauer + */ +public class ReceivingSearchResponse extends ReceivingAsync { + + + private final Registry registry; + private final HttpRequestSender httpReqSender; + + public ExecutorService getExecutorService() { + return executorService; + } + + private final ExecutorService executorService; + + public ReceivingSearchResponse(Registry registry, HttpRequestSender httpRequestSender, IncomingDatagramMessage inputMessage) { + super(new IncomingSearchResponse(inputMessage)); + this.registry = registry; + executorService = registry.getExecutorService(); + this.httpReqSender = httpRequestSender; + } + + protected void execute() throws IOException { + + if (!getInputMessage().isSearchResponseMessage()) { + YaaccLogger.v(getClass().getName(), "Ignoring invalid search response message: " + getInputMessage()); + return; + } + + UDN udn = getInputMessage().getRootDeviceUDN(); + if (udn == null) { + YaaccLogger.v(getClass().getName(), "Ignoring search response message without UDN: " + getInputMessage()); + return; + } + + RemoteDeviceIdentity rdIdentity = new RemoteDeviceIdentity(getInputMessage()); + YaaccLogger.v(getClass().getName(), "Received device search response: " + rdIdentity); + + if (registry.update(rdIdentity)) { + YaaccLogger.v(getClass().getName(), "Remote device was already known: " + udn); + return; + } + + RemoteDevice rd; + try { + rd = new RemoteDevice(rdIdentity); + } catch (ValidationException ex) { + YaaccLogger.w(getClass().getName(), "Validation errors of device during discovery: " + rdIdentity); + for (ValidationError validationError : ex.getErrors()) { + YaaccLogger.w(getClass().getName(), validationError.toString()); + } + return; + } + + if (rdIdentity.getDescriptorURL() == null) { + YaaccLogger.v(getClass().getName(), "Ignoring message without location URL header: " + getInputMessage()); + return; + } + + if (rdIdentity.getMaxAgeSeconds() == null) { + YaaccLogger.v(getClass().getName(), "Ignoring message without max-age header: " + getInputMessage()); + return; + } + + // Unfortunately, we always have to retrieve the descriptor because at this point we + // have no idea if it's a root or embedded device + executorService.execute( + new RetrieveRemoteDescriptors(registry, httpReqSender, rd) + ); + + } + +} diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/RetrieveRemoteDescriptors.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/RetrieveRemoteDescriptors.java similarity index 63% rename from yaacc/src/main/java/org/fourthline/cling/protocol/RetrieveRemoteDescriptors.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/async/RetrieveRemoteDescriptors.java index 254eab3f..89b34f1b 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/RetrieveRemoteDescriptors.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/RetrieveRemoteDescriptors.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,29 +31,25 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol; - -import android.util.Log; +package de.yaacc.upnp.protocol.async; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.binding.xml.DescriptorBindingException; import org.fourthline.cling.binding.xml.DeviceDescriptorBinder; import org.fourthline.cling.binding.xml.ServiceDescriptorBinder; +import org.fourthline.cling.binding.xml.UDA10DeviceDescriptorBinderImpl; +import org.fourthline.cling.binding.xml.UDA10ServiceDescriptorBinderImpl; import org.fourthline.cling.model.ValidationError; import org.fourthline.cling.model.ValidationException; import org.fourthline.cling.model.message.StreamRequestMessage; import org.fourthline.cling.model.message.StreamResponseMessage; -import org.fourthline.cling.model.message.UpnpHeaders; import org.fourthline.cling.model.message.UpnpRequest; import org.fourthline.cling.model.meta.Icon; import org.fourthline.cling.model.meta.RemoteDevice; import org.fourthline.cling.model.meta.RemoteService; import org.fourthline.cling.model.types.ServiceType; import org.fourthline.cling.model.types.UDN; -import org.fourthline.cling.registry.RegistrationException; -import org.fourthline.cling.transport.RouterException; -import org.seamless.util.Exceptions; +import java.io.IOException; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; @@ -43,12 +57,19 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; +import de.yaacc.upnp.registry.RegistrationException; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.upnp.server.YaaccUpnpServerService; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.Exceptions; +import de.yaacc.util.YaaccLogger; + /** * Retrieves all remote device XML descriptors, parses them, creates an immutable device and service metadata graph. *

* This implementation encapsulates all steps which are necessary to create a fully usable and populated * device metadata graph of a particular UPnP device. It starts with an unhydrated and typically just - * discovered {@link org.fourthline.cling.model.meta.RemoteDevice}, the only property that has to be available is + * discovered {@link RemoteDevice}, the only property that has to be available is * its {@link org.fourthline.cling.model.meta.RemoteDeviceIdentity}. *

*

@@ -66,20 +87,19 @@ public class RetrieveRemoteDescriptors implements Runnable { - private final UpnpService upnpService; + private final HttpRequestSender httpRequestSender; + private Registry registry; private RemoteDevice rd; private static final Set activeRetrievals = new CopyOnWriteArraySet(); protected List errorsAlreadyLogged = new ArrayList<>(); - public RetrieveRemoteDescriptors(UpnpService upnpService, RemoteDevice rd) { - this.upnpService = upnpService; + public RetrieveRemoteDescriptors(Registry registry, HttpRequestSender httpRequestSender, RemoteDevice rd) { + this.registry = registry; this.rd = rd; + this.httpRequestSender = httpRequestSender; } - public UpnpService getUpnpService() { - return upnpService; - } public void run() { @@ -90,21 +110,21 @@ public void run() { // processing this several times concurrently. if (activeRetrievals.contains(deviceURL)) { - Log.v(getClass().getName(), "Exiting early, active retrieval for URL already in progress: " + deviceURL); + YaaccLogger.v(getClass().getName(), "Exiting early, active retrieval for URL already in progress: " + deviceURL); return; } // Exit if it has been discovered already, could be we have been waiting in the executor queue too long - if (getUpnpService().getRegistry().getRemoteDevice(rd.getIdentity().getUdn(), true) != null) { - Log.v(getClass().getName(), "Exiting early, already discovered: " + deviceURL); + if (registry.getRemoteDevice(rd.getIdentity().getUdn(), true) != null) { + YaaccLogger.v(getClass().getName(), "Exiting early, already discovered: " + deviceURL); return; } try { activeRetrievals.add(deviceURL); describe(); - } catch (RouterException ex) { - Log.w(getClass().getName(), + } catch (IOException ex) { + YaaccLogger.w(getClass().getName(), "Descriptor retrieval failed: " + deviceURL, ex ); @@ -113,18 +133,13 @@ public void run() { } } - protected void describe() throws RouterException { + protected void describe() throws IOException { // All of the following is a very expensive and time consuming procedure, thanks to the // braindead design of UPnP. Several GET requests, several descriptors, several XML parsing // steps - all of this could be done with one and it wouldn't make a difference. So every // call of this method has to be really necessary and rare. - if (getUpnpService().getRouter() == null) { - Log.w(getClass().getName(), "Router not yet initialized"); - return; - } - StreamRequestMessage deviceDescRetrievalMsg; StreamResponseMessage deviceDescMsg; @@ -132,35 +147,34 @@ protected void describe() throws RouterException { deviceDescRetrievalMsg = new StreamRequestMessage(UpnpRequest.Method.GET, rd.getIdentity().getDescriptorURL()); - - // Extra headers - UpnpHeaders headers = - getUpnpService().getConfiguration().getDescriptorRetrievalHeaders(rd.getIdentity()); - if (headers != null) - deviceDescRetrievalMsg.getHeaders().putAll(headers); - - Log.v(getClass().getName(), "Sending device descriptor retrieval message: " + deviceDescRetrievalMsg); - deviceDescMsg = getUpnpService().getRouter().send(deviceDescRetrievalMsg); - + YaaccLogger.v(getClass().getName(), "Sending device descriptor retrieval message: " + deviceDescRetrievalMsg); + deviceDescMsg = httpRequestSender.send(deviceDescRetrievalMsg); } catch (IllegalArgumentException ex) { // UpnpRequest constructor can throw IllegalArgumentException on invalid URI // IllegalArgumentException can also be thrown by Apache HttpClient on blank URI in send() - Log.w(getClass().getName(), + YaaccLogger.w(getClass().getName(), + "Device descriptor retrieval failed: " + + rd.getIdentity().getDescriptorURL() + + " , possibly invalid URL: ", ex); + return; + } catch (IOException ex) { + YaaccLogger.w(getClass().getName(), "Device descriptor retrieval failed: " + rd.getIdentity().getDescriptorURL() - + ", possibly invalid URL: " + ex); + + " ,exception on send: ", ex); return; } + if (deviceDescMsg == null) { - Log.w(getClass().getName(), + YaaccLogger.w(getClass().getName(), "Device descriptor retrieval failed, no response: " + rd.getIdentity().getDescriptorURL() ); return; } if (deviceDescMsg.getOperation().isFailed()) { - Log.w(getClass().getName(), + YaaccLogger.w(getClass().getName(), "Device descriptor retrieval failed: " + rd.getIdentity().getDescriptorURL() + ", " @@ -170,7 +184,7 @@ protected void describe() throws RouterException { } if (!deviceDescMsg.isContentTypeTextUDA()) { - Log.v(getClass().getName(), + YaaccLogger.v(getClass().getName(), "Received device descriptor without or with invalid Content-Type: " + rd.getIdentity().getDescriptorURL()); // We continue despite the invalid UPnP message because we can still hope to convert the content @@ -178,81 +192,81 @@ protected void describe() throws RouterException { String descriptorContent = deviceDescMsg.getBodyString(); if (descriptorContent == null || descriptorContent.length() == 0) { - Log.w(getClass().getName(), "Received empty device descriptor:" + rd.getIdentity().getDescriptorURL()); + YaaccLogger.w(getClass().getName(), "Received empty device descriptor:" + rd.getIdentity().getDescriptorURL()); return; } - Log.v(getClass().getName(), "Received root device descriptor: " + deviceDescMsg); + YaaccLogger.v(getClass().getName(), "Received root device descriptor: " + deviceDescMsg); describe(descriptorContent); } - protected void describe(String descriptorXML) throws RouterException { + protected void describe(String descriptorXML) throws IOException { boolean notifiedStart = false; RemoteDevice describedDevice = null; try { - DeviceDescriptorBinder deviceDescriptorBinder = - getUpnpService().getConfiguration().getDeviceDescriptorBinderUDA10(); + DeviceDescriptorBinder deviceDescriptorBinder = new UDA10DeviceDescriptorBinderImpl(); + describedDevice = deviceDescriptorBinder.describe( rd, descriptorXML ); - Log.v(getClass().getName(), "Remote device described (without services) notifying listeners: " + describedDevice); - notifiedStart = getUpnpService().getRegistry().notifyDiscoveryStart(describedDevice); + YaaccLogger.v(getClass().getName(), "Remote device described (without services) notifying listeners: " + describedDevice); + notifiedStart = registry.notifyDiscoveryStart(describedDevice); - Log.v(getClass().getName(), "Hydrating described device's services: " + describedDevice); + YaaccLogger.v(getClass().getName(), "Hydrating described device's services: " + describedDevice); RemoteDevice hydratedDevice = describeServices(describedDevice); if (hydratedDevice == null) { if (!errorsAlreadyLogged.contains(rd.getIdentity().getUdn())) { errorsAlreadyLogged.add(rd.getIdentity().getUdn()); - Log.w(getClass().getName(), "Device service description failed: " + rd); + YaaccLogger.w(getClass().getName(), "Device service description failed: " + rd); } if (notifiedStart) - getUpnpService().getRegistry().notifyDiscoveryFailure( + registry.notifyDiscoveryFailure( describedDevice, new DescriptorBindingException("Device service description failed: " + rd) ); return; } else { - Log.v(getClass().getName(), "Adding fully hydrated remote device to registry: " + hydratedDevice); + YaaccLogger.v(getClass().getName(), "Adding fully hydrated remote device to registry: " + hydratedDevice); // The registry will do the right thing: A new root device is going to be added, if it's // already present or we just received the descriptor again (because we got an embedded // devices' notification), it will simply update the expiration timestamp of the root // device. - getUpnpService().getRegistry().addDevice(hydratedDevice); + registry.addDevice(hydratedDevice); } } catch (ValidationException ex) { // Avoid error log spam each time device is discovered, errors are logged once per device. if (!errorsAlreadyLogged.contains(rd.getIdentity().getUdn())) { errorsAlreadyLogged.add(rd.getIdentity().getUdn()); - Log.w(getClass().getName(), "Could not validate device model: " + rd); + YaaccLogger.w(getClass().getName(), "Could not validate device model: " + rd); for (ValidationError validationError : ex.getErrors()) { - Log.w(getClass().getName(), validationError.toString()); + YaaccLogger.w(getClass().getName(), validationError.toString()); } if (describedDevice != null && notifiedStart) - getUpnpService().getRegistry().notifyDiscoveryFailure(describedDevice, ex); + registry.notifyDiscoveryFailure(describedDevice, ex); } } catch (DescriptorBindingException ex) { - Log.w(getClass().getName(), "Could not hydrate device or its services from descriptor: " + rd); - Log.w(getClass().getName(), "Cause was: " + Exceptions.unwrap(ex)); + YaaccLogger.w(getClass().getName(), "Could not hydrate device or its services from descriptor: " + rd); + YaaccLogger.w(getClass().getName(), "Cause was: " + Exceptions.unwrap(ex)); if (describedDevice != null && notifiedStart) - getUpnpService().getRegistry().notifyDiscoveryFailure(describedDevice, ex); + registry.notifyDiscoveryFailure(describedDevice, ex); } catch (RegistrationException ex) { - Log.w(getClass().getName(), "Adding hydrated device to registry failed: " + rd); - Log.w(getClass().getName(), "Cause was: " + ex.toString()); + YaaccLogger.w(getClass().getName(), "Adding hydrated device to registry failed: " + rd); + YaaccLogger.w(getClass().getName(), "Cause was: " + ex.toString()); if (describedDevice != null && notifiedStart) - getUpnpService().getRegistry().notifyDiscoveryFailure(describedDevice, ex); + registry.notifyDiscoveryFailure(describedDevice, ex); } } protected RemoteDevice describeServices(RemoteDevice currentDevice) - throws RouterException, DescriptorBindingException, ValidationException { + throws IOException, DescriptorBindingException, ValidationException { List describedServices = new ArrayList<>(); if (currentDevice.hasServices()) { @@ -263,7 +277,7 @@ protected RemoteDevice describeServices(RemoteDevice currentDevice) if (svc != null) describedServices.add(svc); else - Log.w(getClass().getName(), "Skipping invalid service '" + service + "' of: " + currentDevice); + YaaccLogger.w(getClass().getName(), "Skipping invalid service '" + service + "' of: " + currentDevice); } } @@ -299,34 +313,35 @@ protected RemoteDevice describeServices(RemoteDevice currentDevice) } protected RemoteService describeService(RemoteService service) - throws RouterException, DescriptorBindingException, ValidationException { + throws IOException, DescriptorBindingException, ValidationException { URL descriptorURL; try { descriptorURL = service.getDevice().normalizeURI(service.getDescriptorURI()); } catch (IllegalArgumentException e) { - Log.w(getClass().getName(), "Could not normalize service descriptor URL: " + service.getDescriptorURI()); + YaaccLogger.w(getClass().getName(), "Could not normalize service descriptor URL: " + service.getDescriptorURI()); return null; } StreamRequestMessage serviceDescRetrievalMsg = new StreamRequestMessage(UpnpRequest.Method.GET, descriptorURL); - // Extra headers - UpnpHeaders headers = - getUpnpService().getConfiguration().getDescriptorRetrievalHeaders(service.getDevice().getIdentity()); - if (headers != null) - serviceDescRetrievalMsg.getHeaders().putAll(headers); - Log.v(getClass().getName(), "Sending service descriptor retrieval message: " + serviceDescRetrievalMsg); - StreamResponseMessage serviceDescMsg = getUpnpService().getRouter().send(serviceDescRetrievalMsg); + YaaccLogger.v(getClass().getName(), "Sending service descriptor retrieval message: " + serviceDescRetrievalMsg); + StreamResponseMessage serviceDescMsg = null; + try { + serviceDescMsg = httpRequestSender.send(serviceDescRetrievalMsg); + } catch (IOException e) { + YaaccLogger.w(getClass().getName(), "Exception while sending service descriptor retrieval message: ", e); + return null; + } if (serviceDescMsg == null) { - Log.w(getClass().getName(), "Could not retrieve service descriptor, no response: " + service); + YaaccLogger.w(getClass().getName(), "Could not retrieve service descriptor, no response: " + service); return null; } if (serviceDescMsg.getOperation().isFailed()) { - Log.w(getClass().getName(), "Service descriptor retrieval failed: " + YaaccLogger.w(getClass().getName(), "Service descriptor retrieval failed: " + descriptorURL + ", " + serviceDescMsg.getOperation().getResponseDetails()); @@ -334,25 +349,24 @@ protected RemoteService describeService(RemoteService service) } if (!serviceDescMsg.isContentTypeTextUDA()) { - Log.v(getClass().getName(), "Received service descriptor without or with invalid Content-Type: " + descriptorURL); + YaaccLogger.v(getClass().getName(), "Received service descriptor without or with invalid Content-Type: " + descriptorURL); // We continue despite the invalid UPnP message because we can still hope to convert the content } String descriptorContent = serviceDescMsg.getBodyString(); if (descriptorContent == null || descriptorContent.length() == 0) { - Log.w(getClass().getName(), "Received empty service descriptor:" + descriptorURL); + YaaccLogger.w(getClass().getName(), "Received empty service descriptor:" + descriptorURL); return null; } - Log.v(getClass().getName(), "Received service descriptor, hydrating service model: " + serviceDescMsg); - ServiceDescriptorBinder serviceDescriptorBinder = - getUpnpService().getConfiguration().getServiceDescriptorBinderUDA10(); + YaaccLogger.v(getClass().getName(), "Received service descriptor, hydrating service model: " + serviceDescMsg); + ServiceDescriptorBinder serviceDescriptorBinder = new UDA10ServiceDescriptorBinderImpl(); return serviceDescriptorBinder.describe(service, descriptorContent); } protected List filterExclusiveServices(RemoteService[] services) { - ServiceType[] exclusiveTypes = getUpnpService().getConfiguration().getExclusiveServiceTypes(); + ServiceType[] exclusiveTypes = YaaccUpnpServerService.EXCLUSIVE_SERVER_TYPES; if (exclusiveTypes == null || exclusiveTypes.length == 0) return Arrays.asList(services); @@ -361,10 +375,10 @@ protected List filterExclusiveServices(RemoteService[] services) for (RemoteService discoveredService : services) { for (ServiceType exclusiveType : exclusiveTypes) { if (discoveredService.getServiceType().implementsVersion(exclusiveType)) { - Log.v(getClass().getName(), "Including exclusive service: " + discoveredService); + YaaccLogger.v(getClass().getName(), "Including exclusive service: " + discoveredService); exclusiveServices.add(discoveredService); } else { - Log.v(getClass().getName(), "Excluding unwanted service: " + exclusiveType); + YaaccLogger.v(getClass().getName(), "Excluding unwanted service: " + exclusiveType); } } } diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingNotification.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingNotification.java similarity index 69% rename from yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingNotification.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingNotification.java index 1d67a2e7..944374b8 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingNotification.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingNotification.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,13 +31,11 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.async; +package de.yaacc.upnp.protocol.async; -import android.util.Log; +import android.content.Context; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.Location; -import org.fourthline.cling.model.NetworkAddress; import org.fourthline.cling.model.message.discovery.OutgoingNotificationRequest; import org.fourthline.cling.model.message.discovery.OutgoingNotificationRequestDeviceType; import org.fourthline.cling.model.message.discovery.OutgoingNotificationRequestRootDevice; @@ -28,12 +44,17 @@ import org.fourthline.cling.model.meta.LocalDevice; import org.fourthline.cling.model.types.NotificationSubtype; import org.fourthline.cling.model.types.ServiceType; -import org.fourthline.cling.protocol.SendingAsync; -import org.fourthline.cling.transport.RouterException; +import java.io.IOException; import java.util.ArrayList; import java.util.List; +import de.yaacc.upnp.protocol.SendingAsync; +import de.yaacc.upnp.protocol.UpnpProtocolHandler; +import de.yaacc.upnp.server.udp.UdpTransiver; +import de.yaacc.util.InterfaceResolutionHelper; +import de.yaacc.util.YaaccLogger; + /** * Sending notification messages for a registered local device. *

@@ -46,36 +67,32 @@ public abstract class SendingNotification extends SendingAsync { + private final UdpTransiver udpTransiver; private LocalDevice device; + private Context context; - public SendingNotification(UpnpService upnpService, LocalDevice device) { - super(upnpService); + public SendingNotification(Context context, UdpTransiver udpTransiver, LocalDevice device) { + this.context = context; this.device = device; + this.udpTransiver = udpTransiver; } public LocalDevice getDevice() { return device; } - protected void execute() throws RouterException { + protected void execute() throws IOException { - List activeStreamServers = - getUpnpService().getRouter().getActiveStreamServers(null); - if (activeStreamServers.size() == 0) { - Log.v(getClass().getName(), "Aborting notifications, no active stream servers found (network disabled?)"); - return; - } // Prepare it once, it's the same for each repetition List descriptorLocations = new ArrayList<>(); - for (NetworkAddress activeStreamServer : activeStreamServers) { - descriptorLocations.add( - new Location( - activeStreamServer, - getUpnpService().getConfiguration().getNamespace().getDescriptorPathString(getDevice()) - ) - ); - } + descriptorLocations.add( + new Location( + InterfaceResolutionHelper.getNetworkAddress(context), + UpnpProtocolHandler.NAMESPACE.getDescriptorPathString(getDevice()) + ) + ); + for (int i = 0; i < getBulkRepeat(); i++) { try { @@ -85,11 +102,11 @@ protected void execute() throws RouterException { } // UDA 1.0 is silent about this but UDA 1.1 recomments "a few hundred milliseconds" - Log.v(getClass().getName(), "Sleeping " + getBulkIntervalMilliseconds() + " milliseconds"); + YaaccLogger.v(getClass().getName(), "Sleeping " + getBulkIntervalMilliseconds() + " milliseconds"); Thread.sleep(getBulkIntervalMilliseconds()); } catch (InterruptedException ex) { - Log.w(getClass().getName(), "Advertisement thread was interrupted: " + ex); + YaaccLogger.w(getClass().getName(), "Advertisement thread was interrupted: " + ex); } } } @@ -102,21 +119,21 @@ protected int getBulkIntervalMilliseconds() { return 150; } - public void sendMessages(Location descriptorLocation) throws RouterException { - Log.v(getClass().getName(), "Sending root device messages: " + getDevice()); + public void sendMessages(Location descriptorLocation) throws IOException { + YaaccLogger.v(getClass().getName(), "Sending root device messages: " + getDevice()); List rootDeviceMsgs = createDeviceMessages(getDevice(), descriptorLocation); for (OutgoingNotificationRequest upnpMessage : rootDeviceMsgs) { - getUpnpService().getRouter().send(upnpMessage); + udpTransiver.send(upnpMessage); } if (getDevice().hasEmbeddedDevices()) { for (LocalDevice embeddedDevice : getDevice().findEmbeddedDevices()) { - Log.v(getClass().getName(), "Sending embedded device messages: " + embeddedDevice); + YaaccLogger.v(getClass().getName(), "Sending embedded device messages: " + embeddedDevice); List embeddedDeviceMsgs = createDeviceMessages(embeddedDevice, descriptorLocation); for (OutgoingNotificationRequest upnpMessage : embeddedDeviceMsgs) { - getUpnpService().getRouter().send(upnpMessage); + udpTransiver.send(upnpMessage); } } } @@ -124,9 +141,9 @@ public void sendMessages(Location descriptorLocation) throws RouterException { List serviceTypeMsgs = createServiceTypeMessages(getDevice(), descriptorLocation); if (serviceTypeMsgs.size() > 0) { - Log.v(getClass().getName(), "Sending service type messages"); + YaaccLogger.v(getClass().getName(), "Sending service type messages"); for (OutgoingNotificationRequest upnpMessage : serviceTypeMsgs) { - getUpnpService().getRouter().send(upnpMessage); + udpTransiver.send(upnpMessage); } } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingNotificationAlive.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingNotificationAlive.java new file mode 100644 index 00000000..90821063 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingNotificationAlive.java @@ -0,0 +1,68 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +/* + * Copyright (C) 2013 4th Line GmbH, Switzerland + * + * The contents of this file are subject to the terms of either the GNU + * Lesser General Public License Version 2 or later ("LGPL") or the + * Common Development and Distribution License Version 1 or later + * ("CDDL") (collectively, the "License"). You may not use this file + * except in compliance with the License. See LICENSE.txt for more + * information. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ + +package de.yaacc.upnp.protocol.async; + +import android.content.Context; + +import org.fourthline.cling.model.meta.LocalDevice; +import org.fourthline.cling.model.types.NotificationSubtype; + +import java.io.IOException; + +import de.yaacc.upnp.server.udp.UdpTransiver; +import de.yaacc.util.YaaccLogger; + +/** + * Sending ALIVE notification messages for a registered local device. + * + * @author Christian Bauer + */ +public class SendingNotificationAlive extends SendingNotification { + + + public SendingNotificationAlive(Context context, UdpTransiver udpTransiver, LocalDevice device) { + super(context, udpTransiver, device); + } + + @Override + protected void execute() throws IOException { + YaaccLogger.v(getClass().getName(), "Sending alive messages (" + getBulkRepeat() + " times) for: " + getDevice()); + super.execute(); + } + + protected NotificationSubtype getNotificationSubtype() { + return NotificationSubtype.ALIVE; + } + +} diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingNotificationByebye.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingNotificationByebye.java similarity index 59% rename from yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingNotificationByebye.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingNotificationByebye.java index 5d870e9d..927a2e7c 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingNotificationByebye.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingNotificationByebye.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,17 +31,19 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.async; +package de.yaacc.upnp.protocol.async; -import android.util.Log; +import android.content.Context; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.meta.LocalDevice; import org.fourthline.cling.model.types.NotificationSubtype; -import org.fourthline.cling.transport.RouterException; +import java.io.IOException; import java.util.logging.Logger; +import de.yaacc.upnp.server.udp.UdpTransiver; +import de.yaacc.util.YaaccLogger; + /** * Sending BYEBYE notification messages for a registered local device. * @@ -33,8 +53,8 @@ public class SendingNotificationByebye extends SendingNotification { final private static Logger log = Logger.getLogger(SendingNotification.class.getName()); - public SendingNotificationByebye(UpnpService upnpService, LocalDevice device) { - super(upnpService, device); + public SendingNotificationByebye(Context context, UdpTransiver udpTransiver, LocalDevice device) { + super(context, udpTransiver, device); } // The UDA 1.0 spec says "a message corresponding to /each/ of the ssd:alive messages" but @@ -47,8 +67,8 @@ public SendingNotificationByebye(UpnpService upnpService, LocalDevice device) { // In other words: The superclass method is fine even for byebye. @Override - protected void execute() throws RouterException { - Log.v(getClass().getName(), "Sending byebye messages (" + getBulkRepeat() + " times) for: " + getDevice()); + protected void execute() throws IOException { + YaaccLogger.v(getClass().getName(), "Sending byebye messages (" + getBulkRepeat() + " times) for: " + getDevice()); super.execute(); } diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingSearch.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingSearch.java similarity index 60% rename from yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingSearch.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingSearch.java index 0045b775..af768909 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingSearch.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/async/SendingSearch.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,17 +31,18 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.async; - -import android.util.Log; +package de.yaacc.upnp.protocol.async; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.message.discovery.OutgoingSearchRequest; import org.fourthline.cling.model.message.header.MXHeader; import org.fourthline.cling.model.message.header.STAllHeader; import org.fourthline.cling.model.message.header.UpnpHeader; -import org.fourthline.cling.protocol.SendingAsync; -import org.fourthline.cling.transport.RouterException; + +import java.io.IOException; + +import de.yaacc.upnp.protocol.SendingAsync; +import de.yaacc.upnp.server.udp.UdpTransiver; +import de.yaacc.util.YaaccLogger; /** * Sending search request messages using the supplied search type. @@ -36,28 +55,31 @@ */ public class SendingSearch extends SendingAsync { - private final UpnpHeader searchTarget; + private final int mxSeconds; + private final UpnpHeader searchTarget; + private final UdpTransiver udpTransiver; /** - * Defaults to {@link org.fourthline.cling.model.message.header.STAllHeader} and an MX of 3 seconds. + * Defaults to {@link STAllHeader} and an MX of 3 seconds. */ - public SendingSearch(UpnpService upnpService) { - this(upnpService, new STAllHeader()); + public SendingSearch(UdpTransiver udpTransiver) { + this(udpTransiver, new STAllHeader()); } /** * Defaults to an MX value of 3 seconds. */ - public SendingSearch(UpnpService upnpService, UpnpHeader searchTarget) { - this(upnpService, searchTarget, MXHeader.DEFAULT_VALUE); + public SendingSearch(UdpTransiver udpTransiver, UpnpHeader searchTarget) { + this(udpTransiver, searchTarget, MXHeader.DEFAULT_VALUE); } /** * @param mxSeconds The time in seconds a host should wait before responding. */ - public SendingSearch(UpnpService upnpService, UpnpHeader searchTarget, int mxSeconds) { - super(upnpService); + public SendingSearch(UdpTransiver udpTransiver, UpnpHeader searchTarget, int mxSeconds) { + super(); + this.udpTransiver = udpTransiver; if (!UpnpHeader.Type.ST.isValidHeaderType(searchTarget.getClass())) { throw new IllegalArgumentException( @@ -76,9 +98,9 @@ public int getMxSeconds() { return mxSeconds; } - protected void execute() throws RouterException { + protected void execute() throws IOException { - Log.v(getClass().getName(), "Executing search for target: " + searchTarget.getString() + " with MX seconds: " + getMxSeconds()); + YaaccLogger.v(getClass().getName(), "Executing search for target: " + searchTarget.getString() + " with MX seconds: " + getMxSeconds()); OutgoingSearchRequest msg = new OutgoingSearchRequest(searchTarget, getMxSeconds()); prepareOutgoingSearchRequest(msg); @@ -86,15 +108,15 @@ protected void execute() throws RouterException { for (int i = 0; i < getBulkRepeat(); i++) { try { - getUpnpService().getRouter().send(msg); + udpTransiver.send(msg); // UDA 1.0 is silent about this but UDA 1.1 recommends "a few hundred milliseconds" - Log.v(getClass().getName(), "Sleeping " + getBulkIntervalMilliseconds() + " milliseconds"); + YaaccLogger.v(getClass().getName(), "Sleeping " + getBulkIntervalMilliseconds() + " milliseconds"); Thread.sleep(getBulkIntervalMilliseconds()); } catch (InterruptedException ex) { // Interruption means we stop sending search messages, e.g. on shutdown of thread pool - Log.v(getClass().getName(), "got exception on search", ex); + YaaccLogger.v(getClass().getName(), "got exception on search", ex); break; } } diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingAction.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingAction.java similarity index 63% rename from yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingAction.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingAction.java index 0446bcff..9e962a33 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingAction.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingAction.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,11 +31,8 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.sync; - -import android.util.Log; +package de.yaacc.upnp.protocol.sync; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.UnsupportedDataException; import org.fourthline.cling.model.action.ActionCancelledException; import org.fourthline.cling.model.action.ActionException; @@ -31,9 +46,14 @@ import org.fourthline.cling.model.message.header.UpnpHeader; import org.fourthline.cling.model.resource.ServiceControlResource; import org.fourthline.cling.model.types.ErrorCode; -import org.fourthline.cling.protocol.ReceivingSync; -import org.fourthline.cling.transport.RouterException; -import org.seamless.util.Exceptions; +import org.fourthline.cling.transport.impl.SOAPActionProcessorImpl; + +import java.io.IOException; + +import de.yaacc.upnp.protocol.ReceivingSync; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.util.Exceptions; +import de.yaacc.util.YaaccLogger; /** * Handles reception of control messages, invoking actions on local services. @@ -47,11 +67,15 @@ */ public class ReceivingAction extends ReceivingSync { - public ReceivingAction(UpnpService upnpService, StreamRequestMessage inputMessage) { - super(upnpService, inputMessage); + private final Registry registry; + SOAPActionProcessorImpl soapActionProcessor = new SOAPActionProcessorImpl(); + + public ReceivingAction(Registry registry, StreamRequestMessage inputMessage) { + super(inputMessage); + this.registry = registry; } - protected StreamResponseMessage executeSync() throws RouterException { + protected StreamResponseMessage executeSync() throws IOException { ContentTypeHeader contentTypeHeader = getInputMessage().getHeaders().getFirstHeader(UpnpHeader.Type.CONTENT_TYPE, ContentTypeHeader.class); @@ -60,26 +84,26 @@ protected StreamResponseMessage executeSync() throws RouterException { // 'If the CONTENT-TYPE header specifies an unsupported value (other then "text/xml") the // device must return an HTTP status code "415 Unsupported Media Type".' if (contentTypeHeader != null && !contentTypeHeader.isUDACompliantXML()) { - Log.w(getClass().getName(), "Received invalid Content-Type '" + contentTypeHeader + "': " + getInputMessage()); + YaaccLogger.w(getClass().getName(), "Received invalid Content-Type '" + contentTypeHeader + "': " + getInputMessage()); return new StreamResponseMessage(new UpnpResponse(UpnpResponse.Status.UNSUPPORTED_MEDIA_TYPE)); } if (contentTypeHeader == null) { - Log.w(getClass().getName(), "Received without Content-Type: " + getInputMessage()); + YaaccLogger.w(getClass().getName(), "Received without Content-Type: " + getInputMessage()); } ServiceControlResource resource = - getUpnpService().getRegistry().getResource( + registry.getResource( ServiceControlResource.class, getInputMessage().getUri() ); if (resource == null) { - Log.v(getClass().getName(), "No local resource found: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "No local resource found: " + getInputMessage()); return null; } - Log.v(getClass().getName(), "Found local action resource matching relative request URI: " + getInputMessage().getUri()); + YaaccLogger.v(getClass().getName(), "Found local action resource matching relative request URI: " + getInputMessage().getUri()); RemoteActionInvocation invocation; OutgoingActionResponseMessage responseMessage = null; @@ -90,14 +114,14 @@ protected StreamResponseMessage executeSync() throws RouterException { IncomingActionRequestMessage requestMessage = new IncomingActionRequestMessage(getInputMessage(), resource.getModel()); - Log.v(getClass().getName(), "Created incoming action request message: " + requestMessage); + YaaccLogger.v(getClass().getName(), "Created incoming action request message: " + requestMessage); invocation = new RemoteActionInvocation(requestMessage.getAction(), getRemoteClientInfo()); // Throws UnsupportedDataException if the body can't be read - Log.v(getClass().getName(), "Reading body of request message:" + requestMessage.getBodyString()); - getUpnpService().getConfiguration().getSoapActionProcessor().readBody(requestMessage, invocation); + YaaccLogger.v(getClass().getName(), "Reading body of request message:" + requestMessage.getBodyString()); + soapActionProcessor.readBody(requestMessage, invocation); - Log.v(getClass().getName(), "Executing on local service: " + invocation); + YaaccLogger.v(getClass().getName(), "Executing on local service: " + invocation); resource.getModel().getExecutor(invocation.getAction()).execute(invocation); if (invocation.getFailure() == null) { @@ -106,7 +130,7 @@ protected StreamResponseMessage executeSync() throws RouterException { } else { if (invocation.getFailure() instanceof ActionCancelledException) { - Log.v(getClass().getName(), "Action execution was cancelled, returning 404 to client"); + YaaccLogger.v(getClass().getName(), "Action execution was cancelled, returning 404 to client"); // A 404 status is appropriate for this situation: The resource is gone/not available and it's // a temporary condition. Most likely the cancellation happened because the client connection // has been dropped, so it doesn't really matter what we return here anyway. @@ -121,13 +145,13 @@ protected StreamResponseMessage executeSync() throws RouterException { } } catch (ActionException ex) { - Log.v(getClass().getName(), "Error executing local action: ", ex); + YaaccLogger.v(getClass().getName(), "Error executing local action: ", ex); invocation = new RemoteActionInvocation(ex, getRemoteClientInfo()); responseMessage = new OutgoingActionResponseMessage(UpnpResponse.Status.INTERNAL_SERVER_ERROR); } catch (UnsupportedDataException ex) { - Log.w(getClass().getName(), "Error reading action request XML body: " + ex.toString(), Exceptions.unwrap(ex)); + YaaccLogger.w(getClass().getName(), "Error reading action request XML body: " + ex.toString(), Exceptions.unwrap(ex)); invocation = new RemoteActionInvocation( @@ -142,15 +166,15 @@ protected StreamResponseMessage executeSync() throws RouterException { try { - Log.v(getClass().getName(), "Writing body of response message"); - getUpnpService().getConfiguration().getSoapActionProcessor().writeBody(responseMessage, invocation); + YaaccLogger.v(getClass().getName(), "Writing body of response message"); + soapActionProcessor.writeBody(responseMessage, invocation); - Log.v(getClass().getName(), "Returning finished response message: " + responseMessage); + YaaccLogger.v(getClass().getName(), "Returning finished response message: " + responseMessage); return responseMessage; } catch (UnsupportedDataException ex) { - Log.w(getClass().getName(), "Failure writing body of response message, sending '500 Internal Server Error' without body"); - Log.w(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); + YaaccLogger.w(getClass().getName(), "Failure writing body of response message, sending '500 Internal Server Error' without body"); + YaaccLogger.w(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); return new StreamResponseMessage(UpnpResponse.Status.INTERNAL_SERVER_ERROR); } } diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingEvent.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingEvent.java similarity index 60% rename from yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingEvent.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingEvent.java index d13e88b3..f1f67629 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingEvent.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingEvent.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,11 +31,8 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.sync; - -import android.util.Log; +package de.yaacc.upnp.protocol.sync; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.UnsupportedDataException; import org.fourthline.cling.model.gena.RemoteGENASubscription; import org.fourthline.cling.model.message.StreamRequestMessage; @@ -25,43 +40,55 @@ import org.fourthline.cling.model.message.gena.IncomingEventRequestMessage; import org.fourthline.cling.model.message.gena.OutgoingEventResponseMessage; import org.fourthline.cling.model.resource.ServiceEventCallbackResource; -import org.fourthline.cling.protocol.ReceivingSync; -import org.fourthline.cling.transport.RouterException; +import org.fourthline.cling.transport.impl.GENAEventProcessorImpl; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; + +import de.yaacc.upnp.protocol.ReceivingSync; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.util.YaaccLogger; /** * Handles incoming GENA event messages. *

* Attempts to find an outgoing (remote) subscription matching the callback and subscription identifier. * Once found, the GENA event message payload will be transformed and the - * {@link org.fourthline.cling.model.gena.RemoteGENASubscription#receive(org.fourthline.cling.model.types.UnsignedIntegerFourBytes, + * {@link RemoteGENASubscription#receive(org.fourthline.cling.model.types.UnsignedIntegerFourBytes, * java.util.Collection)} method will be called asynchronously using the executor - * returned by {@link org.fourthline.cling.UpnpServiceConfiguration#getRegistryListenerExecutor()}. + * . *

* * @author Christian Bauer */ public class ReceivingEvent extends ReceivingSync { - public ReceivingEvent(UpnpService upnpService, StreamRequestMessage inputMessage) { - super(upnpService, inputMessage); + private final Registry registry; + private final GENAEventProcessorImpl genaEventProcessor = new GENAEventProcessorImpl(); + private final ExecutorService executorService; + + public ReceivingEvent(Registry registry, StreamRequestMessage inputMessage) { + super(inputMessage); + this.registry = registry; + executorService = registry.getExecutorService(); } - protected OutgoingEventResponseMessage executeSync() throws RouterException { + protected OutgoingEventResponseMessage executeSync() throws IOException { if (!getInputMessage().isContentTypeTextUDA()) { - Log.w(getClass().getName(), "Received without or with invalid Content-Type: " + getInputMessage()); + YaaccLogger.w(getClass().getName(), "Received without or with invalid Content-Type: " + getInputMessage()); // We continue despite the invalid UPnP message because we can still hope to convert the content // return new StreamResponseMessage(new UpnpResponse(UpnpResponse.Status.UNSUPPORTED_MEDIA_TYPE)); } ServiceEventCallbackResource resource = - getUpnpService().getRegistry().getResource( + registry.getResource( ServiceEventCallbackResource.class, getInputMessage().getUri() ); if (resource == null) { - Log.v(getClass().getName(), "No local resource found: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "No local resource found: " + getInputMessage()); return new OutgoingEventResponseMessage(new UpnpResponse(UpnpResponse.Status.NOT_FOUND)); } @@ -70,37 +97,37 @@ protected OutgoingEventResponseMessage executeSync() throws RouterException { // Error conditions UDA 1.0 section 4.2.1 if (requestMessage.getSubscrptionId() == null) { - Log.v(getClass().getName(), "Subscription ID missing in event request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Subscription ID missing in event request: " + getInputMessage()); return new OutgoingEventResponseMessage(new UpnpResponse(UpnpResponse.Status.PRECONDITION_FAILED)); } if (!requestMessage.hasValidNotificationHeaders()) { - Log.v(getClass().getName(), "Missing NT and/or NTS headers in event request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Missing NT and/or NTS headers in event request: " + getInputMessage()); return new OutgoingEventResponseMessage(new UpnpResponse(UpnpResponse.Status.BAD_REQUEST)); } if (!requestMessage.hasValidNotificationHeaders()) { - Log.v(getClass().getName(), "Invalid NT and/or NTS headers in event request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Invalid NT and/or NTS headers in event request: " + getInputMessage()); return new OutgoingEventResponseMessage(new UpnpResponse(UpnpResponse.Status.PRECONDITION_FAILED)); } if (requestMessage.getSequence() == null) { - Log.v(getClass().getName(), "Sequence missing in event request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Sequence missing in event request: " + getInputMessage()); return new OutgoingEventResponseMessage(new UpnpResponse(UpnpResponse.Status.PRECONDITION_FAILED)); } try { - getUpnpService().getConfiguration().getGenaEventProcessor().readBody(requestMessage); + genaEventProcessor.readBody(requestMessage); } catch (final UnsupportedDataException ex) { - Log.v(getClass().getName(), "Can't read event message request body, " + ex); + YaaccLogger.v(getClass().getName(), "Can't read event message request body, " + ex); // Pass the parsing failure on to any listeners, so they can take action if necessary final RemoteGENASubscription subscription = - getUpnpService().getRegistry().getRemoteSubscription(requestMessage.getSubscrptionId()); + registry.getRemoteSubscription(requestMessage.getSubscrptionId()); if (subscription != null) { - getUpnpService().getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { subscription.invalidMessage(ex); @@ -115,17 +142,17 @@ public void run() { // get the remove subscription, if the subscription can't be found, wait for pending subscription // requests to finish final RemoteGENASubscription subscription = - getUpnpService().getRegistry().getWaitRemoteSubscription(requestMessage.getSubscrptionId()); + registry.getWaitRemoteSubscription(requestMessage.getSubscrptionId()); if (subscription == null) { - Log.v(getClass().getName(), "Invalid subscription ID, no active subscription: " + requestMessage); + YaaccLogger.v(getClass().getName(), "Invalid subscription ID, no active subscription: " + requestMessage); return new OutgoingEventResponseMessage(new UpnpResponse(UpnpResponse.Status.PRECONDITION_FAILED)); } - getUpnpService().getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { - Log.v(getClass().getName(), "Calling active subscription with event state variable values"); + YaaccLogger.v(getClass().getName(), "Calling active subscription with event state variable values"); subscription.receive( requestMessage.getSequence(), requestMessage.getStateVariableValues() diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingRetrieval.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingRetrieval.java similarity index 64% rename from yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingRetrieval.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingRetrieval.java index 4803227e..784f2804 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingRetrieval.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingRetrieval.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,14 +31,13 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.sync; - -import android.util.Log; +package de.yaacc.upnp.protocol.sync; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.binding.xml.DescriptorBindingException; import org.fourthline.cling.binding.xml.DeviceDescriptorBinder; import org.fourthline.cling.binding.xml.ServiceDescriptorBinder; +import org.fourthline.cling.binding.xml.UDA10DeviceDescriptorBinderImpl; +import org.fourthline.cling.binding.xml.UDA10ServiceDescriptorBinderSAXImpl; import org.fourthline.cling.model.message.StreamRequestMessage; import org.fourthline.cling.model.message.StreamResponseMessage; import org.fourthline.cling.model.message.UpnpResponse; @@ -34,12 +51,16 @@ import org.fourthline.cling.model.resource.IconResource; import org.fourthline.cling.model.resource.Resource; import org.fourthline.cling.model.resource.ServiceDescriptorResource; -import org.fourthline.cling.protocol.ReceivingSync; -import org.fourthline.cling.transport.RouterException; -import org.seamless.util.Exceptions; +import java.io.IOException; import java.net.URI; +import de.yaacc.upnp.protocol.ReceivingSync; +import de.yaacc.upnp.protocol.UpnpProtocolHandler; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.util.Exceptions; +import de.yaacc.util.YaaccLogger; + /** * Handles reception of device/service descriptor and icon retrieval messages. * @@ -55,26 +76,27 @@ */ public class ReceivingRetrieval extends ReceivingSync { + private Registry registry; - public ReceivingRetrieval(UpnpService upnpService, StreamRequestMessage inputMessage) { - super(upnpService, inputMessage); + public ReceivingRetrieval(Registry registry, StreamRequestMessage inputMessage) { + super(inputMessage); + this.registry = registry; } - protected StreamResponseMessage executeSync() throws RouterException { + protected StreamResponseMessage executeSync() throws IOException { if (!getInputMessage().hasHostHeader()) { - Log.v(getClass().getName(), "Ignoring message, missing HOST header: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Ignoring message, missing HOST header: " + getInputMessage()); return new StreamResponseMessage(new UpnpResponse(UpnpResponse.Status.PRECONDITION_FAILED)); } URI requestedURI = getInputMessage().getOperation().getURI(); - Resource foundResource = getUpnpService().getRegistry().getResource(requestedURI); - + Resource foundResource = registry.getResource(requestedURI); if (foundResource == null) { foundResource = onResourceNotFound(requestedURI); if (foundResource == null) { - Log.v(getClass().getName(), "No local resource found: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "No local resource found: " + getInputMessage()); return null; } } @@ -90,15 +112,15 @@ protected StreamResponseMessage createResponse(URI requestedURI, Resource resour if (DeviceDescriptorResource.class.isAssignableFrom(resource.getClass())) { - Log.v(getClass().getName(), "Found local device matching relative request URI: " + requestedURI); + YaaccLogger.v(getClass().getName(), "Found local device matching relative request URI: " + requestedURI); LocalDevice device = (LocalDevice) resource.getModel(); - DeviceDescriptorBinder deviceDescriptorBinder = - getUpnpService().getConfiguration().getDeviceDescriptorBinderUDA10(); + DeviceDescriptorBinder deviceDescriptorBinder = new UDA10DeviceDescriptorBinderImpl(); + String deviceDescriptor = deviceDescriptorBinder.generate( device, getRemoteClientInfo(), - getUpnpService().getConfiguration().getNamespace() + UpnpProtocolHandler.NAMESPACE ); response = new StreamResponseMessage( deviceDescriptor, @@ -107,11 +129,10 @@ protected StreamResponseMessage createResponse(URI requestedURI, Resource resour } else if (ServiceDescriptorResource.class.isAssignableFrom(resource.getClass())) { - Log.v(getClass().getName(), "Found local service matching relative request URI: " + requestedURI); + YaaccLogger.v(getClass().getName(), "Found local service matching relative request URI: " + requestedURI); LocalService service = (LocalService) resource.getModel(); - ServiceDescriptorBinder serviceDescriptorBinder = - getUpnpService().getConfiguration().getServiceDescriptorBinderUDA10(); + ServiceDescriptorBinder serviceDescriptorBinder = new UDA10ServiceDescriptorBinderSAXImpl(); String serviceDescriptor = serviceDescriptorBinder.generate(service); response = new StreamResponseMessage( serviceDescriptor, @@ -120,19 +141,19 @@ protected StreamResponseMessage createResponse(URI requestedURI, Resource resour } else if (IconResource.class.isAssignableFrom(resource.getClass())) { - Log.v(getClass().getName(), "Found local icon matching relative request URI: " + requestedURI); + YaaccLogger.v(getClass().getName(), "Found local icon matching relative request URI: " + requestedURI); Icon icon = (Icon) resource.getModel(); response = new StreamResponseMessage(icon.getData(), icon.getMimeType()); } else { - Log.v(getClass().getName(), "Ignoring GET for found local resource: " + resource); + YaaccLogger.v(getClass().getName(), "Ignoring GET for found local resource: " + resource); return null; } } catch (DescriptorBindingException ex) { - Log.w(getClass().getName(), "Error generating requested device/service descriptor: " + ex.toString()); - Log.w(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); + YaaccLogger.w(getClass().getName(), "Error generating requested device/service descriptor: " + ex.toString()); + YaaccLogger.w(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); response = new StreamResponseMessage(UpnpResponse.Status.INTERNAL_SERVER_ERROR); } diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingSubscribe.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingSubscribe.java similarity index 60% rename from yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingSubscribe.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingSubscribe.java index ed7a0e31..2b687bd5 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingSubscribe.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingSubscribe.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,11 +31,8 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.sync; - -import android.util.Log; +package de.yaacc.upnp.protocol.sync; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.gena.CancelReason; import org.fourthline.cling.model.gena.LocalGENASubscription; import org.fourthline.cling.model.message.StreamRequestMessage; @@ -27,18 +42,23 @@ import org.fourthline.cling.model.message.gena.OutgoingSubscribeResponseMessage; import org.fourthline.cling.model.meta.LocalService; import org.fourthline.cling.model.resource.ServiceEventSubscriptionResource; -import org.fourthline.cling.protocol.ReceivingSync; -import org.fourthline.cling.transport.RouterException; -import org.seamless.util.Exceptions; +import java.io.IOException; import java.net.URL; import java.util.List; +import java.util.concurrent.ExecutorService; + +import de.yaacc.upnp.protocol.ReceivingSync; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.Exceptions; +import de.yaacc.util.YaaccLogger; /** * Handles reception of GENA event subscription (initial and renewal) messages. *

* This protocol tries to find a local event subscription URI matching the requested URI, - * then creates a new {@link org.fourthline.cling.model.gena.LocalGENASubscription} if no + * then creates a new {@link LocalGENASubscription} if no * subscription identifer was supplied. *

*

@@ -57,26 +77,32 @@ public class ReceivingSubscribe extends ReceivingSync { + private final Registry registry; + private final HttpRequestSender httpRequestSender; + private final ExecutorService executorService; protected LocalGENASubscription subscription; - public ReceivingSubscribe(UpnpService upnpService, StreamRequestMessage inputMessage) { - super(upnpService, inputMessage); + public ReceivingSubscribe(Registry registry, HttpRequestSender httpRequestSender, StreamRequestMessage inputMessage) { + super(inputMessage); + this.httpRequestSender = httpRequestSender; + this.registry = registry; + executorService = registry.getExecutorService(); } - protected OutgoingSubscribeResponseMessage executeSync() throws RouterException { + protected OutgoingSubscribeResponseMessage executeSync() throws IOException { ServiceEventSubscriptionResource resource = - getUpnpService().getRegistry().getResource( + registry.getResource( ServiceEventSubscriptionResource.class, getInputMessage().getUri() ); if (resource == null) { - Log.v(getClass().getName(), "No local resource found: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "No local resource found: " + getInputMessage()); return null; } - Log.v(getClass().getName(), "Found local event subscription matching relative request URI: " + getInputMessage().getUri()); + YaaccLogger.v(getClass().getName(), "Found local event subscription matching relative request URI: " + getInputMessage().getUri()); IncomingSubscribeRequestMessage requestMessage = new IncomingSubscribeRequestMessage(getInputMessage(), resource.getModel()); @@ -84,7 +110,7 @@ protected OutgoingSubscribeResponseMessage executeSync() throws RouterException // Error conditions UDA 1.0 section 4.1.1 and 4.1.2 if (requestMessage.getSubscriptionId() != null && (requestMessage.hasNotificationHeader() || requestMessage.getCallbackURLs() != null)) { - Log.v(getClass().getName(), "Subscription ID and NT or Callback in subscribe request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Subscription ID and NT or Callback in subscribe request: " + getInputMessage()); return new OutgoingSubscribeResponseMessage(UpnpResponse.Status.BAD_REQUEST); } @@ -93,7 +119,7 @@ protected OutgoingSubscribeResponseMessage executeSync() throws RouterException } else if (requestMessage.hasNotificationHeader() && requestMessage.getCallbackURLs() != null) { return processNewSubscription(resource.getModel(), requestMessage); } else { - Log.v(getClass().getName(), "No subscription ID, no NT or Callback, neither subscription or renewal: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "No subscription ID, no NT or Callback, neither subscription or renewal: " + getInputMessage()); return new OutgoingSubscribeResponseMessage(UpnpResponse.Status.PRECONDITION_FAILED); } @@ -102,20 +128,20 @@ protected OutgoingSubscribeResponseMessage executeSync() throws RouterException protected OutgoingSubscribeResponseMessage processRenewal(LocalService service, IncomingSubscribeRequestMessage requestMessage) { - subscription = getUpnpService().getRegistry().getLocalSubscription(requestMessage.getSubscriptionId()); + subscription = registry.getLocalSubscription(requestMessage.getSubscriptionId()); // Error conditions UDA 1.0 section 4.1.1 and 4.1.2 if (subscription == null) { - Log.v(getClass().getName(), "Invalid subscription ID for renewal request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Invalid subscription ID for renewal request: " + getInputMessage()); return new OutgoingSubscribeResponseMessage(UpnpResponse.Status.PRECONDITION_FAILED); } - Log.v(getClass().getName(), "Renewing subscription: " + subscription); + YaaccLogger.v(getClass().getName(), "Renewing subscription: " + subscription); subscription.setSubscriptionDuration(requestMessage.getRequestedTimeoutSeconds()); - if (getUpnpService().getRegistry().updateLocalSubscription(subscription)) { + if (registry.updateLocalSubscription(subscription)) { return new OutgoingSubscribeResponseMessage(subscription); } else { - Log.v(getClass().getName(), "Subscription went away before it could be renewed: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Subscription went away before it could be renewed: " + getInputMessage()); return new OutgoingSubscribeResponseMessage(UpnpResponse.Status.PRECONDITION_FAILED); } } @@ -126,21 +152,18 @@ protected OutgoingSubscribeResponseMessage processNewSubscription(LocalService s // Error conditions UDA 1.0 section 4.1.1 and 4.1.2 if (callbackURLs == null || callbackURLs.size() == 0) { - Log.v(getClass().getName(), "Missing or invalid Callback URLs in subscribe request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Missing or invalid Callback URLs in subscribe request: " + getInputMessage()); return new OutgoingSubscribeResponseMessage(UpnpResponse.Status.PRECONDITION_FAILED); } if (!requestMessage.hasNotificationHeader()) { - Log.v(getClass().getName(), "Missing or invalid NT header in subscribe request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Missing or invalid NT header in subscribe request: " + getInputMessage()); return new OutgoingSubscribeResponseMessage(UpnpResponse.Status.PRECONDITION_FAILED); } Integer timeoutSeconds; - if (getUpnpService().getConfiguration().isReceivedSubscriptionTimeoutIgnored()) { - timeoutSeconds = null; // Use default value - } else { - timeoutSeconds = requestMessage.getRequestedTimeoutSeconds(); - } + timeoutSeconds = requestMessage.getRequestedTimeoutSeconds(); + try { subscription = new LocalGENASubscription(service, timeoutSeconds, callbackURLs) { @@ -152,20 +175,18 @@ public void ended(CancelReason reason) { public void eventReceived() { // The only thing we are interested in, sending an event when the state changes - getUpnpService().getConfiguration().getSyncProtocolExecutorService().execute( - getUpnpService().getProtocolFactory().createSendingEvent(this) - ); + executorService.execute(new SendingEvent(httpRequestSender, this)); } }; } catch (Exception ex) { - Log.w(getClass().getName(), "Couldn't create local subscription to service: ", Exceptions.unwrap(ex)); + YaaccLogger.w(getClass().getName(), "Couldn't create local subscription to service: ", Exceptions.unwrap(ex)); return new OutgoingSubscribeResponseMessage(UpnpResponse.Status.INTERNAL_SERVER_ERROR); } - Log.v(getClass().getName(), "Adding subscription to registry: " + subscription); - getUpnpService().getRegistry().addLocalSubscription(subscription); + YaaccLogger.v(getClass().getName(), "Adding subscription to registry: " + subscription); + registry.addLocalSubscription(subscription); - Log.v(getClass().getName(), "Returning subscription response, waiting to send initial event"); + YaaccLogger.v(getClass().getName(), "Returning subscription response, waiting to send initial event"); return new OutgoingSubscribeResponseMessage(subscription); } @@ -181,31 +202,29 @@ public void responseSent(StreamResponseMessage responseMessage) { // event message arrives later than the first on-change event message. Shouldn't be a problem as the // subscriber is supposed to figure out what to do with out-of-sequence messages. I would be // surprised though if actual implementations won't crash! - Log.v(getClass().getName(), "Establishing subscription"); + YaaccLogger.v(getClass().getName(), "Establishing subscription"); subscription.registerOnService(); subscription.establish(); - Log.v(getClass().getName(), "Response to subscription sent successfully, now sending initial event asynchronously"); - getUpnpService().getConfiguration().getAsyncProtocolExecutor().execute( - getUpnpService().getProtocolFactory().createSendingEvent(subscription) - ); + YaaccLogger.v(getClass().getName(), "Response to subscription sent successfully, now sending initial event asynchronously"); + executorService.execute(new SendingEvent(httpRequestSender, subscription)); } else if (subscription.getCurrentSequence().getValue() == 0) { - Log.v(getClass().getName(), "Subscription request's response aborted, not sending initial event"); + YaaccLogger.v(getClass().getName(), "Subscription request's response aborted, not sending initial event"); if (responseMessage == null) { - Log.v(getClass().getName(), "Reason: No response at all from subscriber"); + YaaccLogger.v(getClass().getName(), "Reason: No response at all from subscriber"); } else { - Log.v(getClass().getName(), "Reason: " + responseMessage.getOperation()); + YaaccLogger.v(getClass().getName(), "Reason: " + responseMessage.getOperation()); } - Log.v(getClass().getName(), "Removing subscription from registry: " + subscription); - getUpnpService().getRegistry().removeLocalSubscription(subscription); + YaaccLogger.v(getClass().getName(), "Removing subscription from registry: " + subscription); + registry.removeLocalSubscription(subscription); } } @Override public void responseException(Throwable t) { if (subscription == null) return; // Nothing to do, we didn't get that far - Log.v(getClass().getName(), "Response could not be send to subscriber, removing local GENA subscription: " + subscription); - getUpnpService().getRegistry().removeLocalSubscription(subscription); + YaaccLogger.v(getClass().getName(), "Response could not be send to subscriber, removing local GENA subscription: " + subscription); + registry.removeLocalSubscription(subscription); } } \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingUnsubscribe.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingUnsubscribe.java similarity index 52% rename from yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingUnsubscribe.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingUnsubscribe.java index 0e92ded9..001ff220 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/ReceivingUnsubscribe.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/ReceivingUnsubscribe.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,19 +31,20 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.sync; - -import android.util.Log; +package de.yaacc.upnp.protocol.sync; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.gena.LocalGENASubscription; import org.fourthline.cling.model.message.StreamRequestMessage; import org.fourthline.cling.model.message.StreamResponseMessage; import org.fourthline.cling.model.message.UpnpResponse; import org.fourthline.cling.model.message.gena.IncomingUnsubscribeRequestMessage; import org.fourthline.cling.model.resource.ServiceEventSubscriptionResource; -import org.fourthline.cling.protocol.ReceivingSync; -import org.fourthline.cling.transport.RouterException; + +import java.io.IOException; + +import de.yaacc.upnp.protocol.ReceivingSync; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.util.YaaccLogger; /** * Handles reception of GENA event unsubscribe messages. @@ -35,24 +54,27 @@ public class ReceivingUnsubscribe extends ReceivingSync { - public ReceivingUnsubscribe(UpnpService upnpService, StreamRequestMessage inputMessage) { - super(upnpService, inputMessage); + private final Registry registry; + + public ReceivingUnsubscribe(Registry registry, StreamRequestMessage inputMessage) { + super(inputMessage); + this.registry = registry; } - protected StreamResponseMessage executeSync() throws RouterException { + protected StreamResponseMessage executeSync() throws IOException { ServiceEventSubscriptionResource resource = - getUpnpService().getRegistry().getResource( + registry.getResource( ServiceEventSubscriptionResource.class, getInputMessage().getUri() ); if (resource == null) { - Log.v(getClass().getName(), "No local resource found: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "No local resource found: " + getInputMessage()); return null; } - Log.v(getClass().getName(), "Found local event subscription matching relative request URI: " + getInputMessage().getUri()); + YaaccLogger.v(getClass().getName(), "Found local event subscription matching relative request URI: " + getInputMessage().getUri()); IncomingUnsubscribeRequestMessage requestMessage = new IncomingUnsubscribeRequestMessage(getInputMessage(), resource.getModel()); @@ -60,23 +82,23 @@ protected StreamResponseMessage executeSync() throws RouterException { // Error conditions UDA 1.0 section 4.1.3 if (requestMessage.getSubscriptionId() != null && (requestMessage.hasNotificationHeader() || requestMessage.hasCallbackHeader())) { - Log.v(getClass().getName(), "Subscription ID and NT or Callback in unsubcribe request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Subscription ID and NT or Callback in unsubcribe request: " + getInputMessage()); return new StreamResponseMessage(UpnpResponse.Status.BAD_REQUEST); } LocalGENASubscription subscription = - getUpnpService().getRegistry().getLocalSubscription(requestMessage.getSubscriptionId()); + registry.getLocalSubscription(requestMessage.getSubscriptionId()); if (subscription == null) { - Log.v(getClass().getName(), "Invalid subscription ID for unsubscribe request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Invalid subscription ID for unsubscribe request: " + getInputMessage()); return new StreamResponseMessage(UpnpResponse.Status.PRECONDITION_FAILED); } - Log.v(getClass().getName(), "Unregistering subscription: " + subscription); - if (getUpnpService().getRegistry().removeLocalSubscription(subscription)) { + YaaccLogger.v(getClass().getName(), "Unregistering subscription: " + subscription); + if (registry.removeLocalSubscription(subscription)) { subscription.end(null); // No reason, just an unsubscribe } else { - Log.v(getClass().getName(), "Subscription was already removed from registry"); + YaaccLogger.v(getClass().getName(), "Subscription was already removed from registry"); } return new StreamResponseMessage(UpnpResponse.Status.OK); diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/SendingAction.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/SendingAction.java similarity index 55% rename from yaacc/src/main/java/org/fourthline/cling/protocol/sync/SendingAction.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/sync/SendingAction.java index b307585e..c2b39d6b 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/SendingAction.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/SendingAction.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,13 +31,11 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.sync; +package de.yaacc.upnp.protocol.sync; -import android.util.Log; +import androidx.annotation.Nullable; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.UnsupportedDataException; -import org.fourthline.cling.model.action.ActionCancelledException; import org.fourthline.cling.model.action.ActionException; import org.fourthline.cling.model.action.ActionInvocation; import org.fourthline.cling.model.message.StreamResponseMessage; @@ -28,22 +44,25 @@ import org.fourthline.cling.model.message.control.OutgoingActionRequestMessage; import org.fourthline.cling.model.meta.Device; import org.fourthline.cling.model.types.ErrorCode; -import org.fourthline.cling.protocol.SendingSync; -import org.fourthline.cling.transport.RouterException; -import org.seamless.util.Exceptions; +import org.fourthline.cling.transport.impl.SOAPActionProcessorImpl; +import java.io.IOException; import java.net.URL; +import de.yaacc.upnp.protocol.SendingSync; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.YaaccLogger; + /** - * Sending control message, transforming a local {@link org.fourthline.cling.model.action.ActionInvocation}. + * Sending control message, transforming a local {@link ActionInvocation}. *

* Writes the outgoing message's body with the {@link org.fourthline.cling.transport.spi.SOAPActionProcessor}. * This protocol will return null if no response was received from the control target host. * In all other cases, even if only the processing of message content failed, this protocol will - * return an {@link org.fourthline.cling.model.message.control.IncomingActionResponseMessage}. Any error - * details of a failed response ({@link org.fourthline.cling.model.message.UpnpResponse#isFailed()}) are + * return an {@link IncomingActionResponseMessage}. Any error + * details of a failed response ({@link UpnpResponse#isFailed()}) are * available with - * {@link org.fourthline.cling.model.action.ActionInvocation#setFailure(org.fourthline.cling.model.action.ActionException)}. + * {@link ActionInvocation#setFailure(ActionException)}. *

* * @author Christian Bauer @@ -51,27 +70,31 @@ public class SendingAction extends SendingSync { final protected ActionInvocation actionInvocation; + SOAPActionProcessorImpl soapActionProcessor = new SOAPActionProcessorImpl(); + private final HttpRequestSender httpRequestSender; - public SendingAction(UpnpService upnpService, ActionInvocation actionInvocation, URL controlURL) { - super(upnpService, new OutgoingActionRequestMessage(actionInvocation, controlURL)); + public SendingAction(HttpRequestSender httpRequestSender, ActionInvocation actionInvocation, URL controlURL) { + super(new OutgoingActionRequestMessage(actionInvocation, controlURL)); this.actionInvocation = actionInvocation; + this.httpRequestSender = httpRequestSender; + } - protected IncomingActionResponseMessage executeSync() throws RouterException { + protected IncomingActionResponseMessage executeSync() throws IOException { return invokeRemote(getInputMessage()); } - protected IncomingActionResponseMessage invokeRemote(OutgoingActionRequestMessage requestMessage) throws RouterException { + protected IncomingActionResponseMessage invokeRemote(OutgoingActionRequestMessage requestMessage) throws IOException { Device device = actionInvocation.getAction().getService().getDevice(); - Log.v(getClass().getName(), "Sending outgoing action call '" + actionInvocation.getAction().getName() + "' to remote service of: " + device); + YaaccLogger.v(getClass().getName(), "Sending outgoing action call '" + actionInvocation.getAction().getName() + "' to remote service of: " + device); IncomingActionResponseMessage responseMessage = null; try { StreamResponseMessage streamResponse = sendRemoteRequest(requestMessage); if (streamResponse == null) { - Log.v(getClass().getName(), "No connection or no no response received, returning null"); + YaaccLogger.v(getClass().getName(), "No connection or no no response received, returning null"); actionInvocation.setFailure(new ActionException(ErrorCode.ACTION_FAILED, "Connection error or no response received")); return null; } @@ -79,7 +102,7 @@ protected IncomingActionResponseMessage invokeRemote(OutgoingActionRequestMessag responseMessage = new IncomingActionResponseMessage(streamResponse); if (responseMessage.isFailedNonRecoverable()) { - Log.v(getClass().getName(), "Response was a non-recoverable failure: " + responseMessage); + YaaccLogger.v(getClass().getName(), "Response was a non-recoverable failure: " + responseMessage); throw new ActionException( ErrorCode.ACTION_FAILED, "Non-recoverable remote execution failure: " + responseMessage.getOperation().getResponseDetails() ); @@ -93,7 +116,7 @@ protected IncomingActionResponseMessage invokeRemote(OutgoingActionRequestMessag } catch (ActionException ex) { - Log.v(getClass().getName(), "Remote action invocation failed, returning Internal Server Error message: " + ex.getMessage()); + YaaccLogger.v(getClass().getName(), "Remote action invocation failed, returning Internal Server Error message: " + ex.getMessage()); actionInvocation.setFailure(ex); if (responseMessage == null || !responseMessage.getOperation().isFailed()) { return new IncomingActionResponseMessage(new UpnpResponse(UpnpResponse.Status.INTERNAL_SERVER_ERROR)); @@ -104,38 +127,45 @@ protected IncomingActionResponseMessage invokeRemote(OutgoingActionRequestMessag } protected StreamResponseMessage sendRemoteRequest(OutgoingActionRequestMessage requestMessage) - throws ActionException, RouterException { + throws ActionException, IOException { try { - Log.v(getClass().getName(), "Writing SOAP request body of: " + requestMessage); - getUpnpService().getConfiguration().getSoapActionProcessor().writeBody(requestMessage, actionInvocation); + YaaccLogger.v(getClass().getName(), "Writing SOAP request body of: " + requestMessage); + soapActionProcessor.writeBody(requestMessage, actionInvocation); - Log.v(getClass().getName(), "Sending SOAP body of message as stream to remote device"); - return getUpnpService().getRouter().send(requestMessage); - } catch (RouterException ex) { - Throwable cause = Exceptions.unwrap(ex); - if (cause instanceof InterruptedException) { - Log.v(getClass().getName(), "Sending action request message was interrupted: " + cause); + YaaccLogger.v(getClass().getName(), "Sending SOAP body of message as stream to remote device"); + return httpRequestSender.send(requestMessage); - throw new ActionCancelledException((InterruptedException) cause); - } - throw ex; } catch (UnsupportedDataException ex) { - Log.v(getClass().getName(), "Error writing SOAP body: " + ex); - Log.v(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); + YaaccLogger.v(getClass().getName(), "Error writing SOAP body: " + ex); + YaaccLogger.v(getClass().getName(), "Exception root cause: ", unwrap(ex)); + + throw new ActionException(ErrorCode.ACTION_FAILED, "Error writing request message. " + ex.getMessage()); + } catch (IOException ex) { + YaaccLogger.v(getClass().getName(), "Error writing SOAP body: " + ex); + YaaccLogger.v(getClass().getName(), "Exception root cause: ", unwrap(ex)); throw new ActionException(ErrorCode.ACTION_FAILED, "Error writing request message. " + ex.getMessage()); } } + @Nullable + private static Throwable unwrap(Exception ex) { + Throwable cause = ex; + for (Throwable current = ex; current != null; current = current.getCause()) { + cause = current; + } + return cause; + } + protected void handleResponse(IncomingActionResponseMessage responseMsg) throws ActionException { try { - Log.v(getClass().getName(), "Received response for outgoing call, reading SOAP response body: " + responseMsg); - getUpnpService().getConfiguration().getSoapActionProcessor().readBody(responseMsg, actionInvocation); + YaaccLogger.v(getClass().getName(), "Received response for outgoing call, reading SOAP response body: " + responseMsg); + soapActionProcessor.readBody(responseMsg, actionInvocation); } catch (UnsupportedDataException ex) { - Log.v(getClass().getName(), "Error reading SOAP body: " + ex); - Log.v(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); + YaaccLogger.v(getClass().getName(), "Error reading SOAP body: " + ex); + YaaccLogger.v(getClass().getName(), "Exception root cause: ", unwrap(ex)); throw new ActionException( ErrorCode.ACTION_FAILED, "Error reading SOAP response message. " + ex.getMessage(), @@ -147,11 +177,12 @@ protected void handleResponse(IncomingActionResponseMessage responseMsg) throws protected void handleResponseFailure(IncomingActionResponseMessage responseMsg) throws ActionException { try { - Log.v(getClass().getName(), "Received response with Internal Server Error, reading SOAP failure message"); - getUpnpService().getConfiguration().getSoapActionProcessor().readBody(responseMsg, actionInvocation); + YaaccLogger.v(getClass().getName(), "Received response with Internal Server Error, reading SOAP failure message"); + + soapActionProcessor.readBody(responseMsg, actionInvocation); } catch (UnsupportedDataException ex) { - Log.v(getClass().getName(), "Error reading SOAP body: " + ex); - Log.v(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); + YaaccLogger.v(getClass().getName(), "Error reading SOAP body: " + ex); + YaaccLogger.v(getClass().getName(), "Exception root cause: ", unwrap(ex)); throw new ActionException( ErrorCode.ACTION_FAILED, "Error reading SOAP response failure message. " + ex.getMessage(), diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/SendingEvent.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/SendingEvent.java similarity index 55% rename from yaacc/src/main/java/org/fourthline/cling/protocol/sync/SendingEvent.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/sync/SendingEvent.java index 6499eda5..afa50fdd 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/SendingEvent.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/SendingEvent.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,24 +31,25 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.sync; - -import android.util.Log; +package de.yaacc.upnp.protocol.sync; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.gena.LocalGENASubscription; import org.fourthline.cling.model.message.StreamResponseMessage; import org.fourthline.cling.model.message.gena.OutgoingEventRequestMessage; import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.protocol.SendingSync; -import org.fourthline.cling.transport.RouterException; +import org.fourthline.cling.transport.impl.GENAEventProcessorImpl; +import java.io.IOException; import java.net.URL; +import de.yaacc.upnp.protocol.SendingSync; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.YaaccLogger; + /** * Sending GENA event messages to remote subscribers. *

- * Any {@link org.fourthline.cling.model.gena.LocalGENASubscription} instantiates and executes this protocol + * Any {@link LocalGENASubscription} instantiates and executes this protocol * when the state of a local service changes. However, a remote subscriber might require event * notification messages on more than one callback URL, so this protocol potentially sends * many messages. What is returned is always the last response, that is, the response for the @@ -44,10 +63,11 @@ public class SendingEvent extends SendingSyncRENEWAL_FAILED reason will be used, however, * the response might be null if no response was received from the remote host. *

@@ -43,27 +65,28 @@ public class SendingRenewal extends SendingSync - * Calls the {@link org.fourthline.cling.model.gena.RemoteGENASubscription#establish()} method + * Calls the {@link RemoteGENASubscription#establish()} method * if the subscription request was responded to correctly. *

*

- * The {@link org.fourthline.cling.model.gena.RemoteGENASubscription#fail(org.fourthline.cling.model.message.UpnpResponse)} + * The {@link RemoteGENASubscription#fail(org.fourthline.cling.model.message.UpnpResponse)} * method will be called if the request failed. No response from the remote host is indicated with * a null argument value. Note that this is also the response if the subscription has * to be aborted early, when no local stream server for callback URL creation is available. This is @@ -48,30 +70,35 @@ public class SendingSubscribe extends SendingSync { final protected RemoteGENASubscription subscription; + private final HttpRequestSender httpRequestSender; + private final Registry registry; + private final ExecutorService executorService; - public SendingSubscribe(UpnpService upnpService, + public SendingSubscribe(Registry registry, HttpRequestSender httpRequestSender, RemoteGENASubscription subscription, - List activeStreamServers) { - super( - upnpService, - new OutgoingSubscribeRequestMessage( + NetworkAddress activeStreamServers) { + super(new OutgoingSubscribeRequestMessage( subscription, subscription.getEventCallbackURLs( - activeStreamServers, - upnpService.getConfiguration().getNamespace() + List.of(activeStreamServers), + UpnpProtocolHandler.NAMESPACE ), - upnpService.getConfiguration().getEventSubscriptionHeaders(subscription.getService()) + null ) ); this.subscription = subscription; + this.httpRequestSender = httpRequestSender; + this.registry = registry; + executorService = Executors.newFixedThreadPool(20); + } - protected IncomingSubscribeResponseMessage executeSync() throws RouterException { + protected IncomingSubscribeResponseMessage executeSync() throws IOException { if (!getInputMessage().hasCallbackURLs()) { - Log.v(getClass().getName(), "Subscription failed, no active local callback URLs available (network disabled?)"); - getUpnpService().getConfiguration().getRegistryListenerExecutor().execute( + YaaccLogger.v(getClass().getName(), "Subscription failed, no active local callback URLs available (network disabled?)"); + executorService.execute( new Runnable() { public void run() { subscription.fail(null); @@ -81,17 +108,17 @@ public void run() { return null; } - Log.v(getClass().getName(), "Sending subscription request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Sending subscription request: " + getInputMessage()); try { // register this pending Subscription to bloc if the notification is received before the // registration result. - getUpnpService().getRegistry().registerPendingRemoteSubscription(subscription); + registry.registerPendingRemoteSubscription(subscription); StreamResponseMessage response = null; try { - response = getUpnpService().getRouter().send(getInputMessage()); - } catch (RouterException ex) { + response = httpRequestSender.send(getInputMessage()); + } catch (IOException ex) { onSubscriptionFailure(); return null; } @@ -104,8 +131,8 @@ public void run() { final IncomingSubscribeResponseMessage responseMessage = new IncomingSubscribeResponseMessage(response); if (response.getOperation().isFailed()) { - Log.v(getClass().getName(), "Subscription failed, response was: " + responseMessage); - getUpnpService().getConfiguration().getRegistryListenerExecutor().execute( + YaaccLogger.v(getClass().getName(), "Subscription failed, response was: " + responseMessage); + executorService.execute( new Runnable() { public void run() { subscription.fail(responseMessage.getOperation()); @@ -113,8 +140,8 @@ public void run() { } ); } else if (!responseMessage.isValidHeaders()) { - Log.v(getClass().getName(), "Subscription failed, invalid or missing (SID, Timeout) response headers"); - getUpnpService().getConfiguration().getRegistryListenerExecutor().execute( + YaaccLogger.v(getClass().getName(), "Subscription failed, invalid or missing (SID, Timeout) response headers"); + executorService.execute( new Runnable() { public void run() { subscription.fail(responseMessage.getOperation()); @@ -123,13 +150,13 @@ public void run() { ); } else { - Log.v(getClass().getName(), "Subscription established, adding to registry, response was: " + response); + YaaccLogger.v(getClass().getName(), "Subscription established, adding to registry, response was: " + response); subscription.setSubscriptionId(responseMessage.getSubscriptionId()); subscription.setActualSubscriptionDurationSeconds(responseMessage.getSubscriptionDurationSeconds()); - getUpnpService().getRegistry().addRemoteSubscription(subscription); + registry.addRemoteSubscription(subscription); - getUpnpService().getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { subscription.establish(); @@ -140,13 +167,13 @@ public void run() { } return responseMessage; } finally { - getUpnpService().getRegistry().unregisterPendingRemoteSubscription(subscription); + registry.unregisterPendingRemoteSubscription(subscription); } } protected void onSubscriptionFailure() { - Log.v(getClass().getName(), "Subscription failed"); - getUpnpService().getConfiguration().getRegistryListenerExecutor().execute( + YaaccLogger.v(getClass().getName(), "Subscription failed"); + executorService.execute( new Runnable() { public void run() { subscription.fail(null); diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/SendingUnsubscribe.java b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/SendingUnsubscribe.java similarity index 50% rename from yaacc/src/main/java/org/fourthline/cling/protocol/sync/SendingUnsubscribe.java rename to yaacc/src/main/java/de/yaacc/upnp/protocol/sync/SendingUnsubscribe.java index d5ae7480..8780917f 100644 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/sync/SendingUnsubscribe.java +++ b/yaacc/src/main/java/de/yaacc/upnp/protocol/sync/SendingUnsubscribe.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,23 +31,27 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.protocol.sync; - -import android.util.Log; +package de.yaacc.upnp.protocol.sync; -import org.fourthline.cling.UpnpService; import org.fourthline.cling.model.gena.CancelReason; import org.fourthline.cling.model.gena.RemoteGENASubscription; import org.fourthline.cling.model.message.StreamResponseMessage; import org.fourthline.cling.model.message.gena.OutgoingUnsubscribeRequestMessage; -import org.fourthline.cling.protocol.SendingSync; -import org.fourthline.cling.transport.RouterException; + +import java.io.IOException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import de.yaacc.upnp.protocol.SendingSync; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.util.YaaccLogger; /** * Disconnecting a GENA event subscription with a remote host. *

- * Calls the {@link org.fourthline.cling.model.gena.RemoteGENASubscription#end(org.fourthline.cling.model.gena.CancelReason, org.fourthline.cling.model.message.UpnpResponse)} - * method if the subscription request was responded to correctly. No {@link org.fourthline.cling.model.gena.CancelReason} + * Calls the {@link RemoteGENASubscription#end(CancelReason, org.fourthline.cling.model.message.UpnpResponse)} + * method if the subscription request was responded to correctly. No {@link CancelReason} * will be provided if the unsubscribe procedure completed as expected, otherwise UNSUBSCRIBE_FAILED * is used. The response might be null if no response was received from the remote host. *

@@ -39,26 +61,29 @@ public class SendingUnsubscribe extends SendingSync { final protected RemoteGENASubscription subscription; + private final HttpRequestSender httpRequestSender; + private final Registry registry; + private final ExecutorService executorService; - public SendingUnsubscribe(UpnpService upnpService, RemoteGENASubscription subscription) { - super( - upnpService, - new OutgoingUnsubscribeRequestMessage( - subscription, - upnpService.getConfiguration().getEventSubscriptionHeaders(subscription.getService()) - ) - ); + public SendingUnsubscribe(Registry registry, HttpRequestSender httpRequestSender, RemoteGENASubscription subscription) { + super(new OutgoingUnsubscribeRequestMessage(subscription, null)); + this.registry = registry; + this.httpRequestSender = httpRequestSender; this.subscription = subscription; + executorService = Executors.newFixedThreadPool(20); + } - protected StreamResponseMessage executeSync() throws RouterException { + protected StreamResponseMessage executeSync() throws IOException { - Log.v(getClass().getName(), "Sending unsubscribe request: " + getInputMessage()); + YaaccLogger.v(getClass().getName(), "Sending unsubscribe request: " + getInputMessage()); StreamResponseMessage response = null; try { - response = getUpnpService().getRouter().send(getInputMessage()); + response = httpRequestSender.send(getInputMessage()); return response; + } catch (IOException e) { + throw new IOException(e); } finally { onUnsubscribe(response); } @@ -66,19 +91,19 @@ protected StreamResponseMessage executeSync() throws RouterException { protected void onUnsubscribe(final StreamResponseMessage response) { // Always remove from the registry and end the subscription properly - even if it's failed - getUpnpService().getRegistry().removeRemoteSubscription(subscription); + registry.removeRemoteSubscription(subscription); - getUpnpService().getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { if (response == null) { - Log.v(getClass().getName(), "Unsubscribe failed, no response received"); + YaaccLogger.v(getClass().getName(), "Unsubscribe failed, no response received"); subscription.end(CancelReason.UNSUBSCRIBE_FAILED, null); } else if (response.getOperation().isFailed()) { - Log.v(getClass().getName(), "Unsubscribe failed, response was: " + response); + YaaccLogger.v(getClass().getName(), "Unsubscribe failed, response was: " + response); subscription.end(CancelReason.UNSUBSCRIBE_FAILED, response.getOperation()); } else { - Log.v(getClass().getName(), "Unsubscribe successful, response was: " + response); + YaaccLogger.v(getClass().getName(), "Unsubscribe successful, response was: " + response); subscription.end(null, response.getOperation()); } } diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/LocalItems.java b/yaacc/src/main/java/de/yaacc/upnp/registry/LocalItems.java similarity index 73% rename from yaacc/src/main/java/org/fourthline/cling/registry/LocalItems.java rename to yaacc/src/main/java/de/yaacc/upnp/registry/LocalItems.java index 2b711e1b..4d2f942f 100644 --- a/yaacc/src/main/java/org/fourthline/cling/registry/LocalItems.java +++ b/yaacc/src/main/java/de/yaacc/upnp/registry/LocalItems.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,9 +31,7 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.registry; - -import android.util.Log; +package de.yaacc.upnp.registry; import org.fourthline.cling.model.DiscoveryOptions; import org.fourthline.cling.model.gena.CancelReason; @@ -23,7 +39,6 @@ import org.fourthline.cling.model.meta.LocalDevice; import org.fourthline.cling.model.resource.Resource; import org.fourthline.cling.model.types.UDN; -import org.fourthline.cling.protocol.SendingAsync; import java.util.Collection; import java.util.Collections; @@ -33,21 +48,33 @@ import java.util.Map; import java.util.Random; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import de.yaacc.upnp.protocol.SendingAsync; +import de.yaacc.util.YaaccLogger; /** - * Internal class, required by {@link RegistryImpl}. + * Internal class, required by {@link de.yaacc.upnp.registry.RegistryImpl}. * * @author Christian Bauer */ class LocalItems extends RegistryItems { - + private final ExecutorService executorService; + int ALIVE_INTERVAL_MILLIS = 0; //Defaults to zero, disabling ALIVE flooding. protected Map discoveryOptions = new HashMap<>(); protected long lastAliveIntervalTimestamp = 0; protected Random randomGenerator = new Random(); LocalItems(RegistryImpl registry) { super(registry); + executorService = Executors.newFixedThreadPool(20); + } + + public void setAliveIntervalMillis(int aliveIntervalMillis) { + this.ALIVE_INTERVAL_MILLIS = aliveIntervalMillis; + YaaccLogger.d(getClass().getName(), "ALIVE interval set to: " + aliveIntervalMillis + "ms"); } protected void setDiscoveryOptions(UDN udn, DiscoveryOptions options) { @@ -81,11 +108,11 @@ void add(final LocalDevice localDevice, DiscoveryOptions options) throws Registr setDiscoveryOptions(localDevice.getIdentity().getUdn(), options); if (registry.getDevice(localDevice.getIdentity().getUdn(), false) != null) { - Log.v(getClass().getName(), "Ignoring addition, device already registered: " + localDevice); + YaaccLogger.v(getClass().getName(), "Ignoring addition, device already registered: " + localDevice); return; } - Log.v(getClass().getName(), "Adding local device to registry: " + localDevice); + YaaccLogger.v(getClass().getName(), "Adding local device to registry: " + localDevice); for (Resource deviceResource : getResources(localDevice)) { @@ -94,11 +121,11 @@ void add(final LocalDevice localDevice, DiscoveryOptions options) throws Registr } registry.addResource(deviceResource); - Log.v(getClass().getName(), "Registered resource: " + deviceResource); + YaaccLogger.v(getClass().getName(), "Registered resource: " + deviceResource); } - Log.v(getClass().getName(), "Adding item to registry with expiration in seconds: " + localDevice.getIdentity().getMaxAgeSeconds()); + YaaccLogger.v(getClass().getName(), "Adding item to registry with expiration in seconds: " + localDevice.getIdentity().getMaxAgeSeconds()); RegistryItem localItem = new RegistryItem<>( localDevice.getIdentity().getUdn(), @@ -107,7 +134,7 @@ void add(final LocalDevice localDevice, DiscoveryOptions options) throws Registr ); getDeviceItems().add(localItem); - Log.v(getClass().getName(), "Registered local device: " + localItem); + YaaccLogger.v(getClass().getName(), "Registered local device: " + localItem); if (isByeByeBeforeFirstAlive(localItem.getKey())) advertiseByebye(localDevice, true); @@ -116,7 +143,7 @@ void add(final LocalDevice localDevice, DiscoveryOptions options) throws Registr advertiseAlive(localDevice); for (final RegistryListener listener : registry.getListeners()) { - registry.getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { listener.localDeviceAdded(registry, localDevice); @@ -144,14 +171,14 @@ boolean remove(final LocalDevice localDevice, boolean shuttingDown) throws Regis LocalDevice registeredDevice = get(localDevice.getIdentity().getUdn(), true); if (registeredDevice != null) { - Log.v(getClass().getName(), "Removing local device from registry: " + localDevice); + YaaccLogger.v(getClass().getName(), "Removing local device from registry: " + localDevice); setDiscoveryOptions(localDevice.getIdentity().getUdn(), null); getDeviceItems().remove(new RegistryItem(localDevice.getIdentity().getUdn())); for (Resource deviceResource : getResources(localDevice)) { if (registry.removeResource(deviceResource)) { - Log.v(getClass().getName(), "Unregistered resource: " + deviceResource); + YaaccLogger.v(getClass().getName(), "Unregistered resource: " + deviceResource); } } @@ -164,10 +191,10 @@ boolean remove(final LocalDevice localDevice, boolean shuttingDown) throws Regis incomingSubscription.getItem().getService().getDevice().getIdentity().getUdn(); if (subscriptionForUDN.equals(registeredDevice.getIdentity().getUdn())) { - Log.v(getClass().getName(), "Removing incoming subscription: " + incomingSubscription.getKey()); + YaaccLogger.v(getClass().getName(), "Removing incoming subscription: " + incomingSubscription.getKey()); it.remove(); if (!shuttingDown) { - registry.getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { incomingSubscription.getItem().end(CancelReason.DEVICE_WAS_REMOVED); @@ -183,7 +210,7 @@ public void run() { if (!shuttingDown) { for (final RegistryListener listener : registry.getListeners()) { - registry.getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { listener.localDeviceRemoved(registry, localDevice); @@ -228,14 +255,14 @@ void maintain() { Set> expiredLocalItems = new HashSet<>(); // "Flooding" is enabled, check if we need to send advertisements for all devices - int aliveIntervalMillis = registry.getConfiguration().getAliveIntervalMillis(); + int aliveIntervalMillis = ALIVE_INTERVAL_MILLIS; if (aliveIntervalMillis > 0) { long now = System.currentTimeMillis(); if (now - lastAliveIntervalTimestamp > aliveIntervalMillis) { lastAliveIntervalTimestamp = now; for (RegistryItem localItem : getDeviceItems()) { if (isAdvertised(localItem.getKey())) { - Log.v(getClass().getName(), "Flooding advertisement of local item: " + localItem); + YaaccLogger.v(getClass().getName(), "Flooding advertisement of local item: " + localItem); expiredLocalItems.add(localItem); } } @@ -247,7 +274,7 @@ void maintain() { // Alive interval is not enabled, regular expiration check of all devices for (RegistryItem localItem : getDeviceItems()) { if (isAdvertised(localItem.getKey()) && localItem.getExpirationDetails().hasExpired(true)) { - Log.v(getClass().getName(), "Local item has expired: " + localItem); + YaaccLogger.v(getClass().getName(), "Local item has expired: " + localItem); expiredLocalItems.add(localItem); } } @@ -255,7 +282,7 @@ void maintain() { // Now execute the advertisements for (RegistryItem expiredLocalItem : expiredLocalItems) { - Log.v(getClass().getName(), "Refreshing local device advertisement: " + expiredLocalItem.getItem()); + YaaccLogger.v(getClass().getName(), "Refreshing local device advertisement: " + expiredLocalItem.getItem()); advertiseAlive(expiredLocalItem.getItem()); expiredLocalItem.getExpirationDetails().stampLastRefresh(); } @@ -268,7 +295,7 @@ void maintain() { } } for (RegistryItem subscription : expiredIncomingSubscriptions) { - Log.v(getClass().getName(), "Removing expired: " + subscription); + YaaccLogger.v(getClass().getName(), "Removing expired: " + subscription); removeSubscription(subscription.getItem()); subscription.getItem().end(CancelReason.EXPIRED); } @@ -278,10 +305,10 @@ void maintain() { /* ############################################################################################################ */ void shutdown() { - Log.v(getClass().getName(), "Clearing all registered subscriptions to local devices during shutdown"); + YaaccLogger.v(getClass().getName(), "Clearing all registered subscriptions to local devices during shutdown"); getSubscriptionItems().clear(); - Log.v(getClass().getName(), "Removing all local devices from registry during shutdown"); + YaaccLogger.v(getClass().getName(), "Removing all local devices from registry during shutdown"); removeAll(true); } @@ -289,18 +316,18 @@ protected void advertiseAlive(final LocalDevice localDevice) { registry.executeAsyncProtocol(new Runnable() { public void run() { try { - Log.v(getClass().getName(), "Sleeping some milliseconds to avoid flooding the network with ALIVE msgs"); + YaaccLogger.v(getClass().getName(), "Sleeping some milliseconds to avoid flooding the network with ALIVE msgs"); Thread.sleep(randomGenerator.nextInt(100)); } catch (InterruptedException ex) { - Log.e(getClass().getName(), "Background execution interrupted: " + ex.getMessage()); + YaaccLogger.e(getClass().getName(), "Background execution interrupted: " + ex.getMessage()); } - registry.getProtocolFactory().createSendingNotificationAlive(localDevice).run(); + registry.getUpnpProtocolHandler().createSendingNotificationAlive(localDevice).run(); } }); } protected void advertiseByebye(final LocalDevice localDevice, boolean asynchronous) { - final SendingAsync prot = registry.getProtocolFactory().createSendingNotificationByebye(localDevice); + final SendingAsync prot = registry.getUpnpProtocolHandler().createSendingNotificationByebye(localDevice); if (asynchronous) { registry.executeAsyncProtocol(prot); } else { diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/RegistrationException.java b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistrationException.java similarity index 61% rename from yaacc/src/main/java/org/fourthline/cling/registry/RegistrationException.java rename to yaacc/src/main/java/de/yaacc/upnp/registry/RegistrationException.java index 0d190403..897f45f8 100644 --- a/yaacc/src/main/java/org/fourthline/cling/registry/RegistrationException.java +++ b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistrationException.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,7 +31,7 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.registry; +package de.yaacc.upnp.registry; import org.fourthline.cling.model.ValidationError; diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/Registry.java b/yaacc/src/main/java/de/yaacc/upnp/registry/Registry.java similarity index 91% rename from yaacc/src/main/java/org/fourthline/cling/registry/Registry.java rename to yaacc/src/main/java/de/yaacc/upnp/registry/Registry.java index 69f3fab3..2bdac0b6 100644 --- a/yaacc/src/main/java/org/fourthline/cling/registry/Registry.java +++ b/yaacc/src/main/java/de/yaacc/upnp/registry/Registry.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,10 +31,8 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.registry; +package de.yaacc.upnp.registry; -import org.fourthline.cling.UpnpService; -import org.fourthline.cling.UpnpServiceConfiguration; import org.fourthline.cling.model.DiscoveryOptions; import org.fourthline.cling.model.ServiceReference; import org.fourthline.cling.model.gena.LocalGENASubscription; @@ -30,10 +46,12 @@ import org.fourthline.cling.model.types.DeviceType; import org.fourthline.cling.model.types.ServiceType; import org.fourthline.cling.model.types.UDN; -import org.fourthline.cling.protocol.ProtocolFactory; import java.net.URI; import java.util.Collection; +import java.util.concurrent.ExecutorService; + +import de.yaacc.upnp.protocol.UpnpProtocolHandler; /** * The core of the UPnP stack, keeping track of known devices and resources. @@ -60,12 +78,6 @@ */ public interface Registry { - public UpnpService getUpnpService(); - - public UpnpServiceConfiguration getConfiguration(); - - public ProtocolFactory getProtocolFactory(); - // ################################################################################################# /** @@ -347,10 +359,10 @@ public interface Registry { public Resource getResource(URI pathQuery) throws IllegalArgumentException; /** - * @param The required subtype of the {@link org.fourthline.cling.model.resource.Resource}. + * @param The required subtype of the {@link Resource}. * @param pathQuery The path and optional query string of the resource's * registration URI (e.g. /dev/somefile.xml?param=value) - * @param resourceType The required subtype of the {@link org.fourthline.cling.model.resource.Resource}. + * @param resourceType The required subtype of the {@link Resource}. * @return Any registered resource that matches the given URI path and subtype. * @throws IllegalArgumentException If the given URI was absolute, only path and query are allowed. */ @@ -362,8 +374,8 @@ public interface Registry { public Collection getResources(); /** - * @param The required subtype of the {@link org.fourthline.cling.model.resource.Resource}. - * @param resourceType The required subtype of the {@link org.fourthline.cling.model.resource.Resource}. + * @param The required subtype of the {@link Resource}. + * @param resourceType The required subtype of the {@link Resource}. * @return Any registered resource that matches the given subtype. */ public Collection getResources(Class resourceType); @@ -451,4 +463,20 @@ public interface Registry { */ public void advertiseLocalDevices(); + /** + * Set the interval for periodic ALIVE announcements. + *

+ * Set to 0 to disable periodic announcements (devices will only announce on expiration). + * Recommended value: 5000ms (5 seconds) for better discovery by clients like Kodi. + *

+ * @param intervalMillis interval in milliseconds between ALIVE announcements + */ + public void setAliveInterval(int intervalMillis); + + + UpnpProtocolHandler getUpnpProtocolHandler(); + + void setUpnpProtocolHandler(UpnpProtocolHandler upnpProtocolHandler); + + ExecutorService getExecutorService(); } diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/RegistryImpl.java b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistryImpl.java similarity index 78% rename from yaacc/src/main/java/org/fourthline/cling/registry/RegistryImpl.java rename to yaacc/src/main/java/de/yaacc/upnp/registry/RegistryImpl.java index 61476008..b7d7a29d 100644 --- a/yaacc/src/main/java/org/fourthline/cling/registry/RegistryImpl.java +++ b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistryImpl.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,12 +31,8 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.registry; - -import android.util.Log; +package de.yaacc.upnp.registry; -import org.fourthline.cling.UpnpService; -import org.fourthline.cling.UpnpServiceConfiguration; import org.fourthline.cling.model.DiscoveryOptions; import org.fourthline.cling.model.ExpirationDetails; import org.fourthline.cling.model.ServiceReference; @@ -33,7 +47,6 @@ import org.fourthline.cling.model.types.DeviceType; import org.fourthline.cling.model.types.ServiceType; import org.fourthline.cling.model.types.UDN; -import org.fourthline.cling.protocol.ProtocolFactory; import java.net.URI; import java.util.ArrayList; @@ -43,67 +56,63 @@ import java.util.Iterator; import java.util.List; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import de.yaacc.upnp.protocol.UpnpProtocolHandler; +import de.yaacc.util.YaaccLogger; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; /** * Default implementation of {@link Registry}. * * @author Christian Bauer */ -@ApplicationScoped public class RegistryImpl implements Registry { + public static final int SLEEP_INTERVAL_MILLIS = 7000; protected final Set pendingSubscriptionsLock = new HashSet<>(); protected final Set registryListeners = new HashSet<>(); protected final Set> resourceItems = new HashSet<>(); protected final List pendingExecutions = new ArrayList<>(); protected final RemoteItems remoteItems = new RemoteItems(this); protected final LocalItems localItems = new LocalItems(this); - protected UpnpService upnpService; + + + private final ExecutorService executorService; + + private UpnpProtocolHandler upnpProtocolHandler; + protected RegistryMaintainer registryMaintainer; - public RegistryImpl() { - } // ################################################################################################# /** * Starts background maintenance immediately. */ - @Inject - public RegistryImpl(UpnpService upnpService) { - Log.v(getClass().getName(), "Creating Registry: " + getClass().getName()); - this.upnpService = upnpService; - - Log.v(getClass().getName(), "Starting registry background maintenance..."); + public RegistryImpl() { + YaaccLogger.v(getClass().getName(), "Creating Registry: " + getClass().getName()); + YaaccLogger.v(getClass().getName(), "Starting registry background maintenance..."); + executorService = Executors.newFixedThreadPool(50); registryMaintainer = createRegistryMaintainer(); if (registryMaintainer != null) { - getConfiguration().getRegistryMaintainerExecutor().execute(registryMaintainer); + executorService.execute(registryMaintainer); } } - public UpnpService getUpnpService() { - return upnpService; - } - - public UpnpServiceConfiguration getConfiguration() { - return getUpnpService().getConfiguration(); - } - - public ProtocolFactory getProtocolFactory() { - return getUpnpService().getProtocolFactory(); - } - - protected RegistryMaintainer createRegistryMaintainer() { + synchronized protected RegistryMaintainer createRegistryMaintainer() { return new RegistryMaintainer( this, - getConfiguration().getRegistryMaintenanceIntervalMillis() + SLEEP_INTERVAL_MILLIS // Preserve battery on Android, only run every 7 seconds ); } + @Override + public ExecutorService getExecutorService() { + return executorService; + } // ################################################################################################# synchronized public void addListener(RegistryListener listener) { @@ -120,12 +129,12 @@ synchronized public Collection getListeners() { synchronized public boolean notifyDiscoveryStart(final RemoteDevice device) { // Exit if we have it already, this is atomic inside this method, finally - if (getUpnpService().getRegistry().getRemoteDevice(device.getIdentity().getUdn(), true) != null) { - Log.v(getClass().getName(), "Not notifying listeners, already registered: " + device); + if (getRemoteDevice(device.getIdentity().getUdn(), true) != null) { + YaaccLogger.v(getClass().getName(), "Not notifying listeners, already registered: " + device); return false; } for (final RegistryListener listener : getListeners()) { - getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { listener.remoteDeviceDiscoveryStarted(RegistryImpl.this, device); @@ -138,7 +147,7 @@ public void run() { synchronized public void notifyDiscoveryFailure(final RemoteDevice device, final Exception ex) { for (final RegistryListener listener : getListeners()) { - getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { listener.remoteDeviceDiscoveryFailed(RegistryImpl.this, device, ex); @@ -167,6 +176,7 @@ synchronized public DiscoveryOptions getDiscoveryOptions(UDN udn) { } synchronized public void addDevice(RemoteDevice remoteDevice) { + YaaccLogger.d(getClass().getName(), "Adding remote device: " + remoteDevice.getIdentity().getDescriptorURL()); remoteItems.add(remoteDevice); } @@ -366,18 +376,33 @@ synchronized public void advertiseLocalDevices() { localItems.advertiseLocalDevices(); } + @Override + synchronized public void setAliveInterval(int intervalMillis) { + localItems.setAliveIntervalMillis(intervalMillis); + } + + /* ############################################################################################################ */ + + public UpnpProtocolHandler getProtocolHandler() { + return upnpProtocolHandler; + } + + public void setProtocolHandler(UpnpProtocolHandler upnpProtocolHandler) { + this.upnpProtocolHandler = upnpProtocolHandler; + } + /* ############################################################################################################ */ // When you call this, make sure you have the Router lock before this lock is obtained! synchronized public void shutdown() { - Log.v(getClass().getName(), "Shutting down registry..."); + YaaccLogger.v(getClass().getName(), "Shutting down registry..."); if (registryMaintainer != null) registryMaintainer.stop(); // Final cleanup run to flush out pending executions which might // not have been caught by the maintainer before it stopped - Log.v(getClass().getName(), "Executing final pending operations on shutdown: " + pendingExecutions.size()); + YaaccLogger.v(getClass().getName(), "Executing final pending operations on shutdown: " + pendingExecutions.size()); runPendingExecutions(false); for (RegistryListener listener : registryListeners) { @@ -399,7 +424,7 @@ synchronized public void shutdown() { synchronized public void pause() { if (registryMaintainer != null) { - Log.v(getClass().getName(), "Pausing registry maintenance"); + YaaccLogger.v(getClass().getName(), "Pausing registry maintenance"); runPendingExecutions(true); registryMaintainer.stop(); registryMaintainer = null; @@ -408,11 +433,11 @@ synchronized public void pause() { synchronized public void resume() { if (registryMaintainer == null) { - Log.v(getClass().getName(), "Resuming registry maintenance"); + YaaccLogger.v(getClass().getName(), "Resuming registry maintenance"); remoteItems.resume(); registryMaintainer = createRegistryMaintainer(); if (registryMaintainer != null) { - getConfiguration().getRegistryMaintainerExecutor().execute(registryMaintainer); + executorService.execute(registryMaintainer); } } } @@ -426,7 +451,7 @@ synchronized public boolean isPaused() { synchronized void maintain() { - Log.v(getClass().getName(), "Maintaining registry..."); + YaaccLogger.v(getClass().getName(), "Maintaining registry..."); // Remove expired resources Iterator> it = resourceItems.iterator(); @@ -434,7 +459,7 @@ synchronized void maintain() { RegistryItem item = it.next(); if (item.getExpirationDetails().hasExpired()) { - Log.v(getClass().getName(), "Removing expired resource: " + item); + YaaccLogger.v(getClass().getName(), "Removing expired resource: " + item); it.remove(); } } @@ -461,14 +486,14 @@ synchronized void executeAsyncProtocol(Runnable runnable) { synchronized void runPendingExecutions(boolean async) { - Log.v(getClass().getName(), "Executing pending operations: " + pendingExecutions.size()); + YaaccLogger.v(getClass().getName(), "Executing pending operations: " + pendingExecutions.size()); for (Runnable pendingExecution : pendingExecutions) { if (async) - getConfiguration().getAsyncProtocolExecutor().execute(pendingExecution); + executorService.execute(pendingExecution); else pendingExecution.run(); } - if (pendingExecutions.size() > 0) { + if (!pendingExecutions.isEmpty()) { pendingExecutions.clear(); } } @@ -477,25 +502,25 @@ synchronized void runPendingExecutions(boolean async) { public void printDebugLog() { { - Log.v(getClass().getName(), "==================================== REMOTE ================================================"); + YaaccLogger.v(getClass().getName(), "==================================== REMOTE ================================================"); for (RemoteDevice remoteDevice : remoteItems.get()) { - Log.v(getClass().getName(), remoteDevice.toString()); + YaaccLogger.v(getClass().getName(), remoteDevice.toString()); } - Log.v(getClass().getName(), "==================================== LOCAL ================================================"); + YaaccLogger.v(getClass().getName(), "==================================== LOCAL ================================================"); for (LocalDevice localDevice : localItems.get()) { - Log.v(getClass().getName(), localDevice.toString()); + YaaccLogger.v(getClass().getName(), localDevice.toString()); } - Log.v(getClass().getName(), "==================================== RESOURCES ================================================"); + YaaccLogger.v(getClass().getName(), "==================================== RESOURCES ================================================"); for (RegistryItem resourceItem : resourceItems) { - Log.v(getClass().getName(), resourceItem.toString()); + YaaccLogger.v(getClass().getName(), resourceItem.toString()); } - Log.v(getClass().getName(), "================================================================================================="); + YaaccLogger.v(getClass().getName(), "================================================================================================="); } @@ -523,7 +548,7 @@ public RemoteGENASubscription getWaitRemoteSubscription(String subscriptionId) { RemoteGENASubscription subscription = getRemoteSubscription(subscriptionId); while (subscription == null && !pendingSubscriptionsLock.isEmpty()) { try { - Log.v(getClass().getName(), "Subscription not found, waiting for pending subscription procedure to terminate."); + YaaccLogger.v(getClass().getName(), "Subscription not found, waiting for pending subscription procedure to terminate."); pendingSubscriptionsLock.wait(); } catch (InterruptedException e) { } @@ -533,4 +558,14 @@ public RemoteGENASubscription getWaitRemoteSubscription(String subscriptionId) { } } + @Override + public UpnpProtocolHandler getUpnpProtocolHandler() { + return upnpProtocolHandler; + } + + @Override + public void setUpnpProtocolHandler(UpnpProtocolHandler upnpProtocolHandler) { + this.upnpProtocolHandler = upnpProtocolHandler; + } + } diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/RegistryItem.java b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistryItem.java similarity index 64% rename from yaacc/src/main/java/org/fourthline/cling/registry/RegistryItem.java rename to yaacc/src/main/java/de/yaacc/upnp/registry/RegistryItem.java index 0087c9cd..3d1110a0 100644 --- a/yaacc/src/main/java/org/fourthline/cling/registry/RegistryItem.java +++ b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistryItem.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,7 +31,7 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.registry; +package de.yaacc.upnp.registry; import org.fourthline.cling.model.ExpirationDetails; @@ -65,6 +83,6 @@ public int hashCode() { @Override public String toString() { - return "("+getClass().getSimpleName()+") " + getExpirationDetails() + " KEY: " + getKey() + " ITEM: " + getItem(); + return "(" + getClass().getSimpleName() + ") " + getExpirationDetails() + " KEY: " + getKey() + " ITEM: " + getItem(); } } diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/RegistryItems.java b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistryItems.java similarity index 81% rename from yaacc/src/main/java/org/fourthline/cling/registry/RegistryItems.java rename to yaacc/src/main/java/de/yaacc/upnp/registry/RegistryItems.java index 523e429b..10c98aa4 100644 --- a/yaacc/src/main/java/org/fourthline/cling/registry/RegistryItems.java +++ b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistryItems.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,12 +31,12 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.registry; +package de.yaacc.upnp.registry; -import org.fourthline.cling.model.resource.Resource; import org.fourthline.cling.model.ValidationException; -import org.fourthline.cling.model.meta.Device; import org.fourthline.cling.model.gena.GENASubscription; +import org.fourthline.cling.model.meta.Device; +import org.fourthline.cling.model.resource.Resource; import org.fourthline.cling.model.types.DeviceType; import org.fourthline.cling.model.types.ServiceType; import org.fourthline.cling.model.types.UDN; @@ -28,6 +46,8 @@ import java.util.HashSet; import java.util.Set; +import de.yaacc.upnp.protocol.UpnpProtocolHandler; + /** * Internal class, required by {@link RegistryImpl}. * @@ -53,19 +73,22 @@ Set> getSubscriptionItems() { } abstract void add(D device); + abstract boolean remove(final D device); + abstract void removeAll(); abstract void maintain(); + abstract void shutdown(); /** * Returns root and embedded devices registered under the given UDN. * - * @param udn A unique device name. + * @param udn A unique device name. * @param rootOnly Set to true if only root devices (no embedded) should be searched * @return Any registered root or embedded device under the given UDN, null if - * no device with the given UDN has been registered. + * no device with the given UDN has been registered. */ D get(UDN udn, boolean rootOnly) { for (RegistryItem item : deviceItems) { @@ -74,7 +97,7 @@ D get(UDN udn, boolean rootOnly) { return device; } if (!rootOnly) { - D foundDevice = (D)item.getItem().findDevice(udn); + D foundDevice = (D) item.getItem().findDevice(udn); if (foundDevice != null) return foundDevice; } } @@ -93,7 +116,7 @@ D get(UDN udn, boolean rootOnly) { Collection get(DeviceType deviceType) { Collection devices = new HashSet<>(); for (RegistryItem item : deviceItems) { - D[] d = (D[])item.getItem().findDevices(deviceType); + D[] d = (D[]) item.getItem().findDevices(deviceType); if (d != null) { devices.addAll(Arrays.asList(d)); } @@ -111,7 +134,7 @@ Collection get(ServiceType serviceType) { Collection devices = new HashSet<>(); for (RegistryItem item : deviceItems) { - D[] d = (D[])item.getItem().findDevices(serviceType); + D[] d = (D[]) item.getItem().findDevices(serviceType); if (d != null) { devices.addAll(Arrays.asList(d)); } @@ -170,7 +193,7 @@ S getSubscription(String subscriptionId) { Resource[] getResources(Device device) throws RegistrationException { try { - return registry.getConfiguration().getNamespace().getResources(device); + return UpnpProtocolHandler.NAMESPACE.getResources(device); } catch (ValidationException ex) { throw new RegistrationException("Resource discover error: " + ex.toString(), ex); } diff --git a/yaacc/src/main/java/de/yaacc/upnp/registry/RegistryListener.java b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistryListener.java new file mode 100644 index 00000000..a6763416 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistryListener.java @@ -0,0 +1,58 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +/* + * Copyright (C) 2013 4th Line GmbH, Switzerland + * + * The contents of this file are subject to the terms of either the GNU + * Lesser General Public License Version 2 or later ("LGPL") or the + * Common Development and Distribution License Version 1 or later + * ("CDDL") (collectively, the "License"). You may not use this file + * except in compliance with the License. See LICENSE.txt for more + * information. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + */ + +package de.yaacc.upnp.registry; + +import org.fourthline.cling.model.meta.LocalDevice; +import org.fourthline.cling.model.meta.RemoteDevice; + +public interface RegistryListener { + + void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device); + + void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex); + + void remoteDeviceAdded(Registry registry, RemoteDevice device); + + void remoteDeviceUpdated(Registry registry, RemoteDevice device); + + void remoteDeviceRemoved(Registry registry, RemoteDevice device); + + void localDeviceAdded(Registry registry, LocalDevice device); + + void localDeviceRemoved(Registry registry, LocalDevice device); + + void beforeShutdown(Registry registry); + + void afterShutdown(); +} diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/RegistryMaintainer.java b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistryMaintainer.java similarity index 52% rename from yaacc/src/main/java/org/fourthline/cling/registry/RegistryMaintainer.java rename to yaacc/src/main/java/de/yaacc/upnp/registry/RegistryMaintainer.java index 7d59feb3..d2e02cb2 100644 --- a/yaacc/src/main/java/org/fourthline/cling/registry/RegistryMaintainer.java +++ b/yaacc/src/main/java/de/yaacc/upnp/registry/RegistryMaintainer.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,12 +31,12 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.registry; +package de.yaacc.upnp.registry; -import android.util.Log; +import de.yaacc.util.YaaccLogger; /** - * Runs periodically and calls {@link org.fourthline.cling.registry.RegistryImpl#maintain()}. + * Runs periodically and calls {@link de.yaacc.upnp.registry.RegistryImpl#maintain()}. * * @author Christian Bauer */ @@ -37,14 +55,14 @@ public RegistryMaintainer(RegistryImpl registry, int sleepIntervalMillis) { public void stop() { - Log.v(getClass().getName(), "Setting stopped status on thread"); + YaaccLogger.v(getClass().getName(), "Setting stopped status on thread"); stopped = true; } public void run() { stopped = false; - Log.v(getClass().getName(), "Running registry maintenance loop every milliseconds: " + sleepIntervalMillis); + YaaccLogger.v(getClass().getName(), "Running registry maintenance loop every milliseconds: " + sleepIntervalMillis); while (!stopped) { try { @@ -55,7 +73,7 @@ public void run() { } } - Log.v(getClass().getName(), "Stopped status on thread received, ending maintenance loop"); + YaaccLogger.v(getClass().getName(), "Stopped status on thread received, ending maintenance loop"); } } \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/RemoteItems.java b/yaacc/src/main/java/de/yaacc/upnp/registry/RemoteItems.java similarity index 71% rename from yaacc/src/main/java/org/fourthline/cling/registry/RemoteItems.java rename to yaacc/src/main/java/de/yaacc/upnp/registry/RemoteItems.java index 110ce994..5d630c39 100644 --- a/yaacc/src/main/java/org/fourthline/cling/registry/RemoteItems.java +++ b/yaacc/src/main/java/de/yaacc/upnp/registry/RemoteItems.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,9 +31,7 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.registry; - -import android.util.Log; +package de.yaacc.upnp.registry; import org.fourthline.cling.model.gena.CancelReason; import org.fourthline.cling.model.gena.RemoteGENASubscription; @@ -32,17 +48,24 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import de.yaacc.util.YaaccLogger; /** - * Internal class, required by {@link RegistryImpl}. + * Internal class, required by {@link de.yaacc.upnp.registry.RegistryImpl}. * * @author Christian Bauer */ class RemoteItems extends RegistryItems { + private final ExecutorService executorService; + RemoteItems(RegistryImpl registry) { super(registry); + executorService = Executors.newFixedThreadPool(20); } /** @@ -60,14 +83,14 @@ class RemoteItems extends RegistryItems { void add(final RemoteDevice device) { if (update(device.getIdentity())) { - Log.v(getClass().getName(), "Ignoring addition, device already registered: " + device); + YaaccLogger.v(getClass().getName(), "Ignoring addition, device already registered: " + device); return; } Resource[] resources = getResources(device); for (Resource deviceResource : resources) { - Log.v(getClass().getName(), "Validating remote device resource; " + deviceResource); + YaaccLogger.v(getClass().getName(), "Validating remote device resource; " + deviceResource); if (registry.getResource(deviceResource.getPathQuery()) != null) { throw new RegistrationException("URI namespace conflict with already registered resource: " + deviceResource); } @@ -75,18 +98,16 @@ void add(final RemoteDevice device) { for (Resource validatedResource : resources) { registry.addResource(validatedResource); - Log.v(getClass().getName(), "Added remote device resource: " + validatedResource); + YaaccLogger.v(getClass().getName(), "Added remote device resource: " + validatedResource); } // Override the device's maximum age if configured (systems without multicast support) RegistryItem item = new RegistryItem( device.getIdentity().getUdn(), device, - registry.getConfiguration().getRemoteDeviceMaxAgeSeconds() != null - ? registry.getConfiguration().getRemoteDeviceMaxAgeSeconds() - : device.getIdentity().getMaxAgeSeconds() + device.getIdentity().getMaxAgeSeconds() ); - Log.v(getClass().getName(), "Adding hydrated remote device to registry with " + YaaccLogger.v(getClass().getName(), "Adding hydrated remote device to registry with " + item.getExpirationDetails().getMaxAgeSeconds() + " seconds expiration: " + device); getDeviceItems().add(item); @@ -98,13 +119,13 @@ void add(final RemoteDevice device) { sb.append(resource).append("\n"); } sb.append("-------------------------- END Registry Namespace -----------------------------------"); - Log.v(getClass().getName(), sb.toString()); + YaaccLogger.v(getClass().getName(), sb.toString()); // Only notify the listeners when the device is fully usable - Log.v(getClass().getName(), "Completely hydrated remote device graph available, calling listeners: " + device); + YaaccLogger.v(getClass().getName(), "Completely hydrated remote device graph available, calling listeners: " + device); for (final RegistryListener listener : registry.getListeners()) { - registry.getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { listener.remoteDeviceAdded(registry, device); @@ -119,7 +140,7 @@ boolean update(RemoteDeviceIdentity rdIdentity) { for (LocalDevice localDevice : registry.getLocalDevices()) { if (localDevice.findDevice(rdIdentity.getUdn()) != null) { - Log.v(getClass().getName(), "Ignoring update, a local device graph contains UDN"); + YaaccLogger.v(getClass().getName(), "Ignoring update, a local device graph contains UDN"); return true; } } @@ -128,7 +149,7 @@ boolean update(RemoteDeviceIdentity rdIdentity) { if (registeredRemoteDevice != null) { if (!registeredRemoteDevice.isRoot()) { - Log.v(getClass().getName(), "Updating root device of embedded: " + registeredRemoteDevice); + YaaccLogger.v(getClass().getName(), "Updating root device of embedded: " + registeredRemoteDevice); registeredRemoteDevice = registeredRemoteDevice.getRoot(); } @@ -136,18 +157,16 @@ boolean update(RemoteDeviceIdentity rdIdentity) { final RegistryItem item = new RegistryItem<>( registeredRemoteDevice.getIdentity().getUdn(), registeredRemoteDevice, - registry.getConfiguration().getRemoteDeviceMaxAgeSeconds() != null - ? registry.getConfiguration().getRemoteDeviceMaxAgeSeconds() - : rdIdentity.getMaxAgeSeconds() + rdIdentity.getMaxAgeSeconds() ); - Log.v(getClass().getName(), "Updating expiration of: " + registeredRemoteDevice); + YaaccLogger.v(getClass().getName(), "Updating expiration of: " + registeredRemoteDevice); getDeviceItems().remove(item); getDeviceItems().add(item); - Log.v(getClass().getName(), "Remote device updated, calling listeners: " + registeredRemoteDevice); + YaaccLogger.v(getClass().getName(), "Remote device updated, calling listeners: " + registeredRemoteDevice); for (final RegistryListener listener : registry.getListeners()) { - registry.getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { listener.remoteDeviceUpdated(registry, item.getItem()); @@ -176,12 +195,12 @@ boolean remove(final RemoteDevice remoteDevice, boolean shuttingDown) throws Reg final RemoteDevice registeredDevice = get(remoteDevice.getIdentity().getUdn(), true); if (registeredDevice != null) { - Log.v(getClass().getName(), "Removing remote device from registry: " + remoteDevice); + YaaccLogger.v(getClass().getName(), "Removing remote device from registry: " + remoteDevice); // Resources for (Resource deviceResource : getResources(registeredDevice)) { if (registry.removeResource(deviceResource)) { - Log.v(getClass().getName(), "Unregistered resource: " + deviceResource); + YaaccLogger.v(getClass().getName(), "Unregistered resource: " + deviceResource); } } @@ -194,10 +213,10 @@ boolean remove(final RemoteDevice remoteDevice, boolean shuttingDown) throws Reg outgoingSubscription.getItem().getService().getDevice().getIdentity().getUdn(); if (subscriptionForUDN.equals(registeredDevice.getIdentity().getUdn())) { - Log.v(getClass().getName(), "Removing outgoing subscription: " + outgoingSubscription.getKey()); + YaaccLogger.v(getClass().getName(), "Removing outgoing subscription: " + outgoingSubscription.getKey()); it.remove(); if (!shuttingDown) { - registry.getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { outgoingSubscription.getItem().end(CancelReason.DEVICE_WAS_REMOVED, null); @@ -211,7 +230,7 @@ public void run() { // Only notify listeners if we are NOT in the process of shutting down the registry if (!shuttingDown) { for (final RegistryListener listener : registry.getListeners()) { - registry.getConfiguration().getRegistryListenerExecutor().execute( + executorService.execute( new Runnable() { public void run() { listener.remoteDeviceRemoved(registry, registeredDevice); @@ -253,9 +272,10 @@ void maintain() { // Remove expired remote devices Map expiredRemoteDevices = new HashMap<>(); - for (RegistryItem remoteItem : getDeviceItems()) { + // Iterate over a copy to avoid ConcurrentModificationException + for (RegistryItem remoteItem : new HashSet<>(getDeviceItems())) { - Log.v(getClass().getName(), "Device '" + remoteItem.getItem() + "' expires in seconds: " + YaaccLogger.v(getClass().getName(), "Device '" + remoteItem.getItem() + "' expires in seconds: " + remoteItem.getExpirationDetails().getSecondsUntilExpiration()); if (remoteItem.getExpirationDetails().hasExpired(false)) { expiredRemoteDevices.put(remoteItem.getKey(), remoteItem.getItem()); @@ -263,28 +283,30 @@ void maintain() { } for (RemoteDevice remoteDevice : expiredRemoteDevices.values()) { - Log.v(getClass().getName(), "Removing expired: " + remoteDevice); + YaaccLogger.v(getClass().getName(), "Removing expired: " + remoteDevice); remove(remoteDevice); } // Renew outgoing subscriptions Set expiredOutgoingSubscriptions = new HashSet<>(); - for (RegistryItem item : getSubscriptionItems()) { + // Iterate over a copy to avoid ConcurrentModificationException + for (RegistryItem item : new HashSet<>(getSubscriptionItems())) { if (item.getExpirationDetails().hasExpired(true)) { expiredOutgoingSubscriptions.add(item.getItem()); } } for (RemoteGENASubscription subscription : expiredOutgoingSubscriptions) { - Log.v(getClass().getName(), "Renewing outgoing subscription: " + subscription); + YaaccLogger.v(getClass().getName(), "Renewing outgoing subscription: " + subscription); renewOutgoingSubscription(subscription); } } public void resume() { - Log.v(getClass().getName(), "Updating remote device expiration timestamps on resume"); + YaaccLogger.v(getClass().getName(), "Updating remote device expiration timestamps on resume"); List toUpdate = new ArrayList<>(); - for (RegistryItem remoteItem : getDeviceItems()) { + // Iterate over a copy to avoid ConcurrentModificationException + for (RegistryItem remoteItem : new HashSet<>(getDeviceItems())) { toUpdate.add(remoteItem.getItem().getIdentity()); } for (RemoteDeviceIdentity identity : toUpdate) { @@ -293,19 +315,20 @@ public void resume() { } void shutdown() { - Log.v(getClass().getName(), "Cancelling all outgoing subscriptions to remote devices during shutdown"); + YaaccLogger.v(getClass().getName(), "Cancelling all outgoing subscriptions to remote devices during shutdown"); List remoteSubscriptions = new ArrayList<>(); - for (RegistryItem item : getSubscriptionItems()) { + // Iterate over a copy to avoid ConcurrentModificationException + for (RegistryItem item : new HashSet<>(getSubscriptionItems())) { remoteSubscriptions.add(item.getItem()); } for (RemoteGENASubscription remoteSubscription : remoteSubscriptions) { // This will remove the active subscription from the registry! - registry.getProtocolFactory() + registry.getProtocolHandler() .createSendingUnsubscribe(remoteSubscription) .run(); } - Log.v(getClass().getName(), "Removing all remote devices from registry during shutdown"); + YaaccLogger.v(getClass().getName(), "Removing all remote devices from registry during shutdown"); removeAll(true); } @@ -313,7 +336,7 @@ void shutdown() { protected void renewOutgoingSubscription(final RemoteGENASubscription subscription) { registry.executeAsyncProtocol( - registry.getProtocolFactory().createSendingRenewal(subscription) + registry.getProtocolHandler().createSendingRenewal(subscription) ); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/NetworkDeviceListener.java b/yaacc/src/main/java/de/yaacc/upnp/server/NetworkDeviceListener.java new file mode 100644 index 00000000..7fdd0baa --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/NetworkDeviceListener.java @@ -0,0 +1,247 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server; + +import android.content.Context; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.wifi.WifiManager; + +import androidx.annotation.NonNull; +import androidx.preference.PreferenceManager; + +import de.yaacc.R; +import de.yaacc.util.YaaccLogger; + +import de.yaacc.upnp.protocol.UpnpProtocolHandler; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.upnp.server.http.HttpRequestSender; +import de.yaacc.upnp.server.udp.MulticastReceiver; +import de.yaacc.upnp.server.udp.UdpTransiver; + +public class NetworkDeviceListener { + private final Context context; + private final WifiManager wifiManager; + private final Registry registry; + private HttpRequestSender httpRequestSender; + private WifiManager.MulticastLock multicastLock; + private WifiManager.WifiLock wifiLock; + private boolean isAppInForeground = true; + private Runnable wifiLockChangeListener; + + private Network currentNetwork; + private MulticastReceiver multicastReceiver; + + private UdpTransiver udpTransiver; + private UpnpProtocolHandler upnpProtocolHandler; + + + public NetworkDeviceListener(Context context, Registry registry) throws IllegalStateException { + this.context = context; + this.registry = registry; + this.wifiManager = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)); + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (!isCellular()) { + currentNetwork = connectivityManager.getActiveNetwork(); + + } + if (currentNetwork != null) { + multicastReceiver = new MulticastReceiver(); + udpTransiver = new UdpTransiver(); + httpRequestSender = new HttpRequestSender(); + enable(); + } + connectivityManager.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() { + @Override + public void onAvailable(@NonNull Network network) { + super.onAvailable(network); + if (!isCellular() && !network.equals(currentNetwork)) { + YaaccLogger.d(getClass().getName(), String.format("Network available %s", network)); + if (currentNetwork != null) { + disable(); + YaaccLogger.d(getClass().getName(), String.format("Network disabled %s", currentNetwork)); + } + currentNetwork = network; + enable(); + YaaccLogger.d(getClass().getName(), String.format("Network enabled %s", currentNetwork)); + } + + } + + @Override + public void onLost(@NonNull Network network) { + super.onLost(network); + if (network.equals(currentNetwork)) { + YaaccLogger.d(getClass().getName(), String.format("Network lost %s", network)); + disable(); + YaaccLogger.d(getClass().getName(), String.format("Network disabled %s", currentNetwork)); + currentNetwork = null; + } + } + }); + } + + public void enable() { + YaaccLogger.v(getClass().getName(), "in android router enable"); + + // Enable multicast on the WiFi network interface, + // requires android.permission.CHANGE_WIFI_MULTICAST_STATE + if (isWifi()) { + setWiFiMulticastLock(true); + setWifiLock(true); + } + upnpProtocolHandler = new UpnpProtocolHandler(context, registry, udpTransiver, multicastReceiver, httpRequestSender); + multicastReceiver.init(context, upnpProtocolHandler); + multicastReceiver.execute(); + udpTransiver.init(context, upnpProtocolHandler); + udpTransiver.execute(); + } + + public void disable() { + YaaccLogger.v(getClass().getName(), "in android router disable"); + // Disable multicast on WiFi network interface, + // requires android.permission.CHANGE_WIFI_MULTICAST_STATE + if (isWifi()) { + setWiFiMulticastLock(false); + setWifiLock(false); + } + upnpProtocolHandler = null; + multicastReceiver.cancel(); + udpTransiver.cancel(); + } + + private boolean isWifi() { + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork()) == null) { + return false; + } + return connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork()).hasTransport(NetworkCapabilities.TRANSPORT_WIFI); + } + + private boolean isCellular() { + ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + if (connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork()) == null) { + return false; + } + return connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork()).hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); + } + + protected void setWiFiMulticastLock(boolean enable) { + if (multicastLock == null) { + multicastLock = wifiManager.createMulticastLock(getClass().getSimpleName()); + } + + if (enable) { + if (multicastLock.isHeld()) { + YaaccLogger.w(getClass().getName(), "WiFi multicast lock already acquired"); + } else { + YaaccLogger.d(getClass().getName(), "WiFi multicast lock acquired"); + multicastLock.acquire(); + } + } else { + if (multicastLock.isHeld()) { + YaaccLogger.d(getClass().getName(), "WiFi multicast lock released"); + multicastLock.release(); + } else { + YaaccLogger.w(getClass().getName(), "WiFi multicast lock already released"); + } + } + } + + protected void setWifiLock(boolean enable) { + if (wifiLock == null) { + wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, getClass().getSimpleName()); + } + + // Smart WiFi lock: only hold if needed + boolean shouldHold = enable && shouldHoldWifiLock(); + + if (shouldHold) { + if (wifiLock.isHeld()) { + YaaccLogger.w(getClass().getName(), "WiFi lock already acquired"); + } else { + YaaccLogger.d(getClass().getName(), "WiFi lock acquired"); + wifiLock.acquire(); + } + } else { + if (wifiLock.isHeld()) { + YaaccLogger.d(getClass().getName(), "WiFi lock released"); + wifiLock.release(); + } else { + YaaccLogger.w(getClass().getName(), "WiFi lock already released"); + } + } + } + + public boolean isWifiLockHeld() { + return wifiLock != null && wifiLock.isHeld(); + } + + private boolean shouldHoldWifiLock() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + // Hold lock if server enabled + boolean serverEnabled = prefs.getBoolean(context.getString(R.string.settings_local_server_chkbx), false); + + // Hold lock if renderer enabled + boolean rendererEnabled = prefs.getBoolean(context.getString(R.string.settings_local_server_receiver_chkbx), false); + + // Hold lock if app in foreground (for discovery) + boolean needsForDiscovery = isAppInForeground; + + boolean shouldHold = serverEnabled || rendererEnabled || needsForDiscovery; + + YaaccLogger.d(getClass().getName(), + "WiFi lock decision: server=" + serverEnabled + + ", renderer=" + rendererEnabled + + ", foreground=" + isAppInForeground + + " -> " + (shouldHold ? "HOLD" : "RELEASE")); + + return shouldHold; + } + + public void setAppInForeground(boolean inForeground) { + if (this.isAppInForeground != inForeground) { + YaaccLogger.d(getClass().getName(), "App foreground state changed: " + inForeground); + this.isAppInForeground = inForeground; + // Update WiFi lock based on new state + if (isWifi()) { + setWifiLock(true); // Will check shouldHoldWifiLock internally + } + } + } + + public UdpTransiver getUdpTransiver() { + return udpTransiver; + } + + public HttpRequestSender getHttpRequestSender() { + return httpRequestSender; + } + + public UpnpProtocolHandler getUpnpProtocolHandler() { + return upnpProtocolHandler; + } + + public boolean isInitalized() { + return udpTransiver != null && multicastReceiver != null && upnpProtocolHandler != null; + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/ServerAutostart.java b/yaacc/src/main/java/de/yaacc/upnp/server/ServerAutostart.java index 969a6f56..508fcd1f 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/ServerAutostart.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/ServerAutostart.java @@ -22,7 +22,7 @@ import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import androidx.preference.PreferenceManager; @@ -40,11 +40,11 @@ public void onReceive(Context context, Intent intent) { if (preferences.getBoolean( context.getString(R.string.settings_local_server_autostart_chkbx), false) && "android.intent.action.BOOT_COMPLETED".equals(intent.getAction())) { - Log.d(this.getClass().toString(), "Starting YAACC server on device start"); + YaaccLogger.d(this.getClass().toString(), "Starting YAACC server on device start"); Intent serviceIntent = new Intent(context, YaaccUpnpServerService.class); context.startForegroundService(serviceIntent); } else { - Log.d(this.getClass().toString(), "Not starting YAACC server on device start"); + YaaccLogger.d(this.getClass().toString(), "Not starting YAACC server on device start"); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolder.java b/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolder.java deleted file mode 100644 index a9a65a25..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolder.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * - * Copyright (C) 2025 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp.server; - - -import android.graphics.drawable.Drawable; -import android.util.TypedValue; -import android.view.View; -import android.widget.CheckBox; -import android.widget.ImageView; -import android.widget.TextView; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -import java.util.Set; - -import de.yaacc.R; -import de.yaacc.upnp.server.contentdirectory.MediaPathFilter; -import de.yaacc.util.ThemeHelper; - - -public class TreeViewHolder extends RecyclerView.ViewHolder { - - - /** - * The default padding value for the TreeNode item - */ - private int nodePadding = 50; - private final TextView fileName; - private final ImageView fileStateIcon; - private final ImageView fileTypeIcon; - - private final CheckBox fileCheckbox; - - public TreeViewHolder(@NonNull View itemView) { - super(itemView); - - this.fileName = itemView.findViewById(R.id.file_name); - this.fileStateIcon = itemView.findViewById(R.id.file_state_icon); - this.fileTypeIcon = itemView.findViewById(R.id.file_type_icon); - this.fileCheckbox = itemView.findViewById(R.id.file_checkbox); - } - - - public void bindTreeNode(TreeNode node) { - int padding = node.getLevel() * nodePadding; - itemView.setPadding( - padding, - itemView.getPaddingTop(), - itemView.getPaddingRight(), - itemView.getPaddingBottom()); - - - fileName.setText(node.getValue().getName()); - fileCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { - Set pathes = MediaPathFilter.getMediaPathesRaw(fileCheckbox.getContext()); - if (isChecked) { - pathes.add(node.getValue().getAbsolutePath()); - } else { - pathes.remove(node.getValue().getAbsolutePath()); - } - MediaPathFilter.saveMediaPaths(fileCheckbox.getContext(), pathes); - }); - if (node.getValue() != null && node.getValue().isDirectory()) { - Drawable icon = ThemeHelper.tintDrawable(fileTypeIcon.getContext().getDrawable(R.drawable.ic_baseline_folder_open_48), fileTypeIcon.getContext().getTheme()); - fileTypeIcon.setImageDrawable(icon); - fileCheckbox.setChecked(MediaPathFilter.getMediaPathesRaw(fileCheckbox.getContext()).contains(node.getValue().getAbsolutePath())); - fileCheckbox.setVisibility(View.VISIBLE); - } else { - fileCheckbox.setVisibility(View.INVISIBLE); - Drawable icon = ThemeHelper.tintDrawable(fileTypeIcon.getContext().getDrawable(R.drawable.ic_baseline_file_48), fileTypeIcon.getContext().getTheme()); - fileTypeIcon.setImageDrawable(icon); - } - - if (node.isSelected()) { - TypedValue typedValue = new TypedValue(); - itemView.getContext().getTheme().resolveAttribute(android.R.attr.colorActivatedHighlight, typedValue, true); - itemView.setBackgroundColor(typedValue.data); - itemView.getContext().getTheme().resolveAttribute(android.R.attr.colorPrimaryDark, typedValue, true); - fileName.setTextColor(typedValue.data); - } else { - TypedValue typedValue = new TypedValue(); - itemView.getContext().getTheme().resolveAttribute(android.R.attr.colorBackground, typedValue, true); - itemView.setBackgroundColor(typedValue.data); - itemView.getContext().getTheme().resolveAttribute(android.R.attr.colorForeground, typedValue, true); - fileName.setTextColor(typedValue.data); - } - fileStateIcon.setVisibility(View.INVISIBLE); - if (node.getValue() != null && node.getValue().isDirectory()) { - if (node.getValue().listFiles() != null && node.getValue().listFiles().length > 0) { - fileStateIcon.setVisibility(View.VISIBLE); - - int stateIcon = node.isExpanded() ? R.drawable.sharp_keyboard_arrow_down_24 : R.drawable.sharp_chevron_right_24; - Drawable icon = ThemeHelper.tintDrawable(fileStateIcon.getContext().getDrawable(stateIcon), fileStateIcon.getContext().getTheme()); - fileStateIcon.setImageDrawable(icon); - } - } - } - -} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccAudioRenderingControlService.java b/yaacc/src/main/java/de/yaacc/upnp/server/YaaccAudioRenderingControlService.java deleted file mode 100644 index f5b220d5..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccAudioRenderingControlService.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * - * Copyright (C) 2013 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp.server; - -import android.util.Log; - -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.model.types.UnsignedIntegerTwoBytes; -import org.fourthline.cling.support.model.Channel; -import org.fourthline.cling.support.renderingcontrol.AbstractAudioRenderingControl; -import org.fourthline.cling.support.renderingcontrol.RenderingControlException; - -import de.yaacc.upnp.UpnpClient; - - -/** - * @author Tobias Schoene (openbit) - */ -public class YaaccAudioRenderingControlService extends - AbstractAudioRenderingControl { - - - private final UpnpClient upnpClient; - - public YaaccAudioRenderingControlService(UpnpClient upnpClient) { - this.upnpClient = upnpClient; - } - - @Override - public boolean getMute(UnsignedIntegerFourBytes instanceId, String channelName) - throws RenderingControlException { - Log.d(getClass().getName(), "getMute() "); - return upnpClient.isMute(); - } - - @Override - public UnsignedIntegerTwoBytes getVolume(UnsignedIntegerFourBytes instanceId, - String channelName) throws RenderingControlException { - Log.d(getClass().getName(), "getVolume() "); - - return new UnsignedIntegerTwoBytes(upnpClient.getVolume()); - } - - @Override - public void setMute(UnsignedIntegerFourBytes instanceId, String channelName, boolean desiredMute) - throws RenderingControlException { - Log.d(getClass().getName(), "setMute()"); - upnpClient.setMute(desiredMute); - - } - - @Override - public void setVolume(UnsignedIntegerFourBytes instanceId, String channelName, - UnsignedIntegerTwoBytes desiredVolume) throws RenderingControlException { - Log.d(getClass().getName(), "setVolume() "); - upnpClient.setVolume(desiredVolume.getValue().intValue()); - } - - @Override - public UnsignedIntegerFourBytes[] getCurrentInstanceIds() { - Log.d(getClass().getName(), " getCurrentInstanceIds() - not yet implemented"); - return null; - } - - @Override - protected Channel[] getCurrentChannels() { - Log.d(getClass().getName(), " getCurrentChannels() - not yet implemented"); - return null; - } - -} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccRouter.java b/yaacc/src/main/java/de/yaacc/upnp/server/YaaccRouter.java deleted file mode 100644 index c0148527..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccRouter.java +++ /dev/null @@ -1,186 +0,0 @@ -package de.yaacc.upnp.server; - -import android.content.Context; -import android.net.ConnectivityManager; -import android.net.Network; -import android.net.NetworkCapabilities; -import android.net.wifi.WifiManager; -import android.util.Log; - -import androidx.annotation.NonNull; - -import org.fourthline.cling.UpnpServiceConfiguration; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.transport.RouterException; -import org.fourthline.cling.transport.RouterImpl; -import org.fourthline.cling.transport.spi.InitializationException; - -public class YaaccRouter extends RouterImpl { - private final Context context; - private final WifiManager wifiManager; - private WifiManager.MulticastLock multicastLock; - private WifiManager.WifiLock wifiLock; - - private Network currentNetwork; - - public YaaccRouter(UpnpServiceConfiguration configuration, - ProtocolFactory protocolFactory, - Context context) throws InitializationException { - super(configuration, protocolFactory); - this.context = context; - this.wifiManager = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)); - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (!isCellular()) { - currentNetwork = connectivityManager.getActiveNetwork(); - - } - if (currentNetwork != null) { - try { - enable(); - } catch (RouterException e) { - Log.e(getClass().getName(), String.format("RouterException network enabling %s", currentNetwork), e); - } - } - connectivityManager.registerDefaultNetworkCallback(new ConnectivityManager.NetworkCallback() { - @Override - public void onAvailable(@NonNull android.net.Network network) { - super.onAvailable(network); - if (!isCellular() && !network.equals(currentNetwork)) { - Log.d(getClass().getName(), String.format("Network available %s", network)); - if (currentNetwork != null) { - try { - disable(); - Log.d(getClass().getName(), String.format("Network disabled %s", currentNetwork)); - } catch (RouterException e) { - Log.e(getClass().getName(), String.format("RouterException network disabling %s", currentNetwork), e); - } - } - currentNetwork = network; - try { - enable(); - Log.d(getClass().getName(), String.format("Network enabled %s", currentNetwork)); - } catch (RouterException e) { - Log.e(getClass().getName(), String.format("RouterException network enabling %s", currentNetwork), e); - } - - } - - } - - @Override - public void onLost(@NonNull android.net.Network network) { - super.onLost(network); - if (network.equals(currentNetwork)) { - Log.d(getClass().getName(), String.format("Network lost %s", network)); - try { - disable(); - Log.d(getClass().getName(), String.format("Network disabled %s", currentNetwork)); - } catch (RouterException e) { - Log.e(getClass().getName(), String.format("RouterException network disabling %s", currentNetwork), e); - } - currentNetwork = null; - } - } - }); - } - - @Override - public boolean enable() throws RouterException { - Log.v(getClass().getName(), "in android router enable"); - lock(writeLock); - try { - boolean enabled = super.enable(); - if (enabled) { - // Enable multicast on the WiFi network interface, - // requires android.permission.CHANGE_WIFI_MULTICAST_STATE - if (isWifi()) { - setWiFiMulticastLock(true); - setWifiLock(true); - } - } - return enabled; - } finally { - unlock(writeLock); - Log.v(getClass().getName(), "leave android router enable"); - } - } - - @Override - public boolean disable() throws RouterException { - Log.v(getClass().getName(), "in android router disable"); - lock(writeLock); - try { - // Disable multicast on WiFi network interface, - // requires android.permission.CHANGE_WIFI_MULTICAST_STATE - if (isWifi()) { - setWiFiMulticastLock(false); - setWifiLock(false); - } - return super.disable(); - } finally { - unlock(writeLock); - Log.v(getClass().getName(), "leave android router disable"); - } - } - - private boolean isWifi() { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork()) == null) { - return false; - } - return connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork()).hasTransport(NetworkCapabilities.TRANSPORT_WIFI); - } - - private boolean isCellular() { - ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); - if (connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork()) == null) { - return false; - } - return connectivityManager.getNetworkCapabilities(connectivityManager.getActiveNetwork()).hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR); - } - - protected void setWiFiMulticastLock(boolean enable) { - if (multicastLock == null) { - multicastLock = wifiManager.createMulticastLock(getClass().getSimpleName()); - } - - if (enable) { - if (multicastLock.isHeld()) { - Log.w(getClass().getName(), "WiFi multicast lock already acquired"); - } else { - Log.d(getClass().getName(), "WiFi multicast lock acquired"); - multicastLock.acquire(); - } - } else { - if (multicastLock.isHeld()) { - Log.d(getClass().getName(), "WiFi multicast lock released"); - multicastLock.release(); - } else { - Log.w(getClass().getName(), "WiFi multicast lock already released"); - } - } - } - - protected void setWifiLock(boolean enable) { - if (wifiLock == null) { - wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, getClass().getSimpleName()); - } - - if (enable) { - if (wifiLock.isHeld()) { - Log.w(getClass().getName(), "WiFi lock already acquired"); - } else { - Log.d(getClass().getName(), "WiFi lock acquired"); - wifiLock.acquire(); - } - } else { - if (wifiLock.isHeld()) { - Log.d(getClass().getName(), "WiFi lock released"); - wifiLock.release(); - } else { - Log.w(getClass().getName(), "WiFi lock already released"); - } - } - } - -} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerControlActivity.java b/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerControlActivity.java deleted file mode 100644 index 3ca2e528..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerControlActivity.java +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Copyright (C) 2025 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp.server; - -import android.app.NotificationManager; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.os.Environment; -import android.util.Log; -import android.util.TypedValue; -import android.view.Menu; -import android.view.MenuItem; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.TextView; - -import androidx.appcompat.app.AppCompatActivity; -import androidx.preference.PreferenceManager; -import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; - -import com.google.android.material.switchmaterial.SwitchMaterial; - -import java.io.File; -import java.util.ArrayList; -import java.util.List; - -import de.yaacc.R; -import de.yaacc.settings.SettingsActivity; -import de.yaacc.upnp.server.contentdirectory.MediaPathFilter; -import de.yaacc.util.AboutActivity; -import de.yaacc.util.NotificationId; -import de.yaacc.util.YaaccLogActivity; - -/** - * Control activity for the yaacc upnp server - * - * @author Tobias Schoene (openbit) - */ -public class YaaccUpnpServerControlActivity extends AppCompatActivity { - - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_yaacc_upnp_server_control); - SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()); - boolean receiverActive = preferences.getBoolean(getString(R.string.settings_local_server_receiver_chkbx), false); - CheckBox receiverCheckBox = findViewById(R.id.receiverEnabled); - receiverCheckBox.setChecked(receiverActive); - receiverCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean(getApplicationContext().getString(R.string.settings_local_server_receiver_chkbx), isChecked); - editor.apply(); - }); - boolean providerActive = preferences.getBoolean(getString(R.string.settings_local_server_provider_chkbx), false); - CheckBox providerCheckBox = findViewById(R.id.providerEnabled); - providerCheckBox.setChecked(providerActive); - providerCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean(getApplicationContext().getString(R.string.settings_local_server_provider_chkbx), isChecked); - editor.apply(); - }); - boolean proxyActive = preferences.getBoolean(getString(R.string.settings_local_server_proxy_chkbx), false); - CheckBox proxyCheckBox = findViewById(R.id.proxyEnabled); - proxyCheckBox.setChecked(proxyActive); - proxyCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean(getApplicationContext().getString(R.string.settings_local_server_proxy_chkbx), isChecked); - editor.apply(); - }); - - boolean filterActive = preferences.getBoolean(getString(R.string.settings_local_server_media_filter_chkbx), true); - CheckBox mediaFilterCheckBox = findViewById(R.id.filterEnabled); - mediaFilterCheckBox.setChecked(filterActive); - mediaFilterCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean(getApplicationContext().getString(R.string.settings_local_server_media_filter_chkbx), isChecked); - editor.apply(); - }); - - - SwitchMaterial localServerEnabledSwitch = findViewById(R.id.serverOnOff); - localServerEnabledSwitch.setChecked(preferences.getBoolean(getApplicationContext().getString(R.string.settings_local_server_chkbx), false)); - localServerEnabledSwitch.setOnClickListener((v -> { - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean(v.getContext().getString(R.string.settings_local_server_chkbx), localServerEnabledSwitch.isChecked()); - editor.apply(); - if (localServerEnabledSwitch.isChecked()) { - start(); - } else { - stop(); - } - })); - Button resetButton = findViewById(R.id.sharedFoldersReset); - resetButton.setOnClickListener(v -> MediaPathFilter.resetMediaPaths(getApplicationContext())); - - TextView localServerControlInterface = findViewById(R.id.localServerControlInterface); - String[] ipConfig = YaaccUpnpServerService.getIfAndIpAddress(this); - localServerControlInterface.setText(ipConfig[1] + "@" + ipConfig[0]); - - - RecyclerView recyclerView = findViewById(R.id.folders_recycler_view); - recyclerView.setLayoutManager(new LinearLayoutManager(this)); - recyclerView.setNestedScrollingEnabled(false); - TypedValue typedValue = new TypedValue(); - getTheme().resolveAttribute(android.R.attr.colorBackground, typedValue, true); - recyclerView.setBackgroundColor(typedValue.data); - - TreeViewHolderFactory factory = (v, layout) -> new TreeViewHolder(v); - TreeViewAdapter treeViewAdapter = new TreeViewAdapter(factory); - recyclerView.setAdapter(treeViewAdapter); - buildFileSystemTree(treeViewAdapter); - } - - - private void buildFileSystemTree(TreeViewAdapter treeViewAdapter) { - List fileRoots = new ArrayList<>(); - File externalStorageRoot = Environment.getExternalStorageDirectory(); // Or any other root path - // Check if external storage is readable - if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || - Environment.MEDIA_MOUNTED_READ_ONLY.equals(Environment.getExternalStorageState())) { - - if (externalStorageRoot.exists() && externalStorageRoot.isDirectory()) { - // Add top-level directories from the chosen root - File[] topLevelFiles = externalStorageRoot.listFiles(); - if (topLevelFiles != null) { - for (File file : topLevelFiles) { - TreeNode node = buildFileSystemNode(file, R.layout.file_list_item); - if (node != null) { - fileRoots.add(node); - } - } - } else { - Log.e(getClass().getName(), "Could not list files in root: " + externalStorageRoot.getAbsolutePath()); - } - } else { - Log.e(getClass().getName(), "Root directory does not exist or is not a directory: " + externalStorageRoot.getAbsolutePath()); - } - } else { - Log.e(getClass().getName(), "External storage not readable."); - } - - if (fileRoots.isEmpty()) { - Log.w(getClass().getName(), "No file system roots found or storage unavailable. Adding a placeholder."); - } - - treeViewAdapter.updateTreeNodes(fileRoots); - - - treeViewAdapter.setTreeNodeClickListener((treeNode, nodeView) -> { - Log.d(getClass().getName(), "Click on TreeNode with value " + treeNode.getValue().toString()); - File file = treeNode.getValue(); - if (file.isDirectory() && file.listFiles() != null && treeNode.getChildren().size() != file.listFiles().length) { - File[] children = file.listFiles(); - if (children != null) { - for (File childFile : children) { - TreeNode childNode = buildFileSystemNode(childFile, treeNode.getLayoutId()); - if (childNode != null) { - treeNode.addChild(childNode); - } - } - } - } - Log.d(getClass().getName(), "Clicked on file: " + file.getAbsolutePath()); - - }); - - treeViewAdapter.setTreeNodeLongClickListener((treeNode, nodeView) -> { - Log.d(getClass().getName(), "LongClick on TreeNode with value " + treeNode.getValue().toString()); - return true; - }); - } - - /** - * Recursively builds a TreeNode structure from the file system. - * - * @param file The current file or directory. - * @param layoutId The layout resource ID for the TreeNode. - * @return A TreeNode representing the file/directory, or null if it should be skipped. - */ - private TreeNode buildFileSystemNode(File file, int layoutId) { - if (file == null || !file.exists()) { - return null; - } - - return new TreeNode(file, layoutId); - - } - - - private void start() { - - YaaccUpnpServerControlActivity.this.startForegroundService(new Intent(getApplicationContext(), - YaaccUpnpServerService.class)); - - - SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()); - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean(getString(R.string.settings_local_server_chkbx), true); - editor.apply(); - } - - private void stop() { - YaaccUpnpServerControlActivity.this.stopService(new Intent(getApplicationContext(), - YaaccUpnpServerService.class)); - SharedPreferences preferences = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()); - SharedPreferences.Editor editor = preferences.edit(); - editor.putBoolean(getString(R.string.settings_local_server_chkbx), false); - editor.apply(); - } - - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - // Inflate the menu; this adds items to the action bar if it is present. - getMenuInflater().inflate(R.menu.activity_yaacc_upnp_server_control, - menu); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - if (item.getItemId() == R.id.menu_exit) { - exit(); - return true; - } else if (item.getItemId() == R.id.menu_settings) { - Intent i = new Intent(this, SettingsActivity.class); - startActivity(i); - return true; - } else if (item.getItemId() == R.id.yaacc_log) { - YaaccLogActivity.showLog(this); - return true; - } else if (item.getItemId() == R.id.yaacc_about) { - AboutActivity.showAbout(this); - return true; - } - return super.onOptionsItemSelected(item); - } - - private void exit() { - stop(); - //FIXME work around to be fixed with new ui - NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - // mId allows you to update the notification later on. - mNotificationManager.cancel(NotificationId.UPNP_SERVER.getId()); - finish(); - } -} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerService.java b/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerService.java index e14a09f5..3e9c14fb 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerService.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerService.java @@ -21,26 +21,24 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.pm.PackageManager.NameNotFoundException; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; +import android.media.projection.MediaProjection; import android.os.Binder; import android.os.IBinder; -import android.util.Log; +import androidx.annotation.RequiresApi; import androidx.core.app.NotificationCompat; import androidx.core.content.res.ResourcesCompat; import androidx.preference.PreferenceManager; -import org.apache.hc.client5.http.classic.methods.HttpHead; -import org.apache.hc.client5.http.config.RequestConfig; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.core5.function.Callback; import org.apache.hc.core5.http.ConnectionClosedException; import org.apache.hc.core5.http.URIScheme; @@ -52,7 +50,6 @@ import org.apache.hc.core5.util.Timeout; import org.fourthline.cling.binding.annotations.AnnotationLocalServiceBinder; import org.fourthline.cling.model.DefaultServiceManager; -import org.fourthline.cling.model.ValidationError; import org.fourthline.cling.model.ValidationException; import org.fourthline.cling.model.meta.DeviceDetails; import org.fourthline.cling.model.meta.DeviceIdentity; @@ -61,12 +58,10 @@ import org.fourthline.cling.model.meta.LocalService; import org.fourthline.cling.model.meta.ManufacturerDetails; import org.fourthline.cling.model.meta.ModelDetails; -import org.fourthline.cling.model.types.DLNACaps; -import org.fourthline.cling.model.types.DLNADoc; +import org.fourthline.cling.model.types.ServiceType; import org.fourthline.cling.model.types.UDADeviceType; +import org.fourthline.cling.model.types.UDAServiceType; import org.fourthline.cling.model.types.UDN; -import org.fourthline.cling.protocol.async.SendingNotificationAlive; -import org.fourthline.cling.support.connectionmanager.ConnectionManagerService; import org.fourthline.cling.support.model.Protocol; import org.fourthline.cling.support.model.ProtocolInfo; import org.fourthline.cling.support.model.ProtocolInfos; @@ -75,27 +70,36 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; -import java.net.InetAddress; import java.net.InetSocketAddress; -import java.net.NetworkInterface; -import java.net.SocketException; import java.net.SocketTimeoutException; import java.net.URI; import java.util.ArrayList; -import java.util.Enumeration; +import java.util.Arrays; import java.util.List; -import java.util.Timer; -import java.util.TimerTask; import java.util.UUID; -import java.util.regex.Pattern; -import java.util.stream.Collectors; import de.yaacc.R; import de.yaacc.Yaacc; import de.yaacc.upnp.UpnpClient; +import de.yaacc.upnp.protocol.UpnpProtocolHandler; +import de.yaacc.upnp.registry.Registry; +import de.yaacc.upnp.registry.RegistryImpl; +import de.yaacc.upnp.server.avtransport.AvTransport; import de.yaacc.upnp.server.avtransport.YaaccAVTransportService; +import de.yaacc.upnp.server.configuration.YaaccUpnpServerControlActivity; +import de.yaacc.upnp.server.connectionmanager.ConnectionManagerService; import de.yaacc.upnp.server.contentdirectory.YaaccContentDirectory; +import de.yaacc.upnp.server.http.YaaccUpnpServerContentHttpHandler; +import de.yaacc.upnp.server.http.YaaccUpnpServerProtocolRequestHandler; +import de.yaacc.upnp.server.media.CombinedCaptureService; +import de.yaacc.upnp.server.media.MediaProjectionHelper; +import de.yaacc.upnp.server.media.ScreenCastCaptureService; +import de.yaacc.upnp.server.media.SystemAudioCaptureService; +import de.yaacc.upnp.server.renderingcontrol.YaaccAudioRenderingControlService; +import de.yaacc.util.InterfaceResolutionHelper; import de.yaacc.util.NotificationId; +import de.yaacc.util.SAFCacheManager; +import de.yaacc.util.YaaccLogger; /** * A simple local upnp server implementation. This class encapsulate the creation @@ -109,24 +113,39 @@ public class YaaccUpnpServerService extends Service implements SharedPreferences public static final String PROXY_LINK_KEY_PREFIX = "proxy_link_"; public static final String PROXY_LINK_MIME_TYPE_KEY_PREFIX = "proxy_link_mime_type"; public static final String PROXY_PATH = "proxy"; + public static final String SAF_PATH = "saf"; public static final int LOCK_TIMEOUT = 5000; - private static final Pattern IPV4_PATTERN = - Pattern.compile( - "^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$"); + public static int PORT = 49157; - public String mediaServerUuid; - public String mediaRendererUuid; + public static ServiceType[] EXCLUSIVE_SERVER_TYPES = new ServiceType[]{ + new UDAServiceType("AVTransport"), + new UDAServiceType("ContentDirectory"), + new UDAServiceType("ConnectionManager"), + new UDAServiceType("RenderingControl"), + new UDAServiceType("X_MS_MediaReceiverRegistrar")}; + + public String locaDeviceUuid; protected IBinder binder = new YaaccUpnpServerServiceBinder(); SharedPreferences preferences; - private LocalDevice localServer; - private LocalDevice localRenderer; - private UpnpClient upnpClient; private LocalService contentDirectoryService; - private boolean watchdog; + private NetworkDeviceListener networkDeviceListener; + + private Registry registry; + private HttpAsyncServer httpServer; - private Timer timer; + private LocalDevice localDevice; + + // Live streaming (Android 10+) + private SystemAudioCaptureService audioCapture; + private ScreenCastCaptureService videoCapture; + private CombinedCaptureService combinedCapture; + private MediaProjection projection; + private static final int HTTP_SERVER_RETRY_DELAY_MS = 2000; + private static final int HTTP_SERVER_MAX_RETRIES = 3; + private final Object initLock = new Object(); + private volatile boolean isInitialized = false; /* * (non-Javadoc) @@ -135,7 +154,7 @@ public class YaaccUpnpServerService extends Service implements SharedPreferences */ @Override public IBinder onBind(Intent intent) { - Log.d(this.getClass().getName(), "On Bind"); + YaaccLogger.d(this.getClass().getName(), "On Bind"); // do nothing return binder; } @@ -143,12 +162,54 @@ public IBinder onBind(Intent intent) { @Override public void onCreate() { super.onCreate(); + YaaccLogger.i(getClass().getName(), "YaaccUpnpServerService onCreate called"); // when the service starts, the preferences are initialized preferences = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); preferences.registerOnSharedPreferenceChangeListener(this); - timer = new Timer(); + + // Register broadcast receiver for cache updates + registerReceiver(cacheUpdateReceiver, new IntentFilter("de.yaacc.CACHE_PRELOAD_COMPLETE")); + registerReceiver(cacheProgressReceiver, new IntentFilter("de.yaacc.CACHE_PRELOAD_PROGRESS")); + registerReceiver(safPathsChangedReceiver, new IntentFilter("de.yaacc.SAF_PATHS_CHANGED")); + + // Start background preloading of SAF durations + SAFCacheManager.getInstance(getApplicationContext()).preloadSafDurations(); + + YaaccLogger.i(getClass().getName(), "YaaccUpnpServerService onCreate complete"); } + private int cacheFilesIndexed = 0; + private String cacheCurrentFolder = ""; + + private final BroadcastReceiver cacheUpdateReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + int total = intent.getIntExtra("files_indexed", 0); + cacheFilesIndexed = total; + showNotification(); // Update notification with final cache status + } + }; + + private final BroadcastReceiver cacheProgressReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + cacheFilesIndexed = intent.getIntExtra("files_indexed", 0); + cacheCurrentFolder = intent.getStringExtra("current_folder"); + showNotification(); // Update notification with progress + } + }; + + private final BroadcastReceiver safPathsChangedReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + // Restart SAF preloading with new paths + cacheFilesIndexed = 0; + cacheCurrentFolder = ""; + SAFCacheManager.getInstance(getApplicationContext()).preloadSafDurations(); + showNotification(); + } + }; + /* * (non-Javadoc) @@ -157,20 +218,22 @@ public void onCreate() { */ @Override public int onStartCommand(Intent intent, int flags, int startId) { + YaaccLogger.i(getClass().getName(), "YaaccUpnpServerService onStartCommand called"); long start = System.currentTimeMillis(); - - mediaServerUuid = preferences.getString(getApplicationContext().getString(R.string.settings_local_server_provider_uuid_key), null); - if (mediaServerUuid == null) { - mediaServerUuid = UUID.randomUUID().toString(); - preferences.edit().putString(getApplicationContext().getString(R.string.settings_local_server_provider_uuid_key), mediaServerUuid).commit(); + if (registry == null) { + registry = new RegistryImpl(); } - mediaRendererUuid = preferences.getString(getApplicationContext().getString(R.string.settings_local_server_receiver_uuid_key), null); - if (mediaRendererUuid == null) { - mediaRendererUuid = UUID.randomUUID().toString(); - preferences.edit().putString(getApplicationContext().getString(R.string.settings_local_server_receiver_uuid_key), mediaRendererUuid).commit(); + if (networkDeviceListener == null) { + networkDeviceListener = new NetworkDeviceListener(getApplicationContext(), registry); + registry.setUpnpProtocolHandler(networkDeviceListener.getUpnpProtocolHandler()); } - if (getUpnpClient() == null) { - setUpnpClient(new UpnpClient()); + // App is active when service starts + networkDeviceListener.setAppInForeground(true); + + locaDeviceUuid = preferences.getString(getApplicationContext().getString(R.string.settings_local_device_uuid_key), null); + if (locaDeviceUuid == null) { + locaDeviceUuid = UUID.randomUUID().toString(); + preferences.edit().putString(getApplicationContext().getString(R.string.settings_local_device_uuid_key), locaDeviceUuid).commit(); } // the footprint of the onStart() method must be small // otherwise android will kill the service @@ -179,42 +242,73 @@ public int onStartCommand(Intent intent, int flags, int startId) { Thread initializationThread = new Thread(this::initialize); initializationThread.start(); showNotification(); - Log.d(this.getClass().getName(), "End On Start"); - Log.d(this.getClass().getName(), "on start took: " + (System.currentTimeMillis() - start)); + YaaccLogger.d(this.getClass().getName(), "End On Start"); + YaaccLogger.d(this.getClass().getName(), "on start took: " + (System.currentTimeMillis() - start)); return START_STICKY; } @Override public void onDestroy() { - Log.d(this.getClass().getName(), "Destroying the service"); + YaaccLogger.d(this.getClass().getName(), "Destroying the service"); + + synchronized (initLock) { + isInitialized = false; + } + if (preferences != null) { preferences.unregisterOnSharedPreferenceChangeListener(this); } - if (getUpnpClient() != null) { - if (localServer != null) { - getUpnpClient().localDeviceRemoved(getUpnpClient().getRegistry(), localServer); - localServer = null; - } - if (localRenderer != null) { - getUpnpClient().localDeviceRemoved(getUpnpClient().getRegistry(), localRenderer); - localRenderer = null; - } + // Unregister broadcast receivers + try { + unregisterReceiver(cacheUpdateReceiver); + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Error unregistering cache receiver", e); + } + try { + unregisterReceiver(cacheProgressReceiver); + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Error unregistering cache progress receiver", e); + } + try { + unregisterReceiver(safPathsChangedReceiver); + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Error unregistering SAF paths receiver", e); + } + + if (localDevice != null) { + registry.removeDevice(localDevice); + localDevice = null; } + + networkDeviceListener.disable(); if (httpServer != null) { httpServer.initiateShutdown(); try { httpServer.awaitShutdown(TimeValue.ofSeconds(3)); } catch (InterruptedException e) { - Log.w(getClass().getName(), "got exception on stream server stop ", e); + YaaccLogger.w(getClass().getName(), "got exception on stream server stop ", e); } - + httpServer = null; } cancleNotification(); + + super.onDestroy(); } + + @Override + public void onTaskRemoved(Intent rootIntent) { + super.onTaskRemoved(rootIntent); + YaaccLogger.d(getClass().getName(), "Task removed - app backgrounded"); + if (networkDeviceListener != null) { + networkDeviceListener.setAppInForeground(false); + updateNotification(); // WiFi lock may have changed + } + } + /** * Displays the notification. */ @@ -222,18 +316,73 @@ private void showNotification() { ((Yaacc) getApplicationContext()).createYaaccGroupNotification(); Intent notificationIntent = new Intent(this, YaaccUpnpServerControlActivity.class); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE); + + // Build status line with active features + StringBuilder statusBuilder = new StringBuilder(); + + // HTTP Server status + if (httpServer != null) { + try { + IOReactorStatus status = httpServer.getStatus(); + statusBuilder.append(status == IOReactorStatus.ACTIVE ? "✓ HTTP" : "⚠ HTTP"); + } catch (Exception e) { + statusBuilder.append("⚠ HTTP"); + } + } else { + statusBuilder.append("⚠ HTTP"); + } + + // Server/Renderer status + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); + boolean serverEnabled = preferences.getBoolean(getString(R.string.settings_local_server_chkbx), false); + boolean poviderEnabled = preferences.getBoolean(getString(R.string.settings_local_server_provider_chkbx), false); + boolean rendererEnabled = preferences.getBoolean(getString(R.string.settings_local_server_receiver_chkbx), false); + boolean proxyEnabled = preferences.getBoolean(getString(R.string.settings_local_server_proxy_chkbx), false); + + if (serverEnabled && poviderEnabled) { + statusBuilder.append(" | ✓ Server"); + } + if (serverEnabled && rendererEnabled) { + statusBuilder.append(" | ✓ Renderer"); + } + if (serverEnabled && proxyEnabled) { + statusBuilder.append(" | ✓ Proxy"); + } + // WiFi lock status + if (networkDeviceListener != null && networkDeviceListener.isWifiLockHeld()) { + statusBuilder.append(" | ✓ WiFi Lock"); + } + + // Duration cache status + SAFCacheManager cacheManager = SAFCacheManager.getInstance(this); + if (cacheManager.isPreloading()) { + statusBuilder.append(" | ⏳ Indexing: ").append(cacheFilesIndexed); + if (!cacheCurrentFolder.isEmpty()) { + statusBuilder.append(" (").append(cacheCurrentFolder).append(")"); + } + } else if (cacheManager.getCacheSize() > 0) { + statusBuilder.append(" | ✓ Cache: ").append(cacheManager.getCacheSize()); + } + NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this, Yaacc.NOTIFICATION_CHANNEL_ID) .setOngoing(true) .setSmallIcon(R.drawable.ic_notification_default) .setSilent(true) - .setContentTitle("Yaacc Upnp Server") + .setContentTitle("Yaacc UPnP Service") .setGroup(Yaacc.NOTIFICATION_GROUP_KEY) - .setContentText(preferences.getString(getApplicationContext().getString(R.string.settings_local_server_name_key), "")); + .setContentText(statusBuilder.toString()); mBuilder.setContentIntent(contentIntent); startForeground(NotificationId.UPNP_SERVER.getId(), mBuilder.build()); } + /** + * Update notification with current server status. + */ + private void updateNotification() { + showNotification(); + } + /** * Cancels the notification. */ @@ -248,292 +397,383 @@ private void cancleNotification() { * */ private void initialize() { - if (getUpnpClient() == null) { - setUpnpClient(new UpnpClient()); - } - if (!getUpnpClient().isInitialized()) { - getUpnpClient().initialize(getApplicationContext()); - watchdog = false; - new Timer().schedule(new TimerTask() { - - @Override - public void run() { - watchdog = true; - } - }, 30000L); // 30 sec. watchdog - - while (!getUpnpClient().isInitialized() && !watchdog) { - // wait for upnpClient initialization + synchronized (initLock) { + if (isInitialized) { + YaaccLogger.d(getClass().getName(), "Already initialized, skipping"); + return; } - } - if (getUpnpClient() != null && getUpnpClient().isInitialized()) { - if (preferences.getBoolean(getApplicationContext().getString(R.string.settings_local_server_provider_chkbx), false)) { - if (localServer == null) { - localServer = createMediaServerDevice(); - } - getUpnpClient().getRegistry().addDevice(localServer); + YaaccLogger.i(getClass().getName(), "initialize() called"); - } - if (preferences.getBoolean(getApplicationContext().getString(R.string.settings_local_server_provider_chkbx), false) - || preferences.getBoolean(getApplicationContext().getString(R.string.settings_local_server_proxy_chkbx), false)) { + // Try to create HTTP server with retries + boolean serverStarted = false; + for (int attempt = 1; attempt <= HTTP_SERVER_MAX_RETRIES && !serverStarted; attempt++) { try { + YaaccLogger.d(getClass().getName(), "Calling createHttpServer() - attempt " + attempt); createHttpServer(); + + // Health check: verify server is actually listening + if (isHttpServerHealthy()) { + YaaccLogger.i(getClass().getName(), "HTTP server started successfully"); + serverStarted = true; + } else { + YaaccLogger.w(getClass().getName(), "HTTP server created but health check failed - attempt " + attempt); + shutdownHttpServer(); + if (attempt < HTTP_SERVER_MAX_RETRIES) { + Thread.sleep(HTTP_SERVER_RETRY_DELAY_MS); + } + } } catch (IOException e) { - Log.e(getClass().getName(), "Error while creating http server", e); + YaaccLogger.e(getClass().getName(), "Error creating HTTP server - attempt " + attempt, e); + if (attempt < HTTP_SERVER_MAX_RETRIES) { + try { + Thread.sleep(HTTP_SERVER_RETRY_DELAY_MS); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + break; + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; } } - if (preferences.getBoolean(getApplicationContext().getString(R.string.settings_local_server_receiver_chkbx), false)) { - if (localRenderer == null) { - localRenderer = createMediaRendererDevice(); - } - getUpnpClient().getRegistry().addDevice(localRenderer); + if (!serverStarted) { + YaaccLogger.e(getClass().getName(), "Failed to start HTTP server after " + HTTP_SERVER_MAX_RETRIES + " attempts"); } - } else { - throw new IllegalStateException("UpnpClient is not initialized!"); - } - startUpnpAliveNotifications(); - } + createUpnpDevice(); + isInitialized = true; - public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - Log.d(this.getClass().getName(), "Preference changed reinitialize the service"); - Thread initializationThread = new Thread(this::initialize); - initializationThread.start(); + // Update notification with server status + updateNotification(); + + YaaccLogger.d(getClass().getName(), "initialize() complete"); + } } - /** - * creates a http request thread - */ - private void createHttpServer() throws IOException { - // Create a HttpService for providing content in the network. - // Set up the HTTP service + private boolean isHttpServerHealthy() { if (httpServer == null) { - IOReactorConfig config = IOReactorConfig.custom() - .setSoKeepAlive(true) - .setTcpNoDelay(true) - .build(); - httpServer = H2ServerBootstrap.bootstrap() - .setIOReactorConfig(config) - .setExceptionCallback(new Callback() { - - @Override - public void execute(Exception ex) { - if (ex instanceof SocketTimeoutException) { - Log.e(getClass().getName(), "connection timeout:", ex); - } else if (ex instanceof ConnectionClosedException) { - Log.e(getClass().getName(), "connection closed:", ex); - } else { - Log.e(getClass().getName(), "connection error:", ex); - } - } + return false; + } - }) - .setCanonicalHostName(getIpAddress(getApplicationContext())) - .register("*", new YaaccUpnpServerServiceHttpHandler(getApplicationContext())) - .create(); - httpServer.start(); - } else { + // Check if server is in ACTIVE state + try { + IOReactorStatus status = httpServer.getStatus(); + boolean healthy = status == IOReactorStatus.ACTIVE; + YaaccLogger.d(getClass().getName(), "HTTP server health check: status=" + status + ", healthy=" + healthy); + return healthy; + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "HTTP server health check failed", e); + return false; + } + } - httpServer.resume(); + private void shutdownHttpServer() { + if (httpServer != null) { + try { + httpServer.initiateShutdown(); + httpServer.awaitShutdown(TimeValue.ofSeconds(1)); + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Error shutting down HTTP server", e); + } + httpServer = null; } - httpServer.listen(new InetSocketAddress(PORT), URIScheme.HTTP); - Log.d(getClass().getName(), "Server status: " + httpServer.getStatus().name()); - Log.d(getClass().getName(), "Server Endpoints: " + httpServer.getEndpoints().size()); - httpServer.getEndpoints().forEach(endpoint -> Log.d(getClass().getName(), "Endpoint: " + endpoint.toString())); -/* - timer.schedule(new TimerTask() { + } - @Override - public void run() { - checkIfHttpServerIsRunning(); + private void createUpnpDevice() { + String versionName; + YaaccLogger.d(this.getClass().getName(), "Create UPNP Device whith ID: " + locaDeviceUuid); + + // Ensure HTTP server is running when creating device + if (httpServer == null) { + YaaccLogger.w(this.getClass().getName(), "HTTP server not running, attempting to create"); + try { + createHttpServer(); + } catch (IOException e) { + YaaccLogger.e(this.getClass().getName(), "Failed to create HTTP server during device creation", e); } - }, 6000L); -*/ + } - } + // Remove old device if it exists (by UDN, not by reference) + if (localDevice != null && registry.getDevices().contains(localDevice)) { + YaaccLogger.d(this.getClass().getName(), "Removing old device before creating new one"); + registry.removeDevice(localDevice); + } + try { + versionName = getApplicationContext().getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), 0).versionName; + } catch (NameNotFoundException ex) { + YaaccLogger.e(this.getClass().getName(), "Error while creating device", ex); + versionName = "??"; + } + try { - private void checkIfHttpServerIsRunning() { - try (CloseableHttpClient httpClient = HttpClients.createDefault()) { - HttpHead httpHead = new HttpHead("http://" + getIpAddress(getApplicationContext()) + ":" + PORT + "/health"); - RequestConfig requestConfig = RequestConfig.custom() - .setConnectionRequestTimeout(Timeout.ofSeconds(5)) - .setConnectTimeout(Timeout.ofSeconds(5)) - .setResponseTimeout(Timeout.ofSeconds(5)) - .build(); - httpHead.setConfig(requestConfig); + // Yaacc Details + // Used for shown name: first part of ManufactDet, first + // part of ModelDet and version number + DeviceDetails yaaccDetails = new DeviceDetails( + getLocalServerName(), new ManufacturerDetails("yaacc.de", + "https://www.yaacc.de"), new ModelDetails(getLocalServerName() + "- UpnP", "Free Android UPnP/DLNA, GNU GPL", + versionName), URI.create("http://" + InterfaceResolutionHelper.getIpAddress(getApplicationContext()) + ":" + PORT)); + + + List> services = new ArrayList(); + services.addAll(Arrays.asList(createCoreServices())); + + boolean serverEnabled = preferences.getBoolean(getApplicationContext().getString(R.string.settings_local_server_chkbx), false); + boolean providerEnabled = preferences.getBoolean(getApplicationContext().getString(R.string.settings_local_server_provider_chkbx), false); + boolean rendererEnabled = preferences.getBoolean(getApplicationContext().getString(R.string.settings_local_server_receiver_chkbx), false); + + DeviceIdentity identity = new DeviceIdentity(new UDN(locaDeviceUuid)); + + // If both server and renderer are enabled, create TWO separate devices + if (serverEnabled && providerEnabled && rendererEnabled) { + // Create MediaServer device + DeviceDetails serverDetails = new DeviceDetails( + getLocalServerName() + " - Server", + new ManufacturerDetails("yaacc.de", "https://www.yaacc.de"), + new ModelDetails(getLocalServerName() + " - UpnP Server", "Free Android UPnP/DLNA, GNU GPL", versionName), + URI.create("http://" + InterfaceResolutionHelper.getIpAddress(getApplicationContext()) + ":" + PORT) + ); + + LocalDevice serverDevice = new LocalDevice( + new DeviceIdentity(new UDN(locaDeviceUuid + "-server")), + new UDADeviceType("MediaServer"), + serverDetails, + createDeviceIcons(), + createMediaServerServices() + ); + + // Create MediaRenderer device + DeviceDetails rendererDetails = new DeviceDetails( + getLocalServerName() + " - Renderer", + new ManufacturerDetails("yaacc.de", "https://www.yaacc.de"), + new ModelDetails(getLocalServerName() + " - UpnP Renderer", "Free Android UPnP/DLNA, GNU GPL", versionName), + URI.create("http://" + InterfaceResolutionHelper.getIpAddress(getApplicationContext()) + ":" + PORT) + ); + + LocalDevice rendererDevice = new LocalDevice( + new DeviceIdentity(new UDN(locaDeviceUuid + "-renderer")), + new UDADeviceType("MediaRenderer"), + rendererDetails, + createDeviceIcons(), + createMediaRendererServices() + ); + + // Register BOTH devices as separate top-level devices + registry.addDevice(serverDevice); + registry.addDevice(rendererDevice); + + localDevice = serverDevice; // Track server device for reference + } else { + // Single device type + if (serverEnabled && providerEnabled) { + services.addAll(Arrays.asList(createMediaServerServices())); + } + if (serverEnabled && rendererEnabled) { + services.addAll(Arrays.asList(createMediaRendererServices())); + } - try (CloseableHttpResponse response = httpClient.execute(httpHead)) { - int statusCode = response.getCode(); - if (statusCode >= 200 && statusCode < 300) { - Log.i(getClass().getName(), "HttpServer responded with HTTP " + statusCode + ". It is operational."); + UDADeviceType deviceType; + if (rendererEnabled && !providerEnabled) { + deviceType = new UDADeviceType("MediaRenderer"); } else { - Log.w(getClass().getName(), "HttpServer responded with HTTP " + statusCode + ". It might be listening but not fully operational."); + deviceType = new UDADeviceType("MediaServer"); } - } - } catch (IOException e) { - Log.e(getClass().getName(), "HttpServer is NOT responding to HTTP requests or is unreachable. Trying restart", e); - //restartServerService(); - if (httpServer != null && httpServer.getStatus() == IOReactorStatus.ACTIVE) { - httpServer.listen(new InetSocketAddress(PORT), URIScheme.HTTP); - timer.schedule(new TimerTask() { - - @Override - public void run() { - Log.d(getClass().getName(), "Server Endpoints after restart listener: " + httpServer.getEndpoints().size()); - httpServer.getEndpoints().forEach(endpoint -> Log.d(getClass().getName(), "Endpoint: " + endpoint.toString())); - } - }, 500L); - } else { - restartServerService(); + + localDevice = new LocalDevice(identity, deviceType, yaaccDetails, createDeviceIcons(), services.toArray(new LocalService[0])); + registry.addDevice(localDevice); } - return; + // Configure ALIVE announcement interval from settings + int aliveInterval = getUpnpNotificationFrequency(); + registry.setAliveInterval(aliveInterval); + YaaccLogger.d(this.getClass().getName(), "UPnP ALIVE interval set to: " + aliveInterval + "ms"); + + } catch (ValidationException e) { + YaaccLogger.e(this.getClass().getName(), "Exception during device creation", e); + if (e.getErrors() != null) { + for (Object error : e.getErrors()) { + YaaccLogger.e(this.getClass().getName(), "Validation error: " + error.toString()); + } + } + throw new IllegalStateException("Exception during device creation", e); } + } - private void restartServerService() { - if (httpServer != null) { - httpServer.initiateShutdown(); - try { - httpServer.awaitShutdown(TimeValue.ofSeconds(3)); - } catch (InterruptedException e) { - Log.w(getClass().getName(), "got exception on stream server stop ", e); - } + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + YaaccLogger.d(this.getClass().getName(), "Preference changed apply change"); + if (registry == null) { + YaaccLogger.d(this.getClass().getName(), "Registry is null"); + registry = new RegistryImpl(); + } + if (getApplicationContext().getString(R.string.settings_local_server_chkbx).equals(key)) { + createUpnpDevice(); + updateNotification(); } - httpServer = null; - timer.schedule(new TimerTask() { - @Override - public void run() { - try { - createHttpServer(); - } catch (IOException e) { - Log.w(getClass().getName(), "got exception on stream server stop ", e); - } - } - }, 600L); + if (getApplicationContext().getString(R.string.settings_local_server_provider_chkbx).equals(key)) { + createUpnpDevice(); + } + if (getApplicationContext().getString(R.string.settings_local_server_receiver_chkbx).equals(key)) { + createUpnpDevice(); + updateNotification(); + } + if (getApplicationContext().getString(R.string.settings_sending_upnp_alive_interval_key).equals(key)) { + int aliveInterval = getUpnpNotificationFrequency(); + registry.setAliveInterval(aliveInterval); + YaaccLogger.d(this.getClass().getName(), "UPnP ALIVE interval updated to: " + aliveInterval + "ms"); + } - } + // Trigger cache update when SAF paths change + if (getApplicationContext().getString(R.string.settings_saf_tree_uris_pref_key).equals(key) || + getApplicationContext().getString(R.string.settings_saf_tree_uris_selected_pref_key).equals(key)) { + YaaccLogger.d(this.getClass().getName(), "SAF paths changed, reloading cache"); + SAFCacheManager.getInstance(getApplicationContext()).preloadSafDurations(); + } - /** - * start sending periodical upnp alive notifications. - */ - private void startUpnpAliveNotifications() { - int upnpNotificationFrequency = getUpnpNotificationFrequency(); - if (upnpNotificationFrequency != -1 && preferences.getBoolean(getString(R.string.settings_local_server_chkbx), false)) { - new Timer().schedule(new TimerTask() { - @Override - public void run() { - Log.v(YaaccUpnpServerService.this.getClass().getName(), "Sending upnp alive notivication"); - SendingNotificationAlive sendingNotificationAlive; - if (localServer != null && getUpnpClient() != null) { - sendingNotificationAlive = new SendingNotificationAlive(getUpnpClient().getRegistry().getUpnpService(), localServer); - sendingNotificationAlive.run(); - } - if (localRenderer != null && getUpnpClient() != null) { - sendingNotificationAlive = new SendingNotificationAlive(getUpnpClient().getRegistry().getUpnpService(), localRenderer); - sendingNotificationAlive.run(); - } - startUpnpAliveNotifications(); + // Handle live streaming toggles (Android 10+) + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + if (getApplicationContext().getString(R.string.settings_local_server_serve_system_audio_chkbx).equals(key)) { + boolean enabled = sharedPreferences.getBoolean(key, false); + YaaccLogger.d(getClass().getName(), "Audio preference changed: " + enabled); + if (enabled) { + startAudioCapture(); + //FIXME experimental checkStartCombinedCapture(); + } else { + stopAudioCapture(); + //FIXME experimentalstopCombinedCapture(); } - }, upnpNotificationFrequency); - + } + if (getApplicationContext().getString(R.string.settings_local_server_serve_screen_cast_chkbx).equals(key)) { + boolean enabled = sharedPreferences.getBoolean(key, false); + YaaccLogger.d(getClass().getName(), "Video preference changed: " + enabled); + if (enabled) { + startVideoCapture(); + //checkStartCombinedCapture(); + //FIXME experimental startCombinedCapture(); + } else { + stopVideoCapture(); + //FIXME experimental stopCombinedCapture(); + } + } } } - /** - * the time between two upnp alive notifications. -1 if never send a - * notification - * - * @return the time - */ - private int getUpnpNotificationFrequency() { - if (getUpnpClient() == null) { - return -1; + @androidx.annotation.RequiresApi(api = android.os.Build.VERSION_CODES.Q) + private void checkStartCombinedCapture() { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean audioEnabled = prefs.getBoolean(getString(R.string.settings_local_server_serve_system_audio_chkbx), false); + boolean videoEnabled = prefs.getBoolean(getString(R.string.settings_local_server_serve_screen_cast_chkbx), false); + + YaaccLogger.d(getClass().getName(), "checkStartCombinedCapture: audio=" + audioEnabled + " video=" + videoEnabled); + + // If both enabled, use combined capture instead of individual + if (audioEnabled && videoEnabled) { + // Stop individual captures if running + stopAudioCapture(); + stopVideoCapture(); + startCombinedCapture(); } - return Integer.parseInt(preferences.getString(getUpnpClient().getContext().getString(R.string.settings_sending_upnp_alive_interval_key), "5000")); } /** - * Create a local upnp renderer device - * - * @return the device + * creates a http request thread */ - private LocalDevice createMediaRendererDevice() { - LocalDevice device; - String versionName; - Log.d(this.getClass().getName(), "Create MediaRenderer with ID: " + mediaServerUuid); - try { - versionName = getApplicationContext().getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), 0).versionName; - } catch (NameNotFoundException ex) { - Log.e(this.getClass().getName(), "Error while creating device", ex); - versionName = "??"; + private void createHttpServer() throws IOException { + // Create a HttpService for providing content in the network. + // Set up the HTTP service + if (httpServer == null) { + try { + YaaccLogger.d(getClass().getName(), "Creating new HTTP server"); + IOReactorConfig config = IOReactorConfig.custom() + .setSoReuseAddress(true) + .setSoKeepAlive(true) + .setTcpNoDelay(true) + .setSoTimeout(Timeout.ofMinutes(5)) + .setIoThreadCount(8) + .build(); + httpServer = H2ServerBootstrap.bootstrap() + .setIOReactorConfig(config) + .setExceptionCallback(new Callback() { + + @Override + public void execute(Exception ex) { + if (ex instanceof SocketTimeoutException) { + YaaccLogger.e(getClass().getName(), "connection timeout:", ex); + } else if (ex instanceof ConnectionClosedException) { + YaaccLogger.e(getClass().getName(), "connection closed:", ex); + } else { + YaaccLogger.e(getClass().getName(), "connection error:", ex); + } + } + + }) + .setCanonicalHostName(InterfaceResolutionHelper.getIpAddress(getApplicationContext())) + .register("*", new YaaccUpnpServerContentHttpHandler(getApplicationContext())) + .register(UpnpProtocolHandler.NAMESPACE.getBasePath().getPath() + "/*", new YaaccUpnpServerProtocolRequestHandler(getNetworkDeviceListener().getUpnpProtocolHandler())) + .create(); + YaaccLogger.d(getClass().getName(), "Starting HTTP server"); + httpServer.start(); + YaaccLogger.d(getClass().getName(), "HTTP server started, status=" + httpServer.getStatus()); + + + // Verify server is actually listening + try { + Thread.sleep(100); // Give it a moment to bind + YaaccLogger.d(getClass().getName(), "HTTP server should now be listening on port " + PORT); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to create HTTP server", e); + httpServer = null; + throw new IOException("Failed to create HTTP server", e); + } + } else { + YaaccLogger.d(getClass().getName(), "HTTP server exists, resuming"); + try { + httpServer.resume(); + YaaccLogger.d(getClass().getName(), "HTTP server resumed, status= " + httpServer.getStatus()); + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to resume HTTP server", e); + httpServer = null; + throw new IOException("Failed to resume HTTP server", e); + } } + try { - device = new LocalDevice(new DeviceIdentity(new UDN(mediaRendererUuid)), new UDADeviceType("MediaRenderer", 3), - // Used for shown name: first part of ManufactDet, first - // part of ModelDet and version number - new DeviceDetails("YAACC - MediaRenderer (" + getLocalServerName() + ")", - new ManufacturerDetails("yaacc", "http://www.yaacc.de"), - new ModelDetails(getLocalServerName() + "-Renderer", "Free Android UPnP AV MediaRender, GNU GPL", versionName), - new DLNADoc[]{ - new DLNADoc("DMS", DLNADoc.Version.V1_5), - new DLNADoc("M-DMS", DLNADoc.Version.V1_5) - }, - new DLNACaps(new String[]{"av-upload", "image-upload", "audio-upload"})), createDeviceIcons(), createMediaRendererServices(), null); - - return device; - } catch (ValidationException e) { - for (ValidationError validationError : e.getErrors()) { - Log.d(getClass().getCanonicalName(), validationError.toString()); + httpServer.listen(new InetSocketAddress(PORT), URIScheme.HTTP); + YaaccLogger.d(getClass().getName(), "Server listening on port " + PORT); + YaaccLogger.d(getClass().getName(), "Server status: " + httpServer.getStatus().name()); + YaaccLogger.d(getClass().getName(), "Server Endpoints: " + httpServer.getEndpoints().size()); + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to bind HTTP server to port " + PORT, e); + if (httpServer != null) { + httpServer.close(); + httpServer = null; } - throw new IllegalStateException("Exception during device creation", e); + throw new IOException("Failed to bind HTTP server to port " + PORT, e); } + httpServer.getEndpoints().forEach(endpoint -> YaaccLogger.d(getClass().getName(), "Endpoint: " + endpoint.toString())); } + /** - * Create a local upnp renderer device + * the time between two upnp alive notifications. -1 if never send a + * notification * - * @return the device + * @return the time */ - private LocalDevice createMediaServerDevice() { - LocalDevice device; - String versionName; - Log.d(this.getClass().getName(), "Create MediaServer whith ID: " + mediaServerUuid); - try { - versionName = getApplicationContext().getPackageManager().getPackageInfo(getApplicationContext().getPackageName(), 0).versionName; - } catch (NameNotFoundException ex) { - Log.e(this.getClass().getName(), "Error while creating device", ex); - versionName = "??"; - } - try { - - // Yaacc Details - // Used for shown name: first part of ManufactDet, first - // part of ModelDet and version number - DeviceDetails yaaccDetails = new DeviceDetails( - "YAACC - MediaServer(" + getLocalServerName() + ")", new ManufacturerDetails("yaacc.de", - "http://www.yaacc.de"), new ModelDetails(getLocalServerName() + "-MediaServer", "Free Android UPnP AV MediaServer, GNU GPL", - versionName), URI.create("http://" + getIpAddress(getApplicationContext()) + ":" + PORT)); - - - DeviceIdentity identity = new DeviceIdentity(new UDN(mediaServerUuid)); - - device = new LocalDevice(identity, new UDADeviceType("MediaServer"), yaaccDetails, createDeviceIcons(), createMediaServerServices()); - - return device; - } catch (ValidationException e) { - Log.e(this.getClass().getName(), "Exception during device creation", e); - Log.e(this.getClass().getName(), "Exception during device creation Errors:" + e.getErrors()); - throw new IllegalStateException("Exception during device creation", e); - } - + private int getUpnpNotificationFrequency() { + return Integer.parseInt(preferences.getString(getApplicationContext().getString(R.string.settings_sending_upnp_alive_interval_key), "5000")); } + private Icon[] createDeviceIcons() { ArrayList icons = new ArrayList<>(); @@ -566,7 +806,12 @@ private String getLocalServerName() { private LocalService[] createMediaServerServices() { List> services = new ArrayList<>(); services.add(createContentDirectoryService()); - services.add(createServerConnectionManagerService()); + return services.toArray(new LocalService[]{}); + } + + private LocalService[] createCoreServices() { + List> services = new ArrayList<>(); + services.add(createConnectionManagerService()); services.add(createMediaReceiverRegistrarService()); return services.toArray(new LocalService[]{}); } @@ -579,7 +824,6 @@ private LocalService[] createMediaServerServices() { private LocalService[] createMediaRendererServices() { List> services = new ArrayList<>(); services.add(createAVTransportService()); - services.add(createRendererConnectionManagerService()); services.add(createRenderingControl()); return services.toArray(new LocalService[]{}); } @@ -602,7 +846,7 @@ protected int getLockTimeoutMillis() { @Override protected YaaccContentDirectory createServiceInstance() { - return new YaaccContentDirectory(getApplicationContext(), getIpAddress(getApplicationContext())); + return new YaaccContentDirectory(getApplicationContext()); } }); return contentDirectoryService; @@ -615,6 +859,12 @@ protected YaaccContentDirectory createServiceInstance() { */ @SuppressWarnings("unchecked") private LocalService createAVTransportService() { + // Set upnpClient for state classes to access (may be null during initialization) + UpnpClient client = ((Yaacc) getApplicationContext()).getUpnpClient(); + if (client != null) { + AvTransport.setUpnpClient(client); + } + LocalService avTransportService = new AnnotationLocalServiceBinder().read(YaaccAVTransportService.class); avTransportService.setManager(new DefaultServiceManager<>(avTransportService, null) { @Override @@ -624,7 +874,7 @@ protected int getLockTimeoutMillis() { @Override protected YaaccAVTransportService createServiceInstance() { - return new YaaccAVTransportService(getUpnpClient()); + return new YaaccAVTransportService(); } }); return avTransportService; @@ -641,7 +891,7 @@ protected int getLockTimeoutMillis() { @Override protected AbstractAudioRenderingControl createServiceInstance() { - return new YaaccAudioRenderingControlService(getUpnpClient()); + return new YaaccAudioRenderingControlService(getApplicationContext()); } }); return renderingControlService; @@ -671,7 +921,7 @@ protected AbstractMediaReceiverRegistrarService createServiceInstance() { * @return the service */ @SuppressWarnings("unchecked") - private LocalService createServerConnectionManagerService() { + private LocalService createConnectionManagerService() { LocalService service = new AnnotationLocalServiceBinder().read(ConnectionManagerService.class); final ProtocolInfos sourceProtocols = getSourceProtocolInfos(); @@ -691,31 +941,6 @@ protected ConnectionManagerService createServiceInstance() { return service; } - /** - * creates a ConnectionManagerService. - * - * @return the service - */ - @SuppressWarnings("unchecked") - private LocalService createRendererConnectionManagerService() { - LocalService service = new AnnotationLocalServiceBinder().read(ConnectionManagerService.class); - final ProtocolInfos sinkProtocols = getSinkProtocolInfos(); - service.setManager(new DefaultServiceManager<>(service, ConnectionManagerService.class) { - - @Override - protected int getLockTimeoutMillis() { - return LOCK_TIMEOUT; - } - - @Override - protected ConnectionManagerService createServiceInstance() { - return new ConnectionManagerService(null, sinkProtocols); - } - }); - - return service; - } - private ProtocolInfos getSourceProtocolInfos() { return new ProtocolInfos( @@ -920,73 +1145,147 @@ private byte[] getIconAsByteArray(int drawableId, Bitmap.CompressFormat format) return result; } - /** - * @return the upnpClient - */ - public UpnpClient getUpnpClient() { - return upnpClient; + + public class YaaccUpnpServerServiceBinder extends Binder { + public YaaccUpnpServerService getService() { + return YaaccUpnpServerService.this; + } } - /** - * @param upnpClient the upnpClient to set - */ - private void setUpnpClient(UpnpClient upnpClient) { - this.upnpClient = upnpClient; + public NetworkDeviceListener getNetworkDeviceListener() { + return networkDeviceListener; } - /** - * get the ip address of the device - * - * @return the address or null if anything went wrong - */ - public static String getIpAddress(Context context) { - return getIfAndIpAddress(context)[0]; + public Registry getRegistry() { + return registry; } - public static String getIfName(Context context) { - return getIfAndIpAddress(context)[1]; + public boolean isInitialized() { + return registry != null && networkDeviceListener.isInitalized(); } + // Live streaming methods (Android 10+) + + @RequiresApi(api = android.os.Build.VERSION_CODES.Q) + private void startAudioCapture() { + if (audioCapture != null && audioCapture.isCapturing()) { + YaaccLogger.w(getClass().getName(), "Audio capture already running"); + return; + } + + projection = MediaProjectionHelper.getMediaProjection(); + + if (projection == null) { + // Try to create from stored permission + if (!MediaProjectionHelper.createMediaProjectionFromStored(this)) { + YaaccLogger.e(getClass().getName(), "No MediaProjection available for audio capture"); + return; + } + projection = MediaProjectionHelper.getMediaProjection(); + } + + if (audioCapture == null) { + audioCapture = new SystemAudioCaptureService(); + //audioCapture = new SystemAudioCaptureServiceAAC(); + } + + if (audioCapture.startCapture(projection)) { + YaaccLogger.d(getClass().getName(), "Audio capture started successfully"); + } else { + YaaccLogger.e(getClass().getName(), "Failed to start audio capture"); + } + } + + @RequiresApi(api = android.os.Build.VERSION_CODES.Q) + private void stopAudioCapture() { + if (audioCapture != null) { + audioCapture.stopCapture(); + YaaccLogger.d(getClass().getName(), "Audio capture stopped"); + } + } + + public SystemAudioCaptureService getAudioCapture() { + return audioCapture; + } + + @RequiresApi(api = android.os.Build.VERSION_CODES.Q) + private void startVideoCapture() { + if (videoCapture != null && videoCapture.isCapturing()) { + YaaccLogger.w(getClass().getName(), "Video capture already running"); + return; + } + + projection = MediaProjectionHelper.getMediaProjection(); + + if (projection == null) { + // Try to create from stored permission + if (!MediaProjectionHelper.createMediaProjectionFromStored(this)) { + YaaccLogger.e(getClass().getName(), "No MediaProjection available for video capture"); + return; + } + projection = MediaProjectionHelper.getMediaProjection(); + } + + if (videoCapture == null) { + videoCapture = new ScreenCastCaptureService(this); + } + + if (videoCapture.startCapture(projection)) { + YaaccLogger.d(getClass().getName(), "Video capture started successfully"); + } else { + YaaccLogger.e(getClass().getName(), "Failed to start video capture"); + } + } + + @RequiresApi(api = android.os.Build.VERSION_CODES.Q) + private void stopVideoCapture() { + if (videoCapture != null) { + videoCapture.stopCapture(); + YaaccLogger.d(getClass().getName(), "Video capture stopped"); + } + } + + public ScreenCastCaptureService getVideoCapture() { + return videoCapture; + } + + @RequiresApi(api = android.os.Build.VERSION_CODES.Q) + private void startCombinedCapture() { + YaaccLogger.i(getClass().getName(), "startCombinedCapture called"); + + // Combined capture needs its own MediaProjection - try to create from stored permission + if (!MediaProjectionHelper.createMediaProjectionFromStored(this)) { + YaaccLogger.w(getClass().getName(), "Cannot start combined capture: failed to create MediaProjection"); + return; + } + + android.media.projection.MediaProjection projection = MediaProjectionHelper.getMediaProjection(); + + if (projection == null) { + YaaccLogger.w(getClass().getName(), "Cannot start combined capture: no MediaProjection"); + return; + } - public static String[] getIfAndIpAddress(Context context) { - String hostAddress = null; - String[] result = new String[2]; - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); - List interfaces = new ArrayList<>(List.of(preferences.getString(context.getString(R.string.settings_local_server_if_filter_key), "lo,dummy,rmnet,ccmni").split(","))); - interfaces.remove(""); //remove empty string, if there, otherwise we got into trouble finding an network interface in code below try { - for (Enumeration networkInterfaces = NetworkInterface - .getNetworkInterfaces(); networkInterfaces - .hasMoreElements(); ) { - NetworkInterface networkInterface = networkInterfaces - .nextElement(); - if (interfaces.stream().filter(i -> networkInterface.getName().startsWith(i.trim())).collect(Collectors.toList()).isEmpty()) { - for (Enumeration inetAddresses = networkInterface - .getInetAddresses(); inetAddresses.hasMoreElements(); ) { - InetAddress inetAddress = inetAddresses.nextElement(); - if (!inetAddress.isLoopbackAddress() && inetAddress - .getHostAddress() != null - && IPV4_PATTERN.matcher(inetAddress - .getHostAddress()).matches()) { - hostAddress = inetAddress.getHostAddress(); - result[1] = networkInterface.getName(); - } - } - } + if (combinedCapture == null) { + combinedCapture = new CombinedCaptureService(this); } - } catch (SocketException se) { - Log.d(YaaccUpnpServerService.class.getName(), - "Error while retrieving network interfaces", se); + combinedCapture.startCapture(projection); + YaaccLogger.d(getClass().getName(), "Combined capture started"); + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to start combined capture", e); } - // maybe wifi is off we have to use the loopback device - hostAddress = hostAddress == null ? "0.0.0.0" : hostAddress; - result[0] = hostAddress; - return result; } - public class YaaccUpnpServerServiceBinder extends Binder { - public YaaccUpnpServerService getService() { - return YaaccUpnpServerService.this; + @RequiresApi(api = android.os.Build.VERSION_CODES.Q) + private void stopCombinedCapture() { + if (combinedCapture != null && combinedCapture.isCapturing()) { + combinedCapture.stopCapture(); + YaaccLogger.d(getClass().getName(), "Combined capture stopped"); } } + + public CombinedCaptureService getCombinedCapture() { + return combinedCapture; + } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerServiceHttpHandler.java b/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerServiceHttpHandler.java deleted file mode 100644 index 7b70d828..00000000 --- a/yaacc/src/main/java/de/yaacc/upnp/server/YaaccUpnpServerServiceHttpHandler.java +++ /dev/null @@ -1,607 +0,0 @@ -/* - * - * Copyright (C) 2013 Tobias Schoene www.yaacc.de - * - * This program is free software; you can redistribute it and/or - * modify it under the terms of the GNU General Public License - * as published by the Free Software Foundation; either version 3 - * of the License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program; if not, write to the Free Software - * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. - */ -package de.yaacc.upnp.server; - -import android.annotation.SuppressLint; -import android.content.ContentUris; -import android.content.Context; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.graphics.drawable.BitmapDrawable; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.os.Build; -import android.provider.MediaStore; -import android.util.Log; -import android.util.Size; - -import androidx.core.content.res.ResourcesCompat; -import androidx.preference.PreferenceManager; - -import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.EntityDetails; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpHeaders; -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.HttpStatus; -import org.apache.hc.core5.http.Message; -import org.apache.hc.core5.http.MethodNotSupportedException; -import org.apache.hc.core5.http.nio.AsyncEntityProducer; -import org.apache.hc.core5.http.nio.AsyncRequestConsumer; -import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; -import org.apache.hc.core5.http.nio.StreamChannel; -import org.apache.hc.core5.http.nio.entity.AbstractBinAsyncEntityProducer; -import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; -import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityConsumer; -import org.apache.hc.core5.http.nio.support.AsyncResponseBuilder; -import org.apache.hc.core5.http.nio.support.BasicRequestConsumer; -import org.apache.hc.core5.http.protocol.HttpContext; -import org.seamless.util.MimeType; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.RandomAccessFile; -import java.net.URL; -import java.net.URLConnection; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Locale; - -import de.yaacc.R; -import de.yaacc.upnp.server.contentdirectory.MediaPathFilter; -import de.yaacc.util.HttpRange; - -/** - * A http service to retrieve media content by an id. - * - * @author Tobias Schoene (tobexyz) - */ -public class YaaccUpnpServerServiceHttpHandler implements AsyncServerRequestHandler> { - - private final Context context; - - public YaaccUpnpServerServiceHttpHandler(Context context) { - this.context = context; - - } - - - @Override - public AsyncRequestConsumer> prepare(HttpRequest request, EntityDetails entityDetails, HttpContext context) { - return new BasicRequestConsumer<>(entityDetails != null ? new BasicAsyncEntityConsumer() : null); - } - - @Override - public void handle(final Message request, - final ResponseTrigger responseTrigger, - final HttpContext context) throws HttpException, IOException { - - Log.d(getClass().getName(), "Processing HTTP request: " - + request.getHead().getRequestUri()); - final AsyncResponseBuilder responseBuilder = AsyncResponseBuilder.create(HttpStatus.SC_OK); - // Extract what we need from the HTTP httpRequest - String requestMethod = request.getHead().getMethod() - .toUpperCase(Locale.ENGLISH); - - // Only accept HTTP-GET - if (!requestMethod.equals("GET") && !requestMethod.equals("HEAD")) { - Log.d(getClass().getName(), - "HTTP request isn't GET or HEAD stop! Method was: " - + requestMethod); - throw new MethodNotSupportedException(requestMethod - + " method not supported"); - } - - Uri requestUri = Uri.parse(request.getHead().getRequestUri()); - List pathSegments = requestUri.getPathSegments(); - if (pathSegments.size() == 1 && "health".equals(pathSegments.get(0))) { - responseBuilder.setStatus(HttpStatus.SC_OK); - responseBuilder.setEntity(AsyncEntityProducers.create("I am alive", ContentType.TEXT_HTML)); - responseTrigger.submitResponse(responseBuilder.build(), context); - return; - } - if (pathSegments.size() < 2 || pathSegments.size() > 3) { - createForbiddenResponse(responseTrigger, context, responseBuilder); - return; - } - - - String type = pathSegments.get(0); - String albumId = ""; - String thumbId = ""; - String contentId = ""; - if ("album".equals(type)) { - albumId = pathSegments.get(1); - try { - Long.parseLong(albumId); - } catch (NumberFormatException nex) { - createForbiddenResponse(responseTrigger, context, responseBuilder); - return; - } - } else if ("thumb".equals(type)) { - thumbId = pathSegments.get(1); - try { - Long.parseLong(thumbId); - } catch (NumberFormatException nex) { - createForbiddenResponse(responseTrigger, context, responseBuilder); - return; - } - } else if ("res".equals(type)) { - contentId = pathSegments.get(1); - try { - Long.parseLong(contentId); - } catch (NumberFormatException nex) { - createForbiddenResponse(responseTrigger, context, responseBuilder); - return; - } - } - Arrays.stream(request.getHead().getHeaders()).forEach(it -> Log.d(getClass().getName(), "HEADER " + it.getName() + ": " + it.getValue())); - List ranges = new ArrayList<>(); - if (request.getHead().getHeader(HttpHeaders.RANGE) != null) { - ranges = HttpRange.parseRangeHeader(request.getHead().getHeader(HttpHeaders.RANGE).getValue().toString()); - } - ContentHolder contentHolder = null; - if (!contentId.isEmpty()) { - contentHolder = lookupContent(contentId, ranges); - } else if (!albumId.isEmpty()) { - contentHolder = lookupAlbumArt(albumId, ranges); - } else if (!thumbId.isEmpty()) { - contentHolder = lookupThumbnail(thumbId, ranges); - } else if (YaaccUpnpServerService.PROXY_PATH.equals(type)) { - contentHolder = lookupProxyContent(pathSegments.get(1), ranges); - } - if (contentHolder == null) { - // tricky but works - Log.d(getClass().getName(), "Resource with id " + contentId - + albumId + thumbId + pathSegments.get(1) + " not found"); - responseBuilder.setStatus(HttpStatus.SC_NOT_FOUND); - String response = - "Resource with id " + contentId + albumId - + thumbId + pathSegments.get(1) + " not found"; - responseBuilder.setEntity(AsyncEntityProducers.create(response, ContentType.TEXT_HTML)); - } else { - - responseBuilder.setStatus(HttpStatus.SC_OK); - responseBuilder.setEntity(contentHolder.getEntityProducer()); - } - responseBuilder.setHeader(HttpHeaders.ACCEPT_RANGES, "none"); - responseTrigger.submitResponse(responseBuilder.build(), context); - Log.d(getClass().getName(), "end doService: "); - } - - private void createForbiddenResponse(ResponseTrigger responseTrigger, HttpContext context, AsyncResponseBuilder responseBuilder) throws HttpException, IOException { - responseBuilder.setStatus(HttpStatus.SC_FORBIDDEN); - responseBuilder.setEntity(AsyncEntityProducers.create("Access denied", ContentType.TEXT_HTML)); - responseTrigger.submitResponse(responseBuilder.build(), context); - Log.d(getClass().getName(), "end doService: Access denied"); - } - - private Context getContext() { - return context; - } - - /** - * Lookup content in the mediastore - * - * @param contentId the id of the content - * @return the content description - */ - private ContentHolder lookupContent(String contentId, List ranges) { - ContentHolder result = null; - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - if (!preferences.getBoolean(getContext().getString(R.string.settings_local_server_chkbx), false)) { - return null; - } - - if (contentId == null) { - return null; - } - Log.d(getClass().getName(), "System media store lookup: " + contentId); - String[] projection = {MediaStore.Files.FileColumns._ID, - MediaStore.Files.FileColumns.MIME_TYPE, - MediaStore.Files.FileColumns.DATA}; - String selection = MediaStore.Files.FileColumns._ID + "=? and (" + MediaPathFilter.makeLikeClause(MediaStore.Files.FileColumns.DATA, MediaPathFilter.getMediaPathes(getContext()).size()) + ")"; - List selectionArgsList = new ArrayList<>(); - selectionArgsList.add(contentId); - selectionArgsList.addAll(MediaPathFilter.getMediaPathesForLikeClause(getContext())); - String[] selectionArgs = selectionArgsList.toArray(new String[0]); - try (Cursor mFilesCursor = getContext().getContentResolver().query( - MediaStore.Files.getContentUri("external"), projection, - selection, selectionArgs, null)) { - - if (mFilesCursor != null) { - mFilesCursor.moveToFirst(); - while (!mFilesCursor.isAfterLast()) { - @SuppressLint("Range") String dataUri = mFilesCursor.getString(mFilesCursor - .getColumnIndex(MediaStore.Files.FileColumns.DATA)); - - @SuppressLint("Range") String mimeTypeStr = mFilesCursor - .getString(mFilesCursor - .getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE)); - MimeType mimeType = MimeType.valueOf("*/*"); - if (mimeTypeStr != null) { - mimeType = MimeType.valueOf(mimeTypeStr); - } - Log.d(getClass().getName(), "Content found: " + mimeType - + " Uri: " + dataUri); - result = new ContentHolder(mimeType, dataUri, ranges); - mFilesCursor.moveToNext(); - } - } else { - Log.d(getClass().getName(), "System media store is empty."); - } - } - - return result; - - } - - /** - * Lookup content in the mediastore - * - * @param albumId the id of the album - * @return the content description - */ - private ContentHolder lookupAlbumArt(String albumId, List ranges) { - - ContentHolder result = new ContentHolder(MimeType.valueOf("image/png"), - getDefaultIcon(), ranges); - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - if (!preferences.getBoolean(getContext().getString(R.string.settings_local_server_chkbx), false)) { - return result; - } - if (albumId == null) { - return result; - } - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { - Log.d(getClass().getName(), "System media store lookup album: " - + albumId); - String[] projection = {MediaStore.Audio.Albums._ID, - // FIXME what is the right mime type? - // MediaStore.Audio.Albums.MIME_TYPE, - MediaStore.Audio.Albums.ALBUM_ART}; - String selection = MediaStore.Audio.Albums._ID + "=?"; - String[] selectionArgs = {albumId}; - try (Cursor cursor = getContext().getContentResolver().query( - MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, projection, - selection, selectionArgs, null)) { - - if (cursor != null) { - cursor.moveToFirst(); - while (!cursor.isAfterLast()) { - @SuppressLint("Range") String dataUri = cursor.getString(cursor - .getColumnIndex(MediaStore.Audio.Albums.ALBUM_ART)); - - // String mimeTypeStr = null; - // FIXME mime type resolving cursor - // .getString(cursor - // .getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE)); - - MimeType mimeType = MimeType.valueOf("image/png"); - // if (mimeTypeStr != null) { - // mimeType = MimeType.valueOf(mimeTypeStr); - // } - if (dataUri != null) { - Log.d(getClass().getName(), "Content found: " + mimeType - + " Uri: " + dataUri); - result = new ContentHolder(mimeType, dataUri, ranges); - } else { - Log.d(getClass().getName(), "Album art not found in media store. Fallback to default"); - Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.yaacc192_32); - - try { - File art = new File(context.getCacheDir(), "albumart" + albumId + ".jpg"); - art.createNewFile(); - FileOutputStream fos = new FileOutputStream(art); - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos); - fos.flush(); - fos.close(); - result = new ContentHolder(mimeType, art.getAbsolutePath(), ranges); - } catch (IOException e) { - Log.e(getClass().getName(), "Error loading album art from file", e); - } - } - cursor.moveToNext(); - } - } else { - Log.d(getClass().getName(), "System media store is empty."); - } - } - } else { - Uri albumArtUri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, Long.parseLong(albumId)); - MimeType mimeType = MimeType.valueOf("image/jpeg"); - Log.d(getClass().getName(), "Content found: " + mimeType - + " Uri: " + albumArtUri); - Bitmap bitmap; - try { - bitmap = context.getContentResolver().loadThumbnail(albumArtUri, new Size(1024, 1024), null); - } catch (IOException io) { - Log.d(getClass().getName(), "Album art not found in media store. Fallback to default"); - bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.yaacc192_32); - } - try { - File art = new File(context.getCacheDir(), "albumart" + albumId + ".jpg"); - art.createNewFile(); - FileOutputStream fos = new FileOutputStream(art); - bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos); - fos.flush(); - fos.close(); - result = new ContentHolder(mimeType, art.getAbsolutePath(), ranges); - } catch (IOException e) { - Log.e(getClass().getName(), "Error loading album art from file", e); - } - - } - return result; - } - - /** - * Lookup a thumbnail content in the mediastore - * - * @param idStr the id of the thumbnail - * @return the content description - */ - private ContentHolder lookupThumbnail(String idStr, List ranges) { - - ContentHolder result = new ContentHolder(MimeType.valueOf("image/png"), - getDefaultIcon(), ranges); - SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); - if (!preferences.getBoolean(getContext().getString(R.string.settings_local_server_chkbx), false)) { - return result; - } - if (idStr == null) { - return result; - } - long id; - try { - id = Long.parseLong(idStr); - } catch (NumberFormatException nfe) { - Log.d(getClass().getName(), "ParsingError of id: " + idStr, nfe); - return result; - } - - Log.d(getClass().getName(), "System media store lookup thumbnail: " - + idStr); - Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail(getContext() - .getContentResolver(), id, - MediaStore.Images.Thumbnails.MINI_KIND, null); - if (bitmap != null) { - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); - byte[] byteArray = stream.toByteArray(); - - MimeType mimeType = MimeType.valueOf("image/png"); - - result = new ContentHolder(mimeType, byteArray, ranges); - - } else { - Log.d(getClass().getName(), "System media store is empty."); - } - return result; - } - - private ContentHolder lookupProxyContent(String contentKey, List ranges) { - - String targetUri = PreferenceManager.getDefaultSharedPreferences(getContext()).getString(YaaccUpnpServerService.PROXY_LINK_KEY_PREFIX + contentKey, null); - if (targetUri == null) { - return null; - } - String targetMimetype = PreferenceManager.getDefaultSharedPreferences(getContext()).getString(YaaccUpnpServerService.PROXY_LINK_MIME_TYPE_KEY_PREFIX + contentKey, null); - MimeType mimeType = MimeType.valueOf("*/*"); - if (targetMimetype != null) { - mimeType = MimeType.valueOf(targetMimetype); - } - return new ContentHolder(mimeType, targetUri, ranges); - } - - private byte[] getDefaultIcon() { - Drawable drawable = ResourcesCompat.getDrawable(getContext().getResources(), - R.drawable.yaacc192_32, getContext().getTheme()); - byte[] result = null; - if (drawable != null) { - Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); - result = stream.toByteArray(); - } - return result; - } - - - /** - * ValueHolder for media content. - */ - static class ContentHolder { - private final MimeType mimeType; - private String uri; - private byte[] content; - - private List ranges; - - public ContentHolder(MimeType mimeType, String uri, List ranges) { - this.uri = uri; - this.mimeType = mimeType; - this.ranges = ranges; - - } - - public ContentHolder(MimeType mimeType, byte[] content, List ranges) { - this.content = content; - this.mimeType = mimeType; - this.ranges = ranges; - - } - - /** - * @return the uri - */ - public String getUri() { - return uri; - } - - /** - * @return the mimeType - */ - public MimeType getMimeType() { - return mimeType; - } - - private byte[] readRangeFormFile(File file, List ranges) throws IOException { - - - try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { - long fileSize = raf.length(); - long startPosition; - long rangeLength; - if (ranges.size() > 1) { - Log.d(getClass().getName(), "More than on ranges requested. Currently only one range is supported. Responding with the first range"); - } - if (ranges.isEmpty()) { - startPosition = 0; - rangeLength = fileSize; - } else { - HttpRange range = ranges.get(0); - startPosition = range.getStart() == null ? 0 : range.getStart(); - if (range.getEnd() == null || range.getEnd() == 0) { - rangeLength = fileSize; - } else { - rangeLength = range.getEnd() - startPosition; - } - if (range.getSuffixLength() != null && range.getSuffixLength() > 0) { - startPosition = fileSize - range.getSuffixLength(); - rangeLength = range.getSuffixLength(); - } - } - - // Read a range of bytes (e.g., bytes 100 to 200) - if (startPosition < 0 || startPosition + rangeLength > fileSize) { - Log.d(getClass().getName(), "Invalid range startPosition: " + startPosition + " rangeLength: " + rangeLength + " fileSize: " + fileSize); - rangeLength = fileSize - startPosition; - Log.d(getClass().getName(), "Adjusted range startPosition: " + startPosition + " rangeLength: " + rangeLength + " fileSize: " + fileSize); - } - - raf.seek(startPosition); // Move to the starting position - byte[] buffer = new byte[(int) rangeLength]; // Create a buffer - raf.read(buffer); - return buffer; - } - - } - - public AsyncEntityProducer getEntityProducer() throws IOException { - AsyncEntityProducer result = null; - if (getUri() != null && !getUri().isEmpty()) { - File file = new File(getUri()); - if (file.exists()) { - if (ranges.isEmpty()) { - result = AsyncEntityProducers.create(file, ContentType.parse(getMimeType().toString())); - Log.d(getClass().getName(), "Return without range request file-Uri: " + getUri() - + " Mimetype: " + getMimeType()); - } else { - result = AsyncEntityProducers.create(readRangeFormFile(file, ranges), ContentType.parse(getMimeType().toString())); - } - } else { - //file not found maybe external url - result = new AbstractBinAsyncEntityProducer(0, ContentType.parse(getMimeType().toString())) { - private InputStream input; - private long length = -1; - - AbstractBinAsyncEntityProducer init() { - try { - if (input == null) { - //https://www.experts-exchange.com/questions/10171110/Reading-a-part-of-a-file-using-URLConnection.html - URLConnection con = new URL(getUri()).openConnection(); - con.setRequestProperty("Range", HttpRange.toHeaderString(ranges)); - input = con.getInputStream(); - length = con.getContentLength(); - } - } catch (IOException e) { - Log.e(getClass().getName(), "Error opening external content", e); - } - return this; - } - - @Override - public long getContentLength() { - return length; - } - - @Override - protected int availableData() { - return Integer.MAX_VALUE; - } - - @Override - protected void produceData(final StreamChannel channel) throws IOException { - try { - if (input == null) { - //retry opening external content if it hasn't been opened yet - URLConnection con = new URL(getUri()).openConnection(); - input = con.getInputStream(); - length = con.getContentLength(); - } - byte[] tempBuffer = new byte[1024]; - int bytesRead; - if (-1 != (bytesRead = input.read(tempBuffer))) { - channel.write(ByteBuffer.wrap(tempBuffer, 0, bytesRead)); - } - if (bytesRead == -1) { - channel.endStream(); - } - - } catch (IOException e) { - Log.e(getClass().getName(), "Error reading external content", e); - throw e; - } - } - - - @Override - public boolean isRepeatable() { - return true; - } - - @Override - public void failed(final Exception cause) { - } - - }.init(); - - Log.d(getClass().getName(), "Return external-Uri: " + getUri() - + "Mimetype: " + getMimeType()); - } - } else if (content != null) { - result = AsyncEntityProducers.create(content, ContentType.parse(getMimeType().toString())); - } - if (result == null) { - Log.d(getClass().getName(), "Resource is null"); - return AsyncEntityProducers.create("

Resource not found

", ContentType.TEXT_HTML); - } - return result; - - } - } -} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransport.java b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransport.java index 38ad60c3..24c2cc12 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransport.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransport.java @@ -22,14 +22,26 @@ import org.fourthline.cling.support.model.AVTransport; import org.fourthline.cling.support.model.StorageMedium; +import de.yaacc.upnp.UpnpClient; + /** * @author Tobias Schoene (TheOpenBit) */ public class AvTransport extends AVTransport { + private static UpnpClient upnpClient; + public AvTransport(UnsignedIntegerFourBytes instanceID, LastChange lastChange, StorageMedium possiblePlayMedium) { super(instanceID, lastChange, possiblePlayMedium); } + public static void setUpnpClient(UpnpClient client) { + upnpClient = client; + } + + public static UpnpClient getUpnpClient() { + return upnpClient; + } + } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererNoMediaPresent.java b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererNoMediaPresent.java index 80b8afd0..124206ae 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererNoMediaPresent.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererNoMediaPresent.java @@ -18,7 +18,7 @@ */ package de.yaacc.upnp.server.avtransport; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.avtransport.impl.state.AbstractState; import org.fourthline.cling.support.avtransport.impl.state.NoMediaPresent; @@ -41,10 +41,8 @@ public class AvTransportMediaRendererNoMediaPresent extends * Constructor. * * @param transport the state holder - * @param upnpClient the upnpClient to use */ - public AvTransportMediaRendererNoMediaPresent(AvTransport transport, - UpnpClient upnpClient) { + public AvTransportMediaRendererNoMediaPresent(AvTransport transport) { super(transport); } @@ -55,7 +53,7 @@ public AvTransportMediaRendererNoMediaPresent(AvTransport transport, @Override public Class> setTransportURI(URI uri, String metaData) { - Log.d(this.getClass().getName(), "set Transport: " + uri + " metaData: " + metaData); + YaaccLogger.d(this.getClass().getName(), "set Transport: " + uri + " metaData: " + metaData); getTransport().setMediaInfo(new MediaInfo(uri.toString(), metaData)); // If you can, you should find and set the duration of the track here! getTransport().setPositionInfo( diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererPaused.java b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererPaused.java index 9b18f197..08e3c2ed 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererPaused.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererPaused.java @@ -18,7 +18,7 @@ */ package de.yaacc.upnp.server.avtransport; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.avtransport.impl.state.AbstractState; import org.fourthline.cling.support.avtransport.impl.state.PausedPlay; @@ -44,12 +44,10 @@ public class AvTransportMediaRendererPaused extends PausedPlay impl * Constructor. * * @param transport the state holder - * @param upnpClient the upnpclient to use */ - public AvTransportMediaRendererPaused(AvTransport transport, - UpnpClient upnpClient) { + public AvTransportMediaRendererPaused(AvTransport transport) { super(transport); - this.upnpClient = upnpClient; + this.upnpClient = AvTransport.getUpnpClient(); } /* (non-Javadoc) @@ -57,7 +55,7 @@ public AvTransportMediaRendererPaused(AvTransport transport, */ @Override public Class> play(String arg0) { - Log.d(this.getClass().getName(), "play"); + YaaccLogger.d(this.getClass().getName(), "play"); return AvTransportMediaRendererPlaying.class; } @@ -66,9 +64,9 @@ public Class> play(String arg0) { */ @Override public Class> setTransportURI(URI uri, String metaData) { - Log.d(this.getClass().getName(), "setTransportURI"); - Log.d(this.getClass().getName(), "uri: " + uri); - Log.d(this.getClass().getName(), "metaData: " + metaData); + YaaccLogger.d(this.getClass().getName(), "setTransportURI"); + YaaccLogger.d(this.getClass().getName(), "uri: " + uri); + YaaccLogger.d(this.getClass().getName(), "metaData: " + metaData); getTransport().setMediaInfo(new MediaInfo(uri.toString(), metaData)); // If you can, you should find and set the duration of the track here! getTransport().setPositionInfo( @@ -93,7 +91,7 @@ public Class> setTransportURI(URI uri, String metaDat */ @Override public Class> stop() { - Log.d(this.getClass().getName(), "stop"); + YaaccLogger.d(this.getClass().getName(), "stop"); return AvTransportMediaRendererStopped.class; } @@ -103,7 +101,7 @@ public Class> stop() { */ @Override public void onEntry() { - Log.d(this.getClass().getName(), "On Entry"); + YaaccLogger.d(this.getClass().getName(), "On Entry"); super.onEntry(); List players = upnpClient.getCurrentPlayers(getTransport()); for (Player player : players) { diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererPlaying.java b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererPlaying.java index bf9a4f7f..b942d022 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererPlaying.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererPlaying.java @@ -19,7 +19,7 @@ package de.yaacc.upnp.server.avtransport; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.avtransport.impl.state.AbstractState; import org.fourthline.cling.support.avtransport.impl.state.Playing; @@ -53,12 +53,10 @@ public class AvTransportMediaRendererPlaying extends Playing implem * Constructor. * * @param transport the state holder - * @param upnpClient the upnpclient to use */ - public AvTransportMediaRendererPlaying(AvTransport transport, - UpnpClient upnpClient) { + public AvTransportMediaRendererPlaying(AvTransport transport) { super(transport); - this.upnpClient = upnpClient; + this.upnpClient = AvTransport.getUpnpClient(); } /* @@ -67,7 +65,7 @@ public AvTransportMediaRendererPlaying(AvTransport transport, */ @Override public void onEntry() { - Log.d(this.getClass().getName(), "On Entry"); + YaaccLogger.d(this.getClass().getName(), "On Entry"); super.onEntry(); if (getTransport() == null || getTransport().getPositionInfo() == null @@ -94,9 +92,9 @@ public void onEntry() { @Override public Class> setTransportURI(URI uri, String metaData) { - Log.d(this.getClass().getName(), "Set TransportURI"); - Log.d(this.getClass().getName(), "uri: " + uri); - Log.d(this.getClass().getName(), "metaData: " + metaData); + YaaccLogger.d(this.getClass().getName(), "Set TransportURI"); + YaaccLogger.d(this.getClass().getName(), "uri: " + uri); + YaaccLogger.d(this.getClass().getName(), "metaData: " + metaData); getTransport().setMediaInfo(new MediaInfo(uri.toString(), metaData)); // If you can, you should find and set the duration of the track here! getTransport().setPositionInfo( @@ -117,7 +115,7 @@ public Class> setTransportURI(URI uri, */ @Override public Class> stop() { - Log.d(this.getClass().getName(), "Stop"); + YaaccLogger.d(this.getClass().getName(), "Stop"); updateTime = false; // Stop playing! return AvTransportMediaRendererStopped.class; @@ -129,7 +127,7 @@ public Class> stop() { */ @Override public Class> play(String speed) { - Log.d(this.getClass().getName(), "play"); + YaaccLogger.d(this.getClass().getName(), "play"); updateTime = true; return AvTransportMediaRendererPlaying.class; } @@ -140,7 +138,7 @@ public Class> play(String speed) { */ @Override public Class> pause() { - Log.d(this.getClass().getName(), "pause"); + YaaccLogger.d(this.getClass().getName(), "pause"); updateTime = false; return AvTransportMediaRendererPaused.class; } @@ -151,7 +149,7 @@ public Class> pause() { */ @Override public Class> next() { - Log.d(this.getClass().getName(), "next"); + YaaccLogger.d(this.getClass().getName(), "next"); updateTime = false; return null; } @@ -162,7 +160,7 @@ public Class> next() { */ @Override public Class> previous() { - Log.d(this.getClass().getName(), "previous"); + YaaccLogger.d(this.getClass().getName(), "previous"); updateTime = false; return null; } @@ -173,7 +171,7 @@ public Class> previous() { */ @Override public Class> seek(SeekMode unit, String target) { - Log.d(this.getClass().getName(), "seek"); + YaaccLogger.d(this.getClass().getName(), "seek"); if (SeekMode.REL_TIME.equals(unit)) { SimpleDateFormat dateFormat = new SimpleDateFormat("HH:mm:ss"); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); @@ -185,7 +183,7 @@ public Class> seek(SeekMode unit, String target) { } } } catch (ParseException pex) { - Log.d(getClass().getName(), "unable to parse target time string", pex); + YaaccLogger.d(getClass().getName(), "unable to parse target time string", pex); } } updateTime = true; @@ -211,14 +209,24 @@ private void setTrackInfo() { private void doSetTrackInfo() { for (Player player : players) { - if (player != null && !player.getDuration().equals("")) { - getTransport().getPositionInfo().setTrackDuration(player.getDuration()); - getTransport().getPositionInfo().setRelTime(player.getElapsedTime()); - Log.d(getClass().getName(), "doSetTrackInfo: " + getTransport() + "Posinfo:" + getTransport().getPositionInfo() + " RelTime: " + getTransport().getPositionInfo().getRelTime()); + if (player != null) { + // Get duration on main thread for Media3 compatibility + final String[] duration = {""}; + final String[] elapsedTime = {""}; + + new android.os.Handler(android.os.Looper.getMainLooper()).post(() -> { + duration[0] = player.getDuration(); + elapsedTime[0] = player.getElapsedTime(); + + if (!duration[0].equals("")) { + getTransport().getPositionInfo().setTrackDuration(duration[0]); + getTransport().getPositionInfo().setRelTime(elapsedTime[0]); + YaaccLogger.d(getClass().getName(), "doSetTrackInfo: " + getTransport() + "Posinfo:" + getTransport().getPositionInfo() + " RelTime: " + getTransport().getPositionInfo().getRelTime()); + } + }); break; } } - } private void updateTime() { diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererStopped.java b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererStopped.java index 54096795..eb087763 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererStopped.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/AvTransportMediaRendererStopped.java @@ -18,7 +18,7 @@ */ package de.yaacc.upnp.server.avtransport; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.avtransport.impl.state.AbstractState; import org.fourthline.cling.support.avtransport.impl.state.Stopped; @@ -45,12 +45,10 @@ public class AvTransportMediaRendererStopped extends Stopped implem * Constructor. * * @param transport the state holder - * @param upnpClient the upnpclient to use */ - public AvTransportMediaRendererStopped(AvTransport transport, - UpnpClient upnpClient) { + public AvTransportMediaRendererStopped(AvTransport transport) { super(transport); - this.upnpClient = upnpClient; + this.upnpClient = AvTransport.getUpnpClient(); } /* @@ -60,7 +58,7 @@ public AvTransportMediaRendererStopped(AvTransport transport, */ @Override public void onEntry() { - Log.d(this.getClass().getName(), "On Entry"); + YaaccLogger.d(this.getClass().getName(), "On Entry"); super.onEntry(); List players = upnpClient.getCurrentPlayers(getTransport()); for (Player player : players) { @@ -80,9 +78,9 @@ public void onEntry() { @Override public Class> setTransportURI(URI uri, String metaData) { - Log.d(this.getClass().getName(), "setTransportURI"); - Log.d(this.getClass().getName(), "uri: " + uri); - Log.d(this.getClass().getName(), "metaData: " + metaData); + YaaccLogger.d(this.getClass().getName(), "setTransportURI"); + YaaccLogger.d(this.getClass().getName(), "uri: " + uri); + YaaccLogger.d(this.getClass().getName(), "metaData: " + metaData); getTransport().setMediaInfo(new MediaInfo(uri.toString(), metaData)); // If you can, you should find and set the duration of the track here! getTransport().setPositionInfo( @@ -109,7 +107,7 @@ public Class> setTransportURI(URI uri, */ @Override public Class> stop() { - Log.d(this.getClass().getName(), "stop"); + YaaccLogger.d(this.getClass().getName(), "stop"); // / Same here, if you are stopped already and someone calls STOP, // well... return AvTransportMediaRendererStopped.class; @@ -124,7 +122,7 @@ public Class> stop() { */ @Override public Class> play(String speed) { - Log.d(this.getClass().getName(), "play"); + YaaccLogger.d(this.getClass().getName(), "play"); // It's easier to let this classes' onEntry() method do the work return AvTransportMediaRendererPlaying.class; } @@ -136,7 +134,7 @@ public Class> play(String speed) { */ @Override public Class> next() { - Log.d(this.getClass().getName(), "next"); + YaaccLogger.d(this.getClass().getName(), "next"); return AvTransportMediaRendererStopped.class; } @@ -147,7 +145,7 @@ public Class> next() { */ @Override public Class> previous() { - Log.d(this.getClass().getName(), "previous"); + YaaccLogger.d(this.getClass().getName(), "previous"); return AvTransportMediaRendererStopped.class; } @@ -160,7 +158,7 @@ public Class> previous() { */ @Override public Class> seek(SeekMode unit, String target) { - Log.d(this.getClass().getName(), "seek"); + YaaccLogger.d(this.getClass().getName(), "seek"); // Implement seeking with the stream in stopped state! return AvTransportMediaRendererStopped.class; } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/YaaccAVTransportService.java b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/YaaccAVTransportService.java index 5df4ae91..849f9979 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/YaaccAVTransportService.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/avtransport/YaaccAVTransportService.java @@ -18,7 +18,7 @@ */ package de.yaacc.upnp.server.avtransport; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.binding.annotations.UpnpAction; import org.fourthline.cling.binding.annotations.UpnpInputArgument; @@ -60,8 +60,6 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import de.yaacc.upnp.UpnpClient; - /** * Implementation of an avtransport service version 3 mainly copied from cling example implementation. @@ -237,7 +235,6 @@ public class YaaccAVTransportService implements LastChangeDelegator { Class stateMachineDefinition; Class> initialState; Class transportClass; - private UpnpClient upnpClient = null; @UpnpStateVariable(eventMaximumRateMilliseconds = 200) private LastChange lastChange = new LastChange(new AVTransportLastChangeParser()); @@ -258,10 +255,9 @@ protected YaaccAVTransportService(Class state /** * */ - public YaaccAVTransportService(UpnpClient upnpClient) { + public YaaccAVTransportService() { this(AvTransportStateMachine.class, AvTransportMediaRendererNoMediaPresent.class); - this.upnpClient = upnpClient; } public static UnsignedIntegerFourBytes getDefaultInstanceID() { @@ -323,9 +319,8 @@ protected AVTransportStateMachine createStateMachine( return StateMachineBuilder.build( AvTransportStateMachine.class, AvTransportMediaRendererNoMediaPresent.class, new Class[]{ - AvTransport.class, UpnpClient.class}, new Object[]{ - new AvTransport(instanceId, getLastChange(), StorageMedium.NETWORK), - upnpClient}); + AvTransport.class}, new Object[]{ + new AvTransport(instanceId, getLastChange(), StorageMedium.NETWORK)}); } @UpnpAction(name = "GetCurrentTransportActions", out = @UpnpOutputArgument(name = "Actions", stateVariable = "CurrentTransportActions")) @@ -343,7 +338,7 @@ protected TransportAction[] getPossibleTransportActions(UnsignedIntegerFourBytes try { return ((YaaccState) stateMachine.getCurrentState()).getPossibleTransportActions(); } catch (TransitionException ex) { - Log.d(getClass().getName(), "Exception in state transition ignoring it", ex); + YaaccLogger.d(getClass().getName(), "Exception in state transition ignoring it", ex); return new TransportAction[0]; } } @@ -354,23 +349,34 @@ public void setAVTransportURI(@UpnpInputArgument(name = "InstanceID") UnsignedIn @UpnpInputArgument(name = "CurrentURI", stateVariable = "AVTransportURI") String currentURI, @UpnpInputArgument(name = "CurrentURIMetaData", stateVariable = "AVTransportURIMetaData") String currentURIMetaData) throws AVTransportException { + YaaccLogger.d(getClass().getName(), "setAVTransportURI called: " + currentURI); URI uri; try { uri = new URI(currentURI); + YaaccLogger.d(getClass().getName(), "URI parsed successfully: " + uri); } catch (Exception ex) { + YaaccLogger.e(getClass().getName(), "URI parsing failed", ex); throw new AVTransportException( ErrorCode.INVALID_ARGS, "CurrentURI can not be null or malformed" ); } try { + YaaccLogger.d(getClass().getName(), "Finding state machine for instance " + instanceId); AVTransportStateMachine transportStateMachine = findStateMachine(instanceId, true); + YaaccLogger.d(getClass().getName(), "State machine found: " + transportStateMachine + ", current state: " + transportStateMachine.getCurrentState().getClass().getSimpleName()); + YaaccLogger.d(getClass().getName(), "Calling setTransportURI on state machine"); transportStateMachine.setTransportURI(uri, currentURIMetaData); + YaaccLogger.d(getClass().getName(), "setTransportURI successful, new state: " + transportStateMachine.getCurrentState().getClass().getSimpleName()); } catch (TransitionException ex) { + YaaccLogger.e(getClass().getName(), "TransitionException in setAVTransportURI", ex); + throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage()); + } catch (Exception ex) { + YaaccLogger.e(getClass().getName(), "Unexpected exception in setAVTransportURI", ex); throw new AVTransportException(AVTransportErrorCode.TRANSITION_NOT_AVAILABLE, ex.getMessage()); } - Log.d(getClass().getName(), "setAVTransportURI: " + uri + " currentURIMetaData: " + currentURIMetaData); + YaaccLogger.d(getClass().getName(), "setAVTransportURI complete: " + uri); } @UpnpAction @@ -400,18 +406,23 @@ public void setNextAVTransportURI(@UpnpInputArgument(name = "InstanceID") Unsign public void setPlayMode(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId, @UpnpInputArgument(name = "NewPlayMode", stateVariable = "CurrentPlayMode") String newPlayMode) throws AVTransportException { - AVTransport transport = findStateMachine(instanceId).getCurrentState().getTransport(); try { - transport.setTransportSettings( - new TransportSettings( - PlayMode.valueOf(newPlayMode), - transport.getTransportSettings().getRecQualityMode() - ) - ); - } catch (IllegalArgumentException ex) { - throw new AVTransportException( - AVTransportErrorCode.PLAYMODE_NOT_SUPPORTED, "Unsupported play mode: " + newPlayMode - ); + AVTransport transport = findStateMachine(instanceId).getCurrentState().getTransport(); + try { + transport.setTransportSettings( + new TransportSettings( + PlayMode.valueOf(newPlayMode), + transport.getTransportSettings().getRecQualityMode() + ) + ); + } catch (IllegalArgumentException ex) { + throw new AVTransportException( + AVTransportErrorCode.PLAYMODE_NOT_SUPPORTED, "Unsupported play mode: " + newPlayMode + ); + } + } catch (AVTransportException e) { + // Silently ignore when no media is present + YaaccLogger.d(getClass().getName(), "setPlayMode called with no media present, ignoring"); } } @@ -447,7 +458,15 @@ public void setRecordQualityMode(@UpnpInputArgument(name = "InstanceID") Unsigne }) public MediaInfo getMediaInfo(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId) throws AVTransportException { - return findStateMachine(instanceId).getCurrentState().getTransport().getMediaInfo(); + try { + AVTransportStateMachine stateMachine = findStateMachine(instanceId); + if (stateMachine.getCurrentState() instanceof AvTransportMediaRendererNoMediaPresent) { + return new MediaInfo(); + } + return stateMachine.getCurrentState().getTransport().getMediaInfo(); + } catch (Exception e) { + return new MediaInfo(); + } } @UpnpAction(out = { @@ -457,7 +476,15 @@ public MediaInfo getMediaInfo(@UpnpInputArgument(name = "InstanceID") UnsignedIn }) public TransportInfo getTransportInfo(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId) throws AVTransportException { - return findStateMachine(instanceId).getCurrentState().getTransport().getTransportInfo(); + try { + AVTransportStateMachine stateMachine = findStateMachine(instanceId); + if (stateMachine.getCurrentState() instanceof AvTransportMediaRendererNoMediaPresent) { + return new TransportInfo(TransportState.NO_MEDIA_PRESENT, TransportStatus.OK, "1"); + } + return stateMachine.getCurrentState().getTransport().getTransportInfo(); + } catch (Exception e) { + return new TransportInfo(TransportState.NO_MEDIA_PRESENT, TransportStatus.OK, "1"); + } } @UpnpAction(out = { @@ -472,8 +499,21 @@ public TransportInfo getTransportInfo(@UpnpInputArgument(name = "InstanceID") Un }) public PositionInfo getPositionInfo(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId) throws AVTransportException { - Log.d(getClass().getName(), "Transport: " + findStateMachine(instanceId).getCurrentState().getTransport() + " PositionInfo: " + findStateMachine(instanceId).getCurrentState().getTransport().getPositionInfo()); - return findStateMachine(instanceId).getCurrentState().getTransport().getPositionInfo(); + YaaccLogger.d(getClass().getName(), "getPositionInfo called"); + try { + AVTransportStateMachine stateMachine = findStateMachine(instanceId); + // Check if in NoMediaPresent state + if (stateMachine.getCurrentState() instanceof AvTransportMediaRendererNoMediaPresent) { + YaaccLogger.d(getClass().getName(), "No media present, returning default PositionInfo"); + return new PositionInfo(1L, "00:00:00", "", "", "00:00:00", "00:00:00", Integer.MAX_VALUE, Integer.MAX_VALUE); + } + YaaccLogger.d(getClass().getName(), "Transport: " + stateMachine.getCurrentState().getTransport() + " PositionInfo: " + stateMachine.getCurrentState().getTransport().getPositionInfo()); + return stateMachine.getCurrentState().getTransport().getPositionInfo(); + } catch (Exception e) { + // Return default position info for any error + YaaccLogger.d(getClass().getName(), "Exception getting position info: " + e.getMessage() + ", returning default"); + return new PositionInfo(1L, "00:00:00", "", "", "00:00:00", "00:00:00", Integer.MAX_VALUE, Integer.MAX_VALUE); + } } @UpnpAction(out = { @@ -483,7 +523,15 @@ public PositionInfo getPositionInfo(@UpnpInputArgument(name = "InstanceID") Unsi }) public DeviceCapabilities getDeviceCapabilities(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId) throws AVTransportException { - return findStateMachine(instanceId).getCurrentState().getTransport().getDeviceCapabilities(); + try { + AVTransportStateMachine stateMachine = findStateMachine(instanceId); + if (stateMachine.getCurrentState() instanceof AvTransportMediaRendererNoMediaPresent) { + return new DeviceCapabilities(new StorageMedium[]{StorageMedium.NETWORK}); + } + return stateMachine.getCurrentState().getTransport().getDeviceCapabilities(); + } catch (Exception e) { + return new DeviceCapabilities(new StorageMedium[]{StorageMedium.NETWORK}); + } } @UpnpAction(out = { @@ -492,7 +540,15 @@ public DeviceCapabilities getDeviceCapabilities(@UpnpInputArgument(name = "Insta }) public TransportSettings getTransportSettings(@UpnpInputArgument(name = "InstanceID") UnsignedIntegerFourBytes instanceId) throws AVTransportException { - return findStateMachine(instanceId).getCurrentState().getTransport().getTransportSettings(); + try { + AVTransportStateMachine stateMachine = findStateMachine(instanceId); + if (stateMachine.getCurrentState() instanceof AvTransportMediaRendererNoMediaPresent) { + return new TransportSettings(); + } + return stateMachine.getCurrentState().getTransport().getTransportSettings(); + } catch (Exception e) { + return new TransportSettings(); + } } @UpnpAction @@ -625,13 +681,13 @@ protected AVTransportStateMachine findStateMachine(UnsignedIntegerFourBytes inst long id = instanceId.getValue(); AVTransportStateMachine stateMachine = stateMachines.get(id); if (stateMachine == null && createDefaultTransport) { - Log.d(getClass().getName(), "Creating stateMachine instance with ID '" + id + "'"); + YaaccLogger.d(getClass().getName(), "Creating stateMachine instance with ID '" + id + "'"); stateMachine = createStateMachine(instanceId); stateMachines.put(id, stateMachine); } else if (stateMachine == null) { throw new AVTransportException(AVTransportErrorCode.INVALID_INSTANCE_ID); } - Log.d(getClass().getName(), "Found transport control with ID '" + id + "'"); + YaaccLogger.d(getClass().getName(), "Found transport control with ID '" + id + "'"); return stateMachine; } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/TreeNode.java b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeNode.java similarity index 72% rename from yaacc/src/main/java/de/yaacc/upnp/server/TreeNode.java rename to yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeNode.java index 9edeeb10..488cea09 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/TreeNode.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeNode.java @@ -16,10 +16,26 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -package de.yaacc.upnp.server; - +package de.yaacc.upnp.server.configuration; +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ -import java.io.File; import java.util.LinkedList; /** @@ -27,7 +43,7 @@ */ public class TreeNode { - private File value; + private Object value; private TreeNode parent; private LinkedList children; private int layoutId; @@ -35,7 +51,7 @@ public class TreeNode { private boolean isExpanded; private boolean isSelected; - public TreeNode(File value, int layoutId) { + public TreeNode(Object value, int layoutId) { this.value = value; this.parent = null; this.children = new LinkedList<>(); @@ -52,11 +68,11 @@ public void addChild(TreeNode child) { updateNodeChildrenDepth(child); } - public void setValue(File value) { + public void setValue(Object value) { this.value = value; } - public File getValue() { + public Object getValue() { return value; } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/TreeNodeManager.java b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeNodeManager.java similarity index 99% rename from yaacc/src/main/java/de/yaacc/upnp/server/TreeNodeManager.java rename to yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeNodeManager.java index 7c43328d..e897fd01 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/TreeNodeManager.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeNodeManager.java @@ -16,7 +16,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -package de.yaacc.upnp.server; +package de.yaacc.upnp.server.configuration; import java.util.LinkedList; import java.util.List; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewAdapter.java b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeViewAdapter.java similarity index 75% rename from yaacc/src/main/java/de/yaacc/upnp/server/TreeViewAdapter.java rename to yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeViewAdapter.java index bcf604a8..9cb056ea 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewAdapter.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeViewAdapter.java @@ -16,7 +16,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -package de.yaacc.upnp.server; +package de.yaacc.upnp.server.configuration; import android.annotation.SuppressLint; import android.view.LayoutInflater; @@ -122,8 +122,8 @@ public TreeViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int layoutId @Override public void onBindViewHolder(@NonNull TreeViewHolder holder, @SuppressLint("RecyclerView") int position) { TreeNode newSelectedNode = treeNodeManager.get(position); + holder.adapter = this; holder.bindTreeNode(newSelectedNode); - holder.itemView.setOnClickListener(v -> { // Handle TreeNode click listener event if (treeNodeClickListener != null) { @@ -214,62 +214,6 @@ public void expandNode(TreeNode node) { } } - /** - * Collapsing full node branches - * - * @param node The node to collapse it - */ - public void collapseNodeBranch(TreeNode node) { - treeNodeManager.collapseNodeBranch(node); - notifyDataSetChanged(); - } - - /** - * Expanding node full branches - * - * @param node The node to expand it - */ - public void expandNodeBranch(TreeNode node) { - treeNodeManager.expandNodeBranch(node); - notifyDataSetChanged(); - } - - /** - * Expanding one node branch to until specific level - * - * @param node to expand branch of it until level - * @param level to expand node branches to it - */ - public void expandNodeToLevel(TreeNode node, int level) { - treeNodeManager.expandNodeToLevel(node, level); - notifyDataSetChanged(); - } - - /** - * Expanding all tree nodes branches to until specific level - * - * @param level to expand all nodes branches to it - */ - public void expandNodesAtLevel(int level) { - treeNodeManager.expandNodesAtLevel(level); - notifyDataSetChanged(); - } - - /** - * Collapsing all nodes in the tree with their children - */ - public void collapseAll() { - treeNodeManager.collapseAll(); - notifyDataSetChanged(); - } - - /** - * Expanding all nodes in the tree with their children - */ - public void expandAll() { - treeNodeManager.expandAll(); - notifyDataSetChanged(); - } /** * Update the list of tree nodes @@ -281,14 +225,6 @@ public void updateTreeNodes(List treeNodes) { notifyDataSetChanged(); } - /** - * Delete all tree nodes - */ - public void clearTreeNodes() { - int size = treeNodeManager.size(); - treeNodeManager.clearNodes(); - notifyItemRangeRemoved(0, size); - } /** * Register a callback to be invoked when this TreeNode is clicked @@ -300,37 +236,14 @@ public void setTreeNodeClickListener(OnTreeNodeClickListener listener) { } /** - * Register a callback to be invoked when this TreeNode is clicked and held - * - * @param listener The callback that will run - */ - public void setTreeNodeLongClickListener(OnTreeNodeLongClickListener listener) { - this.treeNodeLongClickListener = listener; - } - - /** - * Set the current visible tree nodes and notify adapter data - * - * @param treeNodes New tree nodes - */ - public void setTreeNodes(List treeNodes) { - treeNodeManager.setTreeNodes(treeNodes); - notifyDataSetChanged(); - } - - /** - * Get the Current visible Tree nodes + * Remove a node and its children from the tree * - * @return The visible Tree nodes main + * @param node The node to remove */ - public List getTreeNodes() { - return treeNodeManager.getTreeNodes(); - } - - /** - * @return The current selected TreeNode, or null if no node selected - */ - public TreeNode getSelectedNode() { - return currentSelectedNode; + public void removeNode(TreeNode node) { + treeNodeManager.collapseNode(node); + if (treeNodeManager.removeNode(node)) { + notifyDataSetChanged(); + } } } \ No newline at end of file diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeViewHolder.java b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeViewHolder.java new file mode 100644 index 00000000..c357fbdf --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeViewHolder.java @@ -0,0 +1,211 @@ +/* + * + * Copyright (C) 2025 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.configuration; + + +import android.graphics.Color; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; +import android.view.View; +import android.widget.CheckBox; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.documentfile.provider.DocumentFile; +import androidx.recyclerview.widget.RecyclerView; + +import java.io.File; +import java.util.Set; + +import de.yaacc.R; +import de.yaacc.upnp.server.contentdirectory.MediaPathFilter; +import de.yaacc.util.ThemeHelper; + + +public class TreeViewHolder extends RecyclerView.ViewHolder { + + + /** + * The default padding value for the TreeNode item + */ + private int nodePadding = 50; + private final TextView fileName; + private final ImageView fileStateIcon; + private final ImageView fileTypeIcon; + private final ImageButton fileRemoveButton; + private final CheckBox fileCheckbox; + + protected TreeViewAdapter adapter; + + public TreeViewHolder(@NonNull View itemView) { + super(itemView); + + this.fileName = itemView.findViewById(R.id.file_name); + this.fileStateIcon = itemView.findViewById(R.id.file_state_icon); + this.fileTypeIcon = itemView.findViewById(R.id.file_type_icon); + this.fileCheckbox = itemView.findViewById(R.id.file_checkbox); + this.fileRemoveButton = itemView.findViewById(R.id.file_remove); + } + + + public void bindTreeNode(TreeNode node) { + int padding = node.getLevel() * nodePadding; + itemView.setPadding( + padding, + itemView.getPaddingTop(), + itemView.getPaddingRight(), + itemView.getPaddingBottom()); + + String name = getName(node); + fileName.setText(name); + fileCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> { + Set pathes; + String absolutePath = getAbsolutePath(node); + if (isSafNode(node)) { + pathes = MediaPathFilter.getSelectedSafPathes(fileCheckbox.getContext()); + } else { + pathes = MediaPathFilter.getMediaPathesRaw(fileCheckbox.getContext()); + } + if (isChecked) { + pathes.add(absolutePath); + } else { + pathes.remove(absolutePath); + } + if (isSafNode(node)) { + MediaPathFilter.saveSelectedSafPathes(fileCheckbox.getContext(), pathes); + } else { + MediaPathFilter.saveMediaPaths(fileCheckbox.getContext(), pathes); + } + }); + + if (isSafNode(node)) { + String absolutePath = getAbsolutePath(node); + Drawable icon = fileRemoveButton.getContext().getDrawable(R.drawable.ic_baseline_delete_outline_32); + icon = ThemeHelper.tintDrawable(icon, fileRemoveButton.getContext().getTheme()); + if (MediaPathFilter.getSafPathes(fileRemoveButton.getContext()).contains(absolutePath)) { + fileRemoveButton.setVisibility(View.VISIBLE); + } else { + fileRemoveButton.setVisibility(View.INVISIBLE); + } + fileRemoveButton.setImageDrawable(icon); + fileRemoveButton.setOnClickListener(v -> { + Set selectedPathes; + Set safPathes; + selectedPathes = MediaPathFilter.getSelectedSafPathes(fileRemoveButton.getContext()); + safPathes = MediaPathFilter.getSafPathes(fileRemoveButton.getContext()); + selectedPathes.remove(absolutePath); + safPathes.remove(absolutePath); + MediaPathFilter.saveSelectedSafPathes(fileRemoveButton.getContext(), selectedPathes); + MediaPathFilter.saveSafPathes(fileRemoveButton.getContext(), safPathes); + adapter.removeNode(node); + }); + } else { + fileRemoveButton.setVisibility(View.INVISIBLE); + } + if (isDirectory(node)) { + Drawable icon = isSafNode(node) ? fileTypeIcon.getContext().getDrawable(R.drawable.ic_baseline_bookmark_48) : fileTypeIcon.getContext().getDrawable(R.drawable.ic_baseline_folder_open_48); + icon = ThemeHelper.tintDrawable(icon, fileTypeIcon.getContext().getTheme()); + fileTypeIcon.setImageDrawable(icon); + String absolutePath = getAbsolutePath(node); + fileCheckbox.setChecked(isSelected(absolutePath)); + fileCheckbox.setVisibility(View.VISIBLE); + } else { + fileCheckbox.setVisibility(View.INVISIBLE); + Drawable icon = ThemeHelper.tintDrawable(fileTypeIcon.getContext().getDrawable(R.drawable.ic_baseline_file_48), fileTypeIcon.getContext().getTheme()); + fileTypeIcon.setImageDrawable(icon); + } + + if (node.isSelected()) { + itemView.setBackgroundColor(Color.LTGRAY); + TypedValue typedValue = new TypedValue(); + itemView.getContext().getTheme().resolveAttribute(android.R.attr.colorPrimaryDark, typedValue, true); + fileName.setTextColor(typedValue.data); + } else { + TypedValue typedValue = new TypedValue(); + itemView.getContext().getTheme().resolveAttribute(android.R.attr.colorBackground, typedValue, true); + itemView.setBackgroundColor(typedValue.data); + itemView.getContext().getTheme().resolveAttribute(android.R.attr.colorForeground, typedValue, true); + fileName.setTextColor(typedValue.data); + } + fileStateIcon.setVisibility(View.INVISIBLE); + + if (isDirectory(node)) { + if (isDirectoryNotEmpty(node)) { + fileStateIcon.setVisibility(View.VISIBLE); + int stateIcon = node.isExpanded() ? R.drawable.sharp_keyboard_arrow_down_24 : R.drawable.sharp_chevron_right_24; + Drawable icon = ThemeHelper.tintDrawable(fileStateIcon.getContext().getDrawable(stateIcon), fileStateIcon.getContext().getTheme()); + fileStateIcon.setImageDrawable(icon); + } + } + } + + private static boolean isSafNode(TreeNode node) { + return node.getValue() instanceof DocumentFile; + } + + private boolean isSelected(String absolutePath) { + return MediaPathFilter.getMediaPathesRaw(fileCheckbox.getContext()).contains(absolutePath) + || MediaPathFilter.getSelectedSafPathes(fileCheckbox.getContext()).contains(absolutePath); + } + + @NonNull + private static String getAbsolutePath(TreeNode node) { + return node.getValue() instanceof File ? ((File) node.getValue()).getAbsolutePath() : ((DocumentFile) node.getValue()).getUri().toString(); + } + + private static boolean isDirectoryNotEmpty(TreeNode node) { + if (node.getValue() != null) { + if (node.getValue() instanceof File) { + File[] elements = ((File) node.getValue()).listFiles(); + return elements != null && elements.length > 0; + } else if (node.getValue() instanceof DocumentFile) { + DocumentFile[] elements = ((DocumentFile) node.getValue()).listFiles(); + return elements.length > 0; + } + } + return true; + } + + private static boolean isDirectory(TreeNode node) { + if (node.getValue() != null) { + if (node.getValue() instanceof File) { + return ((File) node.getValue()).isDirectory(); + } else if (node.getValue() instanceof DocumentFile) { + return ((DocumentFile) node.getValue()).isDirectory(); + } + } + return false; + } + + @Nullable + private static String getName(TreeNode node) { + if (isSafNode(node)) { + String result = ((DocumentFile) node.getValue()).getName(); + if (result == null) { + result = ((DocumentFile) node.getValue()).getUri().toString(); + } + return result; + } + return ((File) node.getValue()).getName(); + } + +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolderFactory.java b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeViewHolderFactory.java similarity index 96% rename from yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolderFactory.java rename to yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeViewHolderFactory.java index 254cb3d6..f567803f 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/TreeViewHolderFactory.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/TreeViewHolderFactory.java @@ -16,7 +16,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -package de.yaacc.upnp.server; +package de.yaacc.upnp.server.configuration; import android.view.View; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/configuration/YaaccUpnpServerControlActivity.java b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/YaaccUpnpServerControlActivity.java new file mode 100644 index 00000000..870d38f1 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/configuration/YaaccUpnpServerControlActivity.java @@ -0,0 +1,436 @@ +/* + * Copyright (C) 2025 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.configuration; + +import android.app.NotificationManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.util.TypedValue; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.CheckBox; +import android.widget.ImageButton; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.documentfile.provider.DocumentFile; +import androidx.preference.PreferenceManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.google.android.material.switchmaterial.SwitchMaterial; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import de.yaacc.R; +import de.yaacc.settings.SettingsActivity; +import de.yaacc.upnp.server.YaaccUpnpServerService; +import de.yaacc.upnp.server.contentdirectory.MediaPathFilter; +import de.yaacc.util.AboutActivity; +import de.yaacc.util.InterfaceResolutionHelper; +import de.yaacc.util.NotificationId; +import de.yaacc.util.SAFCacheManager; +import de.yaacc.util.SafPermissionManager; +import de.yaacc.util.ThemeHelper; +import de.yaacc.util.YaaccLogActivity; +import de.yaacc.util.YaaccLogger; +import de.yaacc.util.MediaStoreScanner; + +import java.util.Timer; +import java.util.TimerTask; + +/** + * Control activity for the yaacc upnp server + * + * @author Tobias Schoene (openbit) + */ +public class YaaccUpnpServerControlActivity extends AppCompatActivity { + + private static final int REQUEST_CODE_OPEN_DOCUMENT_TREE = 1001; + private TreeViewAdapter treeViewAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_yaacc_upnp_server_control); + SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + boolean receiverActive = preferences.getBoolean(getString(R.string.settings_local_server_receiver_chkbx), false); + CheckBox receiverCheckBox = findViewById(R.id.receiverEnabled); + receiverCheckBox.setChecked(receiverActive); + receiverCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(getApplicationContext().getString(R.string.settings_local_server_receiver_chkbx), isChecked); + editor.apply(); + }); + boolean providerActive = preferences.getBoolean(getString(R.string.settings_local_server_provider_chkbx), false); + CheckBox providerCheckBox = findViewById(R.id.providerEnabled); + providerCheckBox.setChecked(providerActive); + providerCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(getApplicationContext().getString(R.string.settings_local_server_provider_chkbx), isChecked); + editor.apply(); + }); + boolean proxyActive = preferences.getBoolean(getString(R.string.settings_local_server_proxy_chkbx), false); + CheckBox proxyCheckBox = findViewById(R.id.proxyEnabled); + proxyCheckBox.setChecked(proxyActive); + proxyCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(getApplicationContext().getString(R.string.settings_local_server_proxy_chkbx), isChecked); + editor.apply(); + }); + + boolean filterActive = preferences.getBoolean(getString(R.string.settings_local_server_media_filter_chkbx), true); + CheckBox mediaFilterCheckBox = findViewById(R.id.filterEnabled); + mediaFilterCheckBox.setChecked(filterActive); + mediaFilterCheckBox.setOnCheckedChangeListener((buttonView, isChecked) -> { + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(getApplicationContext().getString(R.string.settings_local_server_media_filter_chkbx), isChecked); + editor.apply(); + }); + + + SwitchMaterial localServerEnabledSwitch = findViewById(R.id.serverOnOff); + localServerEnabledSwitch.setChecked(preferences.getBoolean(getApplicationContext().getString(R.string.settings_local_server_chkbx), false)); + localServerEnabledSwitch.setOnClickListener((v -> { + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(v.getContext().getString(R.string.settings_local_server_chkbx), localServerEnabledSwitch.isChecked()); + editor.apply(); + if (localServerEnabledSwitch.isChecked()) { + start(); + } else { + stop(); + } + })); + ImageButton resetButton = findViewById(R.id.sharedFoldersReset); + Drawable icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.outline_database_off_32, getTheme()), getTheme()); + resetButton.setImageDrawable(icon); + resetButton.setOnClickListener(v -> { + MediaPathFilter.resetMediaPaths(getApplicationContext()); + MediaPathFilter.resetSelectedSafPathes(getApplicationContext()); + buildFileSystemTree(treeViewAdapter); + } + ); + + ImageButton safButton = findViewById(R.id.sharedFoldersAddSaf); + icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_search_bookmark, getTheme()), getTheme()); + safButton.setImageDrawable(icon); + safButton.setOnClickListener(v -> selectSafContent()); + + ImageButton clearCacheButton = findViewById(R.id.clearSafCache); + icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_refresh_32, getTheme()), getTheme()); + clearCacheButton.setImageDrawable(icon); + clearCacheButton.setOnClickListener(v -> { + SAFCacheManager.getInstance(getApplicationContext()).clearCache(); + SAFCacheManager.getInstance(getApplicationContext()).preloadSafDurations(); + Toast.makeText(getApplicationContext(), "SAF cache cleared and reindexing started", Toast.LENGTH_SHORT).show(); + }); + + ImageButton mediaStoreRescanButton = findViewById(R.id.mediaStoreRescan); + icon = ThemeHelper.tintDrawable(getResources().getDrawable(R.drawable.ic_baseline_refresh_32, getTheme()), getTheme()); + mediaStoreRescanButton.setImageDrawable(icon); + mediaStoreRescanButton.setOnClickListener(v -> { + new Timer().schedule(new TimerTask() { + @Override + public void run() { + new MediaStoreScanner().scanMediaFiles(YaaccUpnpServerControlActivity.this); + } + }, 10L); + }); + + TextView localServerControlInterface = findViewById(R.id.localServerControlInterface); + String[] ipConfig = InterfaceResolutionHelper.getIfAndIpAddress(this); + localServerControlInterface.setText(ipConfig[1] + "@" + ipConfig[0]); + + + RecyclerView recyclerView = findViewById(R.id.folders_recycler_view); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setNestedScrollingEnabled(false); + TypedValue typedValue = new TypedValue(); + getTheme().resolveAttribute(android.R.attr.colorBackground, typedValue, true); + recyclerView.setBackgroundColor(typedValue.data); + + TreeViewHolderFactory factory = (v, layout) -> new TreeViewHolder(v); + treeViewAdapter = new TreeViewAdapter(factory); + recyclerView.setAdapter(treeViewAdapter); + buildFileSystemTree(treeViewAdapter); + } + + private void selectSafContent() { + if (!SafPermissionManager.canAddMorePermissions(this)) { + YaaccLogger.w(getClass().getName(), "Cannot add more SAF permissions. Limit reached: " + + SafPermissionManager.getPermissionCount(this)); + // TODO: Show user dialog about limit + return; + } + + YaaccLogger.w(getClass().getName(), "Starting SAF picker."); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + startActivityForResult(intent, REQUEST_CODE_OPEN_DOCUMENT_TREE); + } + + private void buildFileSystemTree(TreeViewAdapter treeViewAdapter) { + List fileRoots = new ArrayList<>(); + File externalStorageRoot = Environment.getExternalStorageDirectory(); // Or any other root path + // Check if external storage is readable + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || + Environment.MEDIA_MOUNTED_READ_ONLY.equals(Environment.getExternalStorageState())) { + + if (externalStorageRoot.exists() && externalStorageRoot.isDirectory()) { + // Add top-level directories from the chosen root + File[] topLevelFiles = externalStorageRoot.listFiles(); + if (topLevelFiles != null) { + for (File file : topLevelFiles) { + TreeNode node = buildFileSystemNode(file, R.layout.file_list_item); + if (node != null) { + fileRoots.add(node); + } + } + } else { + YaaccLogger.e(getClass().getName(), "Could not list files in root: " + externalStorageRoot.getAbsolutePath()); + } + } else { + YaaccLogger.e(getClass().getName(), "Root directory does not exist or is not a directory: " + externalStorageRoot.getAbsolutePath()); + } + } else { + YaaccLogger.e(getClass().getName(), "External storage not readable."); + } + + if (fileRoots.isEmpty()) { + YaaccLogger.w(getClass().getName(), "No file system roots found or storage unavailable. Adding a placeholder."); + } + + Set safUris = MediaPathFilter.getSafPathes(getApplicationContext()); + if (safUris != null) { + for (String uriString : safUris) { + try { + Uri uri = Uri.parse(uriString); + DocumentFile docRoot = DocumentFile.fromTreeUri(this, uri); + if (docRoot != null && docRoot.exists()) { + TreeNode node = buildFileSystemNode(docRoot, R.layout.file_list_item); + if (node != null) { + fileRoots.add(node); + } + } else { + YaaccLogger.w(getClass().getName(), "SAF root not accessible: " + uriString); + } + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Error restoring SAF uri: " + uriString, e); + } + } + } + treeViewAdapter.updateTreeNodes(fileRoots); + + + treeViewAdapter.setTreeNodeClickListener((treeNode, nodeView) -> { + YaaccLogger.d(getClass().getName(), "Click on TreeNode with value " + treeNode.getValue().toString()); + Object value = treeNode.getValue(); + if (value instanceof File) { + clickedOnFile(treeNode, (File) value); + } else if (value instanceof DocumentFile) { + clickedOnDocument(treeNode, (DocumentFile) value); + } + }); + } + + private void clickedOnDocument(TreeNode treeNode, DocumentFile value) { + DocumentFile doc = value; + if (doc.isDirectory()) { + DocumentFile[] children = doc.listFiles(); + if (children != null && treeNode.getChildren().size() != children.length) { + for (DocumentFile childDoc : children) { + TreeNode childNode = buildFileSystemNode(childDoc, treeNode.getLayoutId()); + if (childNode != null) { + treeNode.addChild(childNode); + } + } + } + } + YaaccLogger.d(getClass().getName(), "Clicked on document file: " + doc.getUri()); + } + + private void clickedOnFile(TreeNode treeNode, File value) { + File file = value; + if (file.isDirectory() && file.listFiles() != null && treeNode.getChildren().size() != file.listFiles().length) { + File[] children = file.listFiles(); + if (children != null) { + for (File childFile : children) { + TreeNode childNode = buildFileSystemNode(childFile, treeNode.getLayoutId()); + if (childNode != null) { + treeNode.addChild(childNode); + } + } + } + } + YaaccLogger.d(getClass().getName(), "Clicked on file: " + file.getAbsolutePath()); + } + + /** + * Recursively builds a TreeNode structure from the file system or a DocumentFile. + * + * @param fileObj The current File or DocumentFile. + * @param layoutId The layout resource ID for the TreeNode. + * @return A TreeNode representing the file/directory, or null if it should be skipped. + */ + private TreeNode buildFileSystemNode(Object fileObj, int layoutId) { + if (fileObj == null) { + return null; + } + + if (fileObj instanceof File) { + File file = (File) fileObj; + if (!file.exists()) { + return null; + } + return new TreeNode(file, layoutId); + } else if (fileObj instanceof DocumentFile) { + DocumentFile doc = (DocumentFile) fileObj; + if (!doc.exists()) { + return null; + } + return new TreeNode(doc, layoutId); + } + + return null; + } + + + private void start() { + + YaaccUpnpServerControlActivity.this.startForegroundService(new Intent(getApplicationContext(), + YaaccUpnpServerService.class)); + + + SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(getString(R.string.settings_local_server_chkbx), true); + editor.apply(); + } + + private void stop() { + YaaccUpnpServerControlActivity.this.stopService(new Intent(getApplicationContext(), + YaaccUpnpServerService.class)); + SharedPreferences preferences = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()); + SharedPreferences.Editor editor = preferences.edit(); + editor.putBoolean(getString(R.string.settings_local_server_chkbx), false); + editor.apply(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == REQUEST_CODE_OPEN_DOCUMENT_TREE && resultCode == RESULT_OK) { + if (data != null) { + Uri treeUri = data.getData(); + if (treeUri != null) { + try { + final int takeFlags = data.getFlags() & (Intent.FLAG_GRANT_READ_URI_PERMISSION); + getContentResolver().takePersistableUriPermission(treeUri, takeFlags); + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Could not take persistable uri permission", e); + } + + Set uriSet = MediaPathFilter.getSafPathes(getApplicationContext()); + if (uriSet == null) { + uriSet = new HashSet<>(); + } else { + uriSet = new HashSet<>(uriSet); + } + DocumentFile doc = DocumentFile.fromTreeUri(this, treeUri); + if (doc != null) { + String newUri = doc.getUri().toString(); + // Remove any existing parent URIs that are now redundant + uriSet.removeIf(existingUri -> newUri.startsWith(existingUri)); + // Remove any existing child URIs that are now redundant + uriSet.removeIf(existingUri -> existingUri.startsWith(newUri)); + uriSet.add(newUri); + } + MediaPathFilter.saveSafPathes(getApplicationContext(), uriSet); + + // Also add to selected paths for content directory + Set selectedUriSet = MediaPathFilter.getSelectedSafPathes(getApplicationContext()); + if (selectedUriSet == null) { + selectedUriSet = new HashSet<>(); + } else { + selectedUriSet = new HashSet<>(selectedUriSet); + } + String newUri = doc.getUri().toString(); + selectedUriSet.removeIf(existingUri -> newUri.startsWith(existingUri)); + selectedUriSet.removeIf(existingUri -> existingUri.startsWith(newUri)); + selectedUriSet.add(newUri); + MediaPathFilter.saveSelectedSafPathes(getApplicationContext(), selectedUriSet); + + // rebuild tree with newly added SAF root + buildFileSystemTree(treeViewAdapter); + } + } + } + } + + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.activity_yaacc_upnp_server_control, + menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.menu_exit) { + exit(); + return true; + } else if (item.getItemId() == R.id.menu_settings) { + Intent i = new Intent(this, SettingsActivity.class); + startActivity(i); + return true; + } else if (item.getItemId() == R.id.yaacc_log) { + YaaccLogActivity.showLog(this); + return true; + } else if (item.getItemId() == R.id.yaacc_about) { + AboutActivity.showAbout(this); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void exit() { + stop(); + //FIXME work around to be fixed with new ui + NotificationManager mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + // mId allows you to update the notification later on. + mNotificationManager.cancel(NotificationId.UPNP_SERVER.getId()); + finish(); + } +} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/ConnectionManagerErrorCode.java b/yaacc/src/main/java/de/yaacc/upnp/server/connectionmanager/ConnectionManagerErrorCode.java similarity index 72% rename from yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/ConnectionManagerErrorCode.java rename to yaacc/src/main/java/de/yaacc/upnp/server/connectionmanager/ConnectionManagerErrorCode.java index 199b89ae..836c386d 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/ConnectionManagerErrorCode.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/connectionmanager/ConnectionManagerErrorCode.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,7 +31,7 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.support.connectionmanager; +package de.yaacc.upnp.server.connectionmanager; /** * diff --git a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/ConnectionManagerException.java b/yaacc/src/main/java/de/yaacc/upnp/server/connectionmanager/ConnectionManagerException.java similarity index 65% rename from yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/ConnectionManagerException.java rename to yaacc/src/main/java/de/yaacc/upnp/server/connectionmanager/ConnectionManagerException.java index 0d9887f1..073c923d 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/ConnectionManagerException.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/connectionmanager/ConnectionManagerException.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,7 +31,7 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.support.connectionmanager; +package de.yaacc.upnp.server.connectionmanager; import org.fourthline.cling.model.action.ActionException; import org.fourthline.cling.model.types.ErrorCode; diff --git a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/ConnectionManagerService.java b/yaacc/src/main/java/de/yaacc/upnp/server/connectionmanager/ConnectionManagerService.java similarity index 87% rename from yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/ConnectionManagerService.java rename to yaacc/src/main/java/de/yaacc/upnp/server/connectionmanager/ConnectionManagerService.java index af153a57..a752378a 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/ConnectionManagerService.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/connectionmanager/ConnectionManagerService.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * Copyright (C) 2013 4th Line GmbH, Switzerland * @@ -13,9 +31,7 @@ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. */ -package org.fourthline.cling.support.connectionmanager; - -import android.util.Log; +package de.yaacc.upnp.server.connectionmanager; import org.fourthline.cling.binding.annotations.UpnpAction; import org.fourthline.cling.binding.annotations.UpnpInputArgument; @@ -38,6 +54,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import de.yaacc.util.YaaccLogger; + /** * Base for connection management, implements the connection ID "0" behavior. * @@ -121,7 +139,7 @@ public PropertyChangeSupport getPropertyChangeSupport() { }) synchronized public ConnectionInfo getCurrentConnectionInfo(@UpnpInputArgument(name = "ConnectionID") int connectionId) throws ActionException { - Log.v(getClass().getName(), "Getting connection information of connection ID: " + connectionId); + YaaccLogger.v(getClass().getName(), "Getting connection information of connection ID: " + connectionId); ConnectionInfo info; if ((info = activeConnections.get(connectionId)) == null) { throw new ConnectionManagerException( @@ -140,7 +158,7 @@ synchronized public CSV getCurrentConnectionIDs() { for (Integer connectionID : activeConnections.keySet()) { csv.add(new UnsignedIntegerFourBytes(connectionID)); } - Log.v(getClass().getName(), "Returning current connection IDs: " + csv.size()); + YaaccLogger.v(getClass().getName(), "Returning current connection IDs: " + csv.size()); return csv; } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ContentBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ContentBrowser.java index 903d95b1..03da6d20 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ContentBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ContentBrowser.java @@ -19,20 +19,32 @@ package de.yaacc.upnp.server.contentdirectory; import android.content.Context; -import android.util.Log; +import android.util.Base64; +import de.yaacc.util.YaaccLogger; import android.webkit.MimeTypeMap; import org.fourthline.cling.support.model.DIDLObject; +import org.fourthline.cling.support.model.Protocol; +import org.fourthline.cling.support.model.ProtocolInfo; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.item.Item; import org.seamless.util.MimeType; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Set; +import de.yaacc.upnp.model.YaaccItem; +import de.yaacc.upnp.model.YaaccMusicTrack; +import de.yaacc.upnp.model.YaaccPhoto; +import de.yaacc.upnp.model.YaaccRes; import de.yaacc.upnp.server.YaaccUpnpServerService; +import org.fourthline.cling.support.model.item.Item; +import org.fourthline.cling.support.model.item.MusicTrack; +import org.fourthline.cling.support.model.item.Photo; /** @@ -42,6 +54,52 @@ */ public abstract class ContentBrowser { + // Static cache for DLNA attributes - computed once, reused forever + private static final Map DLNA_CACHE = new HashMap<>(); + + // Static cache for ProtocolInfo - computed once, reused forever + private static final Map PROTOCOL_INFO_CACHE = new HashMap<>(); + + static { + // Audio types + DLNA_CACHE.put("audio/mpeg", "DLNA.ORG_PN=MP3;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/mp4", "DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/aac", "DLNA.ORG_PN=AAC_ISO;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/L16", "DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/x-ms-wma", "DLNA.ORG_PN=WMABASE;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/vnd.dlna.adts", "DLNA.ORG_PN=ADTS;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/vnd.dolby.dd-raw", "DLNA.ORG_PN=AC3;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/3gpp", "DLNA.ORG_PN=AMR_3GPP;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/x-sony-oma", "DLNA.ORG_PN=ATRAC3plus;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/ogg", "DLNA.ORG_PN=*;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/flac", "DLNA.ORG_PN=*;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/wav", "DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("audio/x-wav", "DLNA.ORG_PN=LPCM;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + + // Image types + DLNA_CACHE.put("image/jpeg", "DLNA.ORG_PN=JPEG_LRG;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("image/png", "DLNA.ORG_PN=PNG_LRG;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + + // Video types + DLNA_CACHE.put("video/mp4", "DLNA.ORG_PN=MPEG4_P2_MP4_SP_AAC;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("video/mpeg", "DLNA.ORG_PN=MPEG1;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("video/vnd.dlna.mpeg-tts", "DLNA.ORG_PN=MPEG_TS_MP_LL_AAC;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("video/3gpp", "DLNA.ORG_PN=MPEG4_H263_MP4_P0_L10_AAC;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("video/x-matroska", "DLNA.ORG_PN=MATROSKA;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("video/webm", "DLNA.ORG_PN=*;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("video/x-msvideo", "DLNA.ORG_PN=AVI;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + DLNA_CACHE.put("video/quicktime", "DLNA.ORG_PN=*;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + + // Pre-create ProtocolInfo objects for common MIME types + for (Map.Entry entry : DLNA_CACHE.entrySet()) { + String mime = entry.getKey(); + String dlna = entry.getValue(); + PROTOCOL_INFO_CACHE.put(mime, new ProtocolInfo( + Protocol.HTTP_GET.toString() + ":" + ProtocolInfo.WILDCARD + ":" + mime + ":" + dlna + )); + } + } + Context context; @@ -71,77 +129,50 @@ public List browseChildren(YaaccContentDirectory contentDirectory, S } public String getUriString(YaaccContentDirectory contentDirectory, String id, MimeType mimeType) { + return getUriString(contentDirectory, id, mimeType, null); + } + + public String getUriString(YaaccContentDirectory contentDirectory, String id, MimeType mimeType, String contentUri) { String fileExtension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType.toString()); if (fileExtension == null) { - Log.d(getClass().getName(), "Can't lookup file extension from mimetype: " + mimeType); + YaaccLogger.d(getClass().getName(), "Can't lookup file extension from mimetype: " + mimeType); //try subtype fileExtension = mimeType.getSubtype(); } + + if (contentUri != null) { + // contentUri is now a short ID, not a full URI - don't Base64 encode it + return "http://" + contentDirectory.getIpAddress() + ":" + + YaaccUpnpServerService.PORT + "/saf/" + id + "/" + contentUri + "." + fileExtension; + } return "http://" + contentDirectory.getIpAddress() + ":" + YaaccUpnpServerService.PORT + "/res/" + id + "/file." + fileExtension; } public String getDLNAAttributes(MimeType mimetype) { - String result = "DLNA.ORG_PN="; - - if ("audio".equals(mimetype.getType())) { - if ("mpeg".equals(mimetype.getSubtype())) { - result = result + "MP3"; - } else if ("L16".equals(mimetype.getSubtype())) { - result = result + "LPCM"; - } else if ("x-ms-wma".equals(mimetype.getSubtype())) { - result = result + "WMABASE"; - } else if ("vnd.dlna.adts".equals(mimetype.getSubtype())) { - result = result + "ADTS"; - } else if ("mp4".equals(mimetype.getSubtype())) { - result = result + "AAC_ISO"; - } else if ("vnd.dolby.dd-raw".equals(mimetype.getSubtype())) { - result = result + "AC3"; - } else if ("3gpp".equals(mimetype.getSubtype())) { - result = result + "AMR_3GPP"; - } else if ("x-sony-oma".equals(mimetype.getSubtype())) { - result = result + "ATRAC3plus"; - } else { - result = result + "*"; - } - } else if ("image".equals(mimetype.getType())) { - if ("jpeg".equals(mimetype.getSubtype())) { - result = result + "JPEG_LRG"; - } else if ("png".equals(mimetype.getSubtype())) { - result = result + "PNG_LRG"; - } else { - result = result + "*"; - } - } else if ("video".equals(mimetype.getType())) { - if ("x-ms-wmv".equals(mimetype.getSubtype())) { - result = result + "WMVMED_BASE"; - } else if ("avi".equals(mimetype.getSubtype())) { - result = result + "AVI"; - } else if ("divx".equals(mimetype.getSubtype())) { - result = result + "AVI"; - } else if ("mpeg".equals(mimetype.getSubtype())) { - result = result + "MPEG1"; - } else if ("vnd.dlna.mpeg-tts".equals(mimetype.getSubtype())) { - result = result + "MPEG_TS_MP_LL_AAC"; - } else if ("mp4".equals(mimetype.getSubtype())) { - result = result + "MPEG4_P2_MP4_SP_AAC"; - } else if ("3gpp".equals(mimetype.getSubtype())) { - result = result + "MPEG4_H263_MP4_P0_L10_AAC"; - } else if ("x-matroska".equals(mimetype.getSubtype())) { - result = result + "MATROSKA"; - } else if ("mkv".equals(mimetype.getSubtype())) { - result = result + "MATROSKA"; - } else if ("x-ms-asf".equals(mimetype.getSubtype())) { - result = result + "VC1_ASF_AP_L2_WMA"; - } else if ("x-ms-mwv".equals(mimetype.getSubtype())) { - result = result + "VC1_ASF_AP_L2_WMA"; - } else { - result = result + "*"; - } + String mime = mimetype.toString(); + String result = DLNA_CACHE.get(mime); + if (result != null) { + return result; } - result = result + ";DLNA.ORG_OP=01"; - return result; + // Unknown type - return wildcard + return "DLNA.ORG_PN=*;DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"; + } + + /** + * Returns a cached ProtocolInfo for the given MIME type. + * This avoids repeated parsing and object creation for each item. + */ + protected ProtocolInfo getProtocolInfo(MimeType mimeType) { + String mime = mimeType.toString(); + ProtocolInfo cached = PROTOCOL_INFO_CACHE.get(mime); + if (cached != null) { + return cached; + } + // Unknown MIME type - create new + String dlna = getDLNAAttributes(mimeType); + return new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mime, dlna); } @@ -156,4 +187,140 @@ public List getMediaPathesForLikeClause() { public Set getMediaPathes() { return MediaPathFilter.getMediaPathes(getContext()); } + + public Set getSelectedSafPathes() { + return MediaPathFilter.getSelectedSafPathes(getContext()); + } + + /** + * Creates a lightweight Item using YaaccItem/YaaccRes, then converts to Cling Item. + * This avoids Cling Property overhead during item creation. + */ + protected Item createItem(String id, String parentId, String title, String creator, + boolean restricted, MimeType mimeType, String uri, + Long size, String duration) { + // Get cached ProtocolInfo + ProtocolInfo protocolInfo = getProtocolInfo(mimeType); + + // Create lightweight YaaccRes + YaaccRes yaaccRes = new YaaccRes(protocolInfo, size, duration, null, uri); + + // Determine class based on MIME type + String mimeMain = mimeType.getType(); + String clazz = mimeMain.equals("audio") ? "object.item.audioItem" + : mimeMain.equals("video") ? "object.item.videoItem" + : "object.item.imageItem"; + + // Create lightweight YaaccItem + YaaccItem yaaccItem = new YaaccItem(id, parentId, title, creator, restricted, clazz); + yaaccItem.addResource(yaaccRes); + + // Convert to Cling Item for UPnP serialization + return yaaccItem.toClingItem(); + } + + /** + * Creates a lightweight MusicTrack with additional metadata. + */ + protected MusicTrack createMusicTrack(String id, String parentId, String title, String creator, + boolean restricted, MimeType mimeType, String uri, + Long size, String duration, + String album, String artist, Integer trackNumber, + String date, String[] genres, String albumArtUri) { + // Get cached ProtocolInfo + ProtocolInfo protocolInfo = getProtocolInfo(mimeType); + + // Create lightweight YaaccRes with audio properties + YaaccRes yaaccRes = new YaaccRes(protocolInfo, size, duration, null, + 44100L, 2L, 16L, uri); + + // Create lightweight YaaccMusicTrack + YaaccMusicTrack yaaccTrack = new YaaccMusicTrack(id, parentId, title, creator, restricted); + yaaccTrack.addResource(yaaccRes); + + // Set additional properties + if (album != null) yaaccTrack.setAlbum(album); + if (artist != null) yaaccTrack.setArtist(artist); + if (trackNumber != null) yaaccTrack.setTrackNumber(trackNumber); + if (date != null) yaaccTrack.setDate(date); + if (genres != null) yaaccTrack.setGenres(genres); + if (albumArtUri != null) { + try { + yaaccTrack.setAlbumArtUri(new java.net.URI(albumArtUri)); + } catch (Exception e) { + // Ignore invalid URI + } + } + + // Convert to Cling MusicTrack for UPnP serialization + return yaaccTrack.toClingItem(); + } + + /** + * Creates a lightweight MusicTrack with bitrate support (for Android 11+). + */ + protected MusicTrack createMusicTrack(String id, String parentId, String title, String creator, + boolean restricted, MimeType mimeType, String uri, + Long size, String duration, + String album, String artist, Integer trackNumber, + String date, String[] genres, String albumArtUri, + Long bitrate) { + // Get cached ProtocolInfo + ProtocolInfo protocolInfo = getProtocolInfo(mimeType); + + // Create lightweight YaaccRes with audio properties and bitrate + YaaccRes yaaccRes = new YaaccRes(protocolInfo, size, duration, bitrate, + 44100L, 2L, 16L, uri); + + // Create lightweight YaaccMusicTrack + YaaccMusicTrack yaaccTrack = new YaaccMusicTrack(id, parentId, title, creator, restricted); + yaaccTrack.addResource(yaaccRes); + + // Set additional properties + if (album != null) yaaccTrack.setAlbum(album); + if (artist != null) yaaccTrack.setArtist(artist); + if (trackNumber != null) yaaccTrack.setTrackNumber(trackNumber); + if (date != null) yaaccTrack.setDate(date); + if (genres != null) yaaccTrack.setGenres(genres); + if (albumArtUri != null) { + try { + yaaccTrack.setAlbumArtUri(new java.net.URI(albumArtUri)); + } catch (Exception e) { + // Ignore invalid URI + } + } + + // Convert to Cling MusicTrack for UPnP serialization + return yaaccTrack.toClingItem(); + } + + /** + * Creates a lightweight Photo with album art URI. + */ + protected Photo createPhoto(String id, String parentId, String title, String creator, + boolean restricted, MimeType mimeType, String uri, + Long size, String albumArtUri) { + // Get cached ProtocolInfo + ProtocolInfo protocolInfo = getProtocolInfo(mimeType); + + // Create lightweight YaaccRes + YaaccRes yaaccRes = new YaaccRes(protocolInfo, size, null, null, uri); + + // Create lightweight YaaccPhoto + YaaccPhoto yaaccPhoto = new YaaccPhoto(id, parentId, title, creator, restricted); + yaaccPhoto.addResource(yaaccRes); + + // Set album art URI + if (albumArtUri != null) { + try { + yaaccPhoto.setAlbumArtUri(new java.net.URI(albumArtUri)); + } catch (Exception e) { + // Ignore invalid URI + } + } + + // Convert to Cling Photo for UPnP serialization + return yaaccPhoto.toClingItem(); + } + } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ContentDirectoryIDs.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ContentDirectoryIDs.java index 8cac2069..32deb6d6 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ContentDirectoryIDs.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ContentDirectoryIDs.java @@ -40,7 +40,13 @@ public enum ContentDirectoryIDs { MUSIC_ALBUM_ITEM_PREFIX("820999"), MUSIC_ARTISTS_FOLDER("900999"), MUSIC_ARTIST_PREFIX("910999"), - MUSIC_ARTIST_ITEM_PREFIX("920999"); + MUSIC_ARTIST_ITEM_PREFIX("920999"), + SAF_FOLDER("1000999"), + SAF_PREFIX("1100999"), + LIVE_STREAM_FOLDER("1200999"), + LIVE_STREAM_SYSTEM_AUDIO("1210999"), + LIVE_STREAM_SCREEN_CAST("1220999"), + LIVE_STREAM_COMBINED("1230999"); final String id; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImageAllItemBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImageAllItemBrowser.java index 355d27a3..f1cb5839 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImageAllItemBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImageAllItemBrowser.java @@ -22,21 +22,16 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.webkit.MimeTypeMap; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.DIDLObject.Property.UPNP; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.item.Item; import org.fourthline.cling.support.model.item.Photo; import org.seamless.util.MimeType; -import java.net.URI; import java.util.ArrayList; import java.util.List; @@ -79,7 +74,7 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, .getColumnIndex(MediaStore.Images.ImageColumns.SIZE))); @SuppressLint("Range") String mimeTypeSTring = mImageCursor.getString(mImageCursor .getColumnIndex(MediaStore.Images.ImageColumns.MIME_TYPE)); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "Mimetype: " + mimeTypeSTring); MimeType mimeType = MimeType @@ -88,20 +83,25 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, // ability of playing a file by the file extension String uri = "http://" + contentDirectory.getIpAddress() + ":" + YaaccUpnpServerService.PORT + "/?id=" + id + "&f=file." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType.toString()); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - result = new Photo(ContentDirectoryIDs.IMAGE_ALL_PREFIX.getId() + id, - ContentDirectoryIDs.IMAGES_FOLDER.getId(), name, "", "", - resource); - URI albumArtUri = URI.create("http://" - + contentDirectory.getIpAddress() + ":" - + YaaccUpnpServerService.PORT + "/thumb/" + id); - result.replaceFirstProperty(new UPNP.ALBUM_ART_URI( - albumArtUri)); - Log.d(getClass().getName(), "Image: " + id + " Name: " + name + String albumArtUri = "http://" + contentDirectory.getIpAddress() + ":" + + YaaccUpnpServerService.PORT + "/thumb/" + id; + + result = createPhoto( + ContentDirectoryIDs.IMAGE_ALL_PREFIX.getId() + id, + ContentDirectoryIDs.IMAGES_FOLDER.getId(), + name, + "", + false, + mimeType, + uri, + size, + albumArtUri + ); + + YaaccLogger.d(getClass().getName(), "Image: " + id + " Name: " + name + " uri: " + uri); } else { - Log.d(getClass().getName(), "Item " + myId + " not found."); + YaaccLogger.d(getClass().getName(), "Item " + myId + " not found."); } } return result; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImageByBucketNameItemBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImageByBucketNameItemBrowser.java index 5bf09e1f..b9e05dba 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImageByBucketNameItemBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImageByBucketNameItemBrowser.java @@ -22,20 +22,15 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.DIDLObject.Property.UPNP; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.item.Item; import org.fourthline.cling.support.model.item.Photo; import org.seamless.util.MimeType; -import java.net.URI; import java.util.ArrayList; import java.util.List; @@ -59,7 +54,7 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, String[] projection = {MediaStore.Images.Media._ID, MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.MIME_TYPE, MediaStore.Images.Media.SIZE, MediaStore.Images.Media.BUCKET_ID, MediaStore.Images.Media.DATE_TAKEN}; - String selection = MediaStore.Images.Media.BUCKET_ID + "=?"; + String selection = MediaStore.Images.Media._ID + "=?"; String[] selectionArgs = new String[]{myId.substring(ContentDirectoryIDs.IMAGE_BY_BUCKET_PREFIX.getId().length())}; try (Cursor mImageCursor = contentDirectory .getContext() @@ -78,32 +73,37 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, .getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME)); @SuppressLint("Range") Long size = Long.valueOf(mImageCursor.getString(mImageCursor .getColumnIndex(MediaStore.Images.Media.SIZE))); - @SuppressLint("Range") Long dateTaken = Long.valueOf(mImageCursor.getString(mImageCursor - .getColumnIndex(MediaStore.Images.Media.DATE_TAKEN))); + String dateTakenStr = mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.Media.DATE_TAKEN)); + @SuppressLint("Range") Long dateTaken = dateTakenStr != null ? Long.valueOf(dateTakenStr) : 0L; @SuppressLint("Range") String mimeTypeString = mImageCursor.getString(mImageCursor .getColumnIndex(MediaStore.Images.Media.MIME_TYPE)); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "Mimetype: " + mimeTypeString); @SuppressLint("Range") MimeType mimeType = MimeType.valueOf(mimeTypeString); // file parameter only needed for media players which decide the // ability of playing a file by the file extension String uri = getUriString(contentDirectory, id, mimeType); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - result = new Photo(ContentDirectoryIDs.IMAGE_BY_BUCKET_PREFIX.getId() + id, - ContentDirectoryIDs.IMAGES_BY_BUCKET_NAME_PREFIX.getId() + bucketId, name, "", "", - resource); - URI albumArtUri = URI.create("http://" - + contentDirectory.getIpAddress() + ":" - + YaaccUpnpServerService.PORT + "/thumb/" + id); - result.replaceFirstProperty(new UPNP.ALBUM_ART_URI( - albumArtUri)); - Log.d(getClass().getName(), "Image: " + id + " Name: " + name + String albumArtUri = "http://" + contentDirectory.getIpAddress() + ":" + + YaaccUpnpServerService.PORT + "/thumb/" + id; + + result = createPhoto( + ContentDirectoryIDs.IMAGE_BY_BUCKET_PREFIX.getId() + id, + ContentDirectoryIDs.IMAGES_BY_BUCKET_NAME_PREFIX.getId() + bucketId, + name, + "", + false, + mimeType, + uri, + size, + albumArtUri + ); + + YaaccLogger.d(getClass().getName(), "Image: " + id + " Name: " + name + " uri: " + uri); } else { - Log.d(getClass().getName(), "Item " + myId + " not found."); + YaaccLogger.d(getClass().getName(), "Item " + myId + " not found."); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesAllFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesAllFolderBrowser.java index 6c8b6dd5..cdb7e0fd 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesAllFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesAllFolderBrowser.java @@ -22,13 +22,9 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.DIDLObject.Property.UPNP; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.StorageFolder; @@ -100,24 +96,30 @@ public List browseItem(YaaccContentDirectory contentDirectory, String myId @SuppressLint("Range") String id = mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)); @SuppressLint("Range") String name = mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns.DISPLAY_NAME)); @SuppressLint("Range") Long size = Long.valueOf(mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns.SIZE))); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "Mimetype: " + mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns.MIME_TYPE))); MimeType mimeType = MimeType.valueOf(mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns.MIME_TYPE))); // file parameter only needed for media players which decide the // ability of playing a file by the file extension String uri = getUriString(contentDirectory, id, mimeType); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - - Photo photo = new Photo(ContentDirectoryIDs.IMAGE_ALL_PREFIX.getId() + id, ContentDirectoryIDs.IMAGES_ALL_FOLDER.getId(), name, "", "", resource); URI albumArtUri = URI.create("http://" + contentDirectory.getIpAddress() + ":" + YaaccUpnpServerService.PORT + "/thumb/" + id); - photo.replaceFirstProperty(new UPNP.ALBUM_ART_URI( - albumArtUri)); + + Photo photo = createPhoto( + ContentDirectoryIDs.IMAGE_ALL_PREFIX.getId() + id, + ContentDirectoryIDs.IMAGES_ALL_FOLDER.getId(), + name, + "", + false, + mimeType, + uri, + size, + albumArtUri.toString() + ); result.add(photo); - Log.d(getClass().getName(), "Image: " + id + " Name: " + name + " uri: " + uri); + YaaccLogger.d(getClass().getName(), "Image: " + id + " Name: " + name + " uri: " + uri); currentCount++; } currentIndex++; @@ -125,7 +127,7 @@ public List browseItem(YaaccContentDirectory contentDirectory, String myId } } else { - Log.d(getClass().getName(), "System media store is empty."); + YaaccLogger.d(getClass().getName(), "System media store is empty."); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesByBucketNameFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesByBucketNameFolderBrowser.java index a55d6ba0..f9d5021e 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesByBucketNameFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesByBucketNameFolderBrowser.java @@ -22,13 +22,9 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.DIDLObject.Property.UPNP; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.StorageFolder; @@ -36,7 +32,6 @@ import org.fourthline.cling.support.model.item.Photo; import org.seamless.util.MimeType; -import java.net.URI; import java.util.ArrayList; import java.util.List; @@ -119,24 +114,29 @@ public List browseItem(YaaccContentDirectory contentDirectory, String myId @SuppressLint("Range") String id = mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns._ID)); @SuppressLint("Range") String name = mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns.DISPLAY_NAME)); @SuppressLint("Range") Long size = Long.valueOf(mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns.SIZE))); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "Mimetype: " + mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns.MIME_TYPE))); MimeType mimeType = MimeType.valueOf(mImageCursor.getString(mImageCursor.getColumnIndex(MediaStore.Images.ImageColumns.MIME_TYPE))); // file parameter only needed for media players which decide the // ability of playing a file by the file extension String uri = getUriString(contentDirectory, id, mimeType); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - - Photo photo = new Photo(ContentDirectoryIDs.IMAGE_BY_BUCKET_PREFIX.getId() + id, myId, name, "", "", resource); - URI albumArtUri = URI.create("http://" - + contentDirectory.getIpAddress() + ":" - + YaaccUpnpServerService.PORT + "/thumb/" + id); - photo.replaceFirstProperty(new UPNP.ALBUM_ART_URI( - albumArtUri)); + String albumArtUri = "http://" + contentDirectory.getIpAddress() + ":" + + YaaccUpnpServerService.PORT + "/thumb/" + id; + + Photo photo = createPhoto( + ContentDirectoryIDs.IMAGE_BY_BUCKET_PREFIX.getId() + id, + myId, + name, + "", + false, + mimeType, + uri, + size, + albumArtUri + ); result.add(photo); - Log.d(getClass().getName(), "Image: " + id + " Name: " + name + " uri: " + uri); + YaaccLogger.d(getClass().getName(), "Image: " + id + " Name: " + name + " uri: " + uri); currentCount++; } currentIndex++; @@ -144,7 +144,7 @@ public List browseItem(YaaccContentDirectory contentDirectory, String myId } } else { - Log.d(getClass().getName(), "System media store is empty."); + YaaccLogger.d(getClass().getName(), "System media store is empty."); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesByBucketNamesFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesByBucketNamesFolderBrowser.java index caeac43a..0891259c 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesByBucketNamesFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/ImagesByBucketNamesFolderBrowser.java @@ -22,7 +22,7 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; import org.fourthline.cling.support.model.SortCriterion; @@ -110,7 +110,7 @@ public List browseContainer(YaaccContentDirectory contentDirectory, S @SuppressLint("Range") String name = mediaCursor.getString(mediaCursor.getColumnIndex(MediaStore.Images.Media.BUCKET_DISPLAY_NAME)); StorageFolder imageFolder = new StorageFolder(ContentDirectoryIDs.IMAGES_BY_BUCKET_NAME_PREFIX.getId() + id, ContentDirectoryIDs.IMAGES_BY_BUCKET_NAMES_FOLDER.getId(), name, "yaacc", 0, 90700L); folderMap.put(id, imageFolder); - Log.d(getClass().getName(), "image by bucket names folder: " + id + " Name: " + name); + YaaccLogger.d(getClass().getName(), "image by bucket names folder: " + id + " Name: " + name); currentCount++; } currentIndex++; @@ -122,7 +122,7 @@ public List browseContainer(YaaccContentDirectory contentDirectory, S result.add(entry.getValue()); } } else { - Log.d(getClass().getName(), "System media store is empty."); + YaaccLogger.d(getClass().getName(), "System media store is empty."); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/LiveStreamFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/LiveStreamFolderBrowser.java new file mode 100644 index 00000000..284b4601 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/LiveStreamFolderBrowser.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2026 www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.contentdirectory; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Build; + +import androidx.preference.PreferenceManager; + +import org.fourthline.cling.support.model.DIDLObject; +import org.fourthline.cling.support.model.Res; +import org.fourthline.cling.support.model.SortCriterion; +import org.fourthline.cling.support.model.container.Container; +import org.fourthline.cling.support.model.container.StorageFolder; +import org.fourthline.cling.support.model.item.AudioItem; +import org.fourthline.cling.support.model.item.Item; +import org.fourthline.cling.support.model.item.VideoItem; +import org.seamless.util.MimeType; + +import java.util.ArrayList; +import java.util.List; + +import de.yaacc.R; + +/** + * Browser for live streams (system audio and screen cast). + * Only available on Android 10+. + * + * @author Tobias Schoene (tobexyz) + */ +public class LiveStreamFolderBrowser extends ContentBrowser { + + public LiveStreamFolderBrowser(Context context) { + super(context); + } + + @Override + public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, String myId, long firstResult, long maxResults, SortCriterion[] orderby) { + return new StorageFolder( + ContentDirectoryIDs.LIVE_STREAM_FOLDER.getId(), + ContentDirectoryIDs.ROOT.getId(), + getContext().getString(R.string.live_streams), + "yaacc", + getSize(contentDirectory, myId), + null); + } + + @Override + public Integer getSize(YaaccContentDirectory contentDirectory, String myId) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return 0; + } + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + int count = 0; + + if (preferences.getBoolean(getContext().getString(R.string.settings_local_server_serve_system_audio_chkbx), false)) { + count++; + } + if (preferences.getBoolean(getContext().getString(R.string.settings_local_server_serve_screen_cast_chkbx), false)) { + count++; + } + + return count; + } + + @Override + public List browseContainer(YaaccContentDirectory contentDirectory, String myId, long firstResult, long maxResults, SortCriterion[] orderby) { + return new ArrayList<>(); + } + + @Override + public List browseItem(YaaccContentDirectory contentDirectory, String myId, long firstResult, long maxResults, SortCriterion[] orderby) { + List result = new ArrayList<>(); + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return result; + } + + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + + // System audio stream + if (preferences.getBoolean(getContext().getString(R.string.settings_local_server_serve_system_audio_chkbx), false)) { + String streamUrl = "http://" + contentDirectory.getIpAddress() + ":" + + de.yaacc.upnp.server.YaaccUpnpServerService.PORT + "/live/audio"; + + de.yaacc.util.YaaccLogger.i(getClass().getName(), "Creating live audio item with URL: " + streamUrl); + + MimeType mimeType = MimeType.valueOf("audio/wav"); + + Item audioItem = createItem( + ContentDirectoryIDs.LIVE_STREAM_SYSTEM_AUDIO.getId(), + ContentDirectoryIDs.LIVE_STREAM_FOLDER.getId(), + preferences.getString(getContext().getString(R.string.settings_local_server_name), "YAACC") + " " + getContext().getString(R.string.system_audio_stream), + "yaacc", + false, + mimeType, + streamUrl, + null, + null + ); + + // Add custom audio properties + if (audioItem.getResources().size() > 0) { + Res res = audioItem.getResources().get(0); + res.setSampleFrequency(44100L); + res.setNrAudioChannels(2L); + res.setBitsPerSample(16L); + } + + de.yaacc.util.YaaccLogger.i(getClass().getName(), "AudioItem created with URL: " + streamUrl); + + result.add(audioItem); + } + + // Screen cast stream + if (preferences.getBoolean(getContext().getString(R.string.settings_local_server_serve_screen_cast_chkbx), false)) { + String streamUrl = "http://" + contentDirectory.getIpAddress() + ":" + + de.yaacc.upnp.server.YaaccUpnpServerService.PORT + "/live/video"; + + de.yaacc.util.YaaccLogger.i(getClass().getName(), "Creating live video item with URL: " + streamUrl); + + MimeType mimeType = MimeType.valueOf("video/mpeg"); + + Item videoItem = createItem( + ContentDirectoryIDs.LIVE_STREAM_SCREEN_CAST.getId(), + ContentDirectoryIDs.LIVE_STREAM_FOLDER.getId(), + preferences.getString(getContext().getString(R.string.settings_local_server_name), "YAACC") + " " + getContext().getString(R.string.screen_cast_stream), + "yaacc", + false, + mimeType, + streamUrl, + null, + null + ); + + // Add custom video properties + if (videoItem.getResources().size() > 0) { + Res res = videoItem.getResources().get(0); + res.setResolution("1280x720"); + } + + de.yaacc.util.YaaccLogger.i(getClass().getName(), "VideoItem created with URL: " + streamUrl); + + result.add(videoItem); + } + //FIXME experimental not stable working + // Combined video+audio stream (MPEG-TS) + /* + if (preferences.getBoolean(getContext().getString(R.string.settings_local_server_serve_system_audio_chkbx), false) && + preferences.getBoolean(getContext().getString(R.string.settings_local_server_serve_screen_cast_chkbx), false)) { + + String streamUrl = "http://" + contentDirectory.getIpAddress() + ":" + + de.yaacc.upnp.server.YaaccUpnpServerService.PORT + "/live/videoaudio"; + + org.fourthline.cling.support.model.ProtocolInfo protocolInfo = + new org.fourthline.cling.support.model.ProtocolInfo( + org.fourthline.cling.support.model.Protocol.HTTP_GET, + org.fourthline.cling.support.model.ProtocolInfo.WILDCARD, + "video/mp2t", + "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01500000000000000000000000000000"); + + Res res = new Res(protocolInfo, null, streamUrl); + res.setResolution("1280x720"); + + VideoItem combinedItem = new VideoItem( + ContentDirectoryIDs.LIVE_STREAM_COMBINED.getId(), + ContentDirectoryIDs.LIVE_STREAM_FOLDER.getId(), + preferences.getString(getContext().getString(R.string.settings_local_server_name), "YAACC") + " Video+Audio Stream", + "yaacc", + res); + + result.add(combinedItem); + } + */ + return result; + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MediaPathFilter.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MediaPathFilter.java index e0d75e13..9a30853a 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MediaPathFilter.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MediaPathFilter.java @@ -1,6 +1,7 @@ package de.yaacc.upnp.server.contentdirectory; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; import androidx.preference.PreferenceManager; @@ -61,4 +62,54 @@ public static void resetMediaPaths(Context context) { editor.putStringSet(context.getString(R.string.settings_media_paths_pref_key), new HashSet<>()); editor.apply(); } + + public static Set getSafPathes(Context context) { + Set paths = PreferenceManager.getDefaultSharedPreferences(context).getStringSet(context.getString(R.string.settings_saf_tree_uris_pref_key), new HashSet<>()); + if (paths == null) { + return new HashSet<>(); + } + return new HashSet<>(paths); + } + + public static void saveSafPathes(Context context, Set newPathes) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putStringSet(context.getString(R.string.settings_saf_tree_uris_pref_key), newPathes); + editor.apply(); + + // Notify service to restart SAF preloading + Intent intent = new Intent("de.yaacc.SAF_PATHS_CHANGED"); + context.sendBroadcast(intent); + } + + + public static void resetSafPathes(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putStringSet(context.getString(R.string.settings_saf_tree_uris_pref_key), new HashSet<>()); + editor.apply(); + } + + public static Set getSelectedSafPathes(Context context) { + Set paths = PreferenceManager.getDefaultSharedPreferences(context).getStringSet(context.getString(R.string.settings_saf_tree_uris_selected_pref_key), new HashSet<>()); + if (paths == null) { + return new HashSet<>(); + } + return new HashSet<>(paths); + } + + public static void saveSelectedSafPathes(Context context, Set newPaths) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putStringSet(context.getString(R.string.settings_saf_tree_uris_selected_pref_key), newPaths); + editor.apply(); + } + + + public static void resetSelectedSafPathes(Context context) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + editor.putStringSet(context.getString(R.string.settings_saf_tree_uris_selected_pref_key), new HashSet<>()); + editor.apply(); + } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumFolderBrowser.java index 58462684..e1ee79e6 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumFolderBrowser.java @@ -22,14 +22,9 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.DIDLObject.Property.UPNP; -import org.fourthline.cling.support.model.PersonWithRole; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.MusicAlbum; @@ -181,7 +176,17 @@ public List browseItem(YaaccContentDirectory contentDirectory, @SuppressLint("Range") String duration = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.DURATION)); duration = contentDirectory.formatDuration(duration); - Log.d(getClass().getName(), + Integer trackNumber = null; + Integer year = null; + int trackIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.TRACK); + if (trackIdx >= 0) { + trackNumber = mediaCursor.getInt(trackIdx); + } + int yearIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.YEAR); + if (yearIdx >= 0) { + year = mediaCursor.getInt(yearIdx); + } + YaaccLogger.d(getClass().getName(), "Mimetype: " + mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.MIME_TYPE))); @@ -195,30 +200,56 @@ public List browseItem(YaaccContentDirectory contentDirectory, URI albumArtUri = URI.create("http://" + contentDirectory.getIpAddress() + ":" + YaaccUpnpServerService.PORT + "/album/" + albumId); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - resource.setDuration(duration); - - MusicTrack musicTrack = new MusicTrack( - ContentDirectoryIDs.MUSIC_ALBUM_ITEM_PREFIX.getId() - + id, - ContentDirectoryIDs.MUSIC_ALBUM_PREFIX.getId() - + albumId, title, "", - album, artist, resource); - musicTrack.replaceFirstProperty(new UPNP.ALBUM_ART_URI( - albumArtUri)); + + MusicTrack musicTrack; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { @SuppressLint("Range") String genre = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.GENRE)); - @SuppressLint("Range") String bitrate = mediaCursor.getString(mediaCursor + @SuppressLint("Range") String bitrateStr = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.BITRATE)); - resource.setBitrate(Long.valueOf(bitrate)); - musicTrack.setGenres(new String[]{genre}); + Long bitrate = bitrateStr != null ? Long.valueOf(bitrateStr) : null; + + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ALBUM_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ALBUM_PREFIX.getId() + albumId, + title, + "", + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + new String[]{genre}, + albumArtUri.toString(), + bitrate + ); + } else { + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ALBUM_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ALBUM_PREFIX.getId() + albumId, + title, + "", + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + null, + albumArtUri.toString() + ); } - musicTrack.setArtists(new PersonWithRole[]{new PersonWithRole(artist, "AlbumArtist")}); result.add(musicTrack); - Log.d(getClass().getName(), "MusicTrack: " + id + " Name: " + YaaccLogger.d(getClass().getName(), "MusicTrack: " + id + " Name: " + name + " uri: " + uri); currentCount++; } @@ -227,7 +258,7 @@ public List browseItem(YaaccContentDirectory contentDirectory, } } else { - Log.d(getClass().getName(), "System media store is empty."); + YaaccLogger.d(getClass().getName(), "System media store is empty."); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumItemBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumItemBrowser.java index 33dee8eb..91119720 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumItemBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumItemBrowser.java @@ -22,14 +22,9 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.DIDLObject.Property.UPNP; -import org.fourthline.cling.support.model.PersonWithRole; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.item.Item; @@ -115,7 +110,17 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, @SuppressLint("Range") String duration = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.DURATION)); duration = contentDirectory.formatDuration(duration); - Log.d(getClass().getName(), + Integer trackNumber = null; + Integer year = null; + int trackIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.TRACK); + if (trackIdx >= 0) { + trackNumber = mediaCursor.getInt(trackIdx); + } + int yearIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.YEAR); + if (yearIdx >= 0) { + year = mediaCursor.getInt(yearIdx); + } + YaaccLogger.d(getClass().getName(), "Mimetype: " + mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.MIME_TYPE))); @@ -130,32 +135,61 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, URI albumArtUri = URI.create("http://" + contentDirectory.getIpAddress() + ":" + YaaccUpnpServerService.PORT + "/album/" + albumId); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - resource.setDuration(duration); - - MusicTrack musicTrack = new MusicTrack( - ContentDirectoryIDs.MUSIC_ALBUM_ITEM_PREFIX.getId() + id, - ContentDirectoryIDs.MUSIC_ALBUM_PREFIX.getId() + albumId, - title + "-(" + name + ")", "", album, artist, resource); - musicTrack - .replaceFirstProperty(new UPNP.ALBUM_ART_URI(albumArtUri)); - musicTrack.setArtists(new PersonWithRole[]{new PersonWithRole(artist, "AlbumArtist")}); + + MusicTrack musicTrack; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { @SuppressLint("Range") String genre = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.GENRE)); - @SuppressLint("Range") String bitrate = mediaCursor.getString(mediaCursor + @SuppressLint("Range") String bitrateStr = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.BITRATE)); - resource.setBitrate(Long.valueOf(bitrate)); - musicTrack.setGenres(new String[]{genre}); + Long bitrate = bitrateStr != null ? Long.valueOf(bitrateStr) : null; + + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ALBUM_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ALBUM_PREFIX.getId() + albumId, + title + "-(" + name + ")", + artist, + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + new String[]{genre}, + albumArtUri.toString(), + bitrate + ); + } else { + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ALBUM_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ALBUM_PREFIX.getId() + albumId, + title + "-(" + name + ")", + artist, + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + null, + albumArtUri.toString() + ); } + result = musicTrack; - Log.d(getClass().getName(), "MusicTrack: " + id + " Name: " + name + YaaccLogger.d(getClass().getName(), "MusicTrack: " + id + " Name: " + name + " uri: " + uri); } else { - Log.d(getClass().getName(), "Item " + myId + " not found."); + YaaccLogger.d(getClass().getName(), "Item " + myId + " not found."); } } return result; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumsFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumsFolderBrowser.java index 32f33ca3..0a42d1d0 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumsFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAlbumsFolderBrowser.java @@ -22,7 +22,7 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; import org.fourthline.cling.support.model.SortCriterion; @@ -124,12 +124,12 @@ public List browseContainer(YaaccContentDirectory contentDirectory, S int tracks = getMusicTrackSize(contentDirectory, entry.getKey()); entry.getValue().setChildCount(tracks); if (tracks > 0) { - Log.d(getClass().getName(), "Album Folder: " + entry.getValue().getId() + " Name: " + entry.getValue().getTitle()); + YaaccLogger.d(getClass().getName(), "Album Folder: " + entry.getValue().getId() + " Name: " + entry.getValue().getTitle()); result.add(entry.getValue()); } } } else { - Log.d(getClass().getName(), "System media store is empty."); + YaaccLogger.d(getClass().getName(), "System media store is empty."); } } int start = firstResult > 0 ? (int) firstResult : 0; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAllTitleItemBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAllTitleItemBrowser.java index 7f49c609..bd8ba901 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAllTitleItemBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAllTitleItemBrowser.java @@ -22,14 +22,9 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.DIDLObject.Property.UPNP; -import org.fourthline.cling.support.model.PersonWithRole; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.item.Item; @@ -111,7 +106,17 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, @SuppressLint("Range") String duration = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.DURATION)); duration = contentDirectory.formatDuration(duration); - Log.d(getClass().getName(), + Integer trackNumber = null; + Integer year = null; + int trackIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.TRACK); + if (trackIdx >= 0) { + trackNumber = mediaCursor.getInt(trackIdx); + } + int yearIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.YEAR); + if (yearIdx >= 0) { + year = mediaCursor.getInt(yearIdx); + } + YaaccLogger.d(getClass().getName(), "Mimetype: " + mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.MIME_TYPE))); @@ -125,32 +130,59 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, URI albumArtUri = URI.create("http://" + contentDirectory.getIpAddress() + ":" + YaaccUpnpServerService.PORT + "/album/" + albumId); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - resource.setDuration(duration); - MusicTrack musicTrack = new MusicTrack( - ContentDirectoryIDs.MUSIC_ALL_TITLES_ITEM_PREFIX.getId() - + id, - ContentDirectoryIDs.MUSIC_ALL_TITLES_FOLDER.getId(), title - + "-(" + name + ")", "", album, artist, resource); - musicTrack - .replaceFirstProperty(new UPNP.ALBUM_ART_URI(albumArtUri)); - musicTrack.setArtists(new PersonWithRole[]{new PersonWithRole(artist, "AlbumArtist")}); + + MusicTrack musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ALL_TITLES_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ALL_TITLES_FOLDER.getId(), + title + "-(" + name + ")", + "", + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + null, + albumArtUri.toString() + ); + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { @SuppressLint("Range") String genre = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.GENRE)); - @SuppressLint("Range") String bitrate = mediaCursor.getString(mediaCursor + @SuppressLint("Range") String bitrateStr = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.BITRATE)); - resource.setBitrate(Long.valueOf(bitrate)); - musicTrack.setGenres(new String[]{genre}); + Long bitrate = bitrateStr != null ? Long.valueOf(bitrateStr) : null; + // Recreate with genre and bitrate + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ALL_TITLES_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ALL_TITLES_FOLDER.getId(), + title + "-(" + name + ")", + "", + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + new String[]{genre}, + albumArtUri.toString(), + bitrate + ); } result = musicTrack; - Log.d(getClass().getName(), "MusicTrack: " + id + " Name: " + name - + " uri: " + uri); + YaaccLogger.d(getClass().getName(), "MusicTrack: " + id + " Name: " + name + + " uri: " + uri + " trackNumber: " + trackNumber + " year: " + year + + " artist: " + artist + " album: " + album); } else { - Log.d(getClass().getName(), "Item " + myId + " not found."); + YaaccLogger.d(getClass().getName(), "Item " + myId + " not found."); } } return result; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAllTitlesFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAllTitlesFolderBrowser.java index 3b6a0e2c..00a5ee1e 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAllTitlesFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicAllTitlesFolderBrowser.java @@ -22,14 +22,8 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.DIDLObject.Property.UPNP; -import org.fourthline.cling.support.model.PersonWithRole; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.MusicAlbum; @@ -42,6 +36,7 @@ import de.yaacc.R; import de.yaacc.upnp.server.YaaccUpnpServerService; +import de.yaacc.util.YaaccLogger; /** * Browser for the music all titles folder. @@ -74,7 +69,7 @@ public Integer getSize(YaaccContentDirectory contentDirectory, String myId) { .getContentResolver() .query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, projection, selection, selectionArgs, null)) { - Log.d(getClass().getName(), "browseFlag Folder size: " + cursor.getCount()); + YaaccLogger.d(getClass().getName(), "browseFlag Folder size: " + cursor.getCount()); return cursor.getCount(); } } @@ -130,7 +125,7 @@ public List browseItem(YaaccContentDirectory contentDirectory, int currentCount = 0; while (!mediaCursor.isAfterLast() && currentCount < maxResults) { if (firstResult <= currentIndex) { - Log.d(getClass().getName(), "browse firstResult: " + firstResult + " currentIndex:" + currentIndex + " currentCount: " + currentCount); + YaaccLogger.d(getClass().getName(), "browse firstResult: " + firstResult + " currentIndex:" + currentIndex + " currentCount: " + currentCount); @SuppressLint("Range") String id = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media._ID)); @SuppressLint("Range") String name = mediaCursor.getString(mediaCursor @@ -149,7 +144,17 @@ public List browseItem(YaaccContentDirectory contentDirectory, @SuppressLint("Range") String duration = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.DURATION)); duration = contentDirectory.formatDuration(duration); - Log.d(getClass().getName(), + Integer trackNumber = null; + Integer year = null; + int trackIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.TRACK); + if (trackIdx >= 0) { + trackNumber = mediaCursor.getInt(trackIdx); + } + int yearIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.YEAR); + if (yearIdx >= 0) { + year = mediaCursor.getInt(yearIdx); + } + YaaccLogger.d(getClass().getName(), "Mimetype: " + mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.MIME_TYPE))); @@ -164,27 +169,58 @@ public List browseItem(YaaccContentDirectory contentDirectory, URI albumArtUri = URI.create("http://" + contentDirectory.getIpAddress() + ":" + YaaccUpnpServerService.PORT + "/album/" + albumId); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - resource.setDuration(duration); - MusicTrack musicTrack = new MusicTrack( - ContentDirectoryIDs.MUSIC_ALL_TITLES_ITEM_PREFIX.getId() - + id, ContentDirectoryIDs.MUSIC_ALL_TITLES_FOLDER.getId(), - title + "-(" + name + ")", "", album, artist, resource); - musicTrack.replaceFirstProperty(new UPNP.ALBUM_ART_URI( - albumArtUri)); - musicTrack.setArtists(new PersonWithRole[]{new PersonWithRole(artist, "AlbumArtist")}); + + MusicTrack musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ALL_TITLES_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ALL_TITLES_FOLDER.getId(), + title + "-(" + name + ")", + artist, + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + null, // genres - only on Android 11+ + albumArtUri.toString() + ); + + // On Android 11+, add genre and bitrate if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { @SuppressLint("Range") String genre = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.GENRE)); - @SuppressLint("Range") String bitrate = mediaCursor.getString(mediaCursor + @SuppressLint("Range") String bitrateStr = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.BITRATE)); - resource.setBitrate(Long.valueOf(bitrate)); - musicTrack.setGenres(new String[]{genre}); + Long bitrate = bitrateStr != null ? Long.valueOf(bitrateStr) : null; + + // Recreate with genre and bitrate + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ALL_TITLES_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ALL_TITLES_FOLDER.getId(), + title + "-(" + name + ")", + artist, + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + new String[]{genre}, + albumArtUri.toString(), + bitrate + ); } + result.add(musicTrack); - Log.d(getClass().getName(), "MusicTrack: " + id + " Name: " - + name + " uri: " + uri); + YaaccLogger.d(getClass().getName(), "MusicTrack: " + id + " Name: " + + name + " uri: " + uri + " trackNumber: " + trackNumber + " year: " + year + + " artist: " + artist + " album: " + album); currentCount++; } currentIndex++; @@ -193,10 +229,10 @@ public List browseItem(YaaccContentDirectory contentDirectory, } else { - Log.d(getClass().getName(), "System media store is empty."); + YaaccLogger.d(getClass().getName(), "System media store is empty."); } } - Log.d(getClass().getName(), "browseFlag result size: " + result.size()); + YaaccLogger.d(getClass().getName(), "browseFlag result size: " + result.size()); return result; } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistFolderBrowser.java index 17a686ec..5dac7e09 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistFolderBrowser.java @@ -22,14 +22,9 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.DIDLObject.Property.UPNP; -import org.fourthline.cling.support.model.PersonWithRole; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.MusicAlbum; @@ -182,7 +177,17 @@ public List browseItem(YaaccContentDirectory contentDirectory, @SuppressLint("Range") String duration = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.DURATION)); duration = contentDirectory.formatDuration(duration); - Log.d(getClass().getName(), + Integer trackNumber = null; + Integer year = null; + int trackIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.TRACK); + if (trackIdx >= 0) { + trackNumber = mediaCursor.getInt(trackIdx); + } + int yearIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.YEAR); + if (yearIdx >= 0) { + year = mediaCursor.getInt(yearIdx); + } + YaaccLogger.d(getClass().getName(), "Mimetype: " + mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.MIME_TYPE))); @@ -197,28 +202,56 @@ public List browseItem(YaaccContentDirectory contentDirectory, URI albumArtUri = URI.create("http://" + contentDirectory.getIpAddress() + ":" + YaaccUpnpServerService.PORT + "/album/" + albumId); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - resource.setDuration(duration); - MusicTrack musicTrack = new MusicTrack( - ContentDirectoryIDs.MUSIC_ARTIST_ITEM_PREFIX.getId() - + id, - ContentDirectoryIDs.MUSIC_ARTIST_PREFIX.getId() - + artistId, title + "-(" + name + ")", "", - album, artist, resource); - musicTrack - .replaceFirstProperty(new UPNP.ALBUM_ART_URI(albumArtUri)); - musicTrack.setArtists(new PersonWithRole[]{new PersonWithRole(artist, "AlbumArtist")}); + + MusicTrack musicTrack; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { @SuppressLint("Range") String genre = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.GENRE)); - @SuppressLint("Range") String bitrate = mediaCursor.getString(mediaCursor + @SuppressLint("Range") String bitrateStr = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.BITRATE)); - resource.setBitrate(Long.valueOf(bitrate)); - musicTrack.setGenres(new String[]{genre}); + Long bitrate = bitrateStr != null ? Long.valueOf(bitrateStr) : null; + + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ARTIST_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ARTIST_PREFIX.getId() + artistId, + title + "-(" + name + ")", + "", + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + new String[]{genre}, + albumArtUri.toString(), + bitrate + ); + } else { + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ARTIST_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ARTIST_PREFIX.getId() + artistId, + title + "-(" + name + ")", + "", + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + null, + albumArtUri.toString() + ); } + result.add(musicTrack); - Log.d(getClass().getName(), "MusicTrack: " + id + " Name: " + YaaccLogger.d(getClass().getName(), "MusicTrack: " + id + " Name: " + name + " uri: " + uri); currentCount++; } @@ -227,7 +260,7 @@ public List browseItem(YaaccContentDirectory contentDirectory, } } else { - Log.d(getClass().getName(), "System media store is empty."); + YaaccLogger.d(getClass().getName(), "System media store is empty."); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistItemBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistItemBrowser.java index 7faaf719..e0a44ce9 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistItemBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistItemBrowser.java @@ -22,14 +22,9 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.DIDLObject.Property.UPNP; -import org.fourthline.cling.support.model.PersonWithRole; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.item.Item; @@ -120,10 +115,20 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, @SuppressLint("Range") String duration = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.DURATION)); duration = contentDirectory.formatDuration(duration); + Integer trackNumber = null; + Integer year = null; + int trackIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.TRACK); + if (trackIdx >= 0) { + trackNumber = mediaCursor.getInt(trackIdx); + } + int yearIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.YEAR); + if (yearIdx >= 0) { + year = mediaCursor.getInt(yearIdx); + } @SuppressLint("Range") String mimeTypeString = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.MIME_TYPE)); - Log.d(getClass().getName(), "Mimetype: " + mimeTypeString); + YaaccLogger.d(getClass().getName(), "Mimetype: " + mimeTypeString); @SuppressLint("Range") MimeType mimeType = MimeType.valueOf(mimeTypeString); // file parameter only needed for media players which decide // the ability of playing a file by the file extension @@ -131,30 +136,60 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, URI albumArtUri = URI.create("http://" + contentDirectory.getIpAddress() + ":" + YaaccUpnpServerService.PORT + "/album/" + albumId); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - resource.setDuration(duration); - MusicTrack musicTrack = new MusicTrack( - ContentDirectoryIDs.MUSIC_ARTIST_ITEM_PREFIX.getId() + id, - ContentDirectoryIDs.MUSIC_ARTIST_PREFIX.getId() + artistId, - title + "-(" + name + ")", "", album, artist, resource); - musicTrack - .replaceFirstProperty(new UPNP.ALBUM_ART_URI(albumArtUri)); - musicTrack.setArtists(new PersonWithRole[]{new PersonWithRole(artist, "AlbumArtist")}); + + MusicTrack musicTrack; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { @SuppressLint("Range") String genre = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.GENRE)); - @SuppressLint("Range") String bitrate = mediaCursor.getString(mediaCursor + @SuppressLint("Range") String bitrateStr = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.BITRATE)); - resource.setBitrate(Long.valueOf(bitrate)); - musicTrack.setGenres(new String[]{genre}); + Long bitrate = bitrateStr != null ? Long.valueOf(bitrateStr) : null; + + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ARTIST_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ARTIST_PREFIX.getId() + artistId, + title + "-(" + name + ")", + artist, + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + new String[]{genre}, + albumArtUri.toString(), + bitrate + ); + } else { + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_ARTIST_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_ARTIST_PREFIX.getId() + artistId, + title + "-(" + name + ")", + artist, + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + null, + albumArtUri.toString() + ); } + result = musicTrack; - Log.d(getClass().getName(), "MusicTrack: " + id + " Name: " + name + YaaccLogger.d(getClass().getName(), "MusicTrack: " + id + " Name: " + name + " uri: " + uri); } else { - Log.d(getClass().getName(), "Item " + myId + " not found."); + YaaccLogger.d(getClass().getName(), "Item " + myId + " not found."); } } return result; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistsFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistsFolderBrowser.java index 696c69c2..aeee070d 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistsFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicArtistsFolderBrowser.java @@ -22,7 +22,7 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; import org.fourthline.cling.support.model.SortCriterion; @@ -125,12 +125,12 @@ public List browseContainer(YaaccContentDirectory contentDirectory, S int tracks = getMusicTrackSize(contentDirectory, entry.getKey()); entry.getValue().setChildCount(tracks); if (tracks > 0) { - Log.d(getClass().getName(), "Artists Folder: " + entry.getValue().getId() + " Name: " + entry.getValue().getTitle()); + YaaccLogger.d(getClass().getName(), "Artists Folder: " + entry.getValue().getId() + " Name: " + entry.getValue().getTitle()); result.add(entry.getValue()); } } } else { - Log.d(getClass().getName(), "System media store is empty."); + YaaccLogger.d(getClass().getName(), "System media store is empty."); } } int start = firstResult > 0 ? (int) firstResult : 0; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenreFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenreFolderBrowser.java index 1610e469..def726bf 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenreFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenreFolderBrowser.java @@ -22,13 +22,9 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.PersonWithRole; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.MusicAlbum; @@ -253,9 +249,19 @@ public List browseItem(YaaccContentDirectory contentDirectory, .getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.DURATION)); duration = contentDirectory.formatDuration(duration); + Integer trackNumber = null; + Integer year = null; + int trackIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.TRACK); + if (trackIdx >= 0) { + trackNumber = mediaCursor.getInt(trackIdx); + } + int yearIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.YEAR); + if (yearIdx >= 0) { + year = mediaCursor.getInt(yearIdx); + } @SuppressLint("Range") String mimeTypeString = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.MIME_TYPE)); - Log.d(getClass().getName(), + YaaccLogger.d(getClass().getName(), "Mimetype: " + mimeTypeString); MimeType mimeType = MimeType @@ -268,30 +274,56 @@ public List browseItem(YaaccContentDirectory contentDirectory, + contentDirectory.getIpAddress() + ":" + YaaccUpnpServerService.PORT + "/album/" + albumId); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - - resource.setDuration(duration); - MusicTrack musicTrack = new MusicTrack( - ContentDirectoryIDs.MUSIC_GENRE_ITEM_PREFIX.getId() - + id, - ContentDirectoryIDs.MUSIC_GENRE_PREFIX.getId() - + genreId, title + "-(" + name + ")", "", - album, artist, resource); - musicTrack.replaceFirstProperty(new DIDLObject.Property.UPNP.ALBUM_ART_URI( - albumArtUri)); - musicTrack.setArtists(new PersonWithRole[]{new PersonWithRole(artist, "AlbumArtist")}); + MusicTrack musicTrack; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { @SuppressLint("Range") String genre = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.GENRE)); - @SuppressLint("Range") String bitrate = mediaCursor.getString(mediaCursor + @SuppressLint("Range") String bitrateStr = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.BITRATE)); - resource.setBitrate(Long.valueOf(bitrate)); - musicTrack.setGenres(new String[]{genre}); + Long bitrate = bitrateStr != null ? Long.valueOf(bitrateStr) : null; + + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_GENRE_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_GENRE_PREFIX.getId() + genreId, + title + "-(" + name + ")", + "", + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + new String[]{genre}, + albumArtUri.toString(), + bitrate + ); + } else { + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_GENRE_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_GENRE_PREFIX.getId() + genreId, + title + "-(" + name + ")", + "", + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + null, + albumArtUri.toString() + ); } + result.add(musicTrack); - Log.d(getClass().getName(), "MusicTrack: " + id + " Name: " + YaaccLogger.d(getClass().getName(), "MusicTrack: " + id + " Name: " + name + " uri: " + uri); currentCount++; } @@ -300,7 +332,7 @@ public List browseItem(YaaccContentDirectory contentDirectory, } } else { - Log.d(getClass().getName(), "System media store is empty."); + YaaccLogger.d(getClass().getName(), "System media store is empty."); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenreItemBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenreItemBrowser.java index d8b8ffd1..047bcbf6 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenreItemBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenreItemBrowser.java @@ -22,14 +22,9 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.DIDLObject.Property.UPNP; -import org.fourthline.cling.support.model.PersonWithRole; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.item.Item; @@ -140,13 +135,23 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, .getColumnIndex(MediaStore.Audio.Media.ARTIST)); @SuppressLint("Range") String duration = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.DURATION)); + Integer trackNumber = null; + Integer year = null; + int trackIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.TRACK); + if (trackIdx >= 0) { + trackNumber = mediaCursor.getInt(trackIdx); + } + int yearIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.YEAR); + if (yearIdx >= 0) { + year = mediaCursor.getInt(yearIdx); + } if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { @SuppressLint("Range") int genreIdIdx = mediaCursor.getColumnIndex(MediaStore.Audio.Media.GENRE_ID); genreId = mediaCursor.getString(genreIdIdx); } @SuppressLint("Range") String mimeTypeString = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Genres.Members.MIME_TYPE)); - Log.d(getClass().getName(), "Mimetype: " + mimeTypeString); + YaaccLogger.d(getClass().getName(), "Mimetype: " + mimeTypeString); MimeType mimeType = MimeType.valueOf(mimeTypeString); // file parameter only needed for media players which decide // the ability of playing a file by the file extension @@ -154,29 +159,60 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, URI albumArtUri = URI.create("http://" + contentDirectory.getIpAddress() + ":" + YaaccUpnpServerService.PORT + "/album/" + albumId); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - resource.setDuration(duration); - MusicTrack musicTrack = new MusicTrack( - ContentDirectoryIDs.MUSIC_GENRE_ITEM_PREFIX.getId() + id, - ContentDirectoryIDs.MUSIC_GENRE_PREFIX.getId() + genreId, - title + "-(" + name + ")", "", album, artist, resource); - musicTrack.replaceFirstProperty(new UPNP.ALBUM_ART_URI(albumArtUri)); - musicTrack.setArtists(new PersonWithRole[]{new PersonWithRole(artist, "AlbumArtist")}); + + MusicTrack musicTrack; + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) { @SuppressLint("Range") String genre = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.GENRE)); - @SuppressLint("Range") String bitrate = mediaCursor.getString(mediaCursor + @SuppressLint("Range") String bitrateStr = mediaCursor.getString(mediaCursor .getColumnIndex(MediaStore.Audio.Media.BITRATE)); - resource.setBitrate(Long.valueOf(bitrate)); - musicTrack.setGenres(new String[]{genre}); + Long bitrate = bitrateStr != null ? Long.valueOf(bitrateStr) : null; + + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_GENRE_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_GENRE_PREFIX.getId() + genreId, + title + "-(" + name + ")", + artist, + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + new String[]{genre}, + albumArtUri.toString(), + bitrate + ); + } else { + musicTrack = createMusicTrack( + ContentDirectoryIDs.MUSIC_GENRE_ITEM_PREFIX.getId() + id, + ContentDirectoryIDs.MUSIC_GENRE_PREFIX.getId() + genreId, + title + "-(" + name + ")", + artist, + false, + mimeType, + uri, + size, + duration, + album, + artist, + trackNumber, + year != null && year > 0 ? year + "-01-01" : null, + null, + albumArtUri.toString() + ); } + result = musicTrack; - Log.d(getClass().getName(), "MusicTrack: " + id + " Name: " + name + YaaccLogger.d(getClass().getName(), "MusicTrack: " + id + " Name: " + name + " uri: " + uri); } else { - Log.d(getClass().getName(), "Item " + myId + " not found."); + YaaccLogger.d(getClass().getName(), "Item " + myId + " not found."); } } return result; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenresFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenresFolderBrowser.java index d6e10309..e0cace85 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenresFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/MusicGenresFolderBrowser.java @@ -22,7 +22,7 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; import org.fourthline.cling.support.model.SortCriterion; @@ -143,12 +143,12 @@ public List browseContainer(YaaccContentDirectory contentDirectory, S int tracks = getMusicTrackSize(contentDirectory, entry.getKey()); entry.getValue().setChildCount(tracks); if (tracks > 0) { - Log.d(getClass().getName(), "Genre Folder: " + entry.getValue().getId() + " Name: " + entry.getValue().getTitle()); + YaaccLogger.d(getClass().getName(), "Genre Folder: " + entry.getValue().getId() + " Name: " + entry.getValue().getTitle()); result.add(entry.getValue()); } } } else { - Log.d(getClass().getName(), "System media store is empty."); + YaaccLogger.d(getClass().getName(), "System media store is empty."); } } int start = firstResult > 0 ? (int) firstResult : 0; diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/RootFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/RootFolderBrowser.java index a7560d41..13f9c264 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/RootFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/RootFolderBrowser.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.SharedPreferences; +import android.os.Build; import androidx.preference.PreferenceManager; @@ -64,6 +65,12 @@ public Integer getSize(YaaccContentDirectory contentDirectory, String myId) { if (isServingVideos()) { result++; } + if (isServingSaf()) { + result++; + } + if (isServingLiveStreams()) { + result++; + } return result; } @@ -81,6 +88,12 @@ public List browseContainer(YaaccContentDirectory contentDirectory, S if (isServingVideos()) { result.add((Container) new VideosFolderBrowser(getContext()).browseMeta(contentDirectory, ContentDirectoryIDs.VIDEOS_FOLDER.getId(), 0, 1, orderby)); } + if (isServingSaf()) { + result.add((Container) new SafFolderBrowser(getContext()).browseMeta(contentDirectory, ContentDirectoryIDs.SAF_FOLDER.getId(), 0, 1, orderby)); + } + if (isServingLiveStreams()) { + result.add((Container) new LiveStreamFolderBrowser(getContext()).browseMeta(contentDirectory, ContentDirectoryIDs.LIVE_STREAM_FOLDER.getId(), 0, 1, orderby)); + } int start = firstResult > 0 ? (int) firstResult : 0; if (firstResult >= (result.size() - 1)) { start = result.size() - 1; @@ -125,4 +138,27 @@ private boolean isServingMusic() { R.string.settings_local_server_serve_music_chkbx), false); } + + private boolean isServingSaf() { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return preferences.getBoolean( + getContext().getString( + R.string.settings_local_server_serve_saf_chkbx), + false); + } + + private boolean isServingLiveStreams() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return false; + } + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + return preferences.getBoolean( + getContext().getString( + R.string.settings_local_server_serve_system_audio_chkbx), + false) || + preferences.getBoolean( + getContext().getString( + R.string.settings_local_server_serve_screen_cast_chkbx), + false); + } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/SafFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/SafFolderBrowser.java new file mode 100644 index 00000000..4f18ec3a --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/SafFolderBrowser.java @@ -0,0 +1,384 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.contentdirectory; + +import android.content.Context; +import android.content.SharedPreferences; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.util.Base64; + +import androidx.documentfile.provider.DocumentFile; +import androidx.preference.PreferenceManager; + +import org.fourthline.cling.support.model.DIDLObject; +import org.fourthline.cling.support.model.Protocol; +import org.fourthline.cling.support.model.ProtocolInfo; +import org.fourthline.cling.support.model.Res; +import org.fourthline.cling.support.model.SortCriterion; +import org.fourthline.cling.support.model.container.Container; +import org.fourthline.cling.support.model.container.StorageFolder; +import org.fourthline.cling.support.model.item.AudioItem; +import org.fourthline.cling.support.model.item.ImageItem; +import org.fourthline.cling.support.model.item.Item; +import org.fourthline.cling.support.model.item.VideoItem; +import org.seamless.util.MimeType; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import de.yaacc.R; +import de.yaacc.upnp.model.YaaccItem; +import de.yaacc.upnp.model.YaaccRes; +import de.yaacc.util.SAFCacheManager; +import de.yaacc.util.SAFMetadata; +import de.yaacc.util.FormatHelper; +import de.yaacc.util.YaaccLogger; + +/** + * Browser for saf folder. + * + * @author tobexyz + */ +public class SafFolderBrowser extends ContentBrowser { + + public SafFolderBrowser(Context context) { + super(context); + } + + @Override + public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, String myId, long firstResult, long maxResults, SortCriterion[] orderby) { + if (myId.equals(ContentDirectoryIDs.SAF_FOLDER.getId())) { + return new StorageFolder(ContentDirectoryIDs.SAF_FOLDER.getId(), ContentDirectoryIDs.ROOT.getId(), getContext().getString(R.string.saf_content), "yaacc", getSize(contentDirectory, myId), + null); + } else { + // Meta for a subfolder + String pathEnc = myId.substring(ContentDirectoryIDs.SAF_PREFIX.getId().length()); + String path = new String(Base64.decode(pathEnc.getBytes(), Base64.NO_WRAP)); + DocumentFile file = DocumentFile.fromTreeUri(getContext(), Uri.parse(path)); + String title = (file != null && file.getName() != null) ? file.getName() : path; + + // Determine parent ID - if this is a direct child of SAF root, parent is SAF_FOLDER + // Otherwise, find the parent folder + String parentId = ContentDirectoryIDs.SAF_FOLDER.getId(); + DocumentFile parent = file != null ? file.getParentFile() : null; + if (parent != null && !getSelectedSafPathes().contains(parent.getUri().toString())) { + String parentBase64 = Base64.encodeToString(parent.getUri().toString().getBytes(), Base64.NO_WRAP); + parentId = ContentDirectoryIDs.SAF_PREFIX.getId() + parentBase64; + } + DIDLObject result = null; + if (file.isDirectory()) { + result = new StorageFolder(myId, parentId, title, "yaacc", getSize(contentDirectory, myId), null); + } else { + result = createItem(contentDirectory, file.getUri().toString(), file, myId, !file.canRead()); + } + return result; + } + } + + @Override + public Integer getSize(YaaccContentDirectory contentDirectory, String myId) { + if (myId.equals(ContentDirectoryIDs.SAF_FOLDER.getId())) { + return getSelectedSafPathes().size(); + } else { + String shortId = myId.substring(ContentDirectoryIDs.SAF_PREFIX.getId().length()); + String path = SAFCacheManager.getInstance(getContext()).getUriForShortId(shortId); + if (path != null) { + DocumentFile file = DocumentFile.fromTreeUri(getContext(), Uri.parse(path)); + if (file != null && file.isDirectory()) { + return file.listFiles().length; + } + } else { + YaaccLogger.w(getClass().getName(), "Short ID not found: " + shortId + " for id: " + myId); + } + } + return 0; + } + + @Override + public List browseContainer(YaaccContentDirectory contentDirectory, String myId, long firstResult, long maxResults, SortCriterion[] orderby) { + long browseStart = System.currentTimeMillis(); + YaaccLogger.d(getClass().getName(), "browseContainer START: myId=" + myId + ", firstResult=" + firstResult + ", maxResults=" + maxResults); + List result = new ArrayList<>(); + if (myId.equals(ContentDirectoryIDs.SAF_FOLDER.getId())) { + long rootStart = System.currentTimeMillis(); + YaaccLogger.d(getClass().getName(), "Browsing root SAF folder"); + Set safPaths = getSelectedSafPathes(); + YaaccLogger.d(getClass().getName(), "Found " + safPaths.size() + " SAF paths in preferences (took " + (System.currentTimeMillis() - rootStart) + "ms)"); + List sortedPathes = new ArrayList<>(safPaths); + Collections.sort(sortedPathes); + + int start = (int) Math.max(0, firstResult); + int end = (int) Math.min(sortedPathes.size(), start + maxResults); + YaaccLogger.d(getClass().getName(), "Pagination: start=" + start + ", end=" + end + ", total=" + sortedPathes.size()); + + for (int i = start; i < end; i++) { + long itemStart = System.currentTimeMillis(); + String path = sortedPathes.get(i); + DocumentFile file = DocumentFile.fromTreeUri(getContext(), Uri.parse(path)); + YaaccLogger.d(getClass().getName(), "Path[" + i + "] DocumentFile: " + (file != null ? "exists" : "null") + ", isDirectory: " + (file != null && file.isDirectory()) + " (took " + (System.currentTimeMillis() - itemStart) + "ms)"); + if (file != null && file.isDirectory()) { + String title = file.getName() != null ? file.getName() : path; + String shortId = SAFCacheManager.getInstance(getContext()).getOrCreateShortId(file.getUri().toString()); + String folderId = ContentDirectoryIDs.SAF_PREFIX.getId() + shortId; + StorageFolder folder = new StorageFolder(folderId, ContentDirectoryIDs.SAF_FOLDER.getId(), title, "yaacc", 0, null); + result.add(folder); + } + } + YaaccLogger.d(getClass().getName(), "Root browse complete: " + result.size() + " folders (total " + (System.currentTimeMillis() - browseStart) + "ms)"); + } else { + // Browse subfolder + long subfolderStart = System.currentTimeMillis(); + YaaccLogger.d(getClass().getName(), "Browsing subfolder with ID: " + myId); + String shortId = myId.substring(ContentDirectoryIDs.SAF_PREFIX.getId().length()); + String path = SAFCacheManager.getInstance(getContext()).getUriForShortId(shortId); + + if (path == null) { + YaaccLogger.e(getClass().getName(), "Short ID not found: " + shortId); + return result; + } + + YaaccLogger.d(getClass().getName(), "Resolved path from shortId " + shortId + ": " + path); + + Uri uri = Uri.parse(path); + DocumentFile root = null; + + // Check if this is a tree URI or document URI + if (path.contains("/tree/")) { + long treeStart = System.currentTimeMillis(); + root = DocumentFile.fromTreeUri(getContext(), uri); + YaaccLogger.d(getClass().getName(), "Tree URI resolved in " + (System.currentTimeMillis() - treeStart) + "ms"); + } else { + YaaccLogger.w(getClass().getName(), "Document URI detected, skipping: " + path); + return result; + } + + if (root != null && root.isDirectory()) { + long listStart = System.currentTimeMillis(); + DocumentFile[] files = root.listFiles(); + YaaccLogger.d(getClass().getName(), "listFiles() took " + (System.currentTimeMillis() - listStart) + "ms, found " + files.length + " items"); + + int start = (int) Math.max(0, firstResult); + int end = (int) Math.min(files.length, start + maxResults); + YaaccLogger.d(getClass().getName(), "Pagination: start=" + start + ", end=" + end + ", total=" + files.length); + + for (int i = start; i < end; i++) { + long itemStart = System.currentTimeMillis(); + DocumentFile file = files[i]; + if (file.isDirectory()) { + String title = file.getName() != null ? file.getName() : file.getUri().toString(); + try { + String authority = file.getUri().getAuthority(); + String documentId = DocumentsContract.getDocumentId(file.getUri()); + Uri childTreeUri = DocumentsContract.buildTreeDocumentUri(authority, documentId); + DocumentFile testAccess = DocumentFile.fromTreeUri(getContext(), childTreeUri); + if (testAccess != null) { + String childShortId = SAFCacheManager.getInstance(getContext()).getOrCreateShortId(childTreeUri.toString()); + String childId = ContentDirectoryIDs.SAF_PREFIX.getId() + childShortId; + if (!testAccess.canRead()) { + title = "[X] " + title; + } + StorageFolder folder = new StorageFolder(childId, myId, title, "yaacc", 0, null); + folder.setRestricted(testAccess.canRead()); + result.add(folder); + YaaccLogger.d(getClass().getName(), "Child[" + i + "] " + title + " (took " + (System.currentTimeMillis() - itemStart) + "ms)"); + } else { + YaaccLogger.w(getClass().getName(), "Cannot access child: " + title); + } + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Error processing child: " + title, e); + } + } + } + } else { + YaaccLogger.e(getClass().getName(), "Root DocumentFile is null or not a directory"); + } + YaaccLogger.d(getClass().getName(), "Subfolder browse complete: " + result.size() + " folders (total " + (System.currentTimeMillis() - subfolderStart) + "ms)"); + } + YaaccLogger.d(getClass().getName(), "browseContainer END: returning " + result.size() + " containers (total " + (System.currentTimeMillis() - browseStart) + "ms)"); + return result; + } + + @Override + public List browseItem(YaaccContentDirectory contentDirectory, String myId, long firstResult, long maxResults, SortCriterion[] orderby) { + long browseStart = System.currentTimeMillis(); + YaaccLogger.d(getClass().getName(), "browseItem START: myId=" + myId + ", firstResult=" + firstResult + ", maxResults=" + maxResults); + List result = new ArrayList<>(); + if (myId.equals(ContentDirectoryIDs.SAF_FOLDER.getId())) { + long rootStart = System.currentTimeMillis(); + List sortedPathes = new ArrayList<>(getSelectedSafPathes()); + Collections.sort(sortedPathes); + + int start = (int) Math.max(0, firstResult); + int end = (int) Math.min(sortedPathes.size(), start + maxResults); + YaaccLogger.d(getClass().getName(), "Root items: pagination start=" + start + ", end=" + end + ", total=" + sortedPathes.size()); + + for (int i = start; i < end; i++) { + long itemStart = System.currentTimeMillis(); + String path = sortedPathes.get(i); + DocumentFile file = DocumentFile.fromSingleUri(getContext(), Uri.parse(path)); + if (file != null && !file.isDirectory()) { + Item item = createItem(contentDirectory, path, file, myId, !file.canRead()); + if (item != null) result.add(item); + YaaccLogger.d(getClass().getName(), "Item[" + i + "] " + (file.getName() != null ? file.getName() : "unknown") + " (took " + (System.currentTimeMillis() - itemStart) + "ms)"); + } + } + YaaccLogger.d(getClass().getName(), "Root items complete: " + result.size() + " items (total " + (System.currentTimeMillis() - rootStart) + "ms)"); + } else { + // Browse subfolder items + long subfolderStart = System.currentTimeMillis(); + YaaccLogger.d(getClass().getName(), "Browsing subfolder items for: " + myId); + String shortId = myId.substring(ContentDirectoryIDs.SAF_PREFIX.getId().length()); + String path = SAFCacheManager.getInstance(getContext()).getUriForShortId(shortId); + + if (path == null) { + YaaccLogger.e(getClass().getName(), "Short ID not found: " + shortId); + return result; + } + + long treeStart = System.currentTimeMillis(); + DocumentFile root = DocumentFile.fromTreeUri(getContext(), Uri.parse(path)); + YaaccLogger.d(getClass().getName(), "Tree URI resolved in " + (System.currentTimeMillis() - treeStart) + "ms"); + + if (root != null && root.isDirectory()) { + if (root.canRead()) { + long listStart = System.currentTimeMillis(); + DocumentFile[] files = root.listFiles(); + YaaccLogger.d(getClass().getName(), "listFiles() took " + (System.currentTimeMillis() - listStart) + "ms, found " + files.length + " items"); + + int start = (int) Math.max(0, firstResult); + int end = (int) Math.min(files.length, start + maxResults); + YaaccLogger.d(getClass().getName(), "Pagination: start=" + start + ", end=" + end + ", total=" + files.length); + + for (int i = start; i < end; i++) { + long itemStart = System.currentTimeMillis(); + DocumentFile file = files[i]; + if (!file.isDirectory()) { + long createStart = System.currentTimeMillis(); + Item item = createItem(contentDirectory, file.getUri().toString(), file, myId, !file.canRead()); + long createTime = System.currentTimeMillis() - createStart; + if (item != null) result.add(item); + long totalTime = System.currentTimeMillis() - itemStart; + YaaccLogger.d(getClass().getName(), "Item[" + i + "] " + (file.getName() != null ? file.getName() : "unknown") + " - createItem=" + createTime + "ms, total=" + totalTime + "ms"); + } + } + } else { + YaaccLogger.w(getClass().getName(), "Cannot read folder: " + path); + } + } else { + YaaccLogger.e(getClass().getName(), "Root DocumentFile is null or not a directory"); + } + YaaccLogger.d(getClass().getName(), "Subfolder items complete: " + result.size() + " items (total " + (System.currentTimeMillis() - subfolderStart) + "ms)"); + } + YaaccLogger.d(getClass().getName(), "browseItem END: returning " + result.size() + " items (total " + (System.currentTimeMillis() - browseStart) + "ms)"); + return result; + } + + private Item createItem(YaaccContentDirectory contentDirectory, String path, DocumentFile file, String parentId, boolean restricted) { + long createStart = System.currentTimeMillis(); + String fileName = file.getName() != null ? file.getName() : "unknown"; + + if (file.getName() != null && file.getName().endsWith("m3u")) { + return null; + } + + // Get all metadata from cache (duration, MIME type, short ID) + SAFMetadata metadata = SAFCacheManager.getInstance(getContext()).getMetadata(file); + if (metadata == null || metadata.mimeType == null) { + return null; + } + + MimeType mimeType = MimeType.valueOf(metadata.mimeType); + String mimeTypeMain = mimeType.getType(); + + String id = ContentDirectoryIDs.SAF_PREFIX.getId() + metadata.shortId; + String title = file.getName() != null ? file.getName() : path; + if (restricted) { + title = "[X] " + title; + } + + // Use shortId in URI instead of Base64-encoded path + String uri = getUriString(contentDirectory, id, mimeType, metadata.shortId); + YaaccLogger.d(getClass().getName(), "Generated URI for " + title + ": " + uri + " (shortId=" + metadata.shortId + ")"); + + long protocolStart = System.currentTimeMillis(); + ProtocolInfo protocolInfo = getProtocolInfo(mimeType); + long protocolTime = System.currentTimeMillis() - protocolStart; + + String duration = null; + if (mimeTypeMain.equals("audio") && !restricted) { + duration = metadata.duration; + } + + // Create lightweight YaaccRes (no Cling overhead) + YaaccRes yaaccRes = new YaaccRes(protocolInfo, metadata.fileSize, duration, null, uri); + + // Create lightweight YaaccItem (no Cling Property overhead) + long itemStart = System.currentTimeMillis(); + String clazz = mimeTypeMain.equals("audio") ? "object.item.audioItem" + : mimeTypeMain.equals("video") ? "object.item.videoItem" + : "object.item.imageItem"; + YaaccItem yaaccItem = new YaaccItem(id, parentId, title, "yaacc", restricted, clazz); + yaaccItem.addResource(yaaccRes); + long itemTime = System.currentTimeMillis() - itemStart; + + // Convert to Cling Item only at the end (for UPnP serialization) + long convertStart = System.currentTimeMillis(); + Item item = yaaccItem.toClingItem(); + long convertTime = System.currentTimeMillis() - convertStart; + + long totalTime = System.currentTimeMillis() - createStart; + YaaccLogger.d(getClass().getName(), "Item[?] " + fileName + " - protocolInfo=" + protocolTime + "ms, YaaccItem=" + itemTime + "ms, convert=" + convertTime + "ms, total=" + totalTime + "ms"); + return item; + } + + /* + private void loadDurationAsync(DocumentFile file, Item item, Res res) { + + // Use AsyncTask for proper Android background processing + new android.os.AsyncTask() { + @Override + protected String doInBackground(Void... voids) { + return extractDuration(file); + } + + @Override + protected void onPostExecute(String duration) { + if (duration != null) { + // Update the resource with duration + try { + // Create new resource with duration + Res newRes = new Res(res.getProtocolInfo(), res.getSize(), duration, res.getBitrate(), res.getValue()); + // Replace the resource in the item + item.getResources().clear(); + item.addResource(newRes); + YaaccLogger.d(getClass().getName(), "Updated duration for: " + item.getTitle() + " -> " + duration); + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Failed to update duration for: " + item.getTitle(), e); + } + } + YaaccLogger.d(getClass().getName(), "Item ready for playback: " + item.getTitle()); + } + }.execute(); + } + */ +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/VideoItemBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/VideoItemBrowser.java index 1cbe09a0..0f12c87b 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/VideoItemBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/VideoItemBrowser.java @@ -22,16 +22,12 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.item.Item; -import org.fourthline.cling.support.model.item.VideoItem; import org.seamless.util.MimeType; import java.util.ArrayList; @@ -51,7 +47,6 @@ public VideoItemBrowser(Context context) { @Override public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, String myId, long firstResult, long maxResults, SortCriterion[] orderby) { - Item result = null; String[] projection = {MediaStore.Video.Media._ID, MediaStore.Video.Media.DISPLAY_NAME, MediaStore.Video.Media.MIME_TYPE, MediaStore.Video.Media.SIZE, MediaStore.Video.Media.DURATION}; String selection = MediaStore.Video.Media._ID + "=?"; @@ -67,24 +62,30 @@ public DIDLObject browseMeta(YaaccContentDirectory contentDirectory, duration = contentDirectory.formatDuration(duration); @SuppressLint("Range") Long size = Long.valueOf(mediaCursor.getString(mediaCursor.getColumnIndex(MediaStore.Video.VideoColumns.SIZE))); @SuppressLint("Range") String mimeTypeString = mediaCursor.getString(mediaCursor.getColumnIndex(MediaStore.Video.VideoColumns.MIME_TYPE)); - Log.d(getClass().getName(), "Mimetype: " + mimeTypeString); + YaaccLogger.d(getClass().getName(), "Mimetype: " + mimeTypeString); MimeType mimeType = MimeType.valueOf(mimeTypeString); // file parameter only needed for media players which decide the // ability of playing a file by the file extension String uri = getUriString(contentDirectory, id, mimeType); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - resource.setDuration(duration); - result = new VideoItem(ContentDirectoryIDs.VIDEO_PREFIX.getId() + id, ContentDirectoryIDs.VIDEOS_FOLDER.getId(), name, "", resource); - Log.d(getClass().getName(), "VideoItem: " + id + " Name: " + name + " uri: " + uri); - - + Item result = createItem( + ContentDirectoryIDs.VIDEO_PREFIX.getId() + id, + ContentDirectoryIDs.VIDEOS_FOLDER.getId(), + name, + "", + false, + mimeType, + uri, + size, + duration + ); + YaaccLogger.d(getClass().getName(), "VideoItem: " + id + " Name: " + name + " uri: " + uri); + return result; } else { - Log.d(getClass().getName(), "Item " + myId + " not found."); + YaaccLogger.d(getClass().getName(), "Item " + myId + " not found."); } } - return result; + return null; } @Override diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/VideosFolderBrowser.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/VideosFolderBrowser.java index a1fc8cde..92ab6196 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/VideosFolderBrowser.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/VideosFolderBrowser.java @@ -22,17 +22,13 @@ import android.content.Context; import android.database.Cursor; import android.provider.MediaStore; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.model.DIDLObject; -import org.fourthline.cling.support.model.Protocol; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.Res; import org.fourthline.cling.support.model.SortCriterion; import org.fourthline.cling.support.model.container.Container; import org.fourthline.cling.support.model.container.StorageFolder; import org.fourthline.cling.support.model.item.Item; -import org.fourthline.cling.support.model.item.VideoItem; import org.seamless.util.MimeType; import java.util.ArrayList; @@ -98,16 +94,24 @@ public List browseItem(YaaccContentDirectory contentDirectory, String myId duration = contentDirectory.formatDuration(duration); @SuppressLint("Range") Long size = Long.valueOf(mediaCursor.getString(mediaCursor.getColumnIndex(MediaStore.Video.VideoColumns.SIZE))); @SuppressLint("Range") String mimeTypeString = mediaCursor.getString(mediaCursor.getColumnIndex(MediaStore.Video.VideoColumns.MIME_TYPE)); - Log.d(getClass().getName(), "Mimetype: " + mimeTypeString); + YaaccLogger.d(getClass().getName(), "Mimetype: " + mimeTypeString); MimeType mimeType = MimeType.valueOf(mimeTypeString); // file parameter only needed for media players which decide the // ability of playing a file by the file extension String uri = getUriString(contentDirectory, id, mimeType); - ProtocolInfo protocolInfo = new ProtocolInfo(Protocol.HTTP_GET, ProtocolInfo.WILDCARD, mimeType.toString(), getDLNAAttributes(mimeType)); - Res resource = new Res(protocolInfo, size, uri); - resource.setDuration(duration); - result.add(new VideoItem(ContentDirectoryIDs.VIDEO_PREFIX.getId() + id, ContentDirectoryIDs.VIDEOS_FOLDER.getId(), name, "", resource)); - Log.d(getClass().getName(), "VideoItem: " + id + " Name: " + name + " uri: " + uri); + Item item = createItem( + ContentDirectoryIDs.VIDEO_PREFIX.getId() + id, + ContentDirectoryIDs.VIDEOS_FOLDER.getId(), + name, + "", + false, + mimeType, + uri, + size, + duration + ); + result.add(item); + YaaccLogger.d(getClass().getName(), "VideoItem: " + id + " Name: " + name + " uri: " + uri); currentCount++; } currentIndex++; @@ -115,7 +119,7 @@ public List browseItem(YaaccContentDirectory contentDirectory, String myId } } else { - Log.d(getClass().getName(), "System media store is empty."); + YaaccLogger.d(getClass().getName(), "System media store is empty."); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/YaaccContentDirectory.java b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/YaaccContentDirectory.java index 5fe7d6f1..3823b62c 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/YaaccContentDirectory.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/contentdirectory/YaaccContentDirectory.java @@ -22,7 +22,7 @@ import android.content.Context; import android.content.SharedPreferences; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import androidx.preference.PreferenceManager; @@ -69,6 +69,7 @@ import java.util.stream.Collectors; import de.yaacc.R; +import de.yaacc.util.InterfaceResolutionHelper; /** * a content directory which uses the content of the MediaStore in order to @@ -103,9 +104,8 @@ public class YaaccContentDirectory { @UpnpStateVariable(defaultValue = "0", eventMaximumRateMilliseconds = 200) private final UnsignedIntegerFourBytes systemUpdateID = new UnsignedIntegerFourBytes( 0); - private final String ipAddress; - public YaaccContentDirectory(Context context, String ipAddress) { + public YaaccContentDirectory(Context context) { this.context = context; preferences = PreferenceManager.getDefaultSharedPreferences(context); @@ -114,7 +114,6 @@ public YaaccContentDirectory(Context context, String ipAddress) { } this.searchCapabilities = new CSVString(); this.sortCapabilities = new CSVString(); - this.ipAddress = ipAddress; } private boolean isUsingTestContent() { @@ -328,7 +327,7 @@ public BrowseResult browse( } catch (ContentDirectoryException ex) { throw ex; } catch (Exception ex) { - Log.d(getClass().getName(), "exception on browse", ex); + YaaccLogger.d(getClass().getName(), "exception on browse", ex); throw new ContentDirectoryException(ErrorCode.ACTION_FAILED, ex.toString()); } @@ -338,7 +337,7 @@ public BrowseResult browse(String objectID, BrowseFlag browseFlag, String filter, long firstResult, long maxResults, SortCriterion[] orderby) throws ContentDirectoryException { - Log.d(getClass().getName(), "Browse: objectId: " + objectID + YaaccLogger.d(getClass().getName(), "Browse: objectId: " + objectID + " browseFlag: " + browseFlag + " filter: " + filter + " firstResult: " + firstResult + " maxResults: " + maxResults + " orderby: " + stream(orderby).map(SortCriterion::toString).collect(Collectors.joining(","))); @@ -402,7 +401,7 @@ public BrowseResult browse(String objectID, BrowseFlag browseFlag, try { // Generate output with nested items String didlXml = new DIDLParser().generate(didl, false); - Log.d(getClass().getName(), "CDResponse: " + didlXml); + YaaccLogger.d(getClass().getName(), "CDResponse: " + didlXml); result = new BrowseResult(didlXml, childCount, totalMatches); } catch (Exception e) { throw new ContentDirectoryException( @@ -457,8 +456,12 @@ private ContentBrowser findBrowserFor(String objectID) { result = new ImageByBucketNameItemBrowser(getContext()); } else if (objectID.startsWith(ContentDirectoryIDs.VIDEO_PREFIX.getId())) { result = new VideoItemBrowser(getContext()); + } else if (objectID.startsWith(ContentDirectoryIDs.SAF_FOLDER.getId()) || objectID.startsWith(ContentDirectoryIDs.SAF_PREFIX.getId())) { + result = new SafFolderBrowser(getContext()); + } else if (ContentDirectoryIDs.LIVE_STREAM_FOLDER.getId().equals(objectID)) { + result = new LiveStreamFolderBrowser(getContext()); } else { - Log.d(getClass().getName(), "unknown object id: " + objectID); + YaaccLogger.d(getClass().getName(), "unknown object id: " + objectID); result = new RootFolderBrowser(getContext()); } @@ -489,7 +492,7 @@ public String formatDuration(String millisStr) { } public String getIpAddress() { - return ipAddress; + return InterfaceResolutionHelper.getIpAddress(context); } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/YaaccStreamingClientImpl.java b/yaacc/src/main/java/de/yaacc/upnp/server/http/HttpRequestSender.java similarity index 65% rename from yaacc/src/main/java/de/yaacc/upnp/YaaccStreamingClientImpl.java rename to yaacc/src/main/java/de/yaacc/upnp/server/http/HttpRequestSender.java index a21482fa..55131862 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/YaaccStreamingClientImpl.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/http/HttpRequestSender.java @@ -1,6 +1,6 @@ /* * - * Copyright (C) 2023 Tobias Schoene www.yaacc.de + * Copyright (C) 2026 Tobias Schoene www.yaacc.de * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -16,11 +16,10 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -package de.yaacc.upnp; +package de.yaacc.upnp.server.http; -import android.util.Log; +import android.os.Build; -import org.apache.hc.client5.http.classic.methods.HttpUriRequest; import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; import org.apache.hc.client5.http.config.ConnectionConfig; import org.apache.hc.client5.http.impl.DefaultConnectionKeepAliveStrategy; @@ -35,6 +34,7 @@ import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.util.TimeValue; import org.apache.hc.core5.util.Timeout; +import org.fourthline.cling.model.ServerClientTokens; import org.fourthline.cling.model.message.StreamRequestMessage; import org.fourthline.cling.model.message.StreamResponseMessage; import org.fourthline.cling.model.message.UpnpHeaders; @@ -42,8 +42,6 @@ import org.fourthline.cling.model.message.UpnpResponse; import org.fourthline.cling.model.message.header.ContentTypeHeader; import org.fourthline.cling.model.message.header.UpnpHeader; -import org.fourthline.cling.transport.spi.AbstractStreamClient; -import org.fourthline.cling.transport.spi.InitializationException; import org.seamless.util.MimeType; import java.io.IOException; @@ -52,83 +50,59 @@ import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; -public class YaaccStreamingClientImpl extends AbstractStreamClient { +import de.yaacc.util.YaaccLogger; +public class HttpRequestSender { - final protected YaaccStreamingClientConfigurationImpl configuration; final private CloseableHttpClient httpClient; - public YaaccStreamingClientImpl(YaaccStreamingClientConfigurationImpl configuration) throws InitializationException { - this.configuration = configuration; + public HttpRequestSender() { PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(); connectionManager.setDefaultConnectionConfig(ConnectionConfig.custom() - .setSocketTimeout(Timeout.of(60, TimeUnit.SECONDS)) + .setConnectTimeout(Timeout.of(5, TimeUnit.SECONDS)) // Connection timeout + .setSocketTimeout(Timeout.of(10, TimeUnit.SECONDS)) // Reduced from 60s .setValidateAfterInactivity(TimeValue.of(10, TimeUnit.MILLISECONDS)) .build()); connectionManager.setMaxTotal(10); httpClient = HttpClientBuilder.create().setKeepAliveStrategy(DefaultConnectionKeepAliveStrategy.INSTANCE).setConnectionManager(connectionManager).build(); } - @Override - public YaaccStreamingClientConfigurationImpl getConfiguration() { - return configuration; - } - - @Override - protected HttpUriRequest createRequest(StreamRequestMessage requestMessage) { - return new HttpUriRequestBase(requestMessage.getOperation().getHttpMethodName(), requestMessage.getUri()); - } - - @Override - protected Callable createCallable(final StreamRequestMessage requestMessage, - final HttpUriRequest request) { - return () -> { - Log.d(getClass().getName(), "Sending HTTP request: " + requestMessage); - Log.v(getClass().getName(), "Body: " + requestMessage.getBodyString()); - applyRequestHeader(requestMessage, request); - applyRequestBody(requestMessage, request); - return httpClient.execute(request, this::createResponse); - - }; - } + public StreamResponseMessage send(StreamRequestMessage requestMessage) throws IOException { - @Override - protected boolean abort(HttpUriRequest request, String reason) { - Log.d(getClass().getName(), "Received request abort, ignoring it!! Reason:" + reason); - request.abort(); - return true; + YaaccLogger.v(getClass().getName(), "Sending HTTP request: " + requestMessage); + YaaccLogger.v(getClass().getName(), "HTTP body: " + requestMessage.getBodyString()); + HttpUriRequestBase request = new HttpUriRequestBase(requestMessage.getOperation().getHttpMethodName(), requestMessage.getUri()); + applyRequestHeader(requestMessage, request); + applyRequestBody(requestMessage, request); + return httpClient.execute(request, this::createResponse); } - @Override - protected boolean logExecutionException(Throwable t) { - return true; - } - @Override - public void stop() { - try { - httpClient.close(); - } catch (Exception ex) { - Log.i(getClass().getName(), "Error stopping HTTP client: ", ex); - } + public String getUserAgentValue(int majorVersion, int minorVersion) { + // TODO: UPNP VIOLATION: Synology NAS requires User-Agent to contain + // "Android" to return DLNA protocolInfo required to stream to Samsung TV + // see: http://two-play.com/forums/viewtopic.php?f=6&t=81 + ServerClientTokens tokens = new ServerClientTokens(majorVersion, minorVersion); + tokens.setOsName("Android"); + tokens.setOsVersion(Build.VERSION.RELEASE); + return tokens.toString(); } private void applyRequestHeader(StreamRequestMessage requestMessage, ClassicHttpRequest request) { if (!requestMessage.getHeaders().containsKey(UpnpHeader.Type.USER_AGENT)) { - String value = getConfiguration().getUserAgentValue( + String value = getUserAgentValue( requestMessage.getUdaMajorVersion(), requestMessage.getUdaMinorVersion()); - Log.d(getClass().getName(), "Setting header '" + UpnpHeader.Type.USER_AGENT.getHttpName() + "': " + value); + YaaccLogger.d(getClass().getName(), "Setting header '" + UpnpHeader.Type.USER_AGENT.getHttpName() + "': " + value); request.addHeader(UpnpHeader.Type.USER_AGENT.getHttpName(), value); } for (Map.Entry> entry : requestMessage.getHeaders().entrySet()) { for (String v : entry.getValue()) { String headerName = entry.getKey(); - Log.d(getClass().getName(), "Setting header '" + headerName + "': " + v); + YaaccLogger.d(getClass().getName(), "Setting header '" + headerName + "': " + v); request.addHeader(headerName, v); } } @@ -137,7 +111,7 @@ private void applyRequestHeader(StreamRequestMessage requestMessage, ClassicHttp private void applyRequestBody(StreamRequestMessage requestMessage, ClassicHttpRequest request) { // Body if (requestMessage.hasBody()) { - Log.d(getClass().getName(), "Writing textual request body: " + requestMessage); + YaaccLogger.d(getClass().getName(), "Writing textual request body: " + requestMessage); MimeType contentType = requestMessage.getContentTypeHeader() != null ? requestMessage.getContentTypeHeader().getValue() @@ -157,12 +131,13 @@ protected StreamResponseMessage createResponse(ClassicHttpResponse response) thr if (UpnpResponse.Status.getByStatusCode(response.getCode()) == null) { throw new IllegalStateException("can't create UpnpResponse.Status from http response status: " + response.getCode()); } + YaaccLogger.d(getClass().getName(), "Received response code: " + response.getCode()); UpnpResponse responseOperation = new UpnpResponse( response.getCode(), Objects.requireNonNull(UpnpResponse.Status.getByStatusCode(response.getCode())).getStatusMsg() ); - Log.d(getClass().getName(), "Received response: " + responseOperation); + YaaccLogger.d(getClass().getName(), "Received response: " + responseOperation); StreamResponseMessage responseMessage = new StreamResponseMessage(responseOperation); // Headers UpnpHeaders headers = new UpnpHeaders(); @@ -174,21 +149,20 @@ protected StreamResponseMessage createResponse(ClassicHttpResponse response) thr // Body byte[] bytes = EntityUtils.toByteArray(response.getEntity()); if (bytes != null && bytes.length > 0 && responseMessage.isContentTypeMissingOrText()) { - Log.d(getClass().getName(), "Response contains textual entity body, converting then setting string on message"); + YaaccLogger.d(getClass().getName(), "Response contains textual entity body, converting then setting string on message"); try { responseMessage.setBodyCharacters(bytes); } catch (UnsupportedEncodingException ex) { throw new RuntimeException("Unsupported character encoding: " + ex, ex); } } else if (bytes != null && bytes.length > 0) { - Log.d(getClass().getName(), "Response contains binary entity body, setting bytes on message"); + YaaccLogger.d(getClass().getName(), "Response contains binary entity body, setting bytes on message"); responseMessage.setBody(UpnpMessage.BodyType.BYTES, bytes); } else { - Log.d(getClass().getName(), "Response did not contain entity body"); + YaaccLogger.d(getClass().getName(), "Response did not contain entity body"); } - Log.d(getClass().getName(), "Response message complete: " + responseMessage); + YaaccLogger.d(getClass().getName(), "Response message complete: " + responseMessage); return responseMessage; } } - diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/http/YaaccUpnpServerContentHttpHandler.java b/yaacc/src/main/java/de/yaacc/upnp/server/http/YaaccUpnpServerContentHttpHandler.java new file mode 100644 index 00000000..28821316 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/http/YaaccUpnpServerContentHttpHandler.java @@ -0,0 +1,1761 @@ +/* + * + * Copyright (C) 2013 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.http; + +import android.annotation.SuppressLint; +import android.content.ContentUris; +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Build; +import android.provider.MediaStore; +import android.util.Base64; +import android.util.Size; + +import androidx.core.content.res.ResourcesCompat; +import androidx.documentfile.provider.DocumentFile; +import androidx.preference.PreferenceManager; + +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Message; +import org.apache.hc.core5.http.MethodNotSupportedException; +import org.apache.hc.core5.http.nio.AsyncEntityProducer; +import org.apache.hc.core5.http.nio.AsyncRequestConsumer; +import org.apache.hc.core5.http.nio.AsyncServerRequestHandler; +import org.apache.hc.core5.http.nio.StreamChannel; +import org.apache.hc.core5.http.nio.entity.AbstractBinAsyncEntityProducer; +import org.apache.hc.core5.http.nio.entity.AsyncEntityProducers; +import org.apache.hc.core5.http.nio.entity.BasicAsyncEntityConsumer; +import org.apache.hc.core5.http.nio.support.AsyncResponseBuilder; +import org.apache.hc.core5.http.nio.support.BasicRequestConsumer; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.seamless.util.MimeType; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import de.yaacc.R; +import de.yaacc.Yaacc; +import de.yaacc.upnp.server.YaaccUpnpServerService; +import de.yaacc.upnp.server.contentdirectory.ContentDirectoryIDs; +import de.yaacc.upnp.server.contentdirectory.MediaPathFilter; +import de.yaacc.util.SAFCacheManager; +import de.yaacc.upnp.server.media.SystemAudioCaptureService; +import de.yaacc.util.HttpRange; +import de.yaacc.util.YaaccLogger; + +/** + * A http service to retrieve media content by an id. + * + * @author Tobias Schoene (tobexyz) + */ +public class YaaccUpnpServerContentHttpHandler implements AsyncServerRequestHandler> { + + private final Context context; + // Server-side position management for renderers + private static final Map rendererStates = new ConcurrentHashMap<>(); + + SharedPreferences preferences; + + static class RendererState { + long currentTimePosition = 0; // milliseconds + boolean isPaused = false; + String currentUrl = ""; + long totalDuration = 0; + long lastUpdateTime = System.currentTimeMillis(); + long lastBytePosition = 0; // Store last calculated byte position for pause + } + + public YaaccUpnpServerContentHttpHandler(Context context) { + this.context = context; + preferences = PreferenceManager.getDefaultSharedPreferences(context); + } + + private long calculateBytePositionFromTime(String url, long timeMs, long totalDurationMs) { + // More conservative estimation for MP3 files + if (totalDurationMs <= 0) return 0; + + try { + // Get total file size with HEAD request + HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); + con.setRequestMethod("HEAD"); + con.setConnectTimeout(5000); + con.setReadTimeout(5000); + long totalSize = con.getContentLengthLong(); + con.disconnect(); + + if (totalSize > 0) { + // More conservative calculation for MP3 files + // Account for MP3 headers and variable bitrate + double timeRatio = (double) timeMs / totalDurationMs; + + // Assume first 10% of file contains headers/metadata + long dataSize = (long) (totalSize * 0.9); + long headerSize = totalSize - dataSize; + + // Calculate position within the data portion + long estimatedDataPosition = (long) (dataSize * timeRatio); + long estimatedPosition = headerSize + estimatedDataPosition; + + // Additional safety margin - don't go beyond 85% of file size + long maxSafePosition = (long) (totalSize * 0.85); + estimatedPosition = Math.min(estimatedPosition, maxSafePosition); + + YaaccLogger.d(getClass().getName(), "Calculated byte position " + estimatedPosition + " for time " + timeMs + "ms (file size: " + totalSize + ", ratio: " + String.format("%.3f", timeRatio) + ")"); + return estimatedPosition; + } + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Failed to calculate byte position", e); + } + + return 0; + } + + private long getDurationFromUrl(String url) { + try { + // Get file size with HEAD request + HttpURLConnection con = (HttpURLConnection) new URL(url).openConnection(); + con.setRequestMethod("HEAD"); + con.setConnectTimeout(5000); + con.setReadTimeout(5000); + long fileSize = con.getContentLengthLong(); + con.disconnect(); + + if (fileSize > 0) { + // Estimate duration for MP3 files based on file size + // Assume average bitrate of 128 kbps for MP3 files + // Duration (seconds) = (file size in bytes * 8) / (bitrate in bits per second) + long estimatedDurationMs = (fileSize * 8 * 1000) / (128 * 1024); + YaaccLogger.d(getClass().getName(), "Estimated duration: " + estimatedDurationMs + "ms from file size: " + fileSize + " bytes"); + return estimatedDurationMs; + } + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Failed to estimate duration from URL: " + url, e); + } + return 0; + } + + public static void updateRendererPosition(String rendererKey, long timeMs) { + updateRendererPosition(rendererKey, timeMs, false); + } + + public static void updateRendererPosition(String rendererKey, long timeMs, boolean isPaused) { + RendererState state = rendererStates.get(rendererKey); + if (state != null) { + state.currentTimePosition = timeMs; + state.isPaused = isPaused; + state.lastUpdateTime = System.currentTimeMillis(); + YaaccLogger.d("YaaccUpnpServerServiceHttpHandler", "Updated renderer position: " + rendererKey + " -> " + timeMs + "ms, paused=" + isPaused); + } + } + + @Override + public AsyncRequestConsumer> prepare(HttpRequest request, EntityDetails entityDetails, + HttpContext context) { + return new BasicRequestConsumer<>(entityDetails != null ? new BasicAsyncEntityConsumer() : null); + } + + @Override + public void handle(final Message request, + final ResponseTrigger responseTrigger, + final HttpContext context) throws HttpException, IOException { + + YaaccLogger.d(getClass().getName(), "Processing HTTP request: " + + request.getHead().getRequestUri()); + final AsyncResponseBuilder responseBuilder = AsyncResponseBuilder.create(HttpStatus.SC_OK); + // Extract what we need from the HTTP httpRequest + String requestMethod = request.getHead().getMethod() + .toUpperCase(Locale.ENGLISH); + + // Only accept HTTP-GET + if (!requestMethod.equals("GET") && !requestMethod.equals("HEAD")) { + YaaccLogger.d(getClass().getName(), + "HTTP request isn't GET or HEAD stop! Method was: " + + requestMethod); + throw new MethodNotSupportedException(requestMethod + + " method not supported"); + } + + Uri requestUri = Uri.parse(request.getHead().getRequestUri()); + List pathSegments = requestUri.getPathSegments(); + if (pathSegments.size() == 1 && "health".equals(pathSegments.get(0))) { + responseBuilder.setStatus(HttpStatus.SC_OK); + responseBuilder.setEntity( + AsyncEntityProducers.create("I am alive", ContentType.TEXT_HTML)); + responseTrigger.submitResponse(responseBuilder.build(), context); + return; + } + if (pathSegments.size() < 2 || pathSegments.size() > 3) { + createForbiddenResponse(responseTrigger, context, responseBuilder); + return; + } + boolean contentServerEnabled = preferences.getBoolean(getContext().getString(R.string.settings_local_server_provider_chkbx), false); + boolean contentProxyEnabled = preferences.getBoolean(getContext().getString(R.string.settings_local_server_proxy_chkbx), false); + if (!contentServerEnabled && !contentProxyEnabled) { + createForbiddenResponse(responseTrigger, context, responseBuilder); + return; + } + String type = pathSegments.get(0); + String albumId = ""; + String thumbId = ""; + String contentId = ""; + if (contentServerEnabled && "album".equals(type)) { + albumId = pathSegments.get(1); + try { + Long.parseLong(albumId); + } catch (NumberFormatException nex) { + createForbiddenResponse(responseTrigger, context, responseBuilder); + return; + } + } else if (contentServerEnabled && "thumb".equals(type)) { + thumbId = pathSegments.get(1); + try { + Long.parseLong(thumbId); + } catch (NumberFormatException nex) { + createForbiddenResponse(responseTrigger, context, responseBuilder); + return; + } + } else if (contentServerEnabled && "res".equals(type)) { + contentId = pathSegments.get(1); + try { + Long.parseLong(contentId); + } catch (NumberFormatException nex) { + createForbiddenResponse(responseTrigger, context, responseBuilder); + return; + } + } + Arrays.stream(request.getHead().getHeaders()) + .forEach(it -> YaaccLogger.d(getClass().getName(), "HEADER " + it.getName() + ": " + it.getValue())); + List ranges = new ArrayList<>(); + if (request.getHead().getHeader(HttpHeaders.RANGE) != null) { + ranges = HttpRange.parseRangeHeader(request.getHead().getHeader(HttpHeaders.RANGE).getValue().toString()); + } + ContentHolder contentHolder = null; + if (!contentId.isEmpty()) { + contentHolder = lookupContent(contentId, ranges); + } else if (!albumId.isEmpty()) { + contentHolder = lookupAlbumArt(albumId, ranges); + } else if (!thumbId.isEmpty()) { + contentHolder = lookupThumbnail(thumbId, ranges); + } else if (contentProxyEnabled && YaaccUpnpServerService.PROXY_PATH.equals(type)) { + YaaccLogger.d(getClass().getName(), "Processing proxy request: " + requestUri); + // Handle both old and new proxy URL formats + if (pathSegments.size() >= 3) { + // New format: /proxy/encodedDeviceId/contentKey + String encodedDeviceId = pathSegments.get(1); + String deviceId = java.net.URLDecoder.decode(encodedDeviceId, "UTF-8"); + String contentKey = pathSegments.get(2); + contentHolder = lookupProxyContent(contentKey, ranges, deviceId); + } else if (pathSegments.size() >= 2) { + // Old format: /proxy/contentKey (fallback) + contentHolder = lookupProxyContent(pathSegments.get(1), ranges, null); + } + } else if (contentServerEnabled && YaaccUpnpServerService.SAF_PATH.equals(type)) { + contentHolder = lookupSafContent(pathSegments.get(1), pathSegments.get(2), ranges); + } else if (contentServerEnabled && "live".equals(type) && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) { + // Live streaming endpoint + YaaccLogger.d(getClass().getName(), "Live stream request: " + requestUri); + String streamType = pathSegments.get(1); + if ("audio".equals(streamType)) { + YaaccLogger.d(getClass().getName(), "Routing to audio stream"); + contentHolder = serveLiveAudio(ranges); + } else if ("video".equals(streamType)) { + YaaccLogger.d(getClass().getName(), "Routing to video stream"); + contentHolder = serveLiveVideo(ranges); + } else if ("videoaudio".equals(streamType)) { + YaaccLogger.d(getClass().getName(), "Routing to combined video+audio stream"); + contentHolder = serveLiveCombined(ranges); + } else { + YaaccLogger.w(getClass().getName(), "Unknown stream type: " + streamType); + } + } + + if (contentHolder == null) { + // tricky but works + YaaccLogger.d(getClass().getName(), "Resource with id " + contentId + + albumId + thumbId + pathSegments.get(1) + " not found"); + responseBuilder.setStatus(HttpStatus.SC_NOT_FOUND); + String response = "Resource with id " + contentId + albumId + + thumbId + pathSegments.get(1) + " not found"; + responseBuilder.setEntity(AsyncEntityProducers.create(response, ContentType.TEXT_HTML)); + } else { + YaaccLogger.d(getClass().getName(), "Serving content: type=" + type + + " mimeType=" + contentHolder.getMimeType() + + " length=" + contentHolder.getContentLength() + + " ranges=" + ranges.size()); + + if (!ranges.isEmpty()) { + responseBuilder.setStatus(HttpStatus.SC_PARTIAL_CONTENT); + // Add Content-Range header for partial content + HttpRange range = ranges.get(0); + long fileSize = contentHolder.getContentLength(); + + // For external URLs with unknown length, don't send Content-Range header + if (fileSize > 0) { + long start = range.getStart() != null ? range.getStart() : 0; + long end = range.getEnd() != null ? range.getEnd() : fileSize - 1; + responseBuilder.setHeader(HttpHeaders.CONTENT_RANGE, + "bytes " + start + "-" + end + "/" + fileSize); + YaaccLogger.d(getClass().getName(), "Partial content: " + start + "-" + end + "/" + fileSize); + } + } else { + responseBuilder.setStatus(HttpStatus.SC_OK); + } + + // Add essential streaming headers for UPnP renderers + responseBuilder.setHeader(HttpHeaders.CONNECTION, "close"); + responseBuilder.setHeader("transferMode.dlna.org", "Streaming"); + responseBuilder.setHeader("contentFeatures.dlna.org", "DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000"); + + // Add MJPEG-specific headers for Kodi compatibility + if (contentHolder instanceof LiveStreamContentHolder && ((LiveStreamContentHolder) contentHolder).isVideo) { + responseBuilder.setHeader(HttpHeaders.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); + responseBuilder.setHeader(HttpHeaders.PRAGMA, "no-cache"); + responseBuilder.setHeader(HttpHeaders.EXPIRES, "0"); + } + + responseBuilder.setEntity(contentHolder.getEntityProducer()); + } + responseBuilder.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); + responseTrigger.submitResponse(responseBuilder.build(), context); + YaaccLogger.d(getClass().getName(), "end doService: "); + } + + private void createForbiddenResponse(ResponseTrigger responseTrigger, HttpContext context, + AsyncResponseBuilder responseBuilder) throws HttpException, IOException { + responseBuilder.setStatus(HttpStatus.SC_FORBIDDEN); + responseBuilder.setEntity( + AsyncEntityProducers.create("Access denied", ContentType.TEXT_HTML)); + responseTrigger.submitResponse(responseBuilder.build(), context); + YaaccLogger.d(getClass().getName(), "end doService: Access denied"); + } + + private Context getContext() { + return context; + } + + /** + * Serve live audio stream (Android 10+). + */ + @androidx.annotation.RequiresApi(api = android.os.Build.VERSION_CODES.Q) + private ContentHolder serveLiveAudio(List ranges) { + YaaccLogger.d(getClass().getName(), "serveLiveAudio called"); + + YaaccUpnpServerService service = ((Yaacc) getContext().getApplicationContext()) + .getUpnpClient().getYaaccUpnpServerService(); + + if (service == null) { + YaaccLogger.w(getClass().getName(), "Server service not available"); + return null; + } + + SystemAudioCaptureService audioCapture = service.getAudioCapture(); + if (audioCapture == null) { + YaaccLogger.w(getClass().getName(), "Audio capture service is null"); + return null; + } + + if (!audioCapture.isCapturing()) { + YaaccLogger.w(getClass().getName(), "Audio capture not active"); + return null; + } + + YaaccLogger.d(getClass().getName(), "Audio capture is active, creating stream for client"); + + try { + InputStream inputStream = audioCapture.getInputStream(); + if (inputStream == null) { + YaaccLogger.w(getClass().getName(), "Audio input stream not available"); + return null; + } + + YaaccLogger.i(getClass().getName(), "Serving live audio stream to new client"); + + // Serve as audio/wav for better compatibility + MimeType mimeType = MimeType.valueOf("audio/wav"); + return new LiveStreamContentHolder(mimeType, inputStream, context); + } catch (java.io.IOException e) { + YaaccLogger.e(getClass().getName(), "Failed to create audio stream", e); + return null; + } + } + + /** + * Serve live video stream (Android 10+). + */ + @androidx.annotation.RequiresApi(api = android.os.Build.VERSION_CODES.Q) + private ContentHolder serveLiveVideo(List ranges) { + YaaccLogger.d(getClass().getName(), "serveLiveVideo called"); + + YaaccUpnpServerService service = ((Yaacc) getContext().getApplicationContext()) + .getUpnpClient().getYaaccUpnpServerService(); + + if (service == null) { + return null; + } + + de.yaacc.upnp.server.media.ScreenCastCaptureService videoCapture = service.getVideoCapture(); + if (videoCapture == null || !videoCapture.isCapturing()) { + YaaccLogger.w(getClass().getName(), "Video capture not active"); + return null; + } + + try { + java.io.InputStream inputStream = videoCapture.createInputStream(); + if (inputStream == null) { + return null; + } + + YaaccLogger.i(getClass().getName(), "Serving MJPEG video stream"); + + MimeType mimeType = MimeType.valueOf("multipart/x-mixed-replace; boundary=frame"); + return new LiveStreamContentHolder(mimeType, inputStream, context, true); + } catch (java.io.IOException e) { + YaaccLogger.e(getClass().getName(), "Error creating video stream", e); + return null; + } + } + + /** + * Serve combined video+audio stream (MPEG-TS) + */ + @androidx.annotation.RequiresApi(api = android.os.Build.VERSION_CODES.Q) + private ContentHolder serveLiveCombined(List ranges) { + YaaccLogger.d(getClass().getName(), "serveLiveCombined called, ranges: " + (ranges != null ? ranges.size() : 0)); + + YaaccUpnpServerService service = ((Yaacc) getContext().getApplicationContext()) + .getUpnpClient().getYaaccUpnpServerService(); + + if (service == null) { + return null; + } + + de.yaacc.upnp.server.media.CombinedCaptureService combinedCapture = service.getCombinedCapture(); + if (combinedCapture == null || !combinedCapture.isCapturing()) { + YaaccLogger.w(getClass().getName(), "Combined capture not active"); + return null; + } + + java.io.File outputFile = combinedCapture.getOutputFile(); + if (outputFile == null || !outputFile.exists()) { + YaaccLogger.w(getClass().getName(), "Output file not available"); + return null; + } + + try { + YaaccLogger.i(getClass().getName(), "Serving MPEG-TS live stream"); + MimeType mimeType = MimeType.valueOf("video/mp2t"); + + // Create a custom input stream that follows the circular buffer + java.io.InputStream stream = new java.io.InputStream() { + private java.io.RandomAccessFile raf = new java.io.RandomAccessFile(outputFile, "r"); + private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // Must match muxer + private long position = -1; // Will be set on first read + private long startTime = System.currentTimeMillis(); + + @Override + public int read() throws java.io.IOException { + byte[] b = new byte[1]; + int result = read(b, 0, 1); + return result > 0 ? (b[0] & 0xFF) : -1; + } + + @Override + public int read(byte[] b, int off, int len) throws java.io.IOException { + if (position < 0) { + de.yaacc.upnp.server.media.FragmentedMp4Muxer muxer = combinedCapture.getMuxer(); + if (muxer != null) { + position = muxer.getLastKeyframePosition(); + YaaccLogger.i(getClass().getName(), "Starting stream from keyframe at " + position); + } else { + position = 0; + } + } + + // Check how much data is available + de.yaacc.upnp.server.media.FragmentedMp4Muxer muxer = combinedCapture.getMuxer(); + if (muxer == null) return -1; + + long writePos = muxer.getWritePosition(); + long available; + if (writePos >= position) { + available = writePos - position; + } else { + available = (MAX_FILE_SIZE - position) + writePos; + } + + // Wait if no data available + if (available < 188) { + try { + Thread.sleep(5); + } catch (InterruptedException e) { + } + return 0; + } + + int toRead = Math.min((int) Math.min(available, len) / 188 * 188, 188 * 200); + if (toRead == 0) toRead = 188; + + raf.seek(position); + int read = raf.read(b, off, toRead); + + if (read > 0) { + position = (position + read) % MAX_FILE_SIZE; + return read; + } + + return 0; + } + + @Override + public void close() throws java.io.IOException { + raf.close(); + } + }; + + return new LiveStreamContentHolder(mimeType, stream, context, true); + } catch (java.io.IOException e) { + YaaccLogger.e(getClass().getName(), "Error opening stream file", e); + return null; + } + } + + /** + * Lookup content in the mediastore + * + * @param contentId the id of the content + * @return the content description + */ + private ContentHolder lookupContent(String contentId, List ranges) { + ContentHolder result = null; + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + if (!preferences.getBoolean(getContext().getString(R.string.settings_local_server_chkbx), false)) { + return null; + } + + if (contentId == null) { + return null; + } + YaaccLogger.d(getClass().getName(), "System media store lookup: " + contentId); + String[] projection = {MediaStore.Files.FileColumns._ID, + MediaStore.Files.FileColumns.MIME_TYPE, + MediaStore.Files.FileColumns.DATA}; + String selection = MediaStore.Files.FileColumns._ID + "=? and (" + MediaPathFilter.makeLikeClause( + MediaStore.Files.FileColumns.DATA, MediaPathFilter.getMediaPathes(getContext()).size()) + ")"; + List selectionArgsList = new ArrayList<>(); + selectionArgsList.add(contentId); + selectionArgsList.addAll(MediaPathFilter.getMediaPathesForLikeClause(getContext())); + String[] selectionArgs = selectionArgsList.toArray(new String[0]); + try (Cursor mFilesCursor = getContext().getContentResolver().query( + MediaStore.Files.getContentUri("external"), projection, + selection, selectionArgs, null)) { + + if (mFilesCursor != null) { + mFilesCursor.moveToFirst(); + while (!mFilesCursor.isAfterLast()) { + @SuppressLint("Range") + String dataUri = mFilesCursor.getString(mFilesCursor + .getColumnIndex(MediaStore.Files.FileColumns.DATA)); + + @SuppressLint("Range") + String mimeTypeStr = mFilesCursor + .getString(mFilesCursor + .getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE)); + MimeType mimeType = MimeType.valueOf("*/*"); + if (mimeTypeStr != null) { + mimeType = MimeType.valueOf(mimeTypeStr); + } + YaaccLogger.d(getClass().getName(), "Content found: " + mimeType + + " Uri: " + dataUri); + result = new ContentHolder(mimeType, dataUri, ranges, context); + mFilesCursor.moveToNext(); + } + } else { + YaaccLogger.d(getClass().getName(), "System media store is empty."); + } + } + + return result; + + } + + /** + * Lookup content in the mediastore + * + * @param albumId the id of the album + * @return the content description + */ + private ContentHolder lookupAlbumArt(String albumId, List ranges) { + + ContentHolder result = new ContentHolder(MimeType.valueOf("image/png"), + getDefaultIcon(), ranges, context); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + if (!preferences.getBoolean(getContext().getString(R.string.settings_local_server_chkbx), false)) { + return result; + } + if (albumId == null) { + return result; + } + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { + YaaccLogger.d(getClass().getName(), "System media store lookup album: " + + albumId); + String[] projection = {MediaStore.Audio.Albums._ID, + // FIXME what is the right mime type? + // MediaStore.Audio.Albums.MIME_TYPE, + MediaStore.Audio.Albums.ALBUM_ART}; + String selection = MediaStore.Audio.Albums._ID + "=?"; + String[] selectionArgs = {albumId}; + try (Cursor cursor = getContext().getContentResolver().query( + MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, projection, + selection, selectionArgs, null)) { + + if (cursor != null) { + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + @SuppressLint("Range") + String dataUri = cursor.getString(cursor + .getColumnIndex(MediaStore.Audio.Albums.ALBUM_ART)); + + // String mimeTypeStr = null; + // FIXME mime type resolving cursor + // .getString(cursor + // .getColumnIndex(MediaStore.Files.FileColumns.MIME_TYPE)); + + MimeType mimeType = MimeType.valueOf("image/png"); + // if (mimeTypeStr != null) { + // mimeType = MimeType.valueOf(mimeTypeStr); + // } + if (dataUri != null) { + YaaccLogger.d(getClass().getName(), "Content found: " + mimeType + + " Uri: " + dataUri); + result = new ContentHolder(mimeType, dataUri, ranges, context); + } else { + YaaccLogger.d(getClass().getName(), "Album art not found in media store. Fallback to default"); + Bitmap bitmap = BitmapFactory.decodeResource(context.getResources(), + R.drawable.yaacc192_32); + + try { + File art = new File(context.getCacheDir(), "albumart" + albumId + ".jpg"); + art.createNewFile(); + FileOutputStream fos = new FileOutputStream(art); + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos); + fos.flush(); + fos.close(); + result = new ContentHolder(mimeType, art.getAbsolutePath(), ranges, context); + } catch (IOException e) { + YaaccLogger.e(getClass().getName(), "Error loading album art from file", e); + } + } + cursor.moveToNext(); + } + } else { + YaaccLogger.d(getClass().getName(), "System media store is empty."); + } + } + } else { + Uri albumArtUri = ContentUris.withAppendedId(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, + Long.parseLong(albumId)); + MimeType mimeType = MimeType.valueOf("image/jpeg"); + YaaccLogger.d(getClass().getName(), "Content found: " + mimeType + + " Uri: " + albumArtUri); + Bitmap bitmap; + try { + bitmap = context.getContentResolver().loadThumbnail(albumArtUri, new Size(1024, 1024), null); + } catch (IOException io) { + YaaccLogger.d(getClass().getName(), "Album art not found in media store. Fallback to default"); + bitmap = BitmapFactory.decodeResource(context.getResources(), R.drawable.yaacc192_32); + } + try { + File art = new File(context.getCacheDir(), "albumart" + albumId + ".jpg"); + art.createNewFile(); + FileOutputStream fos = new FileOutputStream(art); + bitmap.compress(Bitmap.CompressFormat.JPEG, 90, fos); + fos.flush(); + fos.close(); + result = new ContentHolder(mimeType, art.getAbsolutePath(), ranges, context); + } catch (IOException e) { + YaaccLogger.e(getClass().getName(), "Error loading album art from file", e); + } + + } + return result; + } + + /** + * Lookup a thumbnail content in the mediastore + * + * @param idStr the id of the thumbnail + * @return the content description + */ + private ContentHolder lookupThumbnail(String idStr, List ranges) { + + ContentHolder result = new ContentHolder(MimeType.valueOf("image/png"), + getDefaultIcon(), ranges, context); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + if (!preferences.getBoolean(getContext().getString(R.string.settings_local_server_chkbx), false)) { + return result; + } + if (idStr == null) { + return result; + } + long id; + try { + id = Long.parseLong(idStr); + } catch (NumberFormatException nfe) { + YaaccLogger.d(getClass().getName(), "ParsingError of id: " + idStr, nfe); + return result; + } + + YaaccLogger.d(getClass().getName(), "System media store lookup thumbnail: " + + idStr); + Bitmap bitmap = MediaStore.Images.Thumbnails.getThumbnail(getContext() + .getContentResolver(), id, + MediaStore.Images.Thumbnails.MINI_KIND, null); + if (bitmap != null) { + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + byte[] byteArray = stream.toByteArray(); + + MimeType mimeType = MimeType.valueOf("image/png"); + + result = new ContentHolder(mimeType, byteArray, ranges, context); + + } else { + YaaccLogger.d(getClass().getName(), "System media store is empty."); + } + return result; + } + + private ContentHolder lookupProxyContent(String contentKey, List ranges) { + return lookupProxyContent(contentKey, ranges, null); + } + + private ContentHolder lookupProxyContent(String contentKey, List ranges, String deviceId) { + YaaccLogger.d(getClass().getName(), "Looking up proxy content for key: " + contentKey + ", device: " + deviceId); + + String targetUri = PreferenceManager.getDefaultSharedPreferences(getContext()) + .getString(YaaccUpnpServerService.PROXY_LINK_KEY_PREFIX + contentKey, null); + YaaccLogger.d(getClass().getName(), "Target URI from preferences: " + targetUri); + + if (targetUri == null) { + YaaccLogger.e(getClass().getName(), "No target URI found for proxy key: " + contentKey); + return null; + } + String targetMimetype = PreferenceManager.getDefaultSharedPreferences(getContext()) + .getString(YaaccUpnpServerService.PROXY_LINK_MIME_TYPE_KEY_PREFIX + contentKey, null); + YaaccLogger.d(getClass().getName(), "Target MIME type: " + targetMimetype); + + // Check if this renderer needs server-side position management + boolean useServerPositionManagement = false; // Default: no server-side management + + if (deviceId != null) { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext()); + String prefKey = "manage_external_seeking_" + deviceId; + useServerPositionManagement = preferences.getBoolean(prefKey, false); + YaaccLogger.d(getClass().getName(), "Checking preference key: " + prefKey); + YaaccLogger.d(getClass().getName(), "Server position management for device " + deviceId + ": " + useServerPositionManagement); + } else { + YaaccLogger.d(getClass().getName(), "No device ID available, using default (no server-side management)"); + } + + if (useServerPositionManagement && !ranges.isEmpty()) { + YaaccLogger.d(getClass().getName(), "Server-side management enabled, processing ranges: " + ranges.size()); + // For basic renderers, ignore their range requests and use server-managed position + String rendererKey = deviceId + "_" + contentKey; + RendererState state = rendererStates.get(rendererKey); + YaaccLogger.d(getClass().getName(), "Looking for renderer state: " + rendererKey + ", found: " + (state != null)); + if (state != null && state.currentUrl.equals(targetUri)) { + long bytePosition; + if (state.isPaused) { + // When paused, use the exact same byte position as before + bytePosition = state.lastBytePosition; + YaaccLogger.d(getClass().getName(), "Using cached byte position for pause: " + bytePosition); + } else { + // Calculate new byte position and cache it + // If we don't have duration yet, get it now + if (state.totalDuration <= 0) { + state.totalDuration = getDurationFromUrl(targetUri); + YaaccLogger.d(getClass().getName(), "Updated renderer state duration: " + state.totalDuration + "ms"); + } + bytePosition = calculateBytePositionFromTime(targetUri, state.currentTimePosition, state.totalDuration); + if (bytePosition > 0) { + state.lastBytePosition = bytePosition; // Update cache for future pause + } + } + + if (bytePosition > 0) { + // Override the renderer's incorrect range request + ranges.clear(); + ranges.add(new HttpRange("bytes", bytePosition, null, null)); + YaaccLogger.d(getClass().getName(), "Overriding range request with server-managed position: bytes=" + bytePosition + "- (paused=" + state.isPaused + ")"); + } + } else { + // Initialize renderer state for new playback + RendererState newState = new RendererState(); + newState.currentUrl = targetUri; + // Get current server position (in case this is after a seek) + SharedPreferences serverPrefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + long serverPosition = serverPrefs.getLong("server_position_" + deviceId, 0); + newState.currentTimePosition = serverPosition; + newState.totalDuration = 0; // Will be determined dynamically + newState.isPaused = false; + newState.lastBytePosition = 0; + rendererStates.put(rendererKey, newState); + YaaccLogger.d(getClass().getName(), "Initialized renderer state for: " + rendererKey + " with position: " + serverPosition + "ms"); + + // Calculate byte position for the current server position + if (serverPosition > 0) { + YaaccLogger.d(getClass().getName(), "Attempting to get duration for URL: " + targetUri); + // Get the total duration dynamically from the media file + long totalDuration = getDurationFromUrl(targetUri); + YaaccLogger.d(getClass().getName(), "Retrieved duration: " + totalDuration + "ms for position: " + serverPosition + "ms"); + + if (totalDuration > 0) { + long bytePosition = calculateBytePositionFromTime(targetUri, serverPosition, totalDuration); + YaaccLogger.d(getClass().getName(), "Calculated byte position: " + bytePosition + " for time: " + serverPosition + "ms"); + if (bytePosition > 0) { + // Override the renderer's range request + ranges.clear(); + ranges.add(new HttpRange("bytes", bytePosition, null, null)); + YaaccLogger.d(getClass().getName(), "Overriding range request with server-managed position: bytes=" + bytePosition + "- (time=" + serverPosition + "ms, duration=" + totalDuration + "ms)"); + } else { + YaaccLogger.w(getClass().getName(), "Byte position calculation returned 0 or negative: " + bytePosition); + } + } else { + YaaccLogger.w(getClass().getName(), "Could not determine duration for URL: " + targetUri); + } + } else { + YaaccLogger.d(getClass().getName(), "Server position is 0, no range override needed"); + } + } + } + + MimeType mimeType = MimeType.valueOf("*/*"); + if (targetMimetype != null) { + mimeType = MimeType.valueOf(targetMimetype); + } + + YaaccLogger.d(getClass().getName(), "Creating ContentHolder for proxy: " + targetUri + " with MIME: " + mimeType + ", ranges: " + ranges.size()); + return new ContentHolder(mimeType, targetUri, ranges, context); + } + + private ContentHolder lookupSafContent(String contentKey, String contentEnc, List ranges) { + YaaccLogger.d(getClass().getName(), "lookupSafContent: contentKey=" + contentKey + ", contentEnc=" + contentEnc); + + if (!contentKey.startsWith(ContentDirectoryIDs.SAF_PREFIX.getId())) { + YaaccLogger.d(getClass().getName(), "SAF content id is unknown: " + contentKey); + return null; + } + String shortId = contentKey.substring(ContentDirectoryIDs.SAF_PREFIX.getId().length()); + YaaccLogger.d(getClass().getName(), "Extracted shortId: " + shortId); + + // Extract short ID from contentEnc (format: shortId.ext) + if (contentEnc.indexOf(".") == -1) { + YaaccLogger.d(getClass().getName(), "SAF content id is invalid: " + contentEnc); + return null; + } + String contentEncShortId = contentEnc.substring(0, contentEnc.indexOf(".")); + YaaccLogger.d(getClass().getName(), "contentEnc shortId: " + contentEncShortId); + + if (!shortId.equals(contentEncShortId)) { + YaaccLogger.d(getClass().getName(), "SAF content id mismatch: " + shortId + " != " + contentEncShortId); + return null; + } + + // Lookup URI from short ID + String contentUri = SAFCacheManager.getInstance(getContext()).getUriForShortId(shortId); + YaaccLogger.d(getClass().getName(), "Looked up URI for shortId " + shortId + ": " + contentUri); + + if (contentUri == null) { + YaaccLogger.e(getClass().getName(), "SAF short ID not found in cache: " + shortId); + return null; + } + + DocumentFile file = null; + try { + Uri uri = Uri.parse(contentUri); + // Use fromSingleUri for document URIs, fromTreeUri for tree URIs + if (contentUri.contains("/document/")) { + file = DocumentFile.fromSingleUri(getContext(), uri); + } else { + file = DocumentFile.fromTreeUri(getContext(), uri); + } + + if (file == null || !file.exists()) { + YaaccLogger.d(getClass().getName(), "SAF content uri is unknown: " + contentUri); + return null; + } + + // Check if it's a directory - directories cannot be streamed + if (file.isDirectory()) { + YaaccLogger.d(getClass().getName(), "SAF content is a directory, cannot stream: " + contentUri); + return null; + } + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Error accessing SAF content: " + contentUri, e); + return null; + } + + String mimeTypeStr = null; + try { + mimeTypeStr = file.getType(); + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Error getting MIME type for SAF content", e); + } + + MimeType mimeType = MimeType.valueOf("*/*"); + if (mimeTypeStr != null) { + mimeType = MimeType.valueOf(mimeTypeStr); + } else { + // Fallback: try to determine MIME type from file extension + //String fileName = file.getName(); + String fileName = contentEnc; + YaaccLogger.d(getClass().getName(), "File name: " + fileName); + if (fileName != null) { + String fileExtension = android.webkit.MimeTypeMap.getFileExtensionFromUrl(fileName); + if (fileExtension != null && !fileExtension.isEmpty()) { + String fallbackMimeType = android.webkit.MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileExtension); + if (fallbackMimeType != null) { + mimeType = MimeType.valueOf(fallbackMimeType); + } + } + } + } + + // Check if contentUri or its parents are included in selectedSafPathes + // FIXME + /* + * Set selectedSafPathes = + * MediaPathFilter.getSelectedSafPathes(getContext()); + * boolean isPathAllowed = false; + * + * for (String selectedPath : selectedSafPathes) { + * if (contentUri.startsWith(selectedPath)) { + * isPathAllowed = true; + * break; + * } + * } + * + * if (!isPathAllowed) { + * YaaccLogger.d(getClass().getName(), "SAF content URI not in selected paths: " + + * contentUri); + * return null; + * } + */ + String targetUri = contentUri; + // Pass the DocumentFile instance to avoid re-creating it during streaming + return new SafContentHolder(mimeType, targetUri, ranges, context, file); + } + + /** + * Special ContentHolder for SAF content that avoids blocking DocumentFile operations + */ + static class SafContentHolder extends ContentHolder { + private final DocumentFile documentFile; + + public SafContentHolder(MimeType mimeType, String uri, List ranges, Context context, DocumentFile documentFile) { + super(mimeType, uri, ranges, context); + this.documentFile = documentFile; + } + + @Override + public AsyncEntityProducer getEntityProducer() throws IOException { + if (documentFile != null && documentFile.exists() && !documentFile.isDirectory()) { + return new AbstractBinAsyncEntityProducer(8192, ContentType.parse(getMimeType().toString())) { + private InputStream input; + private long totalBytesRead = 0; + private final long fileLength = documentFile.length(); + private long startPosition = 0; + private long rangeLength = fileLength; + + { + // Calculate range if specified + if (!ranges.isEmpty()) { + HttpRange range = ranges.get(0); + startPosition = range.getStart() == null ? 0 : range.getStart(); + if (range.getEnd() != null && range.getEnd() > 0) { + rangeLength = range.getEnd() - startPosition + 1; + } else { + rangeLength = fileLength - startPosition; + } + if (range.getSuffixLength() != null && range.getSuffixLength() > 0) { + startPosition = Math.max(0, fileLength - range.getSuffixLength()); + rangeLength = range.getSuffixLength(); + } + // Ensure range is valid + if (startPosition >= fileLength) { + startPosition = 0; + rangeLength = fileLength; + } + if (startPosition + rangeLength > fileLength) { + rangeLength = fileLength - startPosition; + } + } + } + + @Override + public long getContentLength() { + return rangeLength; + } + + @Override + protected int availableData() { + return input != null ? 1 : 0; + } + + @Override + protected void produceData(final StreamChannel channel) throws IOException { + if (input == null) { + try { + input = context.getContentResolver().openInputStream(documentFile.getUri()); + if (startPosition > 0) { + input.skip(startPosition); + } + YaaccLogger.d(getClass().getName(), "Opened SAF input stream for: " + documentFile.getUri() + + " range: " + startPosition + "-" + (startPosition + rangeLength - 1)); + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Error opening SAF input stream", e); + channel.endStream(); + return; + } + } + + if (input != null && totalBytesRead < rangeLength) { + byte[] buffer = new byte[8192]; + int maxRead = (int) Math.min(buffer.length, rangeLength - totalBytesRead); + try { + int bytesRead = input.read(buffer, 0, maxRead); + if (bytesRead > 0) { + totalBytesRead += bytesRead; + channel.write(ByteBuffer.wrap(buffer, 0, bytesRead)); + } else { + YaaccLogger.d(getClass().getName(), "End of SAF stream reached. Total bytes: " + totalBytesRead); + input.close(); + channel.endStream(); + } + } catch (IOException e) { + YaaccLogger.e(getClass().getName(), "Error reading from SAF stream", e); + if (input != null) { + try { + input.close(); + } catch (IOException ignored) { + } + } + channel.endStream(); + } + } else { + if (input != null) { + try { + input.close(); + } catch (IOException ignored) { + } + } + channel.endStream(); + } + } + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + public void failed(final Exception cause) { + YaaccLogger.e(getClass().getName(), "SAF streaming failed", cause); + if (input != null) { + try { + input.close(); + } catch (IOException ignored) { + } + } + } + }; + } + YaaccLogger.e(getClass().getName(), "DocumentFile is null, doesn't exist, or is a directory"); + return super.getEntityProducer(); + } + } + + private byte[] getDefaultIcon() { + Drawable drawable = ResourcesCompat.getDrawable(getContext().getResources(), + R.drawable.yaacc192_32, getContext().getTheme()); + byte[] result = null; + if (drawable != null) { + Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap(); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream); + result = stream.toByteArray(); + } + return result; + } + + /** + * ValueHolder for media content. + */ + static class ContentHolder { + protected final MimeType mimeType; + protected String uri; + protected byte[] content; + protected final Context context; + protected List ranges; + + public ContentHolder(MimeType mimeType, String uri, List ranges, Context context) { + this.uri = uri; + this.mimeType = mimeType; + this.ranges = ranges; + this.context = context; + + } + + public ContentHolder(MimeType mimeType, byte[] content, List ranges, Context context) { + this.content = content; + this.mimeType = mimeType; + this.ranges = ranges; + this.context = context; + + } + + /** + * @return the uri + */ + public String getUri() { + return uri; + } + + /** + * @return the mimeType + */ + public MimeType getMimeType() { + return mimeType; + } + + /** + * @return the content length + */ + public long getContentLength() { + if (content != null) { + return content.length; + } else if (uri != null) { + // Check if it's an external URL + if (uri.startsWith("http://") || uri.startsWith("https://")) { + try { + HttpURLConnection con = (HttpURLConnection) new URL(uri).openConnection(); + con.setRequestMethod("HEAD"); + con.setConnectTimeout(5000); // 5 second timeout + con.setReadTimeout(5000); // 5 second timeout + long length = con.getContentLengthLong(); + con.disconnect(); + if (length > 0) { + return length; + } + return -1; // Unknown length for external URLs + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Error getting external content length", e); + return -1; + } + } else { + // Handle local files + File file = new File(uri); + if (file.exists()) { + return file.length(); + } else { + // Handle SAF content + try { + Uri contentUri = Uri.parse(uri); + DocumentFile docFile = DocumentFile.fromSingleUri(context, contentUri); + if (docFile != null) { + return docFile.length(); + } + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Error getting SAF content length", e); + } + } + } + } + return -1; + } + + private byte[] readRangeFormFile(File file, List ranges) throws IOException { + + try (RandomAccessFile raf = new RandomAccessFile(file, "r")) { + long fileSize = raf.length(); + long startPosition; + long rangeLength; + if (ranges.size() > 1) { + YaaccLogger.d(getClass().getName(), + "More than on ranges requested. Currently only one range is supported. Responding with the first range"); + } + if (ranges.isEmpty()) { + startPosition = 0; + rangeLength = fileSize; + } else { + HttpRange range = ranges.get(0); + startPosition = range.getStart() == null ? 0 : range.getStart(); + if (range.getEnd() == null || range.getEnd() == 0) { + rangeLength = fileSize; + } else { + rangeLength = range.getEnd() - startPosition; + } + if (range.getSuffixLength() != null && range.getSuffixLength() > 0) { + startPosition = fileSize - range.getSuffixLength(); + rangeLength = range.getSuffixLength(); + } + } + + // Read a range of bytes (e.g., bytes 100 to 200) + if (startPosition < 0 || startPosition + rangeLength > fileSize) { + YaaccLogger.d(getClass().getName(), "Invalid range startPosition: " + startPosition + " rangeLength: " + + rangeLength + " fileSize: " + fileSize); + rangeLength = fileSize - startPosition; + YaaccLogger.d(getClass().getName(), "Adjusted range startPosition: " + startPosition + " rangeLength: " + + rangeLength + " fileSize: " + fileSize); + } + + raf.seek(startPosition); // Move to the starting position + byte[] buffer = new byte[(int) rangeLength]; // Create a buffer + raf.read(buffer); + return buffer; + } + + } + + public AsyncEntityProducer getEntityProducer() throws IOException { + AsyncEntityProducer result = null; + if (getUri() != null && !getUri().isEmpty()) { + // Check if it's an external URL (http/https) + if (getUri().startsWith("http://") || getUri().startsWith("https://")) { + // Handle external URL directly + result = new AbstractBinAsyncEntityProducer(8192, ContentType.parse(getMimeType().toString())) { + private InputStream input; + private long length = -1; + private long bytesRead = 0; + + AbstractBinAsyncEntityProducer init() { + try { + if (input == null) { + int retries = 3; + Exception lastException = null; + + for (int i = 0; i < retries; i++) { + try { + HttpURLConnection con = (HttpURLConnection) new URL(getUri()).openConnection(); + con.setConnectTimeout(15000); // Increased to 15 seconds + con.setReadTimeout(60000); // Increased to 60 seconds for UPnP renderers + con.setRequestProperty("User-Agent", "YAACC/1.0 (Android UPnP Proxy)"); + con.setRequestProperty("Connection", "keep-alive"); // Try to keep connection alive + // Apply range request to external connection + if (!ranges.isEmpty()) { + con.setRequestProperty("Range", HttpRange.toHeaderString(ranges)); + YaaccLogger.d(getClass().getName(), "Applying range request to external URL: " + HttpRange.toHeaderString(ranges)); + } + con.connect(); + + // Check if we got a partial content response + int responseCode = con.getResponseCode(); + YaaccLogger.d(getClass().getName(), "External server response code: " + responseCode); + + input = con.getInputStream(); + length = con.getContentLengthLong(); + if (length <= 0) { + length = con.getContentLength(); + } + YaaccLogger.d(getClass().getName(), "External connection established on attempt " + (i + 1) + ", content length: " + length); + break; // Success, exit retry loop + } catch (Exception e) { + lastException = e; + YaaccLogger.w(getClass().getName(), "Connection attempt " + (i + 1) + " failed: " + e.getMessage()); + if (i < retries - 1) { + try { + Thread.sleep(1000); + } catch (InterruptedException ignored) { + } + } + } + } + + if (input == null && lastException != null) { + throw new IOException("Failed to connect after " + retries + " attempts", lastException); + } + } + } catch (IOException e) { + YaaccLogger.e(getClass().getName(), "Error opening external content", e); + } + return this; + } + + @Override + public long getContentLength() { + return length; + } + + @Override + protected int availableData() { + // For external URLs, always indicate data is available if stream is open + // input.available() is unreliable for HTTP streams + return input != null ? 8192 : 0; + } + + @Override + protected void produceData(final StreamChannel channel) throws IOException { + try { + if (input != null) { + byte[] buffer = new byte[8192]; + int read = input.read(buffer); + if (read > 0) { + bytesRead += read; + channel.write(ByteBuffer.wrap(buffer, 0, read)); + } else { + input.close(); + channel.endStream(); + } + } else { + YaaccLogger.w(getClass().getName(), "Input stream is null, ending stream"); + channel.endStream(); + } + } catch (IOException e) { + YaaccLogger.e(getClass().getName(), "Error reading external content", e); + if (input != null) { + try { + input.close(); + } catch (IOException ignored) { + } + } + channel.endStream(); + } + } + + @Override + public boolean isRepeatable() { + return true; // Keep as repeatable for compatibility with renderers + } + + @Override + public void failed(final Exception cause) { + YaaccLogger.e(getClass().getName(), "External content streaming failed", cause); + if (input != null) { + try { + input.close(); + } catch (IOException ignored) { + } + } + } + }.init(); + } else { + // Handle local files and SAF content + File file = new File(getUri()); + if (file.exists()) { + if (ranges.isEmpty()) { + result = AsyncEntityProducers.create(file, ContentType.parse(getMimeType().toString())); + YaaccLogger.d(getClass().getName(), "Return without range request file-Uri: " + getUri() + + " Mimetype: " + getMimeType()); + } else { + // For large files (>50MB), use streaming instead of loading into memory + long fileSize = file.length(); + HttpRange range = ranges.get(0); + long rangeSize = fileSize; + if (range.getEnd() != null && range.getEnd() > 0) { + rangeSize = range.getEnd() - (range.getStart() != null ? range.getStart() : 0) + 1; + } + + if (fileSize > 50 * 1024 * 1024 || rangeSize > 50 * 1024 * 1024) { // 50MB threshold + YaaccLogger.d(getClass().getName(), "Using streaming for large file: " + fileSize + " bytes, range: " + rangeSize + " bytes"); + result = createStreamingEntityProducer(file, ranges); + } else { + result = AsyncEntityProducers.create(readRangeFormFile(file, ranges), + ContentType.parse(getMimeType().toString())); + } + } + } else { + // DocumentFile handling - need to read content through InputStream + try { + Uri uri = Uri.parse(getUri()); + // For DocumentFile, we need to handle it differently since AsyncEntityProducers doesn't support it directly + // We'll read the content as bytes and create from that + if (ranges.isEmpty()) { + // DocumentFile handling using ContentResolver + result = new AbstractBinAsyncEntityProducer(8192, ContentType.parse(getMimeType().toString())) { + private InputStream input; + private long length = -1; + + AbstractBinAsyncEntityProducer init() { + try { + input = context.getContentResolver().openInputStream(uri); + DocumentFile docFile = DocumentFile.fromSingleUri(context, uri); + if (docFile != null) { + length = docFile.length(); + } + } catch (IOException e) { + YaaccLogger.e(getClass().getName(), "Error opening DocumentFile", e); + } + return this; + } + + @Override + public long getContentLength() { + return length; + } + + @Override + protected int availableData() { + try { + return input != null ? input.available() : 0; + } catch (IOException e) { + return 0; + } + } + + @Override + protected void produceData(final StreamChannel channel) throws IOException { + if (input != null) { + byte[] buffer = new byte[8192]; + int bytesRead = input.read(buffer); + if (bytesRead > 0) { + channel.write(ByteBuffer.wrap(buffer, 0, bytesRead)); + } else { + input.close(); + channel.endStream(); + } + } else { + channel.endStream(); + } + } + + @Override + public boolean isRepeatable() { + return true; + } + + @Override + public void failed(final Exception cause) { + } + }.init(); + } else { + // Range requests for DocumentFile not implemented yet + result = AsyncEntityProducers.create("DocumentFile range requests not implemented".getBytes(), + ContentType.parse(getMimeType().toString())); + } + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Error handling DocumentFile", e); + } + } + } + } else if (content != null) { + result = AsyncEntityProducers.create(content, ContentType.parse(getMimeType().toString())); + } + + if (result == null) { + YaaccLogger.d(getClass().getName(), "Resource is null"); + return AsyncEntityProducers.create("

Resource not found

", + ContentType.TEXT_HTML); + } + return result; + } + + private AsyncEntityProducer createStreamingEntityProducer(File file, List ranges) { + return new AbstractBinAsyncEntityProducer(8192, ContentType.parse(getMimeType().toString())) { + private RandomAccessFile raf; + private long startPosition = 0; + private long rangeLength; + private long bytesRead = 0; + + { + try { + raf = new RandomAccessFile(file, "r"); + long fileSize = raf.length(); + + if (!ranges.isEmpty()) { + HttpRange range = ranges.get(0); + startPosition = range.getStart() == null ? 0 : range.getStart(); + if (range.getEnd() == null || range.getEnd() == 0) { + rangeLength = fileSize - startPosition; + } else { + rangeLength = range.getEnd() - startPosition + 1; + } + if (range.getSuffixLength() != null && range.getSuffixLength() > 0) { + startPosition = fileSize - range.getSuffixLength(); + rangeLength = range.getSuffixLength(); + } + } else { + rangeLength = fileSize; + } + + raf.seek(startPosition); + } catch (IOException e) { + YaaccLogger.e(getClass().getName(), "Error initializing streaming producer", e); + } + } + + @Override + public long getContentLength() { + return rangeLength; + } + + @Override + protected int availableData() { + return (bytesRead < rangeLength) ? 8192 : 0; + } + + @Override + protected void produceData(final StreamChannel channel) throws IOException { + if (raf != null && bytesRead < rangeLength) { + byte[] buffer = new byte[8192]; + int toRead = (int) Math.min(buffer.length, rangeLength - bytesRead); + int read = raf.read(buffer, 0, toRead); + if (read > 0) { + bytesRead += read; + channel.write(ByteBuffer.wrap(buffer, 0, read)); + } else { + raf.close(); + channel.endStream(); + } + } else { + if (raf != null) { + raf.close(); + } + channel.endStream(); + } + } + + @Override + public void failed(final Exception cause) { + YaaccLogger.e(getClass().getName(), "Streaming failed", cause); + if (raf != null) { + try { + raf.close(); + } catch (IOException ignored) { + } + } + } + + @Override + public boolean isRepeatable() { + return false; // File streaming is not repeatable + } + }; + } + } + + /** + * ContentHolder for live streams (no known length). + */ + static class LiveStreamContentHolder extends ContentHolder { + private final java.io.InputStream inputStream; + private final boolean isVideo; + + public LiveStreamContentHolder(MimeType mimeType, java.io.InputStream inputStream, Context context) { + this(mimeType, inputStream, context, false); + } + + public LiveStreamContentHolder(MimeType mimeType, java.io.InputStream inputStream, Context context, boolean isVideo) { + super(mimeType, (byte[]) null, java.util.Collections.emptyList(), context); + this.inputStream = inputStream; + this.isVideo = isVideo; + } + + @Override + public long getContentLength() { + return -1; // Unknown length for live streams + } + + @Override + public AsyncEntityProducer getEntityProducer() { + return new AbstractBinAsyncEntityProducer(8192, ContentType.parse(mimeType.toString())) { + private boolean endOfStream = false; + + @Override + public boolean isRepeatable() { + return false; // Live streams cannot be repeated + } + + @Override + protected int availableData() { + if (endOfStream) { + return 0; + } + try { + int available = inputStream.available(); + return available > 0 ? available : 1; // Always indicate data might be available + } catch (java.io.IOException e) { + YaaccLogger.e(getClass().getName(), "Error checking available data", e); + endOfStream = true; + return 0; + } + } + + @Override + protected void produceData(org.apache.hc.core5.http.nio.StreamChannel channel) throws java.io.IOException { + byte[] buffer = new byte[8192]; + int bytesRead = inputStream.read(buffer); + if (bytesRead > 0) { + channel.write(java.nio.ByteBuffer.wrap(buffer, 0, bytesRead)); + YaaccLogger.v(getClass().getName(), "Streamed " + bytesRead + " bytes"); + } else if (bytesRead == -1) { + endOfStream = true; + channel.endStream(); + } + } + + @Override + public void failed(Exception cause) { + YaaccLogger.e(getClass().getName(), "Live stream failed", cause); + try { + inputStream.close(); + } catch (java.io.IOException ignored) { + } + } + + @Override + public void releaseResources() { + YaaccLogger.d(getClass().getName(), "Releasing live stream resources"); + try { + inputStream.close(); + } catch (java.io.IOException ignored) { + } + } + }; + } + } + + static class ByteArrayContentHolder extends ContentHolder { + private final byte[] data; + + ByteArrayContentHolder(MimeType mimeType, byte[] data, Context context) { + super(mimeType, data, null, context); + this.data = data; + } + + @Override + public AsyncEntityProducer getEntityProducer() { + return new AbstractBinAsyncEntityProducer(0, ContentType.parse(mimeType.toString())) { + private int position = 0; + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + protected int availableData() { + return data.length - position; + } + + @Override + protected void produceData(StreamChannel channel) throws IOException { + if (position < data.length) { + int remaining = data.length - position; + ByteBuffer buffer = ByteBuffer.wrap(data, position, remaining); + channel.write(buffer); + position += remaining; + channel.endStream(); + } + } + + @Override + public void failed(Exception cause) { + } + + @Override + public void releaseResources() { + } + }; + } + } + + static class FileContentHolder extends ContentHolder { + private final java.io.File file; + private final java.io.FileInputStream fis; + + FileContentHolder(MimeType mimeType, java.io.File file, java.io.FileInputStream fis, Context context) { + super(mimeType, file.getAbsolutePath(), null, context); + this.file = file; + this.fis = fis; + } + + @Override + public AsyncEntityProducer getEntityProducer() { + return new AbstractBinAsyncEntityProducer(8192, ContentType.parse(mimeType.toString())) { + private final byte[] buffer = new byte[8192]; + + @Override + public boolean isRepeatable() { + return false; + } + + @Override + protected int availableData() { + try { + return fis.available(); + } catch (IOException e) { + return 0; + } + } + + @Override + protected void produceData(StreamChannel channel) throws IOException { + int bytesRead = fis.read(buffer); + if (bytesRead > 0) { + channel.write(ByteBuffer.wrap(buffer, 0, bytesRead)); + } + if (bytesRead == -1) { + channel.endStream(); + } + } + + @Override + public void failed(Exception cause) { + try { + fis.close(); + } catch (IOException ignored) { + } + } + + @Override + public void releaseResources() { + try { + fis.close(); + } catch (IOException ignored) { + } + } + }; + } + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/YaaccAsyncStreamServerRequestHandler.java b/yaacc/src/main/java/de/yaacc/upnp/server/http/YaaccUpnpServerProtocolRequestHandler.java similarity index 51% rename from yaacc/src/main/java/de/yaacc/upnp/YaaccAsyncStreamServerRequestHandler.java rename to yaacc/src/main/java/de/yaacc/upnp/server/http/YaaccUpnpServerProtocolRequestHandler.java index 9c80ec0a..048a4ca0 100644 --- a/yaacc/src/main/java/de/yaacc/upnp/YaaccAsyncStreamServerRequestHandler.java +++ b/yaacc/src/main/java/de/yaacc/upnp/server/http/YaaccUpnpServerProtocolRequestHandler.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ /* * * Copyright (C) 2023 Tobias Schoene www.yaacc.de @@ -16,9 +34,7 @@ * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ -package de.yaacc.upnp; - -import android.util.Log; +package de.yaacc.upnp.server.http; import org.apache.hc.core5.http.ContentType; import org.apache.hc.core5.http.EntityDetails; @@ -39,20 +55,25 @@ import org.fourthline.cling.model.message.UpnpHeaders; import org.fourthline.cling.model.message.UpnpMessage; import org.fourthline.cling.model.message.UpnpRequest; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.transport.spi.UpnpStream; -import org.seamless.util.Exceptions; +import org.fourthline.cling.model.message.UpnpResponse; import java.io.IOException; import java.net.URI; import java.util.List; import java.util.Map; -public class YaaccAsyncStreamServerRequestHandler extends UpnpStream implements AsyncServerRequestHandler> { +import de.yaacc.upnp.protocol.ProtocolCreationException; +import de.yaacc.upnp.protocol.ReceivingSync; +import de.yaacc.upnp.protocol.UpnpProtocolHandler; +import de.yaacc.util.YaaccLogger; + +public class YaaccUpnpServerProtocolRequestHandler implements AsyncServerRequestHandler> { + protected ReceivingSync syncProtocol; + private final UpnpProtocolHandler upnpProtocolHandler; - protected YaaccAsyncStreamServerRequestHandler(ProtocolFactory protocolFactory) { - super(protocolFactory); + public YaaccUpnpServerProtocolRequestHandler(UpnpProtocolHandler upnpProtocolHandler) { + this.upnpProtocolHandler = upnpProtocolHandler; } @Override @@ -72,26 +93,26 @@ public void handle( try { StreamRequestMessage requestMessage = readRequestMessage(message); - Log.v(getClass().getName(), "Processing new request message: " + requestMessage + " body: " + requestMessage.getBodyString()); + YaaccLogger.v(getClass().getName(), "Processing new request message: " + requestMessage + " body: " + requestMessage.getBodyString()); StreamResponseMessage responseMessage = process(requestMessage); if (responseMessage != null) { - Log.v(getClass().getName(), "Preparing HTTP response message: " + responseMessage + " body: " + responseMessage.getBodyString()); + YaaccLogger.v(getClass().getName(), "Preparing HTTP response message: " + responseMessage + " body: " + responseMessage.getBodyString()); writeResponseMessage(responseMessage, responseBuilder); } else { // If it's null, it's 404 - Log.v(getClass().getName(), "Sending HTTP response status: " + HttpStatus.SC_NOT_FOUND); + YaaccLogger.v(getClass().getName(), "Sending HTTP response status: " + HttpStatus.SC_NOT_FOUND); responseBuilder.setStatus(HttpStatus.SC_NOT_FOUND); } responseTrigger.submitResponse(responseBuilder.build(), context); } catch (Throwable t) { - Log.i(getClass().getName(), "Exception occurred during UPnP stream processing: " + t); - Log.d(getClass().getName(), "Cause: " + Exceptions.unwrap(t), Exceptions.unwrap(t)); - Log.v(getClass().getName(), "returning INTERNAL SERVER ERROR to client"); + YaaccLogger.i(getClass().getName(), "Exception occurred during UPnP stream processing: " + t); + YaaccLogger.d(getClass().getName(), "Cause: " + unwrap(t), unwrap(t)); + YaaccLogger.v(getClass().getName(), "returning INTERNAL SERVER ERROR to client"); responseBuilder.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); responseTrigger.submitResponse(responseBuilder.build(), context); @@ -105,7 +126,7 @@ protected StreamRequestMessage readRequestMessage(Message m String requestMethod = request.getMethod(); String requestURI = request.getRequestUri(); - Log.v(getClass().getName(), "Processing HTTP request: " + requestMethod + " " + requestURI); + YaaccLogger.v(getClass().getName(), "Processing HTTP request: " + requestMethod + " " + requestURI); StreamRequestMessage requestMessage; try { @@ -135,27 +156,27 @@ protected StreamRequestMessage readRequestMessage(Message m if (bodyBytes == null) { bodyBytes = new byte[]{}; } - Log.v(getClass().getName(), "Reading request body bytes: " + bodyBytes.length); + YaaccLogger.v(getClass().getName(), "Reading request body bytes: " + bodyBytes.length); if (bodyBytes.length > 0 && requestMessage.isContentTypeMissingOrText()) { - Log.v(getClass().getName(), "Request contains textual entity body, converting then setting string on message"); + YaaccLogger.v(getClass().getName(), "Request contains textual entity body, converting then setting string on message"); requestMessage.setBodyCharacters(bodyBytes); } else if (bodyBytes.length > 0) { - Log.v(getClass().getName(), "Request contains binary entity body, setting bytes on message"); + YaaccLogger.v(getClass().getName(), "Request contains binary entity body, setting bytes on message"); requestMessage.setBody(UpnpMessage.BodyType.BYTES, bodyBytes); } else { - Log.v(getClass().getName(), "Request did not contain entity body"); + YaaccLogger.v(getClass().getName(), "Request did not contain entity body"); } return requestMessage; } protected void writeResponseMessage(StreamResponseMessage responseMessage, AsyncResponseBuilder responseBuilder) { - Log.v(getClass().getName(), "Sending HTTP response status: " + responseMessage.getOperation().getStatusCode()); + YaaccLogger.v(getClass().getName(), "Sending HTTP response status: " + responseMessage.getOperation().getStatusCode()); responseBuilder.setStatus(responseMessage.getOperation().getStatusCode()); @@ -173,7 +194,7 @@ protected void writeResponseMessage(StreamResponseMessage responseMessage, Async int contentLength = responseBodyBytes != null ? responseBodyBytes.length : -1; if (contentLength > 0) { - Log.v(getClass().getName(), "Response message has body, writing bytes to stream..."); + YaaccLogger.v(getClass().getName(), "Response message has body, writing bytes to stream..."); ContentType ct = ContentType.APPLICATION_XML; if (responseMessage.getContentTypeHeader() != null) { ct = ContentType.parse(responseMessage.getContentTypeHeader().getValue().toString()); @@ -182,8 +203,78 @@ protected void writeResponseMessage(StreamResponseMessage responseMessage, Async } } + + /** + * Selects a UPnP protocol, runs it within the calling thread, returns the response. + *

+ * This method will return null if the UPnP protocol returned null. + * The HTTP response in this case is always 404 NOT FOUND. Any other (HTTP) error + * condition will be encapsulated in the returned response message and has to be + * passed to the HTTP client as it is. + *

+ * + * @param requestMsg The TCP (HTTP) stream request message. + * @return The TCP (HTTP) stream response message, or null if a 404 should be send to the client. + */ + public StreamResponseMessage process(StreamRequestMessage requestMsg) { + YaaccLogger.v(getClass().getName(), "Processing stream request message: " + requestMsg); + + try { + // Try to get a protocol implementation that matches the request message + syncProtocol = upnpProtocolHandler.createReceivingSync(requestMsg); + } catch (ProtocolCreationException ex) { + YaaccLogger.w(getClass().getName(), "Processing stream request failed - " + unwrap(ex).toString()); + return new StreamResponseMessage(UpnpResponse.Status.NOT_IMPLEMENTED); + } + + // Run it + YaaccLogger.v(getClass().getName(), "Running protocol for synchronous message processing: " + syncProtocol); + syncProtocol.run(); + + // ... then grab the response + StreamResponseMessage responseMsg = syncProtocol.getOutputMessage(); + + if (responseMsg == null) { + // That's ok, the caller is supposed to handle this properly (e.g. convert it to HTTP 404) + YaaccLogger.v(getClass().getName(), "Protocol did not return any response message"); + return null; + } + YaaccLogger.v(getClass().getName(), "Protocol returned response: " + responseMsg); + return responseMsg; + } + + /** + * Must be called by a subclass after the response has been successfully sent to the client. + * + * @param responseMessage The response message successfully sent to the client. + */ + protected void responseSent(StreamResponseMessage responseMessage) { + if (syncProtocol != null) + syncProtocol.responseSent(responseMessage); + } + + /** + * Must be called by a subclass if the response was not delivered to the client. + * + * @param t The reason why the response wasn't delivered. + */ + protected void responseException(Throwable t) { + if (syncProtocol != null) + syncProtocol.responseException(t); + } + @Override - public void run() { - //FIXME why this has to be a runnable? + public String toString() { + return "(" + getClass().getSimpleName() + ")"; + } + + public static Throwable unwrap(Throwable throwable) throws IllegalArgumentException { + if (throwable == null) { + throw new IllegalArgumentException("Cannot unwrap null throwable"); + } + for (Throwable current = throwable; current != null; current = current.getCause()) { + throwable = current; + } + return throwable; } } diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/media/CombinedCaptureService.java b/yaacc/src/main/java/de/yaacc/upnp/server/media/CombinedCaptureService.java new file mode 100644 index 00000000..bafbd37d --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/media/CombinedCaptureService.java @@ -0,0 +1,377 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.media; + +import android.content.Context; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.AudioAttributes; +import android.media.AudioFormat; +import android.media.AudioPlaybackCaptureConfiguration; +import android.media.AudioRecord; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.media.projection.MediaProjection; +import android.view.Surface; + +import java.io.File; +import java.io.IOException; +import java.nio.ByteBuffer; + +import de.yaacc.util.YaaccLogger; + +/** + * Combined video+audio capture using MediaCodec and MediaMuxer + * Creates MP4 segments with H.264 video and AAC audio + * + * @author tobexyz + */ +@androidx.annotation.RequiresApi(api = android.os.Build.VERSION_CODES.Q) +public class CombinedCaptureService { + + private static final int WIDTH = 1280; + private static final int HEIGHT = 720; + private static final int VIDEO_FPS = 30; + private static final int VIDEO_BITRATE = 2000000; + private static final int AUDIO_SAMPLE_RATE = 44100; + private static final int AUDIO_BITRATE = 128000; + + private final Context context; + private MediaProjection mediaProjection; + private VirtualDisplay virtualDisplay; + private MediaCodec videoEncoder; + private MediaCodec audioEncoder; + private AudioRecord audioRecord; + private volatile boolean isCapturing = false; + private Thread videoThread; + private Thread audioThread; + + private FragmentedMp4Muxer muxer; + private boolean muxerStarted = false; + private final Object muxerLock = new Object(); + + private MediaFormat videoFormat; + private MediaFormat audioFormat; + private File outputFile; + private int audioSampleCount = 0; + private ByteBuffer spsBuffer; + private ByteBuffer ppsBuffer; + private byte[] audioConfigData; + + public CombinedCaptureService(Context context) { + this.context = context; + } + + public void startCapture(MediaProjection projection) throws IOException { + if (isCapturing) return; + + this.mediaProjection = projection; + isCapturing = true; + + setupVideoEncoder(); + setupAudioEncoder(); + setupAudioCapture(); + + outputFile = new File(context.getCacheDir(), "combined_stream.ts"); + if (outputFile.exists()) { + outputFile.delete(); + } + + videoThread = new Thread(this::encodeVideo); + audioThread = new Thread(this::encodeAudio); + + videoThread.start(); + audioThread.start(); + + // Wait for formats + int retries = 50; + while ((videoFormat == null || audioFormat == null) && retries-- > 0) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + break; + } + } + + if (videoFormat == null || audioFormat == null) { + YaaccLogger.e(getClass().getName(), "Failed to get encoder formats"); + throw new IOException("Encoder formats not available"); + } + + try { + synchronized (muxerLock) { + muxer = new FragmentedMp4Muxer(outputFile); + if (audioConfigData != null) { + muxer.setAudioConfig(audioConfigData); + } + muxer.start(); + muxerStarted = true; + YaaccLogger.i(getClass().getName(), "FragmentedMp4Muxer started"); + } + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to start muxer", e); + throw new IOException("Muxer failed", e); + } + + YaaccLogger.i(getClass().getName(), "Combined capture started with MPEG-TS streaming"); + } + + private void setupVideoEncoder() throws IOException { + MediaFormat format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, WIDTH, HEIGHT); + format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); + format.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_BITRATE); + format.setInteger(MediaFormat.KEY_FRAME_RATE, VIDEO_FPS); + format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); + + videoEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC); + videoEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + Surface surface = videoEncoder.createInputSurface(); + videoEncoder.start(); + + virtualDisplay = mediaProjection.createVirtualDisplay( + "YAACC-Combined", + WIDTH, HEIGHT, context.getResources().getDisplayMetrics().densityDpi, + DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR | DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION, + surface, null, null); + } + + private void setupAudioEncoder() throws IOException { + MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, AUDIO_SAMPLE_RATE, 2); + format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); + format.setInteger(MediaFormat.KEY_BIT_RATE, AUDIO_BITRATE); + + audioEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC); + audioEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + audioEncoder.start(); + } + + private void setupAudioCapture() { + AudioPlaybackCaptureConfiguration config = new AudioPlaybackCaptureConfiguration.Builder(mediaProjection) + .addMatchingUsage(AudioAttributes.USAGE_MEDIA) + .addMatchingUsage(AudioAttributes.USAGE_GAME) + .addMatchingUsage(AudioAttributes.USAGE_UNKNOWN) + .build(); + + AudioFormat format = new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(AUDIO_SAMPLE_RATE) + .setChannelMask(AudioFormat.CHANNEL_IN_STEREO) + .build(); + + int bufferSize = AudioRecord.getMinBufferSize(AUDIO_SAMPLE_RATE, + AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT) * 4; + + audioRecord = new AudioRecord.Builder() + .setAudioFormat(format) + .setBufferSizeInBytes(bufferSize) + .setAudioPlaybackCaptureConfig(config) + .build(); + + audioRecord.startRecording(); + } + + private void encodeVideo() { + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + int frameCount = 0; + + while (isCapturing) { + try { + int outputIndex = videoEncoder.dequeueOutputBuffer(bufferInfo, 10000); + if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + videoFormat = videoEncoder.getOutputFormat(); + spsBuffer = videoFormat.getByteBuffer("csd-0"); + ppsBuffer = videoFormat.getByteBuffer("csd-1"); + YaaccLogger.i(getClass().getName(), "Video format changed, SPS/PPS extracted"); + } else if (outputIndex >= 0) { + ByteBuffer outputBuffer = videoEncoder.getOutputBuffer(outputIndex); + if (outputBuffer != null && bufferInfo.size > 0) { + synchronized (muxerLock) { + if (muxerStarted) { + boolean keyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0; + + // Prepend SPS/PPS to keyframes + if (keyFrame && spsBuffer != null && ppsBuffer != null) { + outputBuffer.position(bufferInfo.offset); + outputBuffer.limit(bufferInfo.offset + bufferInfo.size); + + int spsSize = spsBuffer.limit(); + int ppsSize = ppsBuffer.limit(); + int frameSize = bufferInfo.size; + int totalSize = spsSize + ppsSize + frameSize; + + ByteBuffer combined = ByteBuffer.allocate(totalSize); + spsBuffer.position(0); + ppsBuffer.position(0); + combined.put(spsBuffer); + combined.put(ppsBuffer); + combined.put(outputBuffer); + combined.flip(); + muxer.writeVideoSample(combined, bufferInfo.presentationTimeUs, keyFrame); + } else { + outputBuffer.position(bufferInfo.offset); + outputBuffer.limit(bufferInfo.offset + bufferInfo.size); + muxer.writeVideoSample(outputBuffer, bufferInfo.presentationTimeUs, keyFrame); + } + + if (++frameCount % 30 == 0) { + YaaccLogger.i(getClass().getName(), "Video frames written: " + frameCount + ", size: " + bufferInfo.size + ", keyframe: " + keyFrame); + } + if (keyFrame) { + YaaccLogger.i(getClass().getName(), "Keyframe at frame " + frameCount); + } + } + } + } + videoEncoder.releaseOutputBuffer(outputIndex, false); + } + } catch (Exception e) { + if (isCapturing) { + YaaccLogger.e(getClass().getName(), "Video encoding error", e); + } + } + } + } + + private void encodeAudio() { + byte[] buffer = new byte[960 * 2 * 2]; + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + long audioTimestamp = 0; + + while (isCapturing) { + try { + int read = audioRecord.read(buffer, 0, buffer.length); + if (read > 0) { + if (audioSampleCount++ % 100 == 0) { + YaaccLogger.i(getClass().getName(), "AudioRecord read: " + read + " bytes"); + } + int inputIndex = audioEncoder.dequeueInputBuffer(10000); + if (inputIndex >= 0) { + ByteBuffer inputBuffer = audioEncoder.getInputBuffer(inputIndex); + if (inputBuffer.remaining() >= read) { + inputBuffer.clear(); + inputBuffer.put(buffer, 0, read); + audioEncoder.queueInputBuffer(inputIndex, 0, read, audioTimestamp, 0); + audioTimestamp += (read / 4) * 1000000L / AUDIO_SAMPLE_RATE; + } + } + } + + int outputIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0); + if (outputIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { + audioFormat = audioEncoder.getOutputFormat(); + YaaccLogger.i(getClass().getName(), "Audio format changed: " + audioFormat); + if (audioFormat.containsKey("csd-0")) { + ByteBuffer csd = audioFormat.getByteBuffer("csd-0"); + audioConfigData = new byte[csd.remaining()]; + csd.get(audioConfigData); + YaaccLogger.i(getClass().getName(), "CSD-0 size: " + audioConfigData.length); + synchronized (muxerLock) { + if (muxer != null) muxer.setAudioConfig(audioConfigData); + } + } + } else if (outputIndex >= 0) { + ByteBuffer outputBuffer = audioEncoder.getOutputBuffer(outputIndex); + if (outputBuffer != null && bufferInfo.size > 0) { + YaaccLogger.i(getClass().getName(), "Audio sample: " + bufferInfo.size + " bytes"); + synchronized (muxerLock) { + if (muxerStarted) { + muxer.writeAudioSample(outputBuffer, bufferInfo.presentationTimeUs); + } + } + } + audioEncoder.releaseOutputBuffer(outputIndex, false); + } + } catch (Exception e) { + if (isCapturing) { + YaaccLogger.e(getClass().getName(), "Audio encoding error", e); + } + } + } + } + + public File getOutputFile() { + return outputFile; + } + + public FragmentedMp4Muxer getMuxer() { + return muxer; + } + + public void stopCapture() { + isCapturing = false; + + if (videoThread != null) { + try { + videoThread.join(1000); + } catch (InterruptedException e) { + } + } + if (audioThread != null) { + try { + audioThread.join(1000); + } catch (InterruptedException e) { + } + } + + synchronized (muxerLock) { + if (muxer != null && muxerStarted) { + try { + muxer.stop(); + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Error stopping muxer", e); + } + muxer = null; + } + } + + if (audioRecord != null) { + audioRecord.stop(); + audioRecord.release(); + audioRecord = null; + } + + if (videoEncoder != null) { + videoEncoder.stop(); + videoEncoder.release(); + videoEncoder = null; + } + + if (audioEncoder != null) { + audioEncoder.stop(); + audioEncoder.release(); + audioEncoder = null; + } + + if (virtualDisplay != null) { + virtualDisplay.release(); + virtualDisplay = null; + } + + if (outputFile != null && outputFile.exists()) { + outputFile.delete(); + } + + YaaccLogger.i(getClass().getName(), "Combined capture stopped"); + } + + public boolean isCapturing() { + return isCapturing; + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/media/FragmentedMp4Muxer.java b/yaacc/src/main/java/de/yaacc/upnp/server/media/FragmentedMp4Muxer.java new file mode 100644 index 00000000..55713a07 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/media/FragmentedMp4Muxer.java @@ -0,0 +1,364 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.media; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.ByteBuffer; + +import de.yaacc.util.YaaccLogger; + +public class FragmentedMp4Muxer { + + private static final int TS_PACKET_SIZE = 188; + private static final int VIDEO_PID = 0x101; + private static final int AUDIO_PID = 0x102; + private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB circular buffer + private static final int PAT_PMT_INTERVAL = 30; // Write PAT/PMT every N video frames + + private final File outputFile; + private RandomAccessFile outputStream; + private boolean started = false; + private int videoContinuity = 0; + private int audioContinuity = 0; + private int patContinuity = 0; + private int pmtContinuity = 0; + private byte[] audioSpecificConfig = null; + private long firstVideoPts = -1; + private long firstAudioPts = -1; + private long writePosition = 0; + private int videoFrameCount = 0; + private long lastKeyframePosition = 0; + private long lastRealPts = -1; + private long ptsOffset = 0; + + public FragmentedMp4Muxer(File outputFile) { + this.outputFile = outputFile; + } + + public void start() throws IOException { + outputStream = new RandomAccessFile(outputFile, "rw"); + outputStream.setLength(MAX_FILE_SIZE); // Pre-allocate + writePosition = 0; + firstVideoPts = -1; + firstAudioPts = -1; + videoContinuity = 0; + audioContinuity = 0; + patContinuity = 0; + pmtContinuity = 0; + writePAT(); + writePMT(); + started = true; + YaaccLogger.i(getClass().getName(), "MPEG-TS muxer started with " + MAX_FILE_SIZE + " byte circular buffer"); + } + + public void setAudioConfig(byte[] config) { + YaaccLogger.i(getClass().getName(), "setAudioConfig called with " + (config != null ? config.length : 0) + " bytes"); + this.audioSpecificConfig = config; + if (config != null && config.length >= 2) { + int profile = ((config[0] >> 3) & 0x1F); + int freqIndex = ((config[0] & 0x07) << 1) | ((config[1] >> 7) & 0x01); + int channelConfig = (config[1] >> 3) & 0x0F; + YaaccLogger.i(getClass().getName(), "AAC config: profile=" + profile + " freq=" + freqIndex + " channels=" + channelConfig + " bytes=" + String.format("%02X %02X", config[0], config[1])); + } + } + + private void writePAT() throws IOException { + byte[] packet = new byte[TS_PACKET_SIZE]; + packet[0] = 0x47; + packet[1] = 0x40; + packet[2] = 0x00; + packet[3] = (byte) (0x10 | (patContinuity & 0x0F)); + packet[4] = 0x00; + packet[5] = 0x00; + packet[6] = (byte) 0xB0; + packet[7] = 0x0D; + packet[8] = 0x00; + packet[9] = 0x01; + packet[10] = (byte) 0xC1; + packet[11] = 0x00; + packet[12] = 0x00; + packet[13] = 0x00; + packet[14] = 0x01; + packet[15] = (byte) 0xE1; + packet[16] = 0x00; + int crc = crc32(packet, 5, 12); + packet[17] = (byte) (crc >> 24); + packet[18] = (byte) (crc >> 16); + packet[19] = (byte) (crc >> 8); + packet[20] = (byte) crc; + for (int i = 21; i < TS_PACKET_SIZE; i++) packet[i] = (byte) 0xFF; + writePacket(packet); + patContinuity = (patContinuity + 1) & 0x0F; + } + + private void writePMT() throws IOException { + byte[] packet = new byte[TS_PACKET_SIZE]; + packet[0] = 0x47; + packet[1] = 0x41; + packet[2] = 0x00; + packet[3] = (byte) (0x10 | (pmtContinuity & 0x0F)); + packet[4] = 0x00; + packet[5] = 0x02; + packet[6] = (byte) 0xB0; + packet[7] = 0x17; + packet[8] = 0x00; + packet[9] = 0x01; + packet[10] = (byte) 0xC1; + packet[11] = 0x00; + packet[12] = 0x00; + packet[13] = (byte) 0xE1; + packet[14] = 0x01; + packet[15] = (byte) 0xF0; + packet[16] = 0x00; + // Video stream + packet[17] = 0x1B; + packet[18] = (byte) 0xE1; + packet[19] = 0x01; + packet[20] = (byte) 0xF0; + packet[21] = 0x00; + // Audio stream - AAC with ADTS + packet[22] = 0x0F; + packet[23] = (byte) 0xE1; + packet[24] = 0x02; + packet[25] = (byte) 0xF0; + packet[26] = 0x00; + + int crc = crc32(packet, 5, 22); + packet[27] = (byte) (crc >> 24); + packet[28] = (byte) (crc >> 16); + packet[29] = (byte) (crc >> 8); + packet[30] = (byte) crc; + for (int i = 31; i < TS_PACKET_SIZE; i++) packet[i] = (byte) 0xFF; + writePacket(packet); + pmtContinuity = (pmtContinuity + 1) & 0x0F; + } + + private void writePacket(byte[] packet) throws IOException { + outputStream.seek(writePosition); + outputStream.write(packet); + writePosition = (writePosition + TS_PACKET_SIZE) % MAX_FILE_SIZE; + } + + public synchronized void writeVideoSample(ByteBuffer buffer, long pts, boolean keyFrame) throws IOException { + if (!started) return; + + // Handle PTS discontinuities by tracking offset + if (firstVideoPts < 0) { + firstVideoPts = pts; + lastRealPts = pts; + ptsOffset = 0; + YaaccLogger.i(getClass().getName(), "First video PTS: " + pts); + } else { + // Detect discontinuity (jump > 5 seconds) + long delta = pts - lastRealPts; + if (delta < 0 || delta > 450000) { // 5 seconds at 90kHz + ptsOffset += (lastRealPts - firstVideoPts); + firstVideoPts = pts; + YaaccLogger.w(getClass().getName(), "PTS discontinuity detected, adjusting offset to " + ptsOffset); + } + lastRealPts = pts; + } + + // Write PAT/PMT at keyframes for clients joining mid-stream + if (keyFrame) { + lastKeyframePosition = writePosition; // Save position before PAT/PMT + // Reset PTS at keyframes to prevent unbounded growth + firstVideoPts = pts; + firstAudioPts = -1; + ptsOffset = 0; + writePAT(); + writePMT(); + } + videoFrameCount++; + + // Use real PTS with offset correction + long continuousPts = (pts - firstVideoPts) + ptsOffset; + if (videoFrameCount % 30 == 0) { + YaaccLogger.i(getClass().getName(), "Frame " + videoFrameCount + " PTS: " + continuousPts + " (" + (continuousPts / 90000.0) + "s)"); + } + writePES(VIDEO_PID, 0xE0, buffer, continuousPts, videoContinuity, true); + } + + public long getLastKeyframePosition() { + return lastKeyframePosition; + } + + public synchronized void writeAudioSample(ByteBuffer buffer, long pts) throws IOException { + if (!started) return; + if (firstAudioPts < 0) firstAudioPts = pts; + + int frameSize = buffer.remaining(); + int packetLen = frameSize + 7; + + byte[] adts = new byte[packetLen]; + adts[0] = (byte) 0xFF; + adts[1] = (byte) 0xF1; + adts[2] = (byte) 0x50; + adts[3] = (byte) (0x80 | ((packetLen >> 11) & 0x03)); + adts[4] = (byte) ((packetLen >> 3) & 0xFF); + adts[5] = (byte) (((packetLen & 0x07) << 5) | 0x1F); + adts[6] = (byte) 0xFC; + buffer.get(adts, 7, frameSize); + + long relativePts = pts - firstAudioPts; + writePES(AUDIO_PID, 0xC0, ByteBuffer.wrap(adts), relativePts, audioContinuity, false); + } + + private void writeRawToTS(int pid, byte[] data, int startContinuity) throws IOException { + synchronized (outputStream) { + int offset = 0; + boolean first = true; + int continuity = startContinuity; + + while (offset < data.length) { + byte[] packet = new byte[TS_PACKET_SIZE]; + packet[0] = 0x47; + packet[1] = (byte) ((first ? 0x40 : 0x00) | ((pid >> 8) & 0x1F)); + packet[2] = (byte) (pid & 0xFF); + packet[3] = (byte) (0x10 | (continuity & 0x0F)); + + int toCopy = Math.min(TS_PACKET_SIZE - 4, data.length - offset); + System.arraycopy(data, offset, packet, 4, toCopy); + for (int i = 4 + toCopy; i < TS_PACKET_SIZE; i++) packet[i] = (byte) 0xFF; + + writePacket(packet); + offset += toCopy; + first = false; + continuity = (continuity + 1) & 0x0F; + } + + if (pid == AUDIO_PID) { + audioContinuity = continuity; + } + } + } + + private void writePES(int pid, int streamId, ByteBuffer data, long pts, int continuity, boolean includePCR) throws IOException { + synchronized (outputStream) { + int size = data.remaining(); + byte[] payload = new byte[size]; + data.get(payload); + + byte[] pes = new byte[14 + size]; + pes[0] = 0; + pes[1] = 0; + pes[2] = 1; + pes[3] = (byte) streamId; + // PES packet length: 0 for video (unbounded), actual length for audio + if (pid == VIDEO_PID) { + pes[4] = 0; + pes[5] = 0; + } else { + pes[4] = (byte) ((size + 8) >> 8); + pes[5] = (byte) (size + 8); + } + pes[6] = (byte) 0x80; + pes[7] = (byte) 0x80; + pes[8] = 0x05; + long ptsValue = pts * 9 / 100; + pes[9] = (byte) (0x21 | ((ptsValue >> 29) & 0x0E)); + pes[10] = (byte) ((ptsValue >> 22) & 0xFF); + pes[11] = (byte) (0x01 | ((ptsValue >> 14) & 0xFE)); + pes[12] = (byte) ((ptsValue >> 7) & 0xFF); + pes[13] = (byte) (0x01 | ((ptsValue << 1) & 0xFE)); + System.arraycopy(payload, 0, pes, 14, size); + + int offset = 0; + boolean first = true; + int packetContinuity = continuity; + + while (offset < pes.length) { + byte[] packet = new byte[TS_PACKET_SIZE]; + packet[0] = 0x47; + packet[1] = (byte) ((first ? 0x40 : 0x00) | ((pid >> 8) & 0x1F)); + packet[2] = (byte) (pid & 0xFF); + + int headerSize = 4; + + if (first && includePCR) { + packet[3] = (byte) (0x30 | (packetContinuity & 0x0F)); + packet[4] = 7; + packet[5] = 0x10; + + long pcrBase = ptsValue / 300; + int pcrExt = (int) (ptsValue % 300); + packet[6] = (byte) ((pcrBase >> 25) & 0xFF); + packet[7] = (byte) ((pcrBase >> 17) & 0xFF); + packet[8] = (byte) ((pcrBase >> 9) & 0xFF); + packet[9] = (byte) ((pcrBase >> 1) & 0xFF); + packet[10] = (byte) (((pcrBase & 1) << 7) | 0x7E | ((pcrExt >> 8) & 1)); + packet[11] = (byte) (pcrExt & 0xFF); + + headerSize = 12; + } else { + packet[3] = (byte) (0x10 | (packetContinuity & 0x0F)); + } + + int toCopy = Math.min(TS_PACKET_SIZE - headerSize, pes.length - offset); + System.arraycopy(pes, offset, packet, headerSize, toCopy); + for (int i = headerSize + toCopy; i < TS_PACKET_SIZE; i++) packet[i] = (byte) 0xFF; + + writePacket(packet); + offset += toCopy; + first = false; + packetContinuity = (packetContinuity + 1) & 0x0F; + } + + if (pid == VIDEO_PID) { + videoContinuity = packetContinuity; + } else if (pid == AUDIO_PID) { + audioContinuity = packetContinuity; + } + } + } + + public void stop() throws IOException { + started = false; + if (outputStream != null) { + outputStream.close(); + outputStream = null; + } + } + + public long getWritePosition() { + return writePosition; + } + + private static final int[] CRC_TABLE = new int[256]; + + static { + for (int i = 0; i < 256; i++) { + int crc = i << 24; + for (int j = 0; j < 8; j++) { + crc = (crc << 1) ^ ((crc & 0x80000000) != 0 ? 0x04C11DB7 : 0); + } + CRC_TABLE[i] = crc; + } + } + + private int crc32(byte[] data, int offset, int length) { + int crc = 0xFFFFFFFF; + for (int i = 0; i < length; i++) { + crc = (crc << 8) ^ CRC_TABLE[((crc >> 24) ^ (data[offset + i] & 0xFF)) & 0xFF]; + } + return crc; + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/media/MediaProjectionHelper.java b/yaacc/src/main/java/de/yaacc/upnp/server/media/MediaProjectionHelper.java new file mode 100644 index 00000000..00c21795 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/media/MediaProjectionHelper.java @@ -0,0 +1,197 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.media; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import de.yaacc.util.YaaccLogger; + +/** + * Manages MediaProjection for capturing system audio and screen. + * Requires Android 10+ (API 29). + * + * @author Tobias Schoene (tobexyz) + */ +@RequiresApi(api = Build.VERSION_CODES.Q) +public class MediaProjectionHelper { + + public static final int REQUEST_CODE_MEDIA_PROJECTION = 2001; + + private static MediaProjection mediaProjection; + private static MediaProjectionManager mediaProjectionManager; + private static int storedResultCode = Activity.RESULT_CANCELED; + private static Intent storedResultData = null; + + // Callback for when MediaProjection stops + public interface MediaProjectionStopCallback { + void onMediaProjectionStopped(); + } + + private static MediaProjectionStopCallback stopCallback; + + /** + * Set callback for when MediaProjection stops. + */ + public static void setStopCallback(MediaProjectionStopCallback callback) { + stopCallback = callback; + } + + /** + * Create intent to request MediaProjection permission. + */ + public static Intent createPermissionIntent(Context context) { + if (mediaProjectionManager == null) { + mediaProjectionManager = (MediaProjectionManager) + context.getSystemService(Context.MEDIA_PROJECTION_SERVICE); + } + return mediaProjectionManager.createScreenCaptureIntent(); + } + + /** + * Handle permission result from activity. + * Note: MediaProjection must be created from a foreground service. + */ + public static boolean handlePermissionResult(Context context, int resultCode, Intent data) { + if (resultCode != Activity.RESULT_OK || data == null) { + YaaccLogger.w(MediaProjectionHelper.class.getName(), + "MediaProjection permission denied"); + storedResultCode = Activity.RESULT_CANCELED; + storedResultData = null; + return false; + } + + // Store the result for later use by the service + storedResultCode = resultCode; + storedResultData = data; + + YaaccLogger.i(MediaProjectionHelper.class.getName(), + "MediaProjection permission granted, stored for service"); + return true; + } + + /** + * Create MediaProjection from a foreground service using stored permission. + * Must be called from a service with FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION. + */ + public static boolean createMediaProjectionFromStored(Context context) { + if (storedResultCode == Activity.RESULT_CANCELED || storedResultData == null) { + YaaccLogger.w(MediaProjectionHelper.class.getName(), + "No stored MediaProjection permission"); + return false; + } + return createMediaProjection(context, storedResultCode, storedResultData); + } + + /** + * Create MediaProjection from a foreground service. + * Must be called from a service with FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION. + */ + public static boolean createMediaProjection(Context context, int resultCode, Intent data) { + if (mediaProjectionManager == null) { + mediaProjectionManager = (MediaProjectionManager) + context.getSystemService(Context.MEDIA_PROJECTION_SERVICE); + } + + stopMediaProjection(); + + try { + mediaProjection = mediaProjectionManager.getMediaProjection(resultCode, data); + + if (mediaProjection != null) { + mediaProjection.registerCallback(new MediaProjection.Callback() { + @Override + public void onStop() { + YaaccLogger.i(MediaProjectionHelper.class.getName(), + "MediaProjection stopped"); + mediaProjection = null; + storedResultCode = Activity.RESULT_CANCELED; + storedResultData = null; + + // Notify callback + if (stopCallback != null) { + stopCallback.onMediaProjectionStopped(); + } + } + }, null); + + YaaccLogger.i(MediaProjectionHelper.class.getName(), + "MediaProjection created successfully"); + return true; + } + } catch (Exception e) { + YaaccLogger.e(MediaProjectionHelper.class.getName(), + "Failed to create MediaProjection", e); + } + + return false; + } + + /** + * Get current MediaProjection instance. + */ + public static MediaProjection getMediaProjection() { + return mediaProjection; + } + + /** + * Check if MediaProjection is active. + */ + public static boolean isActive() { + return mediaProjection != null; + } + + /** + * Check if we have stored permission (either active or pending). + */ + public static boolean hasPermission() { + return isActive() || (storedResultCode == Activity.RESULT_OK && storedResultData != null); + } + + /** + * Clear stored MediaProjection permission. + */ + public static void clearPermission() { + stopMediaProjection(); + storedResultCode = Activity.RESULT_CANCELED; + storedResultData = null; + YaaccLogger.i(MediaProjectionHelper.class.getName(), "MediaProjection permission cleared"); + } + + /** + * Stop MediaProjection and release resources. + */ + public static void stopMediaProjection() { + if (mediaProjection != null) { + try { + mediaProjection.stop(); + } catch (Exception e) { + YaaccLogger.e(MediaProjectionHelper.class.getName(), + "Error stopping MediaProjection", e); + } + mediaProjection = null; + } + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/media/ScreenCastCaptureService.java b/yaacc/src/main/java/de/yaacc/upnp/server/media/ScreenCastCaptureService.java new file mode 100644 index 00000000..69786035 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/media/ScreenCastCaptureService.java @@ -0,0 +1,237 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.media; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.PixelFormat; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.Image; +import android.media.ImageReader; +import android.media.projection.MediaProjection; +import android.os.Build; +import android.util.DisplayMetrics; +import android.view.WindowManager; + +import androidx.annotation.RequiresApi; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.nio.ByteBuffer; +import java.util.List; + +import de.yaacc.util.YaaccLogger; + +/** + * Captures screen as MJPEG stream (Android 10+). + * + * @author tobexyz + */ +@RequiresApi(api = Build.VERSION_CODES.Q) +public class ScreenCastCaptureService { + + private static final int FRAME_RATE = 15; + private static final int JPEG_QUALITY = 75; + + private final Context context; + private VirtualDisplay virtualDisplay; + private ImageReader imageReader; + private Thread captureThread; + private volatile boolean isCapturing = false; + private final List outputStreams = new java.util.concurrent.CopyOnWriteArrayList<>(); + + private int screenWidth; + private int screenHeight; + private int screenDensity; + + public ScreenCastCaptureService(Context context) { + this.context = context; + initScreenMetrics(); + } + + private void initScreenMetrics() { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics metrics = new DisplayMetrics(); + wm.getDefaultDisplay().getRealMetrics(metrics); + + screenWidth = 1280; + screenHeight = 720; + screenDensity = metrics.densityDpi; + } + + public boolean startCapture(MediaProjection mediaProjection) { + if (isCapturing) return false; + + try { + imageReader = ImageReader.newInstance(screenWidth, screenHeight, PixelFormat.RGBA_8888, 2); + + virtualDisplay = mediaProjection.createVirtualDisplay( + "YAACC-ScreenCapture", screenWidth, screenHeight, screenDensity, + DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, imageReader.getSurface(), null, null); + + isCapturing = true; + + captureThread = new Thread(this::captureLoop); + captureThread.setPriority(Thread.MAX_PRIORITY); + captureThread.start(); + + YaaccLogger.i(getClass().getName(), "Screen capture started (MJPEG)"); + return true; + + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to start capture", e); + stopCapture(); + return false; + } + } + + public void stopCapture() { + isCapturing = false; + + if (captureThread != null) { + try { + captureThread.join(1000); + } catch (InterruptedException e) { + } + captureThread = null; + } + + if (virtualDisplay != null) { + virtualDisplay.release(); + virtualDisplay = null; + } + + if (imageReader != null) { + imageReader.close(); + imageReader = null; + } + + for (PipedOutputStream stream : outputStreams) { + try { + stream.close(); + } catch (IOException e) { + } + } + outputStreams.clear(); + + YaaccLogger.i(getClass().getName(), "Capture stopped"); + } + + public java.io.InputStream createInputStream() throws IOException { + PipedOutputStream outputStream = new PipedOutputStream(); + PipedInputStream inputStream = new PipedInputStream(outputStream, 1024 * 1024); + + outputStreams.add(outputStream); + YaaccLogger.i(getClass().getName(), "Created stream, clients: " + outputStreams.size()); + + return inputStream; + } + + public boolean isCapturing() { + return isCapturing; + } + + private void captureLoop() { + long frameInterval = 1000 / FRAME_RATE; + YaaccLogger.i(getClass().getName(), "Capture loop started"); + + while (isCapturing && imageReader != null) { + long frameStart = System.currentTimeMillis(); + + try { + Image image = imageReader.acquireLatestImage(); + if (image != null) { + YaaccLogger.d(getClass().getName(), "Got image"); + try { + byte[] jpegData = imageToJpeg(image); + + if (jpegData != null && jpegData.length > 0) { + List deadStreams = new java.util.ArrayList<>(); + + for (PipedOutputStream stream : outputStreams) { + try { + String header = "--frame\r\n" + + "Content-Type: image/jpeg\r\n" + + "Content-Length: " + jpegData.length + "\r\n\r\n"; + stream.write(header.getBytes()); + stream.write(jpegData); + stream.write("\r\n".getBytes()); + stream.flush(); + } catch (IOException e) { + deadStreams.add(stream); + } + } + + for (PipedOutputStream dead : deadStreams) { + outputStreams.remove(dead); + try { + dead.close(); + } catch (IOException e) { + } + } + } + } finally { + image.close(); + } + } + + long elapsed = System.currentTimeMillis() - frameStart; + long sleep = frameInterval - elapsed; + if (sleep > 0) { + Thread.sleep(sleep); + } + + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } catch (Exception e) { + if (isCapturing) YaaccLogger.e(getClass().getName(), "Capture error", e); + } + } + } + + private byte[] imageToJpeg(Image image) { + try { + Image.Plane[] planes = image.getPlanes(); + ByteBuffer buffer = planes[0].getBuffer(); + int pixelStride = planes[0].getPixelStride(); + int rowStride = planes[0].getRowStride(); + int rowPadding = rowStride - pixelStride * screenWidth; + + Bitmap bitmap = Bitmap.createBitmap(screenWidth + rowPadding / pixelStride, screenHeight, Bitmap.Config.ARGB_8888); + bitmap.copyPixelsFromBuffer(buffer); + + if (rowPadding != 0) { + bitmap = Bitmap.createBitmap(bitmap, 0, 0, screenWidth, screenHeight); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + bitmap.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, out); + bitmap.recycle(); + + return out.toByteArray(); + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Error converting image", e); + return null; + } + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/media/SystemAudioCaptureService.java b/yaacc/src/main/java/de/yaacc/upnp/server/media/SystemAudioCaptureService.java new file mode 100644 index 00000000..75f6561a --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/media/SystemAudioCaptureService.java @@ -0,0 +1,229 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.media; + +import android.media.AudioFormat; +import android.media.AudioPlaybackCaptureConfiguration; +import android.media.AudioRecord; +import android.media.projection.MediaProjection; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.List; + +import de.yaacc.util.YaaccLogger; + +/** + * Captures system audio using AudioPlaybackCapture (Android 10+). + * Provides PCM audio data via InputStream. + * + * @author tobexyz + */ +@RequiresApi(api = Build.VERSION_CODES.Q) +public class SystemAudioCaptureService { + + private static final int SAMPLE_RATE = 44100; + private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; + private static final int AUDIO_FORMAT = AudioFormat.ENCODING_PCM_16BIT; + private static final int BUFFER_SIZE_MULTIPLIER = 8; // Larger buffer for stability + + private AudioRecord audioRecord; + private Thread captureThread; + private volatile boolean isCapturing = false; + private final List outputStreams = new java.util.concurrent.CopyOnWriteArrayList<>(); + + /** + * Start capturing system audio. + */ + public boolean startCapture(MediaProjection mediaProjection) { + if (isCapturing) { + YaaccLogger.w(getClass().getName(), "Already capturing"); + return false; + } + + try { + int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT); + bufferSize *= BUFFER_SIZE_MULTIPLIER; // Increase buffer for stability + + AudioPlaybackCaptureConfiguration config = new AudioPlaybackCaptureConfiguration.Builder(mediaProjection) + .addMatchingUsage(android.media.AudioAttributes.USAGE_MEDIA) + .addMatchingUsage(android.media.AudioAttributes.USAGE_GAME) + .addMatchingUsage(android.media.AudioAttributes.USAGE_UNKNOWN) + .build(); + + audioRecord = new AudioRecord.Builder() + .setAudioPlaybackCaptureConfig(config) + .setAudioFormat(new AudioFormat.Builder() + .setEncoding(AUDIO_FORMAT) + .setSampleRate(SAMPLE_RATE) + .setChannelMask(CHANNEL_CONFIG) + .build()) + .setBufferSizeInBytes(bufferSize) + .build(); + + if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { + YaaccLogger.e(getClass().getName(), "AudioRecord not initialized"); + return false; + } + + audioRecord.startRecording(); + isCapturing = true; + + captureThread = new Thread(this::captureLoop); + captureThread.setPriority(Thread.MAX_PRIORITY); // High priority for audio + captureThread.start(); + + YaaccLogger.i(getClass().getName(), "Audio capture started with buffer size: " + bufferSize); + return true; + + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to start audio capture", e); + stopCapture(); + return false; + } + } + + /** + * Stop capturing audio. + */ + public void stopCapture() { + isCapturing = false; + + if (captureThread != null) { + try { + captureThread.join(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + captureThread = null; + } + + if (audioRecord != null) { + try { + audioRecord.stop(); + audioRecord.release(); + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Error stopping AudioRecord", e); + } + audioRecord = null; + } + + // Close all output streams + for (PipedOutputStream stream : outputStreams) { + try { + stream.close(); + } catch (IOException e) { + YaaccLogger.e(getClass().getName(), "Error closing output stream", e); + } + } + outputStreams.clear(); + + YaaccLogger.i(getClass().getName(), "Audio capture stopped"); + } + + /** + * Create a new input stream for a client. + * Each client gets its own stream for concurrent playback. + */ + public InputStream createInputStream() throws IOException { + int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, AUDIO_FORMAT) * BUFFER_SIZE_MULTIPLIER; + PipedOutputStream outputStream = new PipedOutputStream(); + PipedInputStream inputStream = new PipedInputStream(outputStream, bufferSize * 4); + + outputStreams.add(outputStream); + YaaccLogger.i(getClass().getName(), "Created new input stream, total clients: " + outputStreams.size()); + + return inputStream; + } + + /** + * Remove a client's output stream when they disconnect. + */ + private void removeOutputStream(PipedOutputStream stream) { + if (outputStreams.remove(stream)) { + try { + stream.close(); + } catch (IOException ignored) { + } + YaaccLogger.i(getClass().getName(), "Removed output stream, remaining clients: " + outputStreams.size()); + } + } + + /** + * Get InputStream wrapped with WAV header. + */ + public InputStream getInputStream() throws IOException { + if (!isCapturing) { + return null; + } + InputStream stream = createInputStream(); + return new WavHeaderInputStream(stream, SAMPLE_RATE, 2, 16); + } + + /** + * Check if currently capturing. + */ + public boolean isCapturing() { + return isCapturing; + } + + private void captureLoop() { + byte[] buffer = new byte[8192]; + + while (isCapturing && audioRecord != null) { + int bytesRead = audioRecord.read(buffer, 0, buffer.length); + + if (bytesRead > 0) { + // Broadcast to all connected clients + List deadStreams = new java.util.ArrayList<>(); + + for (PipedOutputStream stream : outputStreams) { + try { + stream.write(buffer, 0, bytesRead); + stream.flush(); + } catch (IOException e) { + // Client disconnected or pipe broken + YaaccLogger.d(getClass().getName(), "Client stream error: " + e.getMessage()); + deadStreams.add(stream); + } + } + + // Remove dead streams + for (PipedOutputStream dead : deadStreams) { + removeOutputStream(dead); + } + + } else if (bytesRead < 0) { + YaaccLogger.e(getClass().getName(), "AudioRecord read error: " + bytesRead); + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + break; + } + } + } + YaaccLogger.i(getClass().getName(), "Capture loop ended"); + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/media/SystemAudioCaptureServiceAAC.java b/yaacc/src/main/java/de/yaacc/upnp/server/media/SystemAudioCaptureServiceAAC.java new file mode 100644 index 00000000..1e81a9c3 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/media/SystemAudioCaptureServiceAAC.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + */ +package de.yaacc.upnp.server.media; + +import android.media.AudioFormat; +import android.media.AudioPlaybackCaptureConfiguration; +import android.media.AudioRecord; +import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaFormat; +import android.media.projection.MediaProjection; +import android.os.Build; + +import androidx.annotation.RequiresApi; + +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.nio.ByteBuffer; + +import de.yaacc.util.YaaccLogger; + +@RequiresApi(api = Build.VERSION_CODES.Q) +public class SystemAudioCaptureServiceAAC { + + private static final int SAMPLE_RATE = 44100; + private static final int BITRATE = 128000; + + private AudioRecord audioRecord; + private MediaCodec audioEncoder; + private Thread captureThread; + private volatile boolean isCapturing = false; + private PipedOutputStream outputStream; + private PipedInputStream inputStream; + + public boolean startCapture(MediaProjection mediaProjection) { + if (isCapturing) { + YaaccLogger.w(getClass().getName(), "Already capturing"); + return false; + } + + try { + setupAudioCapture(mediaProjection); + setupAudioEncoder(); + + outputStream = new PipedOutputStream(); + inputStream = new PipedInputStream(outputStream, 256 * 1024); + + isCapturing = true; + startCaptureThread(); + + YaaccLogger.i(getClass().getName(), "AAC audio capture started"); + return true; + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Failed to start AAC audio capture", e); + stopCapture(); + return false; + } + } + + private void setupAudioCapture(MediaProjection mediaProjection) { + AudioPlaybackCaptureConfiguration config = new AudioPlaybackCaptureConfiguration.Builder(mediaProjection) + .addMatchingUsage(android.media.AudioAttributes.USAGE_MEDIA) + .addMatchingUsage(android.media.AudioAttributes.USAGE_GAME) + .addMatchingUsage(android.media.AudioAttributes.USAGE_UNKNOWN) + .build(); + + int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, + AudioFormat.CHANNEL_IN_STEREO, AudioFormat.ENCODING_PCM_16BIT) * 4; + + audioRecord = new AudioRecord.Builder() + .setAudioPlaybackCaptureConfig(config) + .setAudioFormat(new AudioFormat.Builder() + .setEncoding(AudioFormat.ENCODING_PCM_16BIT) + .setSampleRate(SAMPLE_RATE) + .setChannelMask(AudioFormat.CHANNEL_IN_STEREO) + .build()) + .setBufferSizeInBytes(bufferSize) + .build(); + + audioRecord.startRecording(); + } + + private void setupAudioEncoder() throws IOException { + MediaFormat format = MediaFormat.createAudioFormat(MediaFormat.MIMETYPE_AUDIO_AAC, SAMPLE_RATE, 2); + format.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); + format.setInteger(MediaFormat.KEY_BIT_RATE, BITRATE); + + audioEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_AUDIO_AAC); + audioEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); + audioEncoder.start(); + } + + private void startCaptureThread() { + captureThread = new Thread(() -> { + MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); + byte[] buffer = new byte[4096]; + long audioTimestamp = 0; + + while (isCapturing) { + try { + // Read PCM from AudioRecord + int read = audioRecord.read(buffer, 0, buffer.length); + if (read > 0) { + int inputIndex = audioEncoder.dequeueInputBuffer(10000); + if (inputIndex >= 0) { + ByteBuffer inputBuffer = audioEncoder.getInputBuffer(inputIndex); + if (inputBuffer.remaining() >= read) { + inputBuffer.clear(); + inputBuffer.put(buffer, 0, read); + audioEncoder.queueInputBuffer(inputIndex, 0, read, audioTimestamp, 0); + audioTimestamp += (read / 4) * 1000000L / SAMPLE_RATE; + } + } + } + + // Get encoded AAC output + int outputIndex = audioEncoder.dequeueOutputBuffer(bufferInfo, 0); + if (outputIndex >= 0) { + ByteBuffer outputBuffer = audioEncoder.getOutputBuffer(outputIndex); + if (outputBuffer != null && bufferInfo.size > 0) { + byte[] data = new byte[bufferInfo.size]; + outputBuffer.position(bufferInfo.offset); + outputBuffer.get(data); + + try { + outputStream.write(data); + outputStream.flush(); + } catch (IOException e) { + // Client disconnected + } + } + audioEncoder.releaseOutputBuffer(outputIndex, false); + } + } catch (Exception e) { + if (isCapturing) { + YaaccLogger.e(getClass().getName(), "Capture error", e); + } + } + } + }); + captureThread.start(); + } + + public void stopCapture() { + isCapturing = false; + + if (audioRecord != null) { + try { + audioRecord.stop(); + } catch (Exception e) { + } + try { + audioRecord.release(); + } catch (Exception e) { + } + audioRecord = null; + } + + if (audioEncoder != null) { + try { + audioEncoder.stop(); + } catch (Exception e) { + } + try { + audioEncoder.release(); + } catch (Exception e) { + } + audioEncoder = null; + } + + try { + if (outputStream != null) outputStream.close(); + } catch (Exception e) { + } + + YaaccLogger.i(getClass().getName(), "AAC audio capture stopped"); + } + + public boolean isCapturing() { + return isCapturing; + } + + public PipedInputStream getInputStream() throws IOException { + if (inputStream == null) { + throw new IOException("Audio capture not started"); + } + return inputStream; + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/media/WavHeaderInputStream.java b/yaacc/src/main/java/de/yaacc/upnp/server/media/WavHeaderInputStream.java new file mode 100644 index 00000000..19209833 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/media/WavHeaderInputStream.java @@ -0,0 +1,123 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.media; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Wraps PCM audio stream with WAV header for streaming. + * + * @author tobexyz + */ +public class WavHeaderInputStream extends InputStream { + + private final InputStream pcmStream; + private final byte[] header; + private int headerPos = 0; + private boolean headerSent = false; + + public WavHeaderInputStream(InputStream pcmStream, int sampleRate, int channels, int bitsPerSample) { + this.pcmStream = pcmStream; + this.header = createWavHeader(sampleRate, channels, bitsPerSample); + de.yaacc.util.YaaccLogger.d(getClass().getName(), "Created WAV header wrapper: " + sampleRate + "Hz, " + channels + "ch, " + bitsPerSample + "bit"); + } + + @Override + public int read() throws IOException { + if (!headerSent) { + if (headerPos < header.length) { + return header[headerPos++] & 0xFF; + } + headerSent = true; + } + return pcmStream.read(); + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (!headerSent) { + int headerRemaining = header.length - headerPos; + if (headerRemaining > 0) { + int toCopy = Math.min(headerRemaining, len); + System.arraycopy(header, headerPos, b, off, toCopy); + headerPos += toCopy; + return toCopy; + } + headerSent = true; + } + return pcmStream.read(b, off, len); + } + + @Override + public void close() throws IOException { + pcmStream.close(); + } + + private byte[] createWavHeader(int sampleRate, int channels, int bitsPerSample) { + byte[] header = new byte[44]; + int byteRate = sampleRate * channels * bitsPerSample / 8; + int blockAlign = channels * bitsPerSample / 8; + + // RIFF header + header[0] = 'R'; + header[1] = 'I'; + header[2] = 'F'; + header[3] = 'F'; + writeInt(header, 4, 0x7FFFFFFF); // File size (unknown for streaming) + header[8] = 'W'; + header[9] = 'A'; + header[10] = 'V'; + header[11] = 'E'; + + // fmt chunk + header[12] = 'f'; + header[13] = 'm'; + header[14] = 't'; + header[15] = ' '; + writeInt(header, 16, 16); // fmt chunk size + writeShort(header, 20, (short) 1); // PCM format + writeShort(header, 22, (short) channels); + writeInt(header, 24, sampleRate); + writeInt(header, 28, byteRate); + writeShort(header, 32, (short) blockAlign); + writeShort(header, 34, (short) bitsPerSample); + + // data chunk + header[36] = 'd'; + header[37] = 'a'; + header[38] = 't'; + header[39] = 'a'; + writeInt(header, 40, 0x7FFFFFFF); // Data size (unknown for streaming) + + return header; + } + + private void writeInt(byte[] buffer, int offset, int value) { + buffer[offset] = (byte) (value & 0xFF); + buffer[offset + 1] = (byte) ((value >> 8) & 0xFF); + buffer[offset + 2] = (byte) ((value >> 16) & 0xFF); + buffer[offset + 3] = (byte) ((value >> 24) & 0xFF); + } + + private void writeShort(byte[] buffer, int offset, short value) { + buffer[offset] = (byte) (value & 0xFF); + buffer[offset + 1] = (byte) ((value >> 8) & 0xFF); + } +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/renderingcontrol/YaaccAudioRenderingControlService.java b/yaacc/src/main/java/de/yaacc/upnp/server/renderingcontrol/YaaccAudioRenderingControlService.java new file mode 100644 index 00000000..5df28b13 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/renderingcontrol/YaaccAudioRenderingControlService.java @@ -0,0 +1,112 @@ +/* + * + * Copyright (C) 2013 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.renderingcontrol; + +import android.content.Context; +import android.media.AudioManager; +import de.yaacc.util.YaaccLogger; + +import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; +import org.fourthline.cling.model.types.UnsignedIntegerTwoBytes; +import org.fourthline.cling.support.model.Channel; +import org.fourthline.cling.support.renderingcontrol.AbstractAudioRenderingControl; +import org.fourthline.cling.support.renderingcontrol.RenderingControlException; + + +/** + * @author Tobias Schoene (openbit) + */ +public class YaaccAudioRenderingControlService extends + AbstractAudioRenderingControl { + + + private final Context context; + + public YaaccAudioRenderingControlService(Context context) { + this.context = context; + } + + @Override + public boolean getMute(UnsignedIntegerFourBytes instanceId, String channelName) + throws RenderingControlException { + YaaccLogger.d(getClass().getName(), "getMute() "); + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + if (audioManager != null) { + return audioManager.isStreamMute(AudioManager.STREAM_MUSIC); + } + return false; + } + + @Override + public UnsignedIntegerTwoBytes getVolume(UnsignedIntegerFourBytes instanceId, + String channelName) throws RenderingControlException { + YaaccLogger.d(getClass().getName(), "getVolume() "); + int volume = 0; + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + if (audioManager != null) { + int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + int currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC); + volume = currentVolume * 100 / maxVolume; + } + return new UnsignedIntegerTwoBytes(volume); + } + + @Override + public void setMute(UnsignedIntegerFourBytes instanceId, String channelName, boolean desiredMute) + throws RenderingControlException { + YaaccLogger.d(getClass().getName(), "setMute()"); + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + if (audioManager != null) { + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, desiredMute ? AudioManager.ADJUST_MUTE : AudioManager.ADJUST_UNMUTE, 0); + } + + } + + @Override + public void setVolume(UnsignedIntegerFourBytes instanceId, String channelName, + UnsignedIntegerTwoBytes desiredVolume) throws RenderingControlException { + YaaccLogger.d(getClass().getName(), "setVolume() "); + int desired = desiredVolume.getValue() != null ? desiredVolume.getValue().intValue() : 0; + if (desired < 0) { + desired = 0; + } + if (desired > 100) { + desired = 100; + } + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + if (audioManager != null) { + int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC); + int volume = desired * maxVolume / 100; + audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, AudioManager.FLAG_SHOW_UI); + } + } + + @Override + public UnsignedIntegerFourBytes[] getCurrentInstanceIds() { + YaaccLogger.d(getClass().getName(), " getCurrentInstanceIds() - not yet implemented"); + return null; + } + + @Override + protected Channel[] getCurrentChannels() { + YaaccLogger.d(getClass().getName(), " getCurrentChannels() - not yet implemented"); + return null; + } + +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/udp/DatagramHelper.java b/yaacc/src/main/java/de/yaacc/upnp/server/udp/DatagramHelper.java new file mode 100644 index 00000000..02efbaa5 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/udp/DatagramHelper.java @@ -0,0 +1,141 @@ +package de.yaacc.upnp.server.udp; + +import de.yaacc.util.YaaccLogger; + +import org.fourthline.cling.model.UnsupportedDataException; +import org.fourthline.cling.model.message.IncomingDatagramMessage; +import org.fourthline.cling.model.message.OutgoingDatagramMessage; +import org.fourthline.cling.model.message.UpnpHeaders; +import org.fourthline.cling.model.message.UpnpOperation; +import org.fourthline.cling.model.message.UpnpRequest; +import org.fourthline.cling.model.message.UpnpResponse; +import org.seamless.http.Headers; + +import java.io.ByteArrayInputStream; +import java.io.UnsupportedEncodingException; +import java.net.DatagramPacket; +import java.net.InetAddress; +import java.util.Locale; + +public class DatagramHelper { + public static IncomingDatagramMessage read(InetAddress receivedOnAddress, DatagramPacket datagram) throws UnsupportedDataException { + return read(receivedOnAddress, datagram.getData(), datagram.getAddress(), datagram.getPort()); + } + + public static IncomingDatagramMessage read(InetAddress receivedOnAddress, byte[] datagramData, InetAddress address, int port) throws UnsupportedDataException { + + try { + + YaaccLogger.v(DatagramHelper.class.getName(), "===================================== DATAGRAM BEGIN ============================================"); + YaaccLogger.v(DatagramHelper.class.getName(), new String(datagramData, "UTF-8")); + YaaccLogger.v(DatagramHelper.class.getName(), "-===================================== DATAGRAM END ============================================="); + + + ByteArrayInputStream is = new ByteArrayInputStream(datagramData); + + String[] startLine = Headers.readLine(is).split(" "); + if (startLine[0].startsWith("HTTP/1.")) { + return readResponseMessage(receivedOnAddress, address, port, is, Integer.valueOf(startLine[1]), startLine[2], startLine[0]); + } else { + return readRequestMessage(receivedOnAddress, address, port, is, startLine[0], startLine[2]); + } + + } catch (Exception ex) { + throw new UnsupportedDataException("Could not parse headers: " + ex, ex, datagramData); + } + } + + public static DatagramPacket write(OutgoingDatagramMessage message) throws UnsupportedDataException { + + StringBuilder statusLine = new StringBuilder(); + + UpnpOperation operation = message.getOperation(); + + if (operation instanceof UpnpRequest) { + + UpnpRequest requestOperation = (UpnpRequest) operation; + statusLine.append(requestOperation.getHttpMethodName()).append(" * "); + statusLine.append("HTTP/1.").append(operation.getHttpMinorVersion()).append("\r\n"); + + } else if (operation instanceof UpnpResponse) { + UpnpResponse responseOperation = (UpnpResponse) operation; + statusLine.append("HTTP/1.").append(operation.getHttpMinorVersion()).append(" "); + statusLine.append(responseOperation.getStatusCode()).append(" ").append(responseOperation.getStatusMessage()); + statusLine.append("\r\n"); + } else { + throw new UnsupportedDataException( + "Message operation is not request or response, don't know how to process: " + message + ); + } + + // UDA 1.0, 1.1.2: No body but message must have a blank line after header + StringBuilder messageData = new StringBuilder(); + messageData.append(statusLine); + + messageData.append(message.getHeaders().toString()).append("\r\n"); + + YaaccLogger.v(DatagramHelper.class.getName(), "Writing message data for: " + message); + YaaccLogger.v(DatagramHelper.class.getName(), "---------------------------------------------------------------------------------"); + YaaccLogger.v(DatagramHelper.class.getName(), messageData.toString().substring(0, messageData.length() - 2)); // Don't print the blank lines + YaaccLogger.v(DatagramHelper.class.getName(), "---------------------------------------------------------------------------------"); + + + try { + // According to HTTP 1.0 RFC, headers and their values are US-ASCII + // TODO: Probably should look into escaping rules, too + byte[] data = messageData.toString().getBytes("US-ASCII"); + + YaaccLogger.v(DatagramHelper.class.getName(), "Writing new datagram packet with " + data.length + " bytes for: " + message); + return new DatagramPacket(data, data.length, message.getDestinationAddress(), message.getDestinationPort()); + + } catch (UnsupportedEncodingException ex) { + throw new UnsupportedDataException( + "Can't convert message content to US-ASCII: " + ex.getMessage(), ex, messageData + ); + } + } + + private static IncomingDatagramMessage readRequestMessage(InetAddress receivedOnAddress, + InetAddress address, + int port, + ByteArrayInputStream is, + String requestMethod, + String httpProtocol) throws Exception { + + // Headers + UpnpHeaders headers = new UpnpHeaders(is); + + // Assemble message + IncomingDatagramMessage requestMessage; + UpnpRequest upnpRequest = new UpnpRequest(UpnpRequest.Method.getByHttpName(requestMethod)); + upnpRequest.setHttpMinorVersion(httpProtocol.toUpperCase(Locale.ROOT).equals("HTTP/1.1") ? 1 : 0); + requestMessage = new IncomingDatagramMessage(upnpRequest, address, port, receivedOnAddress); + + requestMessage.setHeaders(headers); + + return requestMessage; + } + + private static IncomingDatagramMessage readResponseMessage(InetAddress receivedOnAddress, + InetAddress address, int port, + ByteArrayInputStream is, + int statusCode, + String statusMessage, + String httpProtocol) throws Exception { + + // Headers + UpnpHeaders headers = new UpnpHeaders(is); + + // Assemble the message + IncomingDatagramMessage responseMessage; + UpnpResponse upnpResponse = new UpnpResponse(statusCode, statusMessage); + upnpResponse.setHttpMinorVersion(httpProtocol.toUpperCase(Locale.ROOT).equals("HTTP/1.1") ? 1 : 0); + responseMessage = new IncomingDatagramMessage(upnpResponse, address, port, receivedOnAddress); + + responseMessage.setHeaders(headers); + + return responseMessage; + } + + +} diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/udp/MulticastReceiver.java b/yaacc/src/main/java/de/yaacc/upnp/server/udp/MulticastReceiver.java new file mode 100644 index 00000000..b2cbb026 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/udp/MulticastReceiver.java @@ -0,0 +1,212 @@ + +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.upnp.server.udp; + +import android.content.Context; +import de.yaacc.util.YaaccLogger; + +import de.yaacc.util.Exceptions; +import org.fourthline.cling.model.UnsupportedDataException; +import org.fourthline.cling.model.message.IncomingDatagramMessage; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.StandardProtocolFamily; +import java.net.StandardSocketOptions; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import de.yaacc.upnp.protocol.ProtocolCreationException; +import de.yaacc.upnp.protocol.ReceivingAsync; +import de.yaacc.upnp.protocol.UpnpProtocolHandler; +import de.yaacc.util.InterfaceResolutionHelper; + +/* +Handling of UDP multicast packages + */ +public class MulticastReceiver { + + public static final int UPNP_MULTICAST_PORT = 1900; + public static final String IPV4_UPNP_MULTICAST_GROUP = "239.255.255.250"; + + protected UpnpProtocolHandler protocolHandler; + + protected NetworkInterface multicastInterface; + protected InetSocketAddress multicastAddress; + //protected MulticastSocket socket; + private DatagramChannel channel; + private Context context; + private ExecutorService receiverExecutor; + private ExecutorService protocolExecutor; + + + public MulticastReceiver() { + receiverExecutor = Executors.newSingleThreadExecutor(); + protocolExecutor = Executors.newFixedThreadPool(100); + + } + + + public void init(Context context, UpnpProtocolHandler protocolHandler) { + this.protocolHandler = protocolHandler; + this.context = context; + initSocket(); + } + + private void initSocket() { + InterfaceResolutionHelper.InterfaceHolder usableInterface = InterfaceResolutionHelper.getNetworkInterface(context); + this.multicastInterface = usableInterface.networkInterface; + + try { + + YaaccLogger.v(getClass().getName(), "Creating wildcard socket (for receiving multicast datagrams) on port: " + UPNP_MULTICAST_PORT); + /* multicastAddress = new InetSocketAddress(getMulticastGroup(), UPNP_MULTICAST_PORT); + + socket = new MulticastSocket(UPNP_MULTICAST_PORT); + socket.setReuseAddress(true); + socket.setReceiveBufferSize(32768); // Keep a backlog of incoming datagrams if we are not fast enough + YaaccLogger.v(getClass().getName(), "Joining multicast group: " + multicastAddress + " on network interface: " + multicastInterface.getDisplayName()); + socket.joinGroup(multicastAddress, multicastInterface); +*/ + + channel = DatagramChannel.open(StandardProtocolFamily.INET); + channel.setOption(StandardSocketOptions.SO_REUSEADDR, true); + channel.bind(new InetSocketAddress(InetAddress.getByName("0.0.0.0"), UPNP_MULTICAST_PORT)); + channel.join(getMulticastGroup(), multicastInterface); + + } catch (Exception ex) { + throw new IllegalStateException("Could not initialize " + getClass().getSimpleName() + ": ", ex); + } + } + + public InetAddress getMulticastGroup() { + try { + return InetAddress.getByName(IPV4_UPNP_MULTICAST_GROUP); + } catch (UnknownHostException ex) { + throw new RuntimeException(ex); + } + } + + public void execute() { + receiverExecutor.execute(() -> { + try { + YaaccLogger.v(getClass().getName(), "Entering blocking receiving loop, listening for UDP datagrams on: " + channel.getLocalAddress() /*socket.getLocalAddress()*/); + } catch (IOException e) { + YaaccLogger.v(getClass().getName(), "Could not get local address: ", e); + } + InetAddress receivedOnLocalAddress = InterfaceResolutionHelper.getBindAddresses(context).next(); + ByteBuffer buffer = ByteBuffer.allocate(1024); + while (true) { + buffer.clear(); + try { + //byte[] buf = new byte[640]; + //DatagramPacket datagram = new DatagramPacket(buf, buf.length); + + if (!channel.isOpen()) { + initSocket(); + } + InetSocketAddress sender = (InetSocketAddress) channel.receive(buffer); + buffer.flip(); + byte[] data = new byte[buffer.remaining()]; + buffer.get(data); + + //socket.receive(datagram); + + YaaccLogger.v(getClass().getName(), + "UDP datagram received from: " + sender.getAddress()//datagram.getAddress().getHostAddress() + + ":" + sender.getPort()//datagram.getPort() + + " on local interface: " + channel.getLocalAddress()//multicastInterface.getDisplayName() + + " and address: " + receivedOnLocalAddress.getHostAddress() + ); + + try { + IncomingDatagramMessage msg = DatagramHelper.read(receivedOnLocalAddress, data, sender.getAddress(), sender.getPort()); + ReceivingAsync protocol = protocolHandler.createReceivingAsync(msg); + if (protocol == null) { + YaaccLogger.v(getClass().getName(), "No protocol, ignoring received message: " + msg); + continue; + } + + YaaccLogger.v(getClass().getName(), "Received asynchronous message: " + msg); + protocolExecutor.execute(protocol); + } catch (ProtocolCreationException ex) { + YaaccLogger.w(getClass().getName(), "Handling received datagram failed - " + Exceptions.unwrap(ex).toString()); + } + + } catch (SocketException ex) { + YaaccLogger.v(getClass().getName(), "Socket closed", ex); + break; + } catch (java.nio.channels.AsynchronousCloseException ex) { + YaaccLogger.v(getClass().getName(), "Channel closed asynchronously", ex); + break; + } catch (UnsupportedDataException ex) { + YaaccLogger.v(getClass().getName(), "Could not read datagram: " + ex.getMessage(), ex); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + try { + if (channel.isOpen()) { + YaaccLogger.v(getClass().getName(), "Closing multicast socket"); + channel.close(); + } + /* + if (!socket.isClosed()) { + YaaccLogger.v(getClass().getName(), "Closing multicast socket"); + socket.close(); + } + */ + } catch (Exception ex) { + throw new RuntimeException(ex); + } + }); + } + + public void cancel() { + /* + if (socket != null && !socket.isClosed()) { + try { + YaaccLogger.v(getClass().getName(), "Leaving multicast group"); + socket.leaveGroup(multicastAddress, multicastInterface); + // Well this doesn't work and I have no idea why I get "java.net.SocketException: Can't assign requested address" + } catch (Exception ex) { + YaaccLogger.v(getClass().getName(), "Could not leave multicast group: ", ex); + } + // So... just close it and ignore the log messages + socket.close(); + }*/ + if (channel != null && channel.isOpen()) { + try { + channel.close(); + } catch (IOException ex) { + YaaccLogger.v(getClass().getName(), "Could not close multicast channel: ", ex); + } + } + + } + +} + diff --git a/yaacc/src/main/java/de/yaacc/upnp/server/udp/UdpTransiver.java b/yaacc/src/main/java/de/yaacc/upnp/server/udp/UdpTransiver.java new file mode 100644 index 00000000..edd4e270 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/upnp/server/udp/UdpTransiver.java @@ -0,0 +1,157 @@ +package de.yaacc.upnp.server.udp; + +import android.content.Context; +import de.yaacc.util.YaaccLogger; + +import de.yaacc.util.Exceptions; +import org.fourthline.cling.model.UnsupportedDataException; +import org.fourthline.cling.model.message.IncomingDatagramMessage; +import org.fourthline.cling.model.message.OutgoingDatagramMessage; + +import java.net.DatagramPacket; +import java.net.InetSocketAddress; +import java.net.MulticastSocket; +import java.net.SocketException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import de.yaacc.upnp.protocol.ProtocolCreationException; +import de.yaacc.upnp.protocol.ReceivingAsync; +import de.yaacc.upnp.protocol.UpnpProtocolHandler; +import de.yaacc.util.InterfaceResolutionHelper; + +public class UdpTransiver { + + private static int TTL = 4; + private UpnpProtocolHandler protocolHandler; + private int MAX_DATAGRAM_BYTES = 640; + + private MulticastSocket socket; + + private ExecutorService receiverExecutor; + private ExecutorService protocolExecutor; + private Context context; + + public UdpTransiver() { + receiverExecutor = Executors.newSingleThreadExecutor(); + protocolExecutor = Executors.newFixedThreadPool(10); + } + + public void init(Context context, UpnpProtocolHandler protocolHandler) { + this.protocolHandler = protocolHandler; + this.context = context; + initSocket(); + } + + private void initSocket() { + InterfaceResolutionHelper.InterfaceHolder usableInterface = InterfaceResolutionHelper.getNetworkInterface(context); + try { + + // TODO: UPNP VIOLATION: The spec does not prohibit using the 1900 port here again, however, the + // Netgear ReadyNAS miniDLNA implementation will no longer answer if it has to send search response + // back via UDP unicast to port 1900... so we use an ephemeral port + YaaccLogger.v(getClass().getName(), "Creating bound socket (for datagram input/output) on: " + usableInterface.inetAddress); + InetSocketAddress localAddress = new InetSocketAddress(usableInterface.inetAddress, 0); + socket = new MulticastSocket(localAddress); + socket.setTimeToLive(TTL); + socket.setReceiveBufferSize(262144); // Keep a backlog of incoming datagrams if we are not fast enough + YaaccLogger.v(getClass().getName(), "Socket created and bound to: " + socket.getLocalSocketAddress() + " on interface: " + usableInterface.networkInterface.getDisplayName()); + } catch (Exception ex) { + throw new IllegalStateException("Could not initialize " + getClass().getSimpleName() + ": " + ex); + } + } + + public void send(OutgoingDatagramMessage message) { + DatagramPacket packet = DatagramHelper.write(message); + YaaccLogger.v(getClass().getName(), "Sending UDP datagram packet to: " + message.getDestinationAddress() + ":" + message.getDestinationPort()); + send(packet); + } + + public void send(DatagramPacket datagram) { + protocolExecutor.execute(() -> { + try { + socket.send(datagram); + } catch (SocketException ex) { + YaaccLogger.v(getClass().getName(), "Socket closed, aborting datagram send to: " + datagram.getAddress()); + } catch (RuntimeException ex) { + throw ex; + } catch (Exception ex) { + try { + YaaccLogger.w(getClass().getName(), socket.getNetworkInterface() + " Exception sending datagram to: " + datagram.getAddress() + ": " + ex, ex); + } catch (SocketException se) { + YaaccLogger.e(getClass().getName(), " Exception sending datagram to: " + datagram.getAddress() + ": " + ex, ex); + } + } + + }); + } + + public void execute() { + YaaccLogger.v(getClass().getName(), "execute() called, submitting receiver task"); + receiverExecutor.execute(() -> { + YaaccLogger.v(getClass().getName(), "Receiver task started"); + try { + YaaccLogger.v(getClass().getName(), "Entering blocking receiving loop, listening for UDP datagrams on: " + socket.getLocalAddress()); + } catch (Exception e) { + YaaccLogger.e(getClass().getName(), "Error getting local address", e); + } + + while (true) { + + try { + byte[] buf = new byte[MAX_DATAGRAM_BYTES]; + DatagramPacket datagram = new DatagramPacket(buf, buf.length); + YaaccLogger.v(getClass().getName(), "UDP before"); + socket.receive(datagram); + + YaaccLogger.v(getClass().getName(), + "UDP datagram received from: " + + datagram.getAddress().getHostAddress() + + ":" + datagram.getPort() + ); + + try { + IncomingDatagramMessage msg = DatagramHelper.read(socket.getInterface(), datagram); + ReceivingAsync protocol = protocolHandler.createReceivingAsync(msg); + if (protocol == null) { + + YaaccLogger.v(getClass().getName(), "No protocol, ignoring received message: " + msg); + continue; + } + + YaaccLogger.v(getClass().getName(), "Received asynchronous message: " + msg); + protocolExecutor.execute(protocol); + } catch (ProtocolCreationException ex) { + YaaccLogger.w(getClass().getName(), "Handling received datagram failed - " + Exceptions.unwrap(ex).toString()); + } + + } catch (SocketException ex) { + YaaccLogger.v(getClass().getName(), "Socket closed", ex); + break; + } catch (java.nio.channels.AsynchronousCloseException ex) { + YaaccLogger.v(getClass().getName(), "Socket closed asynchronously", ex); + break; + } catch (UnsupportedDataException ex) { + YaaccLogger.v(getClass().getName(), "Could not read datagram: " + ex.getMessage(), ex); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + try { + if (!socket.isClosed()) { + YaaccLogger.v(getClass().getName(), "Closing unicast socket"); + socket.close(); + } + } catch (Exception ex) { + throw new RuntimeException(ex); + } + }); + } + + public void cancel() { + if (socket != null && !socket.isClosed()) { + socket.close(); + } + } + +} diff --git a/yaacc/src/main/java/de/yaacc/util/AboutActivity.java b/yaacc/src/main/java/de/yaacc/util/AboutActivity.java index 69a5915f..c8f62cbf 100644 --- a/yaacc/src/main/java/de/yaacc/util/AboutActivity.java +++ b/yaacc/src/main/java/de/yaacc/util/AboutActivity.java @@ -21,7 +21,7 @@ import android.content.Intent; import android.content.pm.PackageManager.NameNotFoundException; import android.os.Bundle; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; @@ -50,7 +50,7 @@ public void onCreate(Bundle savedInstanceState) { CharSequence aboutText = textView.getText(); textView.setText("Yet Another Android Client Controller\nVersion: " + app_ver + "\n\n" + aboutText); } catch (NameNotFoundException e) { - Log.d(getClass().getName(), "Can't find version", e); + YaaccLogger.d(getClass().getName(), "Can't find version", e); } } diff --git a/yaacc/src/main/java/de/yaacc/util/ActivitySwipeDetector.java b/yaacc/src/main/java/de/yaacc/util/ActivitySwipeDetector.java index 807aac71..7f802edc 100644 --- a/yaacc/src/main/java/de/yaacc/util/ActivitySwipeDetector.java +++ b/yaacc/src/main/java/de/yaacc/util/ActivitySwipeDetector.java @@ -17,7 +17,7 @@ */ package de.yaacc.util; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; @@ -34,33 +34,33 @@ public ActivitySwipeDetector(SwipeReceiver swipeReceiver) { } private void onRightToLeftSwipe() { - Log.i(logTag, "RightToLeftSwipe!"); + YaaccLogger.i(logTag, "RightToLeftSwipe!"); swipeReceiver.onRightToLeftSwipe(); } private void onLeftToRightSwipe() { - Log.i(logTag, "LeftToRightSwipe!"); + YaaccLogger.i(logTag, "LeftToRightSwipe!"); swipeReceiver.onLeftToRightSwipe(); } private void onTopToBottomSwipe() { - Log.i(logTag, "onTopToBottomSwipe!"); + YaaccLogger.i(logTag, "onTopToBottomSwipe!"); swipeReceiver.onTopToBottomSwipe(); } private void onBottomToTopSwipe() { - Log.i(logTag, "onBottomToTopSwipe!"); + YaaccLogger.i(logTag, "onBottomToTopSwipe!"); swipeReceiver.onBottomToTopSwipe(); } private void endOnTouchProcessing(View v, MotionEvent event) { - Log.i(logTag, "endOnTouchProcessing!"); + YaaccLogger.i(logTag, "endOnTouchProcessing!"); swipeReceiver.endOnTouchProcessing(v, event); } private void beginOnTouchProcessing(View v, MotionEvent event) { - Log.i(logTag, "beginOnTouchProcessing!"); + YaaccLogger.i(logTag, "beginOnTouchProcessing!"); swipeReceiver.beginOnTouchProcessing(v, event); } @@ -95,7 +95,7 @@ public boolean onTouch(View v, MotionEvent event) { return true; } } else { - Log.i(logTag, "Swipe was only " + Math.abs(deltaX) + YaaccLogger.i(logTag, "Swipe was only " + Math.abs(deltaX) + " long, need at least " + MIN_DISTANCE); return false; // We don't consume the event } @@ -112,7 +112,7 @@ public boolean onTouch(View v, MotionEvent event) { return true; } } else { - Log.i(logTag, "Swipe was only " + Math.abs(deltaX) + YaaccLogger.i(logTag, "Swipe was only " + Math.abs(deltaX) + " long, need at least " + MIN_DISTANCE); return false; // We don't consume the event } diff --git a/yaacc/src/main/java/de/yaacc/util/Exceptions.java b/yaacc/src/main/java/de/yaacc/util/Exceptions.java new file mode 100644 index 00000000..79adcf25 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/util/Exceptions.java @@ -0,0 +1,32 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.util; + +public class Exceptions { + + public static Throwable unwrap(Throwable throwable) { + if (throwable == null) { + throw new IllegalArgumentException("Cannot unwrap null throwable"); + } + while (throwable.getCause() != null) { + throwable = throwable.getCause(); + } + return throwable; + } +} diff --git a/yaacc/src/main/java/de/yaacc/util/FileDownloader.java b/yaacc/src/main/java/de/yaacc/util/FileDownloader.java index de644972..7884834a 100644 --- a/yaacc/src/main/java/de/yaacc/util/FileDownloader.java +++ b/yaacc/src/main/java/de/yaacc/util/FileDownloader.java @@ -22,7 +22,7 @@ import android.content.Context; import android.os.AsyncTask; import android.os.Environment; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import android.webkit.MimeTypeMap; import androidx.core.app.NotificationCompat; @@ -141,7 +141,7 @@ private void cancleNotification() { NotificationManager mNotificationManager = (NotificationManager) upnpClient.getContext() .getSystemService(Context.NOTIFICATION_SERVICE); // mId allows you to update the notification later on. - Log.d(getClass().getName(), "Cancle Notification with ID: " + NotificationId.FILE_DOWNLOADER.getId()); + YaaccLogger.d(getClass().getName(), "Cancle Notification with ID: " + NotificationId.FILE_DOWNLOADER.getId()); mNotificationManager.cancel(NotificationId.FILE_DOWNLOADER.getId()); } diff --git a/yaacc/src/main/java/de/yaacc/util/HttpRange.java b/yaacc/src/main/java/de/yaacc/util/HttpRange.java index f5cee560..d2841c5a 100644 --- a/yaacc/src/main/java/de/yaacc/util/HttpRange.java +++ b/yaacc/src/main/java/de/yaacc/util/HttpRange.java @@ -27,15 +27,15 @@ public class HttpRange { private String unit; - private Integer start; - private Integer end; - private Integer suffixLength; + private Long start; + private Long end; + private Long suffixLength; public HttpRange() { } - public HttpRange(String unit, Integer start, Integer end, Integer suffixLength) { + public HttpRange(String unit, Long start, Long end, Long suffixLength) { this.unit = unit; this.start = start; this.end = end; @@ -92,10 +92,10 @@ public static List parseRangeHeader(String rangeHeader) { if (byteRangeSetMatcher.group("byteRangeSpec") != null) { String start = byteRangeSetMatcher.group("firstBytePos"); String end = byteRangeSetMatcher.group("lastBytePos"); - range.start = Integer.valueOf(start); - range.end = end == null ? null : Integer.valueOf(end); + range.start = Long.valueOf(start); + range.end = end == null ? null : Long.valueOf(end); } else if (byteRangeSetMatcher.group("suffixByteRangeSpec") != null) { - range.suffixLength = Integer.valueOf(byteRangeSetMatcher.group("suffixLength")); + range.suffixLength = Long.valueOf(byteRangeSetMatcher.group("suffixLength")); } else { throw new RuntimeException("Invalid range header"); } @@ -107,15 +107,15 @@ public static List parseRangeHeader(String rangeHeader) { return ranges; } - public Integer getStart() { + public Long getStart() { return start; } - public Integer getSuffixLength() { + public Long getSuffixLength() { return suffixLength; } - public Integer getEnd() { + public Long getEnd() { return end; } } diff --git a/yaacc/src/main/java/de/yaacc/util/InterfaceResolutionHelper.java b/yaacc/src/main/java/de/yaacc/util/InterfaceResolutionHelper.java new file mode 100644 index 00000000..c73afcc3 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/util/InterfaceResolutionHelper.java @@ -0,0 +1,135 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.util; + +import android.content.Context; +import android.content.SharedPreferences; +import de.yaacc.util.YaaccLogger; + +import androidx.preference.PreferenceManager; + +import org.fourthline.cling.model.NetworkAddress; + +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import de.yaacc.R; +import de.yaacc.upnp.server.YaaccUpnpServerService; + +public class InterfaceResolutionHelper { + private static final Pattern IPV4_PATTERN = + Pattern.compile( + "^(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}$"); + + public static class InterfaceHolder { + public NetworkInterface networkInterface; + public InetAddress inetAddress; + } + + public static Iterator getBindAddresses(Context context) { + List result = new ArrayList<>(); + if (getIfName(context) != null) { + try { + if (NetworkInterface.getByName(getIfName(context)) != null) { + Enumeration iter = NetworkInterface.getByName(getIfName(context)).getInetAddresses(); + while (iter.hasMoreElements()) { + result.add(iter.nextElement()); + } + } else { + YaaccLogger.d(InterfaceResolutionHelper.class.getName(), + "network interface not found by name, maybe device is offline"); + } + } catch ( + SocketException se) { + YaaccLogger.d(InterfaceResolutionHelper.class.getName(), + "Error while retrieving network interfaces", se); + } + } else { + YaaccLogger.d(InterfaceResolutionHelper.class.getName(), + "network interface name is null, maybe device is offline"); + } + return result.iterator(); + } + + /** + * get the ip address of the device + * + * @return the address or null if anything went wrong + */ + public static String getIpAddress(Context context) { + return getIfAndIpAddress(context)[0]; + } + + public static String getIfName(Context context) { + return getIfAndIpAddress(context)[1]; + } + + public static String[] getIfAndIpAddress(Context context) { + String hostAddress = null; + String[] result = new String[2]; + InterfaceHolder useableInterface = getNetworkInterface(context); + hostAddress = useableInterface.inetAddress.getHostAddress(); + + // maybe wifi is off we have to use the loopback device + hostAddress = hostAddress == null ? "0.0.0.0" : hostAddress; + result[0] = hostAddress; + result[1] = useableInterface.networkInterface.getName(); + return result; + } + + public static InterfaceHolder getNetworkInterface(Context context) { + InterfaceHolder result = new InterfaceHolder(); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + List interfaces = new ArrayList<>(List.of(preferences.getString(context.getString(R.string.settings_local_server_if_filter_key), "lo,dummy,rmnet,ccmni").split(","))); + interfaces.remove(""); //remove empty string, if there, otherwise we got into trouble finding an network interface in code below + try { + for (Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); networkInterfaces.hasMoreElements(); ) { + NetworkInterface networkInterface = networkInterfaces.nextElement(); + if (interfaces.stream().filter(i -> networkInterface.getName().startsWith(i.trim())).collect(Collectors.toList()).isEmpty()) { + for (Enumeration inetAddresses = networkInterface.getInetAddresses(); inetAddresses.hasMoreElements(); ) { + InetAddress inetAddress = inetAddresses.nextElement(); + if (!inetAddress.isLoopbackAddress() && inetAddress + .getHostAddress() != null + && IPV4_PATTERN.matcher(inetAddress + .getHostAddress()).matches()) { + result.inetAddress = inetAddress; + result.networkInterface = networkInterface; + } + } + } + } + } catch (SocketException se) { + YaaccLogger.d(InterfaceResolutionHelper.class.getName(), + "Error while retrieving network interfaces", se); + } + return result; + } + + public static NetworkAddress getNetworkAddress(Context context) { + return new NetworkAddress(getNetworkInterface(context).inetAddress, YaaccUpnpServerService.PORT); + } + +} diff --git a/yaacc/src/main/java/de/yaacc/util/SAFCacheManager.java b/yaacc/src/main/java/de/yaacc/util/SAFCacheManager.java new file mode 100644 index 00000000..433b50cb --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/util/SAFCacheManager.java @@ -0,0 +1,552 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.util; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.media.MediaMetadataRetriever; +import android.net.Uri; + +import androidx.documentfile.provider.DocumentFile; +import androidx.preference.PreferenceManager; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import de.yaacc.R; + +/** + * LRU cache for SAF (Storage Access Framework) file metadata with background preloading. + * Caches duration, MIME type, and encoded IDs for fast browsing. + */ +public class SAFCacheManager { + private static final int MAX_CACHE_SIZE = 1000; + private static final String CACHE_PREFIX = "saf_cache_"; + private static final String ID_COUNTER_KEY = "saf_id_counter"; + + private static final Map MIME_TYPE_BY_EXT = new ConcurrentHashMap<>(); + + static { + // Audio formats + MIME_TYPE_BY_EXT.put("mp3", "audio/mpeg"); + MIME_TYPE_BY_EXT.put("m4a", "audio/mp4"); + MIME_TYPE_BY_EXT.put("aac", "audio/aac"); + MIME_TYPE_BY_EXT.put("flac", "audio/flac"); + MIME_TYPE_BY_EXT.put("ogg", "audio/ogg"); + MIME_TYPE_BY_EXT.put("opus", "audio/opus"); + MIME_TYPE_BY_EXT.put("wav", "audio/wav"); + MIME_TYPE_BY_EXT.put("wma", "audio/x-ms-wma"); + + // Video formats + MIME_TYPE_BY_EXT.put("mp4", "video/mp4"); + MIME_TYPE_BY_EXT.put("mkv", "video/x-matroska"); + MIME_TYPE_BY_EXT.put("avi", "video/x-msvideo"); + MIME_TYPE_BY_EXT.put("mov", "video/quicktime"); + MIME_TYPE_BY_EXT.put("wmv", "video/x-ms-wmv"); + MIME_TYPE_BY_EXT.put("webm", "video/webm"); + + // Image formats + MIME_TYPE_BY_EXT.put("jpg", "image/jpeg"); + MIME_TYPE_BY_EXT.put("jpeg", "image/jpeg"); + MIME_TYPE_BY_EXT.put("png", "image/png"); + MIME_TYPE_BY_EXT.put("gif", "image/gif"); + MIME_TYPE_BY_EXT.put("webp", "image/webp"); + } + + private final Context context; + private final SharedPreferences preferences; + private final ExecutorService preloadExecutor; + private final LRUCache lruCache; + private final Map shortIdToUri = new ConcurrentHashMap<>(); + private final Map uriToShortId = new ConcurrentHashMap<>(); + private long idCounter = 1; + + private int totalFilesIndexed = 0; + private boolean isPreloading = false; + + private static SAFCacheManager instance; + + public static synchronized SAFCacheManager getInstance(Context context) { + if (instance == null) { + instance = new SAFCacheManager(context.getApplicationContext()); + } + return instance; + } + + private SAFCacheManager(Context context) { + this.context = context; + this.preferences = PreferenceManager.getDefaultSharedPreferences(context); + this.preloadExecutor = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r, "SAFPreloader"); + t.setPriority(Thread.MIN_PRIORITY); + return t; + }); + this.lruCache = new LRUCache(MAX_CACHE_SIZE); + + // Restore ID counter from preferences + this.idCounter = preferences.getLong(ID_COUNTER_KEY, 1); + + loadCacheIndex(); + } + + /** + * Get complete metadata for a SAF file (duration, MIME type, encoded ID). + * Returns cached data if available, otherwise extracts synchronously. + */ + public SAFMetadata getMetadata(DocumentFile file) { + if (file == null) return null; + + long startTime = System.currentTimeMillis(); + String uri = file.getUri().toString(); + String key = CACHE_PREFIX + uri; + + // Check memory cache + String cached = lruCache.get(key); + if (cached != null) { + SAFMetadata metadata = SAFMetadata.deserialize(cached); + if (metadata != null) { + // Migrate old entries without shortId + if (metadata.shortId == null) { + String shortId = getOrCreateShortId(uri); + metadata = new SAFMetadata(metadata.duration, metadata.mimeType, shortId, metadata.fileSize); + String serialized = metadata.serialize(); + lruCache.put(key, serialized); + preferences.edit().putString(key, serialized).apply(); + } + long elapsed = System.currentTimeMillis() - startTime; + YaaccLogger.d(getClass().getName(), "CACHE_HIT_MEMORY: " + file.getName() + " (" + elapsed + "ms)"); + return metadata; + } + } + + // Check disk cache + cached = preferences.getString(key, null); + if (cached != null) { + SAFMetadata metadata = SAFMetadata.deserialize(cached); + if (metadata != null) { + // Migrate old entries without shortId + if (metadata.shortId == null) { + String shortId = getOrCreateShortId(uri); + metadata = new SAFMetadata(metadata.duration, metadata.mimeType, shortId, metadata.fileSize); + String serialized = metadata.serialize(); + lruCache.put(key, serialized); + preferences.edit().putString(key, serialized).apply(); + } else { + lruCache.put(key, cached); + } + long elapsed = System.currentTimeMillis() - startTime; + YaaccLogger.d(getClass().getName(), "CACHE_HIT_DISK: " + file.getName() + " (" + elapsed + "ms)"); + return metadata; + } + } + + // Extract and cache + YaaccLogger.w(getClass().getName(), "CACHE_MISS: " + file.getName() + " - extracting..."); + SAFMetadata metadata = extractMetadata(file); + if (metadata != null) { + String serialized = metadata.serialize(); + lruCache.put(key, serialized); + preferences.edit().putString(key, serialized).apply(); + + // Trim disk cache if memory cache evicted entries + if (!lruCache.getEvictedKeys().isEmpty()) { + trimCache(); + } + } + long elapsed = System.currentTimeMillis() - startTime; + YaaccLogger.w(getClass().getName(), "EXTRACTION_COMPLETE: " + file.getName() + " (" + elapsed + "ms)"); + return metadata; + } + + /** + * Extract all metadata for a file (duration, MIME type, short ID). + */ + private SAFMetadata extractMetadata(DocumentFile file) { + String uri = file.getUri().toString(); + String duration = extractDuration(file.getUri()); + String mimeType = extractMimeType(file); + String shortId = getOrCreateShortId(uri); + long fileSize = file.length(); + return new SAFMetadata(duration, mimeType, shortId, fileSize); + } + + /** + * Get or create a short ID for a URI. + */ + public synchronized String getOrCreateShortId(String uri) { + String existing = uriToShortId.get(uri); + if (existing != null) { + return existing; + } + + String id = String.valueOf(idCounter++); + uriToShortId.put(uri, id); + shortIdToUri.put(id, uri); + + // Persist counter + preferences.edit().putLong(ID_COUNTER_KEY, idCounter).apply(); + + YaaccLogger.d(getClass().getName(), "Created shortId mapping: " + id + " -> " + uri); + return id; + } + + /** + * Get URI for a short ID. + */ + public String getUriForShortId(String shortId) { + return shortIdToUri.get(shortId); + } + + private String extractMimeType(DocumentFile file) { + // Try extension-based lookup first + if (file.getName() != null) { + int dotIndex = file.getName().lastIndexOf('.'); + if (dotIndex > 0) { + String ext = file.getName().substring(dotIndex + 1).toLowerCase(); + String mimeType = MIME_TYPE_BY_EXT.get(ext); + if (mimeType != null) return mimeType; + } + } + // Fall back to system lookup + return file.getType(); + } + + private String extractDuration(Uri uri) { + MediaMetadataRetriever retriever = null; + try { + retriever = new MediaMetadataRetriever(); + retriever.setDataSource(context, uri); + String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + if (durationStr != null) { + long durationMs = Long.parseLong(durationStr); + return FormatHelper.parseMillisToTimeStringTo(durationMs); + } + } catch (IllegalArgumentException e) { + // File not accessible (status 0x80000000) - expected for some files + YaaccLogger.d(getClass().getName(), "Duration not available: " + uri.getLastPathSegment()); + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "Failed to extract duration: " + uri.getLastPathSegment(), e); + } finally { + if (retriever != null) { + try { retriever.release(); } catch (Exception ignored) {} + } + } + return null; + } + + /** + * Get duration from cache or extract if not cached. + * @deprecated Use getMetadata() instead + */ + @Deprecated + public String getDuration(Uri uri) { + long startTime = System.currentTimeMillis(); + String key = CACHE_PREFIX + uri.toString(); + String fileName = uri.getLastPathSegment(); + + // Check memory cache first + String duration = lruCache.get(key); + if (duration != null) { + long elapsed = System.currentTimeMillis() - startTime; + YaaccLogger.d(getClass().getName(), "CACHE_HIT_MEMORY: " + fileName + " -> " + duration + " (" + elapsed + "ms, cache_size=" + lruCache.size() + ")"); + return duration; + } + + // Check SharedPreferences + duration = preferences.getString(key, null); + if (duration != null) { + lruCache.put(key, duration); + long elapsed = System.currentTimeMillis() - startTime; + YaaccLogger.d(getClass().getName(), "CACHE_HIT_DISK: " + fileName + " -> " + duration + " (" + elapsed + "ms, cache_size=" + lruCache.size() + ")"); + return duration; + } + + // Extract and cache + YaaccLogger.w(getClass().getName(), "CACHE_MISS: " + fileName + " - extracting..."); + duration = extractAndCache(uri); + long elapsed = System.currentTimeMillis() - startTime; + YaaccLogger.w(getClass().getName(), "EXTRACTION_COMPLETE: " + fileName + " -> " + duration + " (" + elapsed + "ms, cache_size=" + lruCache.size() + ")"); + return duration; + } + + /** + * Preload durations for SAF files in background. + */ + public void preloadSafDurations() { + if (isPreloading) { + YaaccLogger.w(getClass().getName(), "PRELOAD_ALREADY_RUNNING"); + return; + } + + isPreloading = true; + totalFilesIndexed = 0; + long preloadStart = System.currentTimeMillis(); + YaaccLogger.i(getClass().getName(), "PRELOAD_START"); + + preloadExecutor.execute(() -> { + try { + Set safUris = de.yaacc.upnp.server.contentdirectory.MediaPathFilter.getSafPathes(context); + YaaccLogger.i(getClass().getName(), "PRELOAD_SCANNING: " + safUris.size() + " SAF roots"); + + for (String uriString : safUris) { + // Check timeout (max 5 minutes total) + if (System.currentTimeMillis() - preloadStart > 5 * 60 * 1000) { + YaaccLogger.w(getClass().getName(), "PRELOAD_TIMEOUT: Stopping after 5 minutes"); + break; + } + + try { + Uri safUri = Uri.parse(uriString); + DocumentFile root = DocumentFile.fromTreeUri(context, safUri); + if (root != null) { + traverseAndCache(root, preloadStart); + } + } catch (Exception e) { + YaaccLogger.w(getClass().getName(), "PRELOAD_ERROR: Failed to preload SAF: " + uriString, e); + } + } + long elapsed = System.currentTimeMillis() - preloadStart; + YaaccLogger.i(getClass().getName(), "PRELOAD_COMPLETE: " + totalFilesIndexed + " files indexed in " + elapsed + "ms (cache_size=" + lruCache.size() + ")"); + } finally { + isPreloading = false; + notifyPreloadComplete(); + } + }); + } + + private void notifyPreloadProgress(int filesIndexed, String currentFolder) { + Intent intent = new Intent("de.yaacc.CACHE_PRELOAD_PROGRESS"); + intent.putExtra("files_indexed", filesIndexed); + intent.putExtra("current_folder", currentFolder); + context.sendBroadcast(intent); + } + + private void notifyPreloadComplete() { + Intent intent = new Intent("de.yaacc.CACHE_PRELOAD_COMPLETE"); + intent.putExtra("files_indexed", totalFilesIndexed); + context.sendBroadcast(intent); + } + + private void traverseAndCache(DocumentFile dir, long startTime) { + if (!dir.isDirectory()) return; + + // Check timeout + if (System.currentTimeMillis() - startTime > 5 * 60 * 1000) { + return; + } + + // Check if we can read this directory + if (!dir.canRead()) { + YaaccLogger.d(getClass().getName(), "PRELOAD_SKIP: No read permission for " + dir.getName()); + return; + } + + DocumentFile[] files = dir.listFiles(); + if (files == null) return; + + String folderName = dir.getName() != null ? dir.getName() : "Unknown"; + YaaccLogger.d(getClass().getName(), "PRELOAD_TRAVERSE: " + folderName + " (" + files.length + " items)"); + + for (DocumentFile file : files) { + if (file.isDirectory()) { + traverseAndCache(file, startTime); + } else if (isMediaFile(file)) { + String key = CACHE_PREFIX + file.getUri().toString(); + if (!preferences.contains(key)) { + long extractStart = System.currentTimeMillis(); + extractAndCache(file.getUri()); + long elapsed = System.currentTimeMillis() - extractStart; + YaaccLogger.d(getClass().getName(), "PRELOAD_EXTRACTED: " + file.getName() + " (" + elapsed + "ms)"); + } else { + YaaccLogger.d(getClass().getName(), "PRELOAD_CACHED: " + file.getName()); + } + + totalFilesIndexed++; + + // Notify progress every 10 files + if (totalFilesIndexed % 10 == 0) { + notifyPreloadProgress(totalFilesIndexed, folderName); + } + } + } + } + + private boolean isMediaFile(DocumentFile file) { + String type = file.getType(); + return type != null && (type.startsWith("audio/") || type.startsWith("video/") || type.startsWith("image/")); + } + + private String extractAndCache(Uri uri) { + long extractStart = System.currentTimeMillis(); + MediaMetadataRetriever retriever = null; + try { + long createStart = System.currentTimeMillis(); + retriever = new MediaMetadataRetriever(); + long createTime = System.currentTimeMillis() - createStart; + + long setDataStart = System.currentTimeMillis(); + retriever.setDataSource(context, uri); + long setDataTime = System.currentTimeMillis() - setDataStart; + + long metadataStart = System.currentTimeMillis(); + String durationStr = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + long metadataTime = System.currentTimeMillis() - metadataStart; + + YaaccLogger.d(getClass().getName(), "EXTRACT_BREAKDOWN: " + uri.getLastPathSegment() + " - create=" + createTime + "ms, setDataSource=" + setDataTime + "ms, extractMetadata=" + metadataTime + "ms"); + + if (durationStr != null) { + long durationMs = Long.parseLong(durationStr); + String formatted = FormatHelper.parseMillisToTimeStringTo(durationMs); + + String key = CACHE_PREFIX + uri.toString(); + lruCache.put(key, formatted); + preferences.edit().putString(key, formatted).apply(); + + long elapsed = System.currentTimeMillis() - extractStart; + YaaccLogger.d(getClass().getName(), "EXTRACT_SUCCESS: " + uri.getLastPathSegment() + " -> " + formatted + " (" + elapsed + "ms)"); + return formatted; + } + } catch (Exception e) { + long elapsed = System.currentTimeMillis() - extractStart; + YaaccLogger.w(getClass().getName(), "EXTRACT_FAILED: " + uri.getLastPathSegment() + " (" + elapsed + "ms)", e); + } finally { + if (retriever != null) { + try { + retriever.release(); + } catch (Exception ignored) {} + } + } + return null; + } + + private void loadCacheIndex() { + long loadStart = System.currentTimeMillis(); + Map all = preferences.getAll(); + int count = 0; + for (Map.Entry entry : all.entrySet()) { + String key = entry.getKey(); + if (key.startsWith(CACHE_PREFIX) && entry.getValue() instanceof String) { + lruCache.put(key, (String) entry.getValue()); + + // Restore ID mappings from cached metadata + SAFMetadata metadata = SAFMetadata.deserialize((String) entry.getValue()); + if (metadata != null && metadata.shortId != null) { + String uri = key.substring(CACHE_PREFIX.length()); + shortIdToUri.put(metadata.shortId, uri); + uriToShortId.put(uri, metadata.shortId); + + // Update counter to avoid ID collisions + try { + long id = Long.parseLong(metadata.shortId); + if (id >= idCounter) { + idCounter = id + 1; + } + } catch (NumberFormatException e) { + // Ignore non-numeric IDs + } + } + + count++; + } + } + long elapsed = System.currentTimeMillis() - loadStart; + YaaccLogger.i(getClass().getName(), "CACHE_LOADED: " + count + " entries in " + elapsed + "ms (cache_size=" + lruCache.size() + ", id_mappings=" + shortIdToUri.size() + ")"); + } + + /** + * Clear old entries beyond MAX_CACHE_SIZE. + */ + public void trimCache() { + SharedPreferences.Editor editor = preferences.edit(); + for (String key : lruCache.getEvictedKeys()) { + editor.remove(key); + } + editor.apply(); + } + + /** + * Clear all cached metadata and ID mappings. + */ + public void clearCache() { + lruCache.clear(); + shortIdToUri.clear(); + uriToShortId.clear(); + idCounter = 1; + + SharedPreferences.Editor editor = preferences.edit(); + Map all = preferences.getAll(); + for (String key : all.keySet()) { + if (key.startsWith(CACHE_PREFIX) || key.equals(ID_COUNTER_KEY)) { + editor.remove(key); + } + } + editor.apply(); + + YaaccLogger.i(getClass().getName(), "Cache cleared"); + } + + public void shutdown() { + preloadExecutor.shutdown(); + trimCache(); + } + + public boolean isPreloading() { + return isPreloading; + } + + public int getTotalFilesIndexed() { + return totalFilesIndexed; + } + + public int getCacheSize() { + return lruCache.size(); + } + + /** + * Simple LRU cache using LinkedHashMap. + */ + private static class LRUCache extends LinkedHashMap { + private final int maxSize; + private final java.util.List evictedKeys = new java.util.ArrayList<>(); + + LRUCache(int maxSize) { + super(16, 0.75f, true); // accessOrder = true + this.maxSize = maxSize; + } + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > maxSize) { + evictedKeys.add(eldest.getKey()); + return true; + } + return false; + } + + java.util.List getEvictedKeys() { + java.util.List keys = new java.util.ArrayList<>(evictedKeys); + evictedKeys.clear(); + return keys; + } + } +} diff --git a/yaacc/src/main/java/de/yaacc/util/SAFMetadata.java b/yaacc/src/main/java/de/yaacc/util/SAFMetadata.java new file mode 100644 index 00000000..15e7e57e --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/util/SAFMetadata.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.util; + +/** + * Cached metadata for SAF (Storage Access Framework) files. + */ +public class SAFMetadata { + public final String duration; // "HH:MM:SS" format + public final String mimeType; // "audio/mpeg", "video/mp4", etc. + public final String shortId; // Short numeric ID for UPnP + public final long fileSize; // File size in bytes + public final long timestamp; // When cached (System.currentTimeMillis()) + + public SAFMetadata(String duration, String mimeType, String shortId, long fileSize) { + this.duration = duration; + this.mimeType = mimeType; + this.shortId = shortId; + this.fileSize = fileSize; + this.timestamp = System.currentTimeMillis(); + } + + // For serialization to SharedPreferences + public String serialize() { + return duration + "|" + mimeType + "|" + shortId + "|" + fileSize + "|" + timestamp; + } + + // For deserialization from SharedPreferences + public static SAFMetadata deserialize(String data) { + if (data == null) return null; + String[] parts = data.split("\\|", 6); + if (parts.length < 3) return null; + String shortId = parts.length >= 4 ? parts[2] : null; + long fileSize = parts.length >= 5 ? Long.parseLong(parts[3]) : 0; + return new SAFMetadata(parts[0], parts[1], shortId, fileSize); + } +} diff --git a/yaacc/src/main/java/de/yaacc/util/SafPermissionManager.java b/yaacc/src/main/java/de/yaacc/util/SafPermissionManager.java new file mode 100644 index 00000000..9d43e6df --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/util/SafPermissionManager.java @@ -0,0 +1,123 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.UriPermission; +import android.net.Uri; + +import androidx.documentfile.provider.DocumentFile; +import androidx.preference.PreferenceManager; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import de.yaacc.R; +import de.yaacc.upnp.server.contentdirectory.MediaPathFilter; + +public class SafPermissionManager { + + private static final int MAX_PERMISSIONS = 120; // Leave buffer below Android's 128 limit + + public static void validateAndCleanupPermissions(Context context) { + Set storedUris = MediaPathFilter.getSafPathes(context); + List grantedPermissions = context.getContentResolver().getPersistedUriPermissions(); + Set toBeRemoved = new HashSet<>(); + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context); + YaaccLogger.d(SafPermissionManager.class.getName(), "Checking " + storedUris.size() + " stored SAF URIs, " + + grantedPermissions.size() + " total permissions"); + + // Check each stored URI to see if we still have permission + for (String uriString : storedUris) { + try { + Uri uri = Uri.parse(uriString); + boolean hasPermission = grantedPermissions.stream() + .anyMatch(perm -> uriString.contains(perm.getUri().toString()) && perm.isReadPermission()); + + grantedPermissions.stream().forEach(it -> YaaccLogger.d(SafPermissionManager.class.getName(), "Permission: " + it.getUri() + " " + it.isReadPermission())); + YaaccLogger.d(SafPermissionManager.class.getName(), "Checking permission for SAF URI: " + uriString + " -> " + hasPermission); + if (!hasPermission) { + YaaccLogger.d(SafPermissionManager.class.getName(), "Lost permission for SAF URI removing: " + uriString); + toBeRemoved.add(uriString); + } else { + YaaccLogger.d(SafPermissionManager.class.getName(), "Permission OK for SAF URI: " + uriString); + if (!DocumentFile.fromTreeUri(context, uri).exists()) { + YaaccLogger.d(SafPermissionManager.class.getName(), "SAF URI does not exist anymore removing: " + uriString); + toBeRemoved.add(uriString); + } + } + } catch (Exception e) { + YaaccLogger.w(SafPermissionManager.class.getName(), "Error checking permission for URI: " + uriString, e); + } + } + //release orphaned permissions from MediaPathFilter + Set storedUriSet = new HashSet<>(storedUris); + storedUriSet.removeAll(toBeRemoved); + MediaPathFilter.saveSafPathes(context, storedUriSet); + Set selectedUriSet = new HashSet<>(MediaPathFilter.getSelectedSafPathes(context)); + selectedUriSet.removeAll(toBeRemoved); + //clean up selected uris that are not in the stored uris + selectedUriSet = selectedUriSet.stream().filter(s -> storedUriSet.contains(s)).collect(Collectors.toSet()); + MediaPathFilter.saveSelectedSafPathes(context, selectedUriSet); + for (String uriString : toBeRemoved) { + YaaccLogger.d(SafPermissionManager.class.getName(), "Removing duration cache entry for SAF URI: " + uriString); + if (preferences.contains(context.getString(R.string.settings_duration_format_key) + toBeRemoved)) { + preferences.edit().remove(context.getString(R.string.settings_duration_format_key) + toBeRemoved); + } + } + + // Clean up orphaned permissions if we're approaching the limit + if (grantedPermissions.size() >= 110) { + cleanupOrphanedPermissions(context, storedUriSet, grantedPermissions); + } + + YaaccLogger.i(SafPermissionManager.class.getName(), "SAF permission check complete"); + } + + private static void cleanupOrphanedPermissions(Context context, Set validUris, List grantedPermissions) { + Set validUriSet = new HashSet<>(validUris); + + for (UriPermission permission : grantedPermissions) { + String uriString = permission.getUri().toString(); + if (!validUriSet.contains(uriString)) { + try { + context.getContentResolver().releasePersistableUriPermission( + permission.getUri(), + android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION + ); + YaaccLogger.i(SafPermissionManager.class.getName(), "Released orphaned permission: " + uriString); + } catch (Exception e) { + YaaccLogger.w(SafPermissionManager.class.getName(), "Failed to release permission: " + uriString, e); + } + } + } + } + + public static boolean canAddMorePermissions(Context context) { + List grantedPermissions = context.getContentResolver().getPersistedUriPermissions(); + return grantedPermissions.size() < MAX_PERMISSIONS; + } + + public static int getPermissionCount(Context context) { + return context.getContentResolver().getPersistedUriPermissions().size(); + } +} diff --git a/yaacc/src/main/java/de/yaacc/util/ShutdownTimerListener.java b/yaacc/src/main/java/de/yaacc/util/ShutdownTimerListener.java index 8cb0bc7a..cbfd7238 100644 --- a/yaacc/src/main/java/de/yaacc/util/ShutdownTimerListener.java +++ b/yaacc/src/main/java/de/yaacc/util/ShutdownTimerListener.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ package de.yaacc.util; public interface ShutdownTimerListener { diff --git a/yaacc/src/main/java/de/yaacc/util/ThemeHelper.java b/yaacc/src/main/java/de/yaacc/util/ThemeHelper.java index de95e0a9..247fc970 100644 --- a/yaacc/src/main/java/de/yaacc/util/ThemeHelper.java +++ b/yaacc/src/main/java/de/yaacc/util/ThemeHelper.java @@ -1,3 +1,21 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ package de.yaacc.util; import android.content.res.Resources; diff --git a/yaacc/src/main/java/de/yaacc/util/YaaccLogger.java b/yaacc/src/main/java/de/yaacc/util/YaaccLogger.java new file mode 100644 index 00000000..84bbf2c0 --- /dev/null +++ b/yaacc/src/main/java/de/yaacc/util/YaaccLogger.java @@ -0,0 +1,138 @@ +/* + * + * Copyright (C) 2026 Tobias Schoene www.yaacc.de + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 3 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + */ +package de.yaacc.util; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.util.Log; + +public class YaaccLogger { + + private static final int VERBOSE = 2; + private static final int DEBUG = 3; + private static final int INFO = 4; + private static final int WARN = 5; + private static final int ERROR = 6; + private static final int FATAL = 7; + + private static Context appContext; + private static int currentLogLevel = ERROR; + + public static void initialize(Context context) { + appContext = context.getApplicationContext(); + updateLogLevel(); + } + + public static void updateLogLevel() { + if (appContext == null) return; + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(appContext); + String logLevel = preferences.getString("settings_log_level_key", "E"); + currentLogLevel = parseLogLevel(logLevel); + } + + public static void setLogLevel(String level) { + currentLogLevel = parseLogLevel(level); + } + + private static int parseLogLevel(String level) { + switch (level) { + case "V": + return VERBOSE; + case "D": + return DEBUG; + case "I": + return INFO; + case "W": + return WARN; + case "E": + return ERROR; + case "F": + return FATAL; + default: + return ERROR; + } + } + + public static void v(String tag, String msg) { + if (currentLogLevel <= VERBOSE) { + Log.v(tag, msg); + } + } + + public static void v(String tag, String msg, Throwable tr) { + if (currentLogLevel <= VERBOSE) { + Log.v(tag, msg, tr); + } + } + + public static void d(String tag, String msg) { + if (currentLogLevel <= DEBUG) { + Log.d(tag, msg); + } + } + + public static void d(String tag, String msg, Throwable tr) { + if (currentLogLevel <= DEBUG) { + Log.d(tag, msg, tr); + } + } + + public static void i(String tag, String msg) { + if (currentLogLevel <= INFO) { + Log.i(tag, msg); + } + } + + public static void i(String tag, String msg, Throwable tr) { + if (currentLogLevel <= INFO) { + Log.i(tag, msg, tr); + } + } + + public static void w(String tag, String msg) { + if (currentLogLevel <= WARN) { + Log.w(tag, msg); + } + } + + public static void w(String tag, String msg, Throwable tr) { + if (currentLogLevel <= WARN) { + Log.w(tag, msg, tr); + } + } + + public static void w(String tag, Throwable tr) { + if (currentLogLevel <= WARN) { + Log.w(tag, tr); + } + } + + public static void e(String tag, String msg) { + if (currentLogLevel <= ERROR) { + Log.e(tag, msg); + } + } + + public static void e(String tag, String msg, Throwable tr) { + if (currentLogLevel <= ERROR) { + Log.e(tag, msg, tr); + } + } +} diff --git a/yaacc/src/main/java/de/yaacc/util/image/IconDownloadTask.java b/yaacc/src/main/java/de/yaacc/util/image/IconDownloadTask.java index ee9885fe..1630b658 100644 --- a/yaacc/src/main/java/de/yaacc/util/image/IconDownloadTask.java +++ b/yaacc/src/main/java/de/yaacc/util/image/IconDownloadTask.java @@ -35,18 +35,40 @@ public class IconDownloadTask extends AsyncTask { private final ImageView imageView; private final IconDownloadCacheHandler cache; private final BrowseContentItemAdapter browseContentItemAdapter; + private final String deviceId; + private final int targetWidth; + private final int targetHeight; public IconDownloadTask(ImageView imageView) { - this.imageView = imageView; - this.browseContentItemAdapter = null; - this.cache = IconDownloadCacheHandler.getInstance(); + this(imageView, 48, 48); } public IconDownloadTask(ImageView imageView, BrowseContentItemAdapter browseContentItemAdapter) { this.imageView = imageView; this.cache = IconDownloadCacheHandler.getInstance(); this.browseContentItemAdapter = browseContentItemAdapter; + this.deviceId = null; + this.targetWidth = 48; + this.targetHeight = 48; + } + + public IconDownloadTask(ImageView imageView, String deviceId) { + this.imageView = imageView; + this.cache = IconDownloadCacheHandler.getInstance(); + this.browseContentItemAdapter = null; + this.deviceId = deviceId; + this.targetWidth = 48; + this.targetHeight = 48; + } + + public IconDownloadTask(ImageView imageView, int width, int height) { + this.imageView = imageView; + this.cache = IconDownloadCacheHandler.getInstance(); + this.browseContentItemAdapter = null; + this.deviceId = null; + this.targetWidth = width; + this.targetHeight = height; } @@ -58,8 +80,8 @@ public IconDownloadTask(ImageView imageView, BrowseContentItemAdapter browseCont */ @Override protected Bitmap doInBackground(Uri... uri) { - int defaultHeight = 48; - int defaultWidth = 48; + int defaultHeight = targetHeight; + int defaultWidth = targetWidth; Bitmap result = null; if (cache != null) { result = cache.getBitmap(uri[0], defaultHeight, defaultWidth); @@ -83,7 +105,15 @@ protected Bitmap doInBackground(Uri... uri) { */ @Override protected void onPostExecute(Bitmap result) { - imageView.setImageBitmap(result); + // Check if ImageView still belongs to the same device + if (deviceId != null && !deviceId.equals(imageView.getTag())) { + return; // Skip - ImageView has been recycled for different device + } + + if (result != null) { + imageView.setVisibility(android.view.View.VISIBLE); + imageView.setImageBitmap(result); + } if (browseContentItemAdapter != null) { browseContentItemAdapter.removeTask(this); } diff --git a/yaacc/src/main/java/de/yaacc/util/image/ImageDownloadTask.java b/yaacc/src/main/java/de/yaacc/util/image/ImageDownloadTask.java index 30889915..2e9ad9cc 100644 --- a/yaacc/src/main/java/de/yaacc/util/image/ImageDownloadTask.java +++ b/yaacc/src/main/java/de/yaacc/util/image/ImageDownloadTask.java @@ -23,6 +23,8 @@ import android.os.AsyncTask; import android.widget.ImageView; +import androidx.media3.ui.PlayerNotificationManager; + /** * AsyncTask fpr retrieving icons while browsing. * @@ -32,6 +34,7 @@ public class ImageDownloadTask extends AsyncTask { private final ImageView imageView; + private final PlayerNotificationManager.BitmapCallback bitmapCallback; private final IconDownloadCacheHandler cache; /** @@ -41,6 +44,18 @@ public class ImageDownloadTask extends AsyncTask { */ public ImageDownloadTask(ImageView imageView) { this.imageView = imageView; + this.bitmapCallback = null; + this.cache = IconDownloadCacheHandler.getInstance(); + } + + /** + * Initialize a new download with a callback for notification manager + * + * @param callback bitmap callback + */ + public ImageDownloadTask(PlayerNotificationManager.BitmapCallback callback) { + this.imageView = null; + this.bitmapCallback = callback; this.cache = IconDownloadCacheHandler.getInstance(); } @@ -52,11 +67,14 @@ public ImageDownloadTask(ImageView imageView) { */ @Override protected Bitmap doInBackground(Uri... uri) { - if (cache.getBitmap(uri[0], imageView.getWidth(), imageView.getHeight()) == null) { - cache.addBitmap(uri[0], imageView.getWidth(), imageView.getHeight(), new ImageDownloader().retrieveImageWithCertainSize(uri[0], imageView.getWidth(), imageView.getHeight())); + int width = imageView != null ? imageView.getWidth() : 512; + int height = imageView != null ? imageView.getHeight() : 512; + + if (cache.getBitmap(uri[0], width, height) == null) { + cache.addBitmap(uri[0], width, height, new ImageDownloader().retrieveImageWithCertainSize(uri[0], width, height)); } - return cache.getBitmap(uri[0], imageView.getWidth(), imageView.getHeight()); + return cache.getBitmap(uri[0], width, height); } /** @@ -66,6 +84,10 @@ protected Bitmap doInBackground(Uri... uri) { */ @Override protected void onPostExecute(Bitmap result) { - imageView.setImageBitmap(result); + if (imageView != null) { + imageView.setImageBitmap(result); + } else if (bitmapCallback != null) { + bitmapCallback.onBitmap(result); + } } } \ No newline at end of file diff --git a/yaacc/src/main/java/de/yaacc/util/image/ImageDownloader.java b/yaacc/src/main/java/de/yaacc/util/image/ImageDownloader.java index 5a1c1664..093ff40a 100644 --- a/yaacc/src/main/java/de/yaacc/util/image/ImageDownloader.java +++ b/yaacc/src/main/java/de/yaacc/util/image/ImageDownloader.java @@ -22,7 +22,7 @@ import android.graphics.BitmapFactory; import android.net.Uri; import android.util.DisplayMetrics; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import java.io.FilterInputStream; import java.io.IOException; @@ -47,7 +47,7 @@ public ImageDownloader() { * @return image */ public Bitmap retrieveImageWithCertainSize(Uri imageUri, int imageWidth, int imageHeight) { - Log.d(getClass().getName(), "retrieveImage size:" + imageWidth + "x" + imageHeight); + YaaccLogger.d(getClass().getName(), "retrieveImage size:" + imageWidth + "x" + imageHeight); return decodeSampledBitmapFromStream(imageUri, imageWidth, imageHeight); } @@ -67,7 +67,7 @@ private Bitmap decodeSampledBitmapFromStream(Uri imageUri, int reqWidth, try { InputStream is = getUriAsStream(imageUri); - Log.d(this.getClass().getName(), "image uri to load: " + imageUri.toString()); + YaaccLogger.d(this.getClass().getName(), "image uri to load: " + imageUri.toString()); final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = false; options.outWidth = reqWidth; @@ -76,17 +76,17 @@ private Bitmap decodeSampledBitmapFromStream(Uri imageUri, int reqWidth, options.inTempStorage = new byte[7680016]; options.inSampleSize = 1; - Log.d(this.getClass().getName(), + YaaccLogger.d(this.getClass().getName(), "displaying image size width, height, inSampleSize " + options.outWidth + "," + options.outHeight + "," + options.inSampleSize); - Log.d(this.getClass().getName(), "free memory before image load: " + YaaccLogger.d(this.getClass().getName(), "free memory before image load: " + Runtime.getRuntime().freeMemory()); bitmap = BitmapFactory.decodeStream(new FlushedInputStream(is), null, options); - Log.d(this.getClass().getName(), "free memory after image load: " + YaaccLogger.d(this.getClass().getName(), "free memory after image load: " + Runtime.getRuntime().freeMemory()); @@ -104,18 +104,18 @@ private Bitmap decodeSampledBitmapFromStream(Uri imageUri, int reqWidth, outHeight = reqHeight; } bitmap = Bitmap.createScaledBitmap(bitmap, outWidth, outHeight, false); - Log.d(this.getClass().getName(), "free memory after image scaling: " + YaaccLogger.d(this.getClass().getName(), "free memory after image scaling: " + Runtime.getRuntime().freeMemory()); } if (bitmap == null) { - Log.w(this.getClass().getName(), "Bitmap is null !!!"); + YaaccLogger.w(this.getClass().getName(), "Bitmap is null !!!"); } else if (bitmap.getHeight() != reqHeight) { - Log.w(this.getClass().getName(), "Bitmap has wrong size !!! height: " + bitmap.getHeight() + " width: " + bitmap.getWidth()); + YaaccLogger.w(this.getClass().getName(), "Bitmap has wrong size !!! height: " + bitmap.getHeight() + " width: " + bitmap.getWidth()); } } catch (Exception e) { - Log.d(this.getClass().getName(), "while decoding image: " + e.getMessage()); + YaaccLogger.d(this.getClass().getName(), "while decoding image: " + e.getMessage()); } return bitmap; @@ -132,12 +132,12 @@ private Bitmap decodeSampledBitmapFromStream(Uri imageUri, int reqWidth, private InputStream getUriAsStream(Uri imageUri) throws IOException, MalformedURLException { InputStream is; - Log.d(getClass().getName(), "Start load: " + System.currentTimeMillis()); + YaaccLogger.d(getClass().getName(), "Start load: " + System.currentTimeMillis()); is = (InputStream) new java.net.URL(imageUri.toString()) .getContent(); - Log.d(getClass().getName(), "Stop load: " + System.currentTimeMillis()); - Log.d(getClass().getName(), "InputStream: " + is); + YaaccLogger.d(getClass().getName(), "Stop load: " + System.currentTimeMillis()); + YaaccLogger.d(getClass().getName(), "InputStream: " + is); return is; } diff --git a/yaacc/src/main/java/org/fourthline/cling/DefaultUpnpServiceConfiguration.java b/yaacc/src/main/java/org/fourthline/cling/DefaultUpnpServiceConfiguration.java deleted file mode 100644 index ff1c703a..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/DefaultUpnpServiceConfiguration.java +++ /dev/null @@ -1,372 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling; - -import android.util.Log; - -import org.fourthline.cling.binding.xml.DeviceDescriptorBinder; -import org.fourthline.cling.binding.xml.ServiceDescriptorBinder; -import org.fourthline.cling.binding.xml.UDA10DeviceDescriptorBinderImpl; -import org.fourthline.cling.binding.xml.UDA10ServiceDescriptorBinderImpl; -import org.fourthline.cling.model.ModelUtil; -import org.fourthline.cling.model.Namespace; -import org.fourthline.cling.model.message.UpnpHeaders; -import org.fourthline.cling.model.meta.RemoteDeviceIdentity; -import org.fourthline.cling.model.meta.RemoteService; -import org.fourthline.cling.model.types.ServiceType; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.transport.impl.DatagramIOConfigurationImpl; -import org.fourthline.cling.transport.impl.DatagramIOImpl; -import org.fourthline.cling.transport.impl.DatagramProcessorImpl; -import org.fourthline.cling.transport.impl.GENAEventProcessorImpl; -import org.fourthline.cling.transport.impl.MulticastReceiverConfigurationImpl; -import org.fourthline.cling.transport.impl.MulticastReceiverImpl; -import org.fourthline.cling.transport.impl.NetworkAddressFactoryImpl; -import org.fourthline.cling.transport.impl.SOAPActionProcessorImpl; -import org.fourthline.cling.transport.spi.DatagramIO; -import org.fourthline.cling.transport.spi.DatagramProcessor; -import org.fourthline.cling.transport.spi.GENAEventProcessor; -import org.fourthline.cling.transport.spi.MulticastReceiver; -import org.fourthline.cling.transport.spi.NetworkAddressFactory; -import org.fourthline.cling.transport.spi.SOAPActionProcessor; -import org.fourthline.cling.transport.spi.StreamClient; -import org.fourthline.cling.transport.spi.StreamServer; -import org.seamless.util.Exceptions; - -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.SynchronousQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import jakarta.enterprise.inject.Alternative; - -/** - * Default configuration data of a typical UPnP stack. - *

- * This configuration utilizes the default network transport implementation found in - * {@link org.fourthline.cling.transport.impl}. - *

- *

- * This configuration utilizes the DOM default descriptor binders found in - * {@link org.fourthline.cling.binding.xml}. - *

- *

- * The thread Executor is an Executors.newCachedThreadPool() with - * a custom {@link ClingThreadFactory} (it only sets a thread name). - *

- *

- * Note that this pool is effectively unlimited, so the number of threads will - * grow (and shrink) as needed - or restricted by your JVM. - *

- *

- * The default {@link org.fourthline.cling.model.Namespace} is configured without any - * base path or prefix. - *

- * - * @author Christian Bauer - */ -@Alternative -public class DefaultUpnpServiceConfiguration implements UpnpServiceConfiguration { - - - final private int streamListenPort; - - final private ExecutorService defaultExecutorService; - - final private DatagramProcessor datagramProcessor; - final private SOAPActionProcessor soapActionProcessor; - final private GENAEventProcessor genaEventProcessor; - - final private DeviceDescriptorBinder deviceDescriptorBinderUDA10; - final private ServiceDescriptorBinder serviceDescriptorBinderUDA10; - - final private Namespace namespace; - - /** - * Defaults to port '0', ephemeral. - */ - public DefaultUpnpServiceConfiguration() { - this(NetworkAddressFactoryImpl.DEFAULT_TCP_HTTP_LISTEN_PORT); - } - - public DefaultUpnpServiceConfiguration(int streamListenPort) { - this(streamListenPort, true); - } - - protected DefaultUpnpServiceConfiguration(boolean checkRuntime) { - this(NetworkAddressFactoryImpl.DEFAULT_TCP_HTTP_LISTEN_PORT, checkRuntime); - } - - protected DefaultUpnpServiceConfiguration(int streamListenPort, boolean checkRuntime) { - if (checkRuntime && ModelUtil.ANDROID_RUNTIME) { - throw new Error("Unsupported runtime environment, use org.fourthline.cling.android.AndroidUpnpServiceConfiguration"); - } - - this.streamListenPort = streamListenPort; - - defaultExecutorService = createDefaultExecutorService(); - - datagramProcessor = createDatagramProcessor(); - soapActionProcessor = createSOAPActionProcessor(); - genaEventProcessor = createGENAEventProcessor(); - - deviceDescriptorBinderUDA10 = createDeviceDescriptorBinderUDA10(); - serviceDescriptorBinderUDA10 = createServiceDescriptorBinderUDA10(); - - namespace = createNamespace(); - } - - public DatagramProcessor getDatagramProcessor() { - return datagramProcessor; - } - - public SOAPActionProcessor getSoapActionProcessor() { - return soapActionProcessor; - } - - public GENAEventProcessor getGenaEventProcessor() { - return genaEventProcessor; - } - - - public StreamClient createStreamClient() { - /*return new StreamClientImpl( - new StreamClientConfigurationImpl( - getSyncProtocolExecutorService() - ) - );*/ - return null; - } - - public MulticastReceiver createMulticastReceiver(NetworkAddressFactory networkAddressFactory) { - return new MulticastReceiverImpl( - new MulticastReceiverConfigurationImpl( - networkAddressFactory.getMulticastGroup(), - networkAddressFactory.getMulticastPort() - ) - ); - } - - public DatagramIO createDatagramIO(NetworkAddressFactory networkAddressFactory) { - return new DatagramIOImpl(new DatagramIOConfigurationImpl()); - } - - @Override - public StreamServer createStreamServer(ProtocolFactory protocolFactory, NetworkAddressFactory networkAddressFactory) { - return null; - } - - - public Executor getMulticastReceiverExecutor() { - return getDefaultExecutorService(); - } - - public Executor getDatagramIOExecutor() { - return getDefaultExecutorService(); - } - - public ExecutorService getStreamServerExecutorService() { - return getDefaultExecutorService(); - } - - public DeviceDescriptorBinder getDeviceDescriptorBinderUDA10() { - return deviceDescriptorBinderUDA10; - } - - public ServiceDescriptorBinder getServiceDescriptorBinderUDA10() { - return serviceDescriptorBinderUDA10; - } - - public ServiceType[] getExclusiveServiceTypes() { - return new ServiceType[0]; - } - - /** - * @return Defaults to false. - */ - public boolean isReceivedSubscriptionTimeoutIgnored() { - return false; - } - - public UpnpHeaders getDescriptorRetrievalHeaders(RemoteDeviceIdentity identity) { - return null; - } - - public UpnpHeaders getEventSubscriptionHeaders(RemoteService service) { - return null; - } - - /** - * @return Defaults to 1000 milliseconds. - */ - public int getRegistryMaintenanceIntervalMillis() { - return 1000; - } - - /** - * @return Defaults to zero, disabling ALIVE flooding. - */ - public int getAliveIntervalMillis() { - return 0; - } - - public Integer getRemoteDeviceMaxAgeSeconds() { - return null; - } - - public Executor getAsyncProtocolExecutor() { - return getDefaultExecutorService(); - } - - public ExecutorService getSyncProtocolExecutorService() { - return getDefaultExecutorService(); - } - - public Namespace getNamespace() { - return namespace; - } - - public Executor getRegistryMaintainerExecutor() { - return getDefaultExecutorService(); - } - - public Executor getRegistryListenerExecutor() { - return getDefaultExecutorService(); - } - - public NetworkAddressFactory createNetworkAddressFactory() { - return createNetworkAddressFactory(streamListenPort); - } - - public void shutdown() { - Log.v(getClass().getName(), "Shutting down default executor service"); - getDefaultExecutorService().shutdownNow(); - } - - protected NetworkAddressFactory createNetworkAddressFactory(int streamListenPort) { - return new NetworkAddressFactoryImpl(streamListenPort); - } - - protected DatagramProcessor createDatagramProcessor() { - return new DatagramProcessorImpl(); - } - - protected SOAPActionProcessor createSOAPActionProcessor() { - return new SOAPActionProcessorImpl(); - } - - protected GENAEventProcessor createGENAEventProcessor() { - return new GENAEventProcessorImpl(); - } - - protected DeviceDescriptorBinder createDeviceDescriptorBinderUDA10() { - return new UDA10DeviceDescriptorBinderImpl(); - } - - protected ServiceDescriptorBinder createServiceDescriptorBinderUDA10() { - return new UDA10ServiceDescriptorBinderImpl(); - } - - protected Namespace createNamespace() { - return new Namespace(); - } - - protected ExecutorService getDefaultExecutorService() { - return defaultExecutorService; - } - - protected ExecutorService createDefaultExecutorService() { - return new ClingExecutor(); - } - - public static class ClingExecutor extends ThreadPoolExecutor { - - public ClingExecutor() { - this(new ClingThreadFactory(), - new ThreadPoolExecutor.DiscardPolicy() { - // The pool is unbounded but rejections will happen during shutdown - @Override - public void rejectedExecution(Runnable runnable, ThreadPoolExecutor threadPoolExecutor) { - // Log and discard - Log.v(getClass().getName(), "Thread pool rejected execution of " + runnable.getClass()); - super.rejectedExecution(runnable, threadPoolExecutor); - } - } - ); - } - - public ClingExecutor(ThreadFactory threadFactory, RejectedExecutionHandler rejectedHandler) { - // This is the same as Executors.newCachedThreadPool - super(0, - Integer.MAX_VALUE, - 60L, - TimeUnit.SECONDS, - new SynchronousQueue(), - threadFactory, - rejectedHandler - ); - } - - @Override - protected void afterExecute(Runnable runnable, Throwable throwable) { - super.afterExecute(runnable, throwable); - if (throwable != null) { - Throwable cause = Exceptions.unwrap(throwable); - if (cause instanceof InterruptedException) { - // Ignore this, might happen when we shutdownNow() the executor. We can't - // log at this point as the logging system might be stopped already (e.g. - // if it's a CDI component). - return; - } - // Log only - Log.w(getClass().getName(), "Thread terminated " + runnable + " abruptly with exception: " + throwable); - Log.w(getClass().getName(), "Root cause: " + cause); - } - } - } - - // Executors.DefaultThreadFactory is package visibility (...no touching, you unworthy JDK user!) - public static class ClingThreadFactory implements ThreadFactory { - - protected final ThreadGroup group; - protected final AtomicInteger threadNumber = new AtomicInteger(1); - protected final String namePrefix = "cling-"; - - public ClingThreadFactory() { - SecurityManager s = System.getSecurityManager(); - group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup(); - } - - public Thread newThread(Runnable r) { - Thread t = new Thread( - group, r, - namePrefix + threadNumber.getAndIncrement(), - 0 - ); - if (t.isDaemon()) - t.setDaemon(false); - if (t.getPriority() != Thread.NORM_PRIORITY) - t.setPriority(Thread.NORM_PRIORITY); - - return t; - } - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/UpnpService.java b/yaacc/src/main/java/org/fourthline/cling/UpnpService.java deleted file mode 100644 index efda5287..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/UpnpService.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling; - -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.registry.Registry; -import org.fourthline.cling.transport.Router; - -/** - * Primary interface of the Cling Core UPnP stack. - *

- * An implementation can either start immediately when constructed or offer an additional - * method that starts the UPnP stack on-demand. Implementations are not required to be - * restartable after shutdown. - *

- *

- * Implementations are always thread-safe and can be shared and called concurrently. - *

- * - * @author Christian Bauer - */ -public interface UpnpService { - - public UpnpServiceConfiguration getConfiguration(); - - public ControlPoint getControlPoint(); - - public ProtocolFactory getProtocolFactory(); - - public Registry getRegistry(); - - public Router getRouter(); - - /** - * Stopping the UPnP stack. - *

- * Clients are required to stop the UPnP stack properly. Notifications for - * disappearing devices will be multicast'ed, existing event subscriptions cancelled. - *

- */ - public void shutdown(); - - static public class Start { - - } - - static public class Shutdown { - - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/UpnpServiceConfiguration.java b/yaacc/src/main/java/org/fourthline/cling/UpnpServiceConfiguration.java deleted file mode 100644 index 3b2e809e..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/UpnpServiceConfiguration.java +++ /dev/null @@ -1,256 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling; - -import org.fourthline.cling.binding.xml.DeviceDescriptorBinder; -import org.fourthline.cling.binding.xml.ServiceDescriptorBinder; -import org.fourthline.cling.model.Namespace; -import org.fourthline.cling.model.message.UpnpHeaders; -import org.fourthline.cling.model.meta.RemoteDeviceIdentity; -import org.fourthline.cling.model.meta.RemoteService; -import org.fourthline.cling.model.types.ServiceType; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.transport.spi.DatagramIO; -import org.fourthline.cling.transport.spi.DatagramProcessor; -import org.fourthline.cling.transport.spi.GENAEventProcessor; -import org.fourthline.cling.transport.spi.MulticastReceiver; -import org.fourthline.cling.transport.spi.NetworkAddressFactory; -import org.fourthline.cling.transport.spi.SOAPActionProcessor; -import org.fourthline.cling.transport.spi.StreamClient; -import org.fourthline.cling.transport.spi.StreamServer; - -import java.util.concurrent.Executor; -import java.util.concurrent.ExecutorService; - -/** - * Shared configuration data of the UPnP stack. - *

- * This interface offers methods for retrieval of configuration data by the - * {@link org.fourthline.cling.transport.Router} and the {@link org.fourthline.cling.registry.Registry}, - * as well as other parts of the UPnP stack. - *

- *

- * You can re-use this interface if you implement a subclass of {@link UpnpServiceImpl} or - * if you create a new implementation of {@link UpnpService}. - *

- * - * @author Christian Bauer - */ -public interface UpnpServiceConfiguration { - - /** - * @return A new instance of the {@link org.fourthline.cling.transport.spi.NetworkAddressFactory} interface. - */ - public NetworkAddressFactory createNetworkAddressFactory(); - - /** - * @return The shared implementation of {@link org.fourthline.cling.transport.spi.DatagramProcessor}. - */ - public DatagramProcessor getDatagramProcessor(); - - /** - * @return The shared implementation of {@link org.fourthline.cling.transport.spi.SOAPActionProcessor}. - */ - public SOAPActionProcessor getSoapActionProcessor(); - - /** - * @return The shared implementation of {@link org.fourthline.cling.transport.spi.GENAEventProcessor}. - */ - public GENAEventProcessor getGenaEventProcessor(); - - /** - * @return A new instance of the {@link org.fourthline.cling.transport.spi.StreamClient} interface. - */ - public StreamClient createStreamClient(); - - /** - * @param networkAddressFactory The configured {@link org.fourthline.cling.transport.spi.NetworkAddressFactory}. - * @return A new instance of the {@link org.fourthline.cling.transport.spi.MulticastReceiver} interface. - */ - public MulticastReceiver createMulticastReceiver(NetworkAddressFactory networkAddressFactory); - - /** - * @param networkAddressFactory The configured {@link org.fourthline.cling.transport.spi.NetworkAddressFactory}. - * @return A new instance of the {@link org.fourthline.cling.transport.spi.DatagramIO} interface. - */ - public DatagramIO createDatagramIO(NetworkAddressFactory networkAddressFactory); - - /** - * @param networkAddressFactory The configured {@link org.fourthline.cling.transport.spi.NetworkAddressFactory}. - * @return A new instance of the {@link org.fourthline.cling.transport.spi.StreamServer} interface. - */ - public StreamServer createStreamServer(ProtocolFactory protocolFactory, NetworkAddressFactory networkAddressFactory); - - /** - * @return The executor which runs the listening background threads for multicast datagrams. - */ - public Executor getMulticastReceiverExecutor(); - - /** - * @return The executor which runs the listening background threads for unicast datagrams. - */ - public Executor getDatagramIOExecutor(); - - /** - * @return The executor which runs the listening background threads for HTTP requests. - */ - public ExecutorService getStreamServerExecutorService(); - - /** - * @return The shared implementation of {@link org.fourthline.cling.binding.xml.DeviceDescriptorBinder} for the UPnP 1.0 Device Architecture.. - */ - public DeviceDescriptorBinder getDeviceDescriptorBinderUDA10(); - - /** - * @return The shared implementation of {@link org.fourthline.cling.binding.xml.ServiceDescriptorBinder} for the UPnP 1.0 Device Architecture.. - */ - public ServiceDescriptorBinder getServiceDescriptorBinderUDA10(); - - /** - * Returns service types that can be handled by this UPnP stack, all others will be ignored. - *

- * Return null to completely disable remote device and service discovery. - * All incoming notifications and search responses will then be dropped immediately. - * This is mostly useful in applications that only provide services with no (remote) - * control point functionality. - *

- *

- * Note that a discovered service type with version 2 or 3 will match an exclusive - * service type with version 1. UPnP services are required to be backwards - * compatible, version 2 is a superset of version 1, and version 3 is a superset - * of version 2, etc. - *

- * - * @return An array of service types that are exclusively discovered, no other service will - * be discovered. A null return value will disable discovery! - * An empty array means all services will be discovered. - */ - public ServiceType[] getExclusiveServiceTypes(); - - /** - * @return The time in milliseconds to wait between each registry maintenance operation. - */ - public int getRegistryMaintenanceIntervalMillis(); - - /** - * Optional setting for flooding alive NOTIFY messages for local devices. - *

- * Use this to advertise local devices at the specified interval, independent of its - * {@link org.fourthline.cling.model.meta.DeviceIdentity#maxAgeSeconds} value. Note - * that this will increase network traffic. - *

- *

- * Some control points (XBMC and other Platinum UPnP SDK based devices, OPPO-93) seem - * to not properly receive SSDP M-SEARCH replies sent by Cling, but will handle NOTIFY - * alive messages just fine. - *

- * - * @return The time in milliseconds for ALIVE message intervals, set to 0 to disable - */ - public int getAliveIntervalMillis(); - - /** - * Ignore the received event subscription timeout from remote control points. - *

- * Some control points have trouble renewing subscriptions properly; enabling this option - * in conjunction with a high value for - * {@link org.fourthline.cling.model.UserConstants#DEFAULT_SUBSCRIPTION_DURATION_SECONDS} - * ensures that your devices will not disappear on such control points. - *

- * - * @return true if the timeout in incoming event subscriptions should be ignored - * and the default value ({@link org.fourthline.cling.model.UserConstants#DEFAULT_SUBSCRIPTION_DURATION_SECONDS}) - * should be used instead. - */ - public boolean isReceivedSubscriptionTimeoutIgnored(); - - /** - * Returns the time in seconds a remote device will be registered until it is expired. - *

- * This setting is useful on systems which do not support multicast networking - * (Android on HTC phones, for example). On such a system you will not receive messages when a - * remote device disappears from the network and you will not receive its periodic heartbeat - * alive messages. Only an initial search response (UDP unicast) has been received from the - * remote device, with its proposed maximum age. To avoid (early) expiration of the remote - * device, you can override its maximum age with this configuration setting, ignoring the - * initial maximum age sent by the device. You most likely want to return - * 0 in this case, so that the remote device is never expired unless you - * manually remove it from the {@link org.fourthline.cling.registry.Registry}. You typically remove - * the device when an action or GENA subscription request to the remote device failed. - *

- * - * @return null (the default) to accept the remote device's proposed maximum age, or - * 0 for unlimited age, or a value in seconds. - */ - public Integer getRemoteDeviceMaxAgeSeconds(); - - /** - * Optional extra headers for device descriptor retrieval HTTP requests. - *

- * Some devices might require extra headers to recognize your control point, use this - * method to set these headers. They will be used for every descriptor (XML) retrieval - * HTTP request by Cling. See {@link org.fourthline.cling.model.profile.ClientInfo} for - * action request messages. - *

- * - * @param identity The (so far) discovered identity of the remote device. - * @return null or extra HTTP headers. - */ - public UpnpHeaders getDescriptorRetrievalHeaders(RemoteDeviceIdentity identity); - - /** - * Optional extra headers for event subscription (almost HTTP) messages. - *

- * Some devices might require extra headers to recognize your control point, use this - * method to set these headers for GENA subscriptions. Note that the headers will - * not be applied to actual event messages, only subscribe, unsubscribe, and renewal. - *

- * - * @return null or extra HTTP headers. - */ - public UpnpHeaders getEventSubscriptionHeaders(RemoteService service); - - /** - * @return The executor which runs the processing of asynchronous aspects of the UPnP stack (discovery). - */ - public Executor getAsyncProtocolExecutor(); - - /** - * @return The executor service which runs the processing of synchronous aspects of the UPnP stack (description, control, GENA). - */ - public ExecutorService getSyncProtocolExecutorService(); - - /** - * @return An instance of {@link org.fourthline.cling.model.Namespace} for this UPnP stack. - */ - public Namespace getNamespace(); - - /** - * @return The executor which runs the background thread for maintaining the registry. - */ - public Executor getRegistryMaintainerExecutor(); - - /** - * @return The executor which runs the notification threads of registry listeners. - */ - public Executor getRegistryListenerExecutor(); - - /** - * Called by the {@link org.fourthline.cling.UpnpService} on shutdown, useful to e.g. shutdown thread pools. - */ - public void shutdown(); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/UpnpServiceImpl.java b/yaacc/src/main/java/org/fourthline/cling/UpnpServiceImpl.java deleted file mode 100644 index f5df2787..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/UpnpServiceImpl.java +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.controlpoint.ControlPointImpl; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.protocol.ProtocolFactoryImpl; -import org.fourthline.cling.registry.Registry; -import org.fourthline.cling.registry.RegistryImpl; -import org.fourthline.cling.registry.RegistryListener; -import org.fourthline.cling.transport.Router; -import org.fourthline.cling.transport.RouterException; -import org.fourthline.cling.transport.RouterImpl; -import org.seamless.util.Exceptions; - -import jakarta.enterprise.inject.Alternative; - -/** - * Default implementation of {@link UpnpService}, starts immediately on construction. - *

- * If no {@link UpnpServiceConfiguration} is provided it will automatically - * instantiate {@link DefaultUpnpServiceConfiguration}. This configuration does not - * work on Android! Use the {@link org.fourthline.cling.android.AndroidUpnpService} - * application component instead. - *

- *

- * Override the various create...() methods to customize instantiation of protocol factory, - * router, etc. - *

- * - * @author Christian Bauer - */ -@Alternative -public class UpnpServiceImpl implements UpnpService { - - - protected final UpnpServiceConfiguration configuration; - protected final ControlPoint controlPoint; - protected final ProtocolFactory protocolFactory; - protected final Registry registry; - protected final Router router; - - public UpnpServiceImpl() { - this(new DefaultUpnpServiceConfiguration()); - } - - public UpnpServiceImpl(RegistryListener... registryListeners) { - this(new DefaultUpnpServiceConfiguration(), registryListeners); - } - - public UpnpServiceImpl(UpnpServiceConfiguration configuration, RegistryListener... registryListeners) { - this.configuration = configuration; - - Log.v(getClass().getName(), ">>> Starting UPnP service..."); - - Log.v(getClass().getName(), "Using configuration: " + getConfiguration().getClass().getName()); - - // Instantiation order is important: Router needs to start its network services after registry is ready - - this.protocolFactory = createProtocolFactory(); - - this.registry = createRegistry(protocolFactory); - for (RegistryListener registryListener : registryListeners) { - this.registry.addListener(registryListener); - } - - this.router = createRouter(protocolFactory, registry); - - try { - this.router.enable(); - } catch (RouterException ex) { - throw new RuntimeException("Enabling network router failed: " + ex, ex); - } - - this.controlPoint = createControlPoint(protocolFactory, registry); - - Log.v(getClass().getName(), "<<< UPnP service started successfully"); - } - - protected ProtocolFactory createProtocolFactory() { - return new ProtocolFactoryImpl(this); - } - - protected Registry createRegistry(ProtocolFactory protocolFactory) { - return new RegistryImpl(this); - } - - protected Router createRouter(ProtocolFactory protocolFactory, Registry registry) { - return new RouterImpl(getConfiguration(), protocolFactory); - } - - protected ControlPoint createControlPoint(ProtocolFactory protocolFactory, Registry registry) { - return new ControlPointImpl(getConfiguration(), protocolFactory, registry); - } - - public UpnpServiceConfiguration getConfiguration() { - return configuration; - } - - public ControlPoint getControlPoint() { - return controlPoint; - } - - public ProtocolFactory getProtocolFactory() { - return protocolFactory; - } - - public Registry getRegistry() { - return registry; - } - - public Router getRouter() { - return router; - } - - synchronized public void shutdown() { - shutdown(false); - } - - protected void shutdown(boolean separateThread) { - Runnable shutdown = new Runnable() { - @Override - public void run() { - Log.v(getClass().getName(), ">>> Shutting down UPnP service..."); - shutdownRegistry(); - shutdownRouter(); - shutdownConfiguration(); - Log.v(getClass().getName(), "<<< UPnP service shutdown completed"); - } - }; - if (separateThread) { - // This is not a daemon thread, it has to complete! - new Thread(shutdown).start(); - } else { - shutdown.run(); - } - } - - protected void shutdownRegistry() { - getRegistry().shutdown(); - } - - protected void shutdownRouter() { - try { - getRouter().shutdown(); - } catch (RouterException ex) { - Throwable cause = Exceptions.unwrap(ex); - if (cause instanceof InterruptedException) { - Log.v(getClass().getName(), "Router shutdown was interrupted: " + ex, cause); - } else { - Log.e(getClass().getName(), "Router error on shutdown: " + ex, cause); - } - } - } - - protected void shutdownConfiguration() { - getConfiguration().shutdown(); - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationActionBinder.java b/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationActionBinder.java index 72548d73..e6d48311 100644 --- a/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationActionBinder.java +++ b/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationActionBinder.java @@ -15,7 +15,7 @@ package org.fourthline.cling.binding.annotations; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.binding.LocalServiceBindingException; import org.fourthline.cling.model.Constants; @@ -83,7 +83,7 @@ public Action appendAction(Map actions) throws LocalServ name = AnnotationLocalServiceBinder.toUpnpActionName(getMethod().getName()); } - Log.v(getClass().getName(), "Creating action and executor: " + name); + YaaccLogger.v(getClass().getName(), "Creating action and executor: " + name); List inputArguments = createInputArguments(); Map, StateVariableAccessor> outputArguments = createOutputArguments(); @@ -193,7 +193,7 @@ protected Map, StateVariableAccessor> createOutputA hasMultipleOutputArguments ); - Log.v(getClass().getName(), "Found related state variable for output argument '" + argumentName + "': " + stateVariable); + YaaccLogger.v(getClass().getName(), "Found related state variable for output argument '" + argumentName + "': " + stateVariable); ActionArgument outputArgument = new ActionArgument( argumentName, @@ -216,7 +216,7 @@ protected StateVariableAccessor findOutputArgumentAccessor(StateVariable stateVa if (isVoid) { if (getterName != null && getterName.length() > 0) { - Log.v(getClass().getName(), "Action method is void, will use getter method named: " + getterName); + YaaccLogger.v(getClass().getName(), "Action method is void, will use getter method named: " + getterName); // Use the same class as the action method Method getter = Reflections.getMethod(getMethod().getDeclaringClass(), getterName); @@ -230,13 +230,13 @@ protected StateVariableAccessor findOutputArgumentAccessor(StateVariable stateVa return new GetterStateVariableAccessor(getter); } else { - Log.v(getClass().getName(), "Action method is void, trying to find existing accessor of related: " + stateVariable); + YaaccLogger.v(getClass().getName(), "Action method is void, trying to find existing accessor of related: " + stateVariable); return getStateVariables().get(stateVariable); } } else if (getterName != null && getterName.length() > 0) { - Log.v(getClass().getName(), "Action method is not void, will use getter method on returned instance: " + getterName); + YaaccLogger.v(getClass().getName(), "Action method is not void, will use getter method on returned instance: " + getterName); // Use the returned class Method getter = Reflections.getMethod(getMethod().getReturnType(), getterName); @@ -250,7 +250,7 @@ protected StateVariableAccessor findOutputArgumentAccessor(StateVariable stateVa return new GetterStateVariableAccessor(getter); } else if (!multipleArguments) { - Log.v(getClass().getName(), "Action method is not void, will use the returned instance: " + getMethod().getReturnType()); + YaaccLogger.v(getClass().getName(), "Action method is not void, will use the returned instance: " + getMethod().getReturnType()); validateType(stateVariable, getMethod().getReturnType()); } @@ -268,7 +268,7 @@ protected StateVariable findRelatedStateVariable(String declaredName, String arg if (relatedStateVariable == null && argumentName != null && argumentName.length() > 0) { String actualName = AnnotationLocalServiceBinder.toUpnpStateVariableName(argumentName); - Log.v(getClass().getName(), "Finding related state variable with argument name (converted to UPnP name): " + actualName); + YaaccLogger.v(getClass().getName(), "Finding related state variable with argument name (converted to UPnP name): " + actualName); relatedStateVariable = getStateVariable(argumentName); } @@ -276,7 +276,7 @@ protected StateVariable findRelatedStateVariable(String declaredName, String arg // Try with A_ARG_TYPE prefix String actualName = AnnotationLocalServiceBinder.toUpnpStateVariableName(argumentName); actualName = Constants.ARG_TYPE_PREFIX + actualName; - Log.v(getClass().getName(), "Finding related state variable with prefixed argument name (converted to UPnP name): " + actualName); + YaaccLogger.v(getClass().getName(), "Finding related state variable with prefixed argument name (converted to UPnP name): " + actualName); relatedStateVariable = getStateVariable(actualName); } @@ -284,7 +284,7 @@ protected StateVariable findRelatedStateVariable(String declaredName, String arg // TODO: Well, this is often a nice shortcut but sometimes might have false positives String methodPropertyName = Reflections.getMethodPropertyName(methodName); if (methodPropertyName != null) { - Log.v(getClass().getName(), "Finding related state variable with method property name: " + methodPropertyName); + YaaccLogger.v(getClass().getName(), "Finding related state variable with method property name: " + methodPropertyName); relatedStateVariable = getStateVariable( AnnotationLocalServiceBinder.toUpnpStateVariableName(methodPropertyName) @@ -305,7 +305,7 @@ protected void validateType(StateVariable stateVariable, Class type) throws Loca ? Datatype.Default.STRING : Datatype.Default.getByJavaType(type); - Log.v(getClass().getName(), "Expecting '" + stateVariable + "' to match default mapping: " + expectedDefaultMapping); + YaaccLogger.v(getClass().getName(), "Expecting '" + stateVariable + "' to match default mapping: " + expectedDefaultMapping); if (expectedDefaultMapping != null && !stateVariable.getTypeDetails().getDatatype().isHandlingJavaType(expectedDefaultMapping.getJavaType())) { @@ -323,7 +323,7 @@ protected void validateType(StateVariable stateVariable, Class type) throws Loca ); } - Log.v(getClass().getName(), "State variable matches required argument datatype (or can't be validated because it is custom)"); + YaaccLogger.v(getClass().getName(), "State variable matches required argument datatype (or can't be validated because it is custom)"); } protected StateVariable getStateVariable(String name) { diff --git a/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationLocalServiceBinder.java b/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationLocalServiceBinder.java index fd43f714..d61dafbd 100644 --- a/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationLocalServiceBinder.java +++ b/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationLocalServiceBinder.java @@ -15,7 +15,7 @@ package org.fourthline.cling.binding.annotations; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.binding.LocalServiceBinder; import org.fourthline.cling.binding.LocalServiceBindingException; @@ -58,7 +58,7 @@ public class AnnotationLocalServiceBinder implements LocalServiceBinder { public LocalService read(Class clazz) throws LocalServiceBindingException { - Log.v(getClass().getName(), "Reading and binding annotations of service implementation class: " + clazz); + YaaccLogger.v(getClass().getName(), "Reading and binding annotations of service implementation class: " + clazz); // Read the service ID and service type from the annotation if (clazz.isAnnotationPresent(UpnpService.class)) { @@ -106,9 +106,9 @@ public LocalService read(Class clazz, ServiceId id, ServiceType type, return new LocalService(type, id, actions, stateVariables, stringConvertibleTypes, supportsQueryStateVariables); } catch (ValidationException ex) { - Log.e(getClass().getName(), "Could not validate device model: " + ex.toString()); + YaaccLogger.e(getClass().getName(), "Could not validate device model: " + ex.toString()); for (ValidationError validationError : ex.getErrors()) { - Log.e(getClass().getName(), validationError.toString()); + YaaccLogger.e(getClass().getName(), validationError.toString()); } throw new LocalServiceBindingException("Validation of model failed, check the log"); } @@ -168,7 +168,7 @@ protected Map readStateVariables(Class } else if (getter != null) { accessor = new GetterStateVariableAccessor(getter); } else { - Log.v(getClass().getName(), "No field or getter found for state variable, skipping accessor: " + v.name()); + YaaccLogger.v(getClass().getName(), "No field or getter found for state variable, skipping accessor: " + v.name()); } StateVariable stateVar = diff --git a/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationStateVariableBinder.java b/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationStateVariableBinder.java index 3ed1553a..7d7b1965 100644 --- a/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationStateVariableBinder.java +++ b/yaacc/src/main/java/org/fourthline/cling/binding/annotations/AnnotationStateVariableBinder.java @@ -15,7 +15,7 @@ package org.fourthline.cling.binding.annotations; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.binding.AllowedValueProvider; import org.fourthline.cling.binding.AllowedValueRangeProvider; @@ -67,7 +67,7 @@ public Set getStringConvertibleTypes() { protected StateVariable createStateVariable() throws LocalServiceBindingException { - Log.v(getClass().getName(), "Creating state variable '" + getName() + "' with accessor: " + getAccessor()); + YaaccLogger.v(getClass().getName(), "Creating state variable '" + getName() + "' with accessor: " + getAccessor()); // Datatype Datatype datatype = createDatatype(); @@ -88,7 +88,7 @@ protected StateVariable createStateVariable() throws LocalServiceBindingExceptio } else if (getAccessor() != null && getAccessor().getReturnType().isEnum()) { allowedValues = getAllowedValues(getAccessor().getReturnType()); } else { - Log.v(getClass().getName(), "Not restricting allowed values (of string typed state var): " + getName()); + YaaccLogger.v(getClass().getName(), "Not restricting allowed values (of string typed state var): " + getName()); } if (allowedValues != null && defaultValue != null) { @@ -122,7 +122,7 @@ protected StateVariable createStateVariable() throws LocalServiceBindingExceptio getAnnotation().allowedValueStep() ); } else { - Log.v(getClass().getName(), "Not restricting allowed value range (of numeric typed state var): " + getName()); + YaaccLogger.v(getClass().getName(), "Not restricting allowed value range (of numeric typed state var): " + getName()); } // Check if the default value is an allowed value @@ -157,13 +157,13 @@ protected StateVariable createStateVariable() throws LocalServiceBindingExceptio int eventMinimumDelta = 0; if (sendEvents) { if (getAnnotation().eventMaximumRateMilliseconds() > 0) { - Log.v(getClass().getName(), "Moderating state variable events using maximum rate (milliseconds): " + getAnnotation().eventMaximumRateMilliseconds()); + YaaccLogger.v(getClass().getName(), "Moderating state variable events using maximum rate (milliseconds): " + getAnnotation().eventMaximumRateMilliseconds()); eventMaximumRateMillis = getAnnotation().eventMaximumRateMilliseconds(); } if (getAnnotation().eventMinimumDelta() > 0 && Datatype.Builtin.isNumeric(datatype.getBuiltin())) { // TODO: Doesn't consider floating point types! - Log.v(getClass().getName(), "Moderating state variable events using minimum delta: " + getAnnotation().eventMinimumDelta()); + YaaccLogger.v(getClass().getName(), "Moderating state variable events using minimum delta: " + getAnnotation().eventMinimumDelta()); eventMinimumDelta = getAnnotation().eventMinimumDelta(); } } @@ -183,16 +183,16 @@ protected Datatype createDatatype() throws LocalServiceBindingException { if (declaredDatatype.length() == 0 && getAccessor() != null) { Class returnType = getAccessor().getReturnType(); - Log.v(getClass().getName(), "Using accessor return type as state variable type: " + returnType); + YaaccLogger.v(getClass().getName(), "Using accessor return type as state variable type: " + returnType); if (ModelUtil.isStringConvertibleType(getStringConvertibleTypes(), returnType)) { // Enums and toString() convertible types are always state variables with type STRING - Log.v(getClass().getName(), "Return type is string-convertible, using string datatype"); + YaaccLogger.v(getClass().getName(), "Return type is string-convertible, using string datatype"); return Datatype.Default.STRING.getBuiltinType().getDatatype(); } else { Datatype.Default defaultDatatype = Datatype.Default.getByJavaType(returnType); if (defaultDatatype != null) { - Log.v(getClass().getName(), "Return type has default UPnP datatype: " + defaultDatatype); + YaaccLogger.v(getClass().getName(), "Return type has default UPnP datatype: " + defaultDatatype); return defaultDatatype.getBuiltinType().getDatatype(); } } @@ -201,7 +201,7 @@ protected Datatype createDatatype() throws LocalServiceBindingException { // We can also guess that if the allowed values are set then it's a string if ((declaredDatatype == null || declaredDatatype.length() == 0) && (getAnnotation().allowedValues().length > 0 || getAnnotation().allowedValuesEnum() != void.class)) { - Log.v(getClass().getName(), "State variable has restricted allowed values, hence using 'string' datatype"); + YaaccLogger.v(getClass().getName(), "State variable has restricted allowed values, hence using 'string' datatype"); declaredDatatype = "string"; } @@ -210,12 +210,12 @@ protected Datatype createDatatype() throws LocalServiceBindingException { throw new LocalServiceBindingException("Could not detect datatype of state variable: " + getName()); } - Log.v(getClass().getName(), "Trying to find built-in UPnP datatype for detected name: " + declaredDatatype); + YaaccLogger.v(getClass().getName(), "Trying to find built-in UPnP datatype for detected name: " + declaredDatatype); // Now try to find the actual UPnP datatype by mapping the Default to Builtin Datatype.Builtin builtin = Datatype.Builtin.getByDescriptorName(declaredDatatype); if (builtin != null) { - Log.v(getClass().getName(), "Found built-in UPnP datatype: " + builtin); + YaaccLogger.v(getClass().getName(), "Found built-in UPnP datatype: " + builtin); return builtin.getDatatype(); } else { // TODO @@ -230,7 +230,7 @@ protected String createDefaultValue(Datatype datatype) throws LocalServiceBindin // The declared default value needs to match the datatype try { datatype.valueOf(getAnnotation().defaultValue()); - Log.v(getClass().getName(), "Found state variable default value: " + getAnnotation().defaultValue()); + YaaccLogger.v(getClass().getName(), "Found state variable default value: " + getAnnotation().defaultValue()); return getAnnotation().defaultValue(); } catch (Exception ex) { throw new LocalServiceBindingException( @@ -248,7 +248,7 @@ protected String[] getAllowedValues(Class enumType) throws LocalServiceBindingEx throw new LocalServiceBindingException("Allowed values type is not an Enum: " + enumType); } - Log.v(getClass().getName(), "Restricting allowed values of state variable to Enum: " + getName()); + YaaccLogger.v(getClass().getName(), "Restricting allowed values of state variable to Enum: " + getName()); String[] allowedValueStrings = new String[enumType.getEnumConstants().length]; for (int i = 0; i < enumType.getEnumConstants().length; i++) { Object o = enumType.getEnumConstants()[i]; @@ -257,7 +257,7 @@ protected String[] getAllowedValues(Class enumType) throws LocalServiceBindingEx "Allowed value string (that is, Enum constant name) is longer than 32 characters: " + o.toString() ); } - Log.v(getClass().getName(), "Adding allowed value (converted to string): " + o.toString()); + YaaccLogger.v(getClass().getName(), "Adding allowed value (converted to string): " + o.toString()); allowedValueStrings[i] = o.toString(); } diff --git a/yaacc/src/main/java/org/fourthline/cling/binding/xml/RecoveringUDA10DeviceDescriptorBinderImpl.java b/yaacc/src/main/java/org/fourthline/cling/binding/xml/RecoveringUDA10DeviceDescriptorBinderImpl.java index 03251d0b..8d34b007 100644 --- a/yaacc/src/main/java/org/fourthline/cling/binding/xml/RecoveringUDA10DeviceDescriptorBinderImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/binding/xml/RecoveringUDA10DeviceDescriptorBinderImpl.java @@ -15,7 +15,7 @@ package org.fourthline.cling.binding.xml; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.ValidationException; import org.fourthline.cling.model.meta.Device; @@ -47,7 +47,7 @@ public D describe(D undescribedDevice, String descriptorXml) device = super.describe(undescribedDevice, descriptorXml); return device; } catch (DescriptorBindingException ex) { - Log.w(getClass().getName(), "Regular parsing failed: " + Exceptions.unwrap(ex).getMessage()); + YaaccLogger.w(getClass().getName(), "Regular parsing failed: " + Exceptions.unwrap(ex).getMessage()); originalException = ex; } @@ -60,7 +60,7 @@ public D describe(D undescribedDevice, String descriptorXml) device = super.describe(undescribedDevice, fixedXml); return device; } catch (DescriptorBindingException ex) { - Log.w(getClass().getName(), "Removing leading garbage didn't work: " + Exceptions.unwrap(ex).getMessage()); + YaaccLogger.w(getClass().getName(), "Removing leading garbage didn't work: " + Exceptions.unwrap(ex).getMessage()); } } @@ -70,7 +70,7 @@ public D describe(D undescribedDevice, String descriptorXml) device = super.describe(undescribedDevice, fixedXml); return device; } catch (DescriptorBindingException ex) { - Log.w(getClass().getName(), "Removing trailing garbage didn't work: " + Exceptions.unwrap(ex).getMessage()); + YaaccLogger.w(getClass().getName(), "Removing trailing garbage didn't work: " + Exceptions.unwrap(ex).getMessage()); } } @@ -84,7 +84,7 @@ public D describe(D undescribedDevice, String descriptorXml) device = super.describe(undescribedDevice, fixedXml); return device; } catch (DescriptorBindingException ex) { - Log.w(getClass().getName(), "Fixing namespace prefix didn't work: " + Exceptions.unwrap(ex).getMessage()); + YaaccLogger.w(getClass().getName(), "Fixing namespace prefix didn't work: " + Exceptions.unwrap(ex).getMessage()); lastException = ex; } } else { @@ -98,7 +98,7 @@ public D describe(D undescribedDevice, String descriptorXml) device = super.describe(undescribedDevice, fixedXml); return device; } catch (DescriptorBindingException ex) { - Log.w(getClass().getName(), "Fixing XML entities didn't work: " + Exceptions.unwrap(ex).getMessage()); + YaaccLogger.w(getClass().getName(), "Fixing XML entities didn't work: " + Exceptions.unwrap(ex).getMessage()); } } @@ -138,11 +138,11 @@ private String fixGarbageLeadingChars(String descriptorXml) { protected String fixGarbageTrailingChars(String descriptorXml, DescriptorBindingException ex) { int index = descriptorXml.indexOf(""); if (index == -1) { - Log.w(getClass().getName(), "No closing element in descriptor"); + YaaccLogger.w(getClass().getName(), "No closing element in descriptor"); return null; } if (descriptorXml.length() != index + "".length()) { - Log.w(getClass().getName(), "Detected garbage characters after node, removing"); + YaaccLogger.w(getClass().getName(), "Detected garbage characters after node, removing"); return descriptorXml.substring(0, index) + ""; } return null; @@ -170,24 +170,24 @@ protected String fixMissingNamespaces(String descriptorXml, DescriptorBindingExc } String missingNS = matcher.group(1); - Log.w(getClass().getName(), "Fixing missing namespace declaration for: " + missingNS); + YaaccLogger.w(getClass().getName(), "Fixing missing namespace declaration for: " + missingNS); // Extract attributes pattern = Pattern.compile("]*)"); matcher = pattern.matcher(descriptorXml); if (!matcher.find() || matcher.groupCount() != 1) { - Log.v(getClass().getName(), "Could not find element attributes"); + YaaccLogger.v(getClass().getName(), "Could not find element attributes"); return null; } String rootAttributes = matcher.group(1); - Log.v(getClass().getName(), "Preserving existing element attributes/namespace declarations: " + matcher.group(0)); + YaaccLogger.v(getClass().getName(), "Preserving existing element attributes/namespace declarations: " + matcher.group(0)); // Extract body pattern = Pattern.compile("]*>(.*)", Pattern.DOTALL); matcher = pattern.matcher(descriptorXml); if (!matcher.find() || matcher.groupCount() != 1) { - Log.v(getClass().getName(), "Could not extract body of element"); + YaaccLogger.v(getClass().getName(), "Could not extract body of element"); return null; } diff --git a/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderImpl.java b/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderImpl.java index 53cc0efc..d86f2e09 100644 --- a/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderImpl.java @@ -18,7 +18,7 @@ import static org.fourthline.cling.model.XMLUtil.appendNewElement; import static org.fourthline.cling.model.XMLUtil.appendNewElementIfNotNull; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.binding.staging.MutableDevice; import org.fourthline.cling.binding.staging.MutableIcon; @@ -75,7 +75,7 @@ public D describe(D undescribedDevice, String descriptorXml) } try { - Log.v(getClass().getName(), "Populating device from XML descriptor: " + undescribedDevice); + YaaccLogger.v(getClass().getName(), "Populating device from XML descriptor: " + undescribedDevice); // We can not validate the XML document. There is no possible XML schema (maybe RELAX NG) that would properly // constrain the UDA 1.0 device descriptor documents: Any unknown element or attribute must be ignored, order of elements // is not guaranteed. Try to write a schema for that! No combination of and @@ -107,7 +107,7 @@ public D describe(D undescribedDevice, String descriptorXml) public D describe(D undescribedDevice, Document dom) throws DescriptorBindingException, ValidationException { try { - Log.v(getClass().getName(), "Populating device from DOM: " + undescribedDevice); + YaaccLogger.v(getClass().getName(), "Populating device from DOM: " + undescribedDevice); // Read the XML into a mutable descriptor graph MutableDevice descriptor = new MutableDevice(); @@ -131,7 +131,7 @@ public D buildInstance(D undescribedDevice, MutableDevice des protected void hydrateRoot(MutableDevice descriptor, Element rootElement) throws DescriptorBindingException { if (rootElement.getNamespaceURI() == null || !rootElement.getNamespaceURI().equals(Descriptor.Device.NAMESPACE_URI)) { - Log.w(getClass().getName(), "Wrong XML namespace declared on root element: " + rootElement.getNamespaceURI()); + YaaccLogger.w(getClass().getName(), "Wrong XML namespace declared on root element: " + rootElement.getNamespaceURI()); } if (!rootElement.getNodeName().equals(ELEMENT.root.name())) { @@ -166,7 +166,7 @@ protected void hydrateRoot(MutableDevice descriptor, Element rootElement) throws throw new DescriptorBindingException("Found multiple elements in "); deviceNode = rootChild; } else { - Log.w(getClass().getName(), "Ignoring unknown element: " + rootChild.getNodeName()); + YaaccLogger.w(getClass().getName(), "Ignoring unknown element: " + rootChild.getNodeName()); } } @@ -188,14 +188,14 @@ public void hydrateSpecVersion(MutableDevice descriptor, Node specVersionNode) t if (ELEMENT.major.equals(specVersionChild)) { String version = XMLUtil.getTextContent(specVersionChild).trim(); if (!version.equals("1")) { - Log.w(getClass().getName(), "Unsupported UDA major version, ignoring: " + version); + YaaccLogger.w(getClass().getName(), "Unsupported UDA major version, ignoring: " + version); version = "1"; } descriptor.udaVersion.major = Integer.valueOf(version); } else if (ELEMENT.minor.equals(specVersionChild)) { String version = XMLUtil.getTextContent(specVersionChild).trim(); if (!version.equals("0")) { - Log.w(getClass().getName(), "Unsupported UDA minor version, ignoring: " + version); + YaaccLogger.w(getClass().getName(), "Unsupported UDA minor version, ignoring: " + version); version = "0"; } descriptor.udaVersion.minor = Integer.valueOf(version); @@ -250,7 +250,7 @@ public void hydrateDevice(MutableDevice descriptor, Node deviceNode) throws Desc try { descriptor.dlnaDocs.add(DLNADoc.valueOf(txt)); } catch (InvalidValueException ex) { - Log.v(getClass().getName(), "Invalid X_DLNADOC value, ignoring value: " + txt); + YaaccLogger.v(getClass().getName(), "Invalid X_DLNADOC value, ignoring value: " + txt); } } else if (ELEMENT.X_DLNACAP.equals(deviceNodeChild) && Descriptor.Device.DLNA_PREFIX.equals(deviceNodeChild.getPrefix())) { @@ -289,7 +289,7 @@ public void hydrateIconList(MutableDevice descriptor, Node iconListNode) throws try { icon.depth = (Integer.valueOf(depth)); } catch (NumberFormatException ex) { - Log.w(getClass().getName(), "Invalid icon depth '" + depth + "', using 16 as default: " + ex); + YaaccLogger.w(getClass().getName(), "Invalid icon depth '" + depth + "', using 16 as default: " + ex); icon.depth = 16; } } else if (ELEMENT.url.equals(iconChild)) { @@ -299,7 +299,7 @@ public void hydrateIconList(MutableDevice descriptor, Node iconListNode) throws icon.mimeType = XMLUtil.getTextContent(iconChild); MimeType.valueOf(icon.mimeType); } catch (IllegalArgumentException ex) { - Log.w(getClass().getName(), "Ignoring invalid icon mime type: " + icon.mimeType); + YaaccLogger.w(getClass().getName(), "Ignoring invalid icon mime type: " + icon.mimeType); icon.mimeType = ""; } } @@ -349,7 +349,7 @@ public void hydrateServiceList(MutableDevice descriptor, Node serviceListNode) t descriptor.services.add(service); } catch (InvalidValueException ex) { - Log.w(getClass().getName(), + YaaccLogger.w(getClass().getName(), "UPnP specification violation, skipping invalid service declaration. " + ex.getMessage() ); } @@ -378,7 +378,7 @@ public void hydrateDeviceList(MutableDevice descriptor, Node deviceListNode) thr public String generate(Device deviceModel, RemoteClientInfo info, Namespace namespace) throws DescriptorBindingException { try { - Log.v(getClass().getName(), "Generating XML descriptor from device model: " + deviceModel); + YaaccLogger.v(getClass().getName(), "Generating XML descriptor from device model: " + deviceModel); return XMLUtil.documentToString(buildDOM(deviceModel, info, namespace)); @@ -390,7 +390,7 @@ public String generate(Device deviceModel, RemoteClientInfo info, Namespace name public Document buildDOM(Device deviceModel, RemoteClientInfo info, Namespace namespace) throws DescriptorBindingException { try { - Log.v(getClass().getName(), "Generating DOM from device model: " + deviceModel); + YaaccLogger.v(getClass().getName(), "Generating DOM from device model: " + deviceModel); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); @@ -563,7 +563,7 @@ protected void generateDeviceList(Namespace namespace, Device deviceModel, Docum } public void warning(SAXParseException e) throws SAXException { - Log.w(getClass().getName(), e.toString()); + YaaccLogger.w(getClass().getName(), e.toString()); } public void error(SAXParseException e) throws SAXException { @@ -604,7 +604,7 @@ static protected URI parseURI(String uri) { at java.net.URI.(URI.java:87) at java.net.URI.create(URI.java:968) */ - Log.v(UDA10DeviceDescriptorBinderImpl.class.getName(), "Illegal URI, trying with ./ prefix: " + Exceptions.unwrap(ex)); + YaaccLogger.v(UDA10DeviceDescriptorBinderImpl.class.getName(), "Illegal URI, trying with ./ prefix: " + Exceptions.unwrap(ex)); // Ignore } try { @@ -617,7 +617,7 @@ static protected URI parseURI(String uri) { // return URI.create("./" + uri); } catch (IllegalArgumentException ex) { - Log.w(UDA10DeviceDescriptorBinderImpl.class.getName(), "Illegal URI '" + uri + "', ignoring value: " + Exceptions.unwrap(ex)); + YaaccLogger.w(UDA10DeviceDescriptorBinderImpl.class.getName(), "Illegal URI '" + uri + "', ignoring value: " + Exceptions.unwrap(ex)); // Ignore } return null; diff --git a/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderSAXImpl.java b/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderSAXImpl.java index 2a109ccb..8f45a8d0 100644 --- a/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderSAXImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10DeviceDescriptorBinderSAXImpl.java @@ -17,7 +17,7 @@ import static org.fourthline.cling.binding.xml.Descriptor.Device.ELEMENT; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.binding.staging.MutableDevice; import org.fourthline.cling.binding.staging.MutableIcon; @@ -59,7 +59,7 @@ public D describe(D undescribedDevice, String descriptorXml) } try { - Log.v(getClass().getName(), "Populating device from XML descriptor: " + undescribedDevice); + YaaccLogger.v(getClass().getName(), "Populating device from XML descriptor: " + undescribedDevice); // Read the XML into a mutable descriptor graph @@ -138,7 +138,7 @@ public void endElement(ELEMENT element) throws SAXException { case major: String majorVersion = getCharacters().trim(); if (!majorVersion.equals("1")) { - Log.w(getClass().getName(), "Unsupported UDA major version, ignoring: " + majorVersion); + YaaccLogger.w(getClass().getName(), "Unsupported UDA major version, ignoring: " + majorVersion); majorVersion = "1"; } getInstance().major = Integer.valueOf(majorVersion); @@ -146,7 +146,7 @@ public void endElement(ELEMENT element) throws SAXException { case minor: String minorVersion = getCharacters().trim(); if (!minorVersion.equals("0")) { - Log.w(getClass().getName(), "Unsupported UDA minor version, ignoring: " + minorVersion); + YaaccLogger.w(getClass().getName(), "Unsupported UDA minor version, ignoring: " + minorVersion); minorVersion = "0"; } getInstance().minor = Integer.valueOf(minorVersion); @@ -234,7 +234,7 @@ public void endElement(ELEMENT element) throws SAXException { try { getInstance().dlnaDocs.add(DLNADoc.valueOf(txt)); } catch (InvalidValueException ex) { - Log.v(getClass().getName(), "Invalid X_DLNADOC value, ignoring value: " + txt); + YaaccLogger.v(getClass().getName(), "Invalid X_DLNADOC value, ignoring value: " + txt); } break; case X_DLNACAP: @@ -293,7 +293,7 @@ public void endElement(ELEMENT element) throws SAXException { try { getInstance().depth = Integer.valueOf(getCharacters()); } catch (NumberFormatException ex) { - Log.w(getClass().getName(), "Invalid icon depth '" + getCharacters() + "', using 16 as default: " + ex); + YaaccLogger.w(getClass().getName(), "Invalid icon depth '" + getCharacters() + "', using 16 as default: " + ex); getInstance().depth = 16; } break; @@ -305,7 +305,7 @@ public void endElement(ELEMENT element) throws SAXException { getInstance().mimeType = getCharacters(); MimeType.valueOf(getInstance().mimeType); } catch (IllegalArgumentException ex) { - Log.w(getClass().getName(), "Ignoring invalid icon mime type: " + getInstance().mimeType); + YaaccLogger.w(getClass().getName(), "Ignoring invalid icon mime type: " + getInstance().mimeType); getInstance().mimeType = ""; } break; @@ -379,7 +379,7 @@ public void endElement(ELEMENT element) throws SAXException { break; } } catch (InvalidValueException ex) { - Log.v(getClass().getName(), + YaaccLogger.v(getClass().getName(), "UPnP specification violation, skipping invalid service declaration. " + ex.getMessage() ); } diff --git a/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderImpl.java b/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderImpl.java index c7ee49b6..294ec53a 100644 --- a/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderImpl.java @@ -20,7 +20,7 @@ import static org.fourthline.cling.model.XMLUtil.appendNewElement; import static org.fourthline.cling.model.XMLUtil.appendNewElementIfNotNull; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.binding.staging.MutableAction; import org.fourthline.cling.binding.staging.MutableActionArgument; @@ -69,7 +69,7 @@ public S describe(S undescribedService, String descriptorXml } try { - Log.v(getClass().getName(), "Populating service from XML descriptor: " + undescribedService); + YaaccLogger.v(getClass().getName(), "Populating service from XML descriptor: " + undescribedService); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); @@ -94,7 +94,7 @@ public S describe(S undescribedService, String descriptorXml public S describe(S undescribedService, Document dom) throws DescriptorBindingException, ValidationException { try { - Log.v(getClass().getName(), "Populating service from DOM: " + undescribedService); + YaaccLogger.v(getClass().getName(), "Populating service from DOM: " + undescribedService); // Read the XML into a mutable descriptor graph MutableService descriptor = new MutableService(); @@ -156,7 +156,7 @@ protected void hydrateRoot(MutableService descriptor, Element rootElement) } else if (ELEMENT.serviceStateTable.equals(rootChild)) { hydrateServiceStateTableList(descriptor, rootChild); } else { - Log.v(getClass().getName(), "Ignoring unknown element: " + rootChild.getNodeName()); + YaaccLogger.v(getClass().getName(), "Ignoring unknown element: " + rootChild.getNodeName()); } } @@ -247,7 +247,7 @@ public void hydrateActionArgument(MutableActionArgument actionArgument, Node act actionArgument.direction = ActionArgument.Direction.valueOf(directionString.toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException ex) { // TODO: UPNP VIOLATION: Pelco SpectraIV-IP uses illegal value INOUT - Log.w(getClass().getName(), "UPnP specification violation: Invalid action argument direction, assuming 'IN': " + directionString); + YaaccLogger.w(getClass().getName(), "UPnP specification violation: Invalid action argument direction, assuming 'IN': " + directionString); actionArgument.direction = ActionArgument.Direction.IN; } } else if (ELEMENT.relatedStateVariable.equals(argumentNodeChild)) { @@ -350,7 +350,7 @@ public void hydrateStateVariable(MutableStateVariable stateVariable, Element sta public String generate(Service service) throws DescriptorBindingException { try { - Log.v(getClass().getName(), "Generating XML descriptor from service model: " + service); + YaaccLogger.v(getClass().getName(), "Generating XML descriptor from service model: " + service); return XMLUtil.documentToString(buildDOM(service)); @@ -362,7 +362,7 @@ public String generate(Service service) throws DescriptorBindingException { public Document buildDOM(Service service) throws DescriptorBindingException { try { - Log.v(getClass().getName(), "Generating XML descriptor from service model: " + service); + YaaccLogger.v(getClass().getName(), "Generating XML descriptor from service model: " + service); DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); @@ -427,7 +427,7 @@ private void generateActionArgument(ActionArgument actionArgument, Document desc appendNewElementIfNotNull(descriptor, actionArgumentElement, ELEMENT.direction, actionArgument.getDirection().toString().toLowerCase(Locale.ROOT)); if (actionArgument.isReturnValue()) { // TODO: UPNP VIOLATION: WMP12 will discard RenderingControl service if it contains tags - Log.w(getClass().getName(), "UPnP specification violation: Not producing element to be compatible with WMP12: " + actionArgument); + YaaccLogger.w(getClass().getName(), "UPnP specification violation: Not producing element to be compatible with WMP12: " + actionArgument); // appendNewElement(descriptor, actionArgumentElement, ELEMENT.retval); } appendNewElementIfNotNull(descriptor, actionArgumentElement, ELEMENT.relatedStateVariable, actionArgument.getRelatedStateVariableName()); @@ -491,7 +491,7 @@ private void generateStateVariable(StateVariable stateVariable, Document descrip } public void warning(SAXParseException e) throws SAXException { - Log.w(getClass().getName(), e.toString()); + YaaccLogger.w(getClass().getName(), e.toString()); } public void error(SAXParseException e) throws SAXException { diff --git a/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderSAXImpl.java b/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderSAXImpl.java index 0d487df8..7d9c92d3 100644 --- a/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderSAXImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/binding/xml/UDA10ServiceDescriptorBinderSAXImpl.java @@ -18,7 +18,7 @@ import static org.fourthline.cling.binding.xml.Descriptor.Service.ATTRIBUTE; import static org.fourthline.cling.binding.xml.Descriptor.Service.ELEMENT; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.binding.staging.MutableAction; import org.fourthline.cling.binding.staging.MutableActionArgument; @@ -57,7 +57,7 @@ public S describe(S undescribedService, String descriptorXml } try { - Log.v(getClass().getName(), "Reading service from XML descriptor"); + YaaccLogger.v(getClass().getName(), "Reading service from XML descriptor"); SAXParser parser = new SAXParser(); @@ -242,7 +242,7 @@ public void endElement(ELEMENT element) throws SAXException { getInstance().direction = ActionArgument.Direction.valueOf(directionString.toUpperCase(Locale.ROOT)); } catch (IllegalArgumentException ex) { // TODO: UPNP VIOLATION: Pelco SpectraIV-IP uses illegal value INOUT - Log.w(getClass().getName(), "UPnP specification violation: Invalid action argument direction, assuming 'IN': " + directionString); + YaaccLogger.w(getClass().getName(), "UPnP specification violation: Invalid action argument direction, assuming 'IN': " + directionString); getInstance().direction = ActionArgument.Direction.IN; } break; diff --git a/yaacc/src/main/java/org/fourthline/cling/controlpoint/ActionCallback.java b/yaacc/src/main/java/org/fourthline/cling/controlpoint/ActionCallback.java deleted file mode 100644 index c734083c..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/controlpoint/ActionCallback.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.controlpoint; - -import org.fourthline.cling.model.action.ActionException; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.model.message.control.IncomingActionResponseMessage; -import org.fourthline.cling.model.meta.LocalService; -import org.fourthline.cling.model.meta.RemoteService; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.protocol.sync.SendingAction; - -import java.net.URL; - -/** - * Execute actions on any service. - *

- * Usage example for asynchronous execution in a background thread: - *

- *
- * Service service = device.findService(new UDAServiceId("SwitchPower"));
- * Action getStatusAction = service.getAction("GetStatus");
- * ActionInvocation getStatusInvocation = new ActionInvocation(getStatusAction);
- *
- * ActionCallback getStatusCallback = new ActionCallback(getStatusInvocation) {
- *
- *      public void success(ActionInvocation invocation) {
- *          ActionArgumentValue status  = invocation.getOutput("ResultStatus");
- *          assertEquals((Boolean) status.getValue(), Boolean.valueOf(false));
- *      }
- *
- *      public void failure(ActionInvocation invocation, UpnpResponse res) {
- *          System.err.println(
- *              createDefaultFailureMessage(invocation, res)
- *          );
- *      }
- * };
- *
- * upnpService.getControlPoint().execute(getStatusCallback)
- * 
- *

- * You can also execute the action synchronously in the same thread using the - * {@link org.fourthline.cling.controlpoint.ActionCallback.Default} implementation: - *

- *
- * myActionInvocation.setInput("foo", bar);
- * new ActionCallback.Default(myActionInvocation, upnpService.getControlPoint()).run();
- * myActionInvocation.getOutput("baz");
- * 
- * - * @author Christian Bauer - */ -public abstract class ActionCallback implements Runnable { - - /** - * Empty implementation of callback methods, simplifies synchronous - * execution of an {@link org.fourthline.cling.model.action.ActionInvocation}. - */ - public static final class Default extends ActionCallback { - - public Default(ActionInvocation actionInvocation, ControlPoint controlPoint) { - super(actionInvocation, controlPoint); - } - - @Override - public void success(ActionInvocation invocation) { - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - - } - } - - protected final ActionInvocation actionInvocation; - - protected ControlPoint controlPoint; - - protected ActionCallback(ActionInvocation actionInvocation, ControlPoint controlPoint) { - this.actionInvocation = actionInvocation; - this.controlPoint = controlPoint; - } - - protected ActionCallback(ActionInvocation actionInvocation) { - this.actionInvocation = actionInvocation; - } - - public ActionInvocation getActionInvocation() { - return actionInvocation; - } - - synchronized public ControlPoint getControlPoint() { - return controlPoint; - } - - synchronized public ActionCallback setControlPoint(ControlPoint controlPoint) { - this.controlPoint = controlPoint; - return this; - } - - public void run() { - Service service = actionInvocation.getAction().getService(); - - // Local execution - if (service instanceof LocalService) { - LocalService localService = (LocalService)service; - - // Executor validates input inside the execute() call immediately - localService.getExecutor(actionInvocation.getAction()).execute(actionInvocation); - - if (actionInvocation.getFailure() != null) { - failure(actionInvocation, null); - } else { - success(actionInvocation); - } - - // Remote execution - } else if (service instanceof RemoteService){ - - if (getControlPoint() == null) { - throw new IllegalStateException("Callback must be executed through ControlPoint"); - } - - RemoteService remoteService = (RemoteService)service; - - // Figure out the remote URL where we'd like to send the action request to - URL controLURL; - try { - controLURL = remoteService.getDevice().normalizeURI(remoteService.getControlURI()); - } catch(IllegalArgumentException e) { - failure(actionInvocation, null, "bad control URL: " + remoteService.getControlURI()); - return ; - } - - // Do it - SendingAction prot = getControlPoint().getProtocolFactory().createSendingAction(actionInvocation, controLURL); - prot.run(); - - IncomingActionResponseMessage response = prot.getOutputMessage(); - - if (response == null) { - failure(actionInvocation, null); - } else if (response.getOperation().isFailed()) { - failure(actionInvocation, response.getOperation()); - } else { - success(actionInvocation); - } - } - } - - protected String createDefaultFailureMessage(ActionInvocation invocation, UpnpResponse operation) { - String message = "Error: "; - final ActionException exception = invocation.getFailure(); - if (exception != null) { - message = message + exception.getMessage(); - } - if (operation != null) { - message = message + " (HTTP response was: " + operation.getResponseDetails() + ")"; - } - return message; - } - - protected void failure(ActionInvocation invocation, UpnpResponse operation) { - failure(invocation, operation, createDefaultFailureMessage(invocation, operation)); - } - - /** - * Called when the action invocation succeeded. - * - * @param invocation The successful invocation, call its getOutput() method for results. - */ - public abstract void success(ActionInvocation invocation); - - /** - * Called when the action invocation failed. - * - * @param invocation The failed invocation, call its getFailure() method for more details. - * @param operation If the invocation was on a remote service, the response message, otherwise null. - * @param defaultMsg A user-friendly error message generated from the invocation exception and response. - * @see #createDefaultFailureMessage - */ - public abstract void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg); - - @Override - public String toString() { - return "(ActionCallback) " + actionInvocation; - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/controlpoint/ControlPoint.java b/yaacc/src/main/java/org/fourthline/cling/controlpoint/ControlPoint.java deleted file mode 100644 index 2fa3a14b..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/controlpoint/ControlPoint.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.controlpoint; - -import org.fourthline.cling.model.message.header.UpnpHeader; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.UpnpServiceConfiguration; -import org.fourthline.cling.registry.Registry; - -import java.util.concurrent.Future; - -/** - * Unified API for the asynchronous execution of network searches, actions, event subscriptions. - * - * @author Christian Bauer - */ -public interface ControlPoint { - - public UpnpServiceConfiguration getConfiguration(); - public ProtocolFactory getProtocolFactory(); - public Registry getRegistry(); - - public void search(); - public void search(UpnpHeader searchType); - public void search(int mxSeconds); - public void search(UpnpHeader searchType, int mxSeconds); - public Future execute(ActionCallback callback); - public void execute(SubscriptionCallback callback); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/controlpoint/ControlPointImpl.java b/yaacc/src/main/java/org/fourthline/cling/controlpoint/ControlPointImpl.java deleted file mode 100644 index 27b654ef..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/controlpoint/ControlPointImpl.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.controlpoint; - -import android.util.Log; - -import org.fourthline.cling.UpnpServiceConfiguration; -import org.fourthline.cling.controlpoint.event.ExecuteAction; -import org.fourthline.cling.controlpoint.event.Search; -import org.fourthline.cling.model.message.header.MXHeader; -import org.fourthline.cling.model.message.header.STAllHeader; -import org.fourthline.cling.model.message.header.UpnpHeader; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.registry.Registry; - -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; -import jakarta.inject.Inject; - -/** - * Default implementation. - *

- * This implementation uses the executor returned by - * {@link org.fourthline.cling.UpnpServiceConfiguration#getSyncProtocolExecutorService()}. - *

- * - * @author Christian Bauer - */ -@ApplicationScoped -public class ControlPointImpl implements ControlPoint { - - - protected UpnpServiceConfiguration configuration; - protected ProtocolFactory protocolFactory; - protected Registry registry; - - protected ControlPointImpl() { - } - - @Inject - public ControlPointImpl(UpnpServiceConfiguration configuration, ProtocolFactory protocolFactory, Registry registry) { - Log.v(getClass().getName(), "Creating ControlPoint: " + getClass().getName()); - - this.configuration = configuration; - this.protocolFactory = protocolFactory; - this.registry = registry; - } - - public UpnpServiceConfiguration getConfiguration() { - return configuration; - } - - public ProtocolFactory getProtocolFactory() { - return protocolFactory; - } - - public Registry getRegistry() { - return registry; - } - - public void search(@Observes Search search) { - search(search.getSearchType(), search.getMxSeconds()); - } - - public void search() { - search(new STAllHeader(), MXHeader.DEFAULT_VALUE); - } - - public void search(UpnpHeader searchType) { - search(searchType, MXHeader.DEFAULT_VALUE); - } - - public void search(int mxSeconds) { - search(new STAllHeader(), mxSeconds); - } - - public void search(UpnpHeader searchType, int mxSeconds) { - Log.v(getClass().getName(), "Sending asynchronous search for: " + searchType.getString()); - getConfiguration().getAsyncProtocolExecutor().execute( - getProtocolFactory().createSendingSearch(searchType, mxSeconds) - ); - } - - public void execute(ExecuteAction executeAction) { - execute(executeAction.getCallback()); - } - - public Future execute(ActionCallback callback) { - Log.v(getClass().getName(), "Invoking action in background: " + callback); - callback.setControlPoint(this); - ExecutorService executor = getConfiguration().getSyncProtocolExecutorService(); - return executor.submit(callback); - } - - public void execute(SubscriptionCallback callback) { - Log.v(getClass().getName(), "Invoking subscription in background: " + callback); - callback.setControlPoint(this); - getConfiguration().getSyncProtocolExecutorService().execute(callback); - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/controlpoint/SubscriptionCallback.java b/yaacc/src/main/java/org/fourthline/cling/controlpoint/SubscriptionCallback.java deleted file mode 100644 index 3b3de141..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/controlpoint/SubscriptionCallback.java +++ /dev/null @@ -1,373 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.controlpoint; - -import android.util.Log; - -import org.fourthline.cling.model.UnsupportedDataException; -import org.fourthline.cling.model.UserConstants; -import org.fourthline.cling.model.gena.CancelReason; -import org.fourthline.cling.model.gena.GENASubscription; -import org.fourthline.cling.model.gena.LocalGENASubscription; -import org.fourthline.cling.model.gena.RemoteGENASubscription; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.model.meta.LocalService; -import org.fourthline.cling.model.meta.RemoteService; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.protocol.ProtocolCreationException; -import org.fourthline.cling.protocol.sync.SendingSubscribe; -import org.seamless.util.Exceptions; - -import java.util.Collections; - -/** - * Subscribe and receive events from a service through GENA. - *

- * Usage example, establishing a subscription with a {@link org.fourthline.cling.model.meta.Service}: - *

- *
- * SubscriptionCallback callback = new SubscriptionCallback(service, 600) { // Timeout in seconds
- *
- *      public void established(GENASubscription sub) {
- *          System.out.println("Established: " + sub.getSubscriptionId());
- *      }
- *
- *      public void failed(GENASubscription sub, UpnpResponse response, Exception ex) {
- *          System.err.println(
- *              createDefaultFailureMessage(response, ex)
- *          );
- *      }
- *
- *      public void ended(GENASubscription sub, CancelReason reason, UpnpResponse response) {
- *          // Reason should be null, or it didn't end regularly
- *      }
- *
- *      public void eventReceived(GENASubscription sub) {
- *          System.out.println("Event: " + sub.getCurrentSequence().getValue());
- *          Map<String, StateVariableValue> values = sub.getCurrentValues();
- *          StateVariableValue status = values.get("Status");
- *          System.out.println("Status is: " + status.toString());
- *      }
- *
- *      public void eventsMissed(GENASubscription sub, int numberOfMissedEvents) {
- *          System.out.println("Missed events: " + numberOfMissedEvents);
- *      }
- * };
- *
- * upnpService.getControlPoint().execute(callback);
- * 
- * - * @author Christian Bauer - */ -public abstract class SubscriptionCallback implements Runnable { - - protected final Service service; - protected final Integer requestedDurationSeconds; - - private ControlPoint controlPoint; - private GENASubscription subscription; - - protected SubscriptionCallback(Service service) { - this.service = service; - this.requestedDurationSeconds = UserConstants.DEFAULT_SUBSCRIPTION_DURATION_SECONDS; - } - - protected SubscriptionCallback(Service service, int requestedDurationSeconds) { - this.service = service; - this.requestedDurationSeconds = requestedDurationSeconds; - } - - /** - * @param responseStatus The (HTTP) response or null if there was no response. - * @param exception The exception or null if there was no exception. - * @return A human-friendly error message. - */ - public static String createDefaultFailureMessage(UpnpResponse responseStatus, Exception exception) { - String message = "Subscription failed: "; - if (responseStatus != null) { - message = message + " HTTP response was: " + responseStatus.getResponseDetails(); - } else if (exception != null) { - message = message + " Exception occured: " + exception; - } else { - message = message + " No response received."; - } - return message; - } - - public Service getService() { - return service; - } - - synchronized public ControlPoint getControlPoint() { - return controlPoint; - } - - synchronized public void setControlPoint(ControlPoint controlPoint) { - this.controlPoint = controlPoint; - } - - synchronized public GENASubscription getSubscription() { - return subscription; - } - - synchronized public void setSubscription(GENASubscription subscription) { - this.subscription = subscription; - } - - synchronized public void run() { - if (getControlPoint() == null) { - throw new IllegalStateException("Callback must be executed through ControlPoint"); - } - - if (getService() instanceof LocalService) { - establishLocalSubscription((LocalService) service); - } else if (getService() instanceof RemoteService) { - establishRemoteSubscription((RemoteService) service); - } - } - - private void establishLocalSubscription(LocalService service) { - - if (getControlPoint().getRegistry().getLocalDevice(service.getDevice().getIdentity().getUdn(), false) == null) { - Log.v(getClass().getName(), "Local device service is currently not registered, failing subscription immediately"); - failed(null, null, new IllegalStateException("Local device is not registered")); - return; - } - - // Local execution of subscription on local service re-uses the procedure and lifecycle that is - // used for inbound subscriptions from remote control points on local services! - // Except that it doesn't ever expire, we override the requested duration with Integer.MAX_VALUE! - - LocalGENASubscription localSubscription = null; - try { - localSubscription = - new LocalGENASubscription(service, Integer.MAX_VALUE, Collections.EMPTY_LIST) { - - public void failed(Exception ex) { - synchronized (SubscriptionCallback.this) { - SubscriptionCallback.this.setSubscription(null); - SubscriptionCallback.this.failed(null, null, ex); - } - } - - public void established() { - synchronized (SubscriptionCallback.this) { - SubscriptionCallback.this.setSubscription(this); - SubscriptionCallback.this.established(this); - } - } - - public void ended(CancelReason reason) { - synchronized (SubscriptionCallback.this) { - SubscriptionCallback.this.setSubscription(null); - SubscriptionCallback.this.ended(this, reason, null); - } - } - - public void eventReceived() { - synchronized (SubscriptionCallback.this) { - Log.v(getClass().getName(), "Local service state updated, notifying callback, sequence is: " + getCurrentSequence()); - SubscriptionCallback.this.eventReceived(this); - incrementSequence(); - } - } - }; - - Log.v(getClass().getName(), "Local device service is currently registered, also registering subscription"); - getControlPoint().getRegistry().addLocalSubscription(localSubscription); - - Log.v(getClass().getName(), "Notifying subscription callback of local subscription availablity"); - localSubscription.establish(); - - Log.v(getClass().getName(), "Simulating first initial event for local subscription callback, sequence: " + localSubscription.getCurrentSequence()); - eventReceived(localSubscription); - localSubscription.incrementSequence(); - - Log.v(getClass().getName(), "Starting to monitor state changes of local service"); - localSubscription.registerOnService(); - - } catch (Exception ex) { - Log.v(getClass().getName(), "Local callback creation failed: " + ex.toString()); - Log.v(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); - if (localSubscription != null) - getControlPoint().getRegistry().removeLocalSubscription(localSubscription); - failed(localSubscription, null, ex); - } - } - - private void establishRemoteSubscription(RemoteService service) { - RemoteGENASubscription remoteSubscription = - new RemoteGENASubscription(service, requestedDurationSeconds) { - - public void failed(UpnpResponse responseStatus) { - synchronized (SubscriptionCallback.this) { - SubscriptionCallback.this.setSubscription(null); - SubscriptionCallback.this.failed(this, responseStatus, null); - } - } - - public void established() { - synchronized (SubscriptionCallback.this) { - SubscriptionCallback.this.setSubscription(this); - SubscriptionCallback.this.established(this); - } - } - - public void ended(CancelReason reason, UpnpResponse responseStatus) { - synchronized (SubscriptionCallback.this) { - SubscriptionCallback.this.setSubscription(null); - SubscriptionCallback.this.ended(this, reason, responseStatus); - } - } - - public void eventReceived() { - synchronized (SubscriptionCallback.this) { - SubscriptionCallback.this.eventReceived(this); - } - } - - public void eventsMissed(int numberOfMissedEvents) { - synchronized (SubscriptionCallback.this) { - SubscriptionCallback.this.eventsMissed(this, numberOfMissedEvents); - } - } - - public void invalidMessage(UnsupportedDataException ex) { - synchronized (SubscriptionCallback.this) { - SubscriptionCallback.this.invalidMessage(this, ex); - } - } - }; - - SendingSubscribe protocol; - try { - protocol = getControlPoint().getProtocolFactory().createSendingSubscribe(remoteSubscription); - } catch (ProtocolCreationException ex) { - failed(subscription, null, ex); - return; - } - protocol.run(); - } - - synchronized public void end() { - if (subscription == null) return; - if (subscription instanceof LocalGENASubscription) { - endLocalSubscription((LocalGENASubscription) subscription); - } else if (subscription instanceof RemoteGENASubscription) { - endRemoteSubscription((RemoteGENASubscription) subscription); - } - } - - private void endLocalSubscription(LocalGENASubscription subscription) { - Log.v(getClass().getName(), "Removing local subscription and ending it in callback: " + subscription); - getControlPoint().getRegistry().removeLocalSubscription(subscription); - subscription.end(null); // No reason, on controlpoint request - } - - private void endRemoteSubscription(RemoteGENASubscription subscription) { - Log.v(getClass().getName(), "Ending remote subscription: " + subscription); - getControlPoint().getConfiguration().getSyncProtocolExecutorService().execute( - getControlPoint().getProtocolFactory().createSendingUnsubscribe(subscription) - ); - } - - protected void failed(GENASubscription subscription, UpnpResponse responseStatus, Exception exception) { - failed(subscription, responseStatus, exception, createDefaultFailureMessage(responseStatus, exception)); - } - - /** - * Called when establishing a local or remote subscription failed. To get a nice error message that - * transparently detects local or remote errors use createDefaultFailureMessage(). - * - * @param subscription The failed subscription object, not very useful at this point. - * @param responseStatus For a remote subscription, if a response was received at all, this is it, otherwise null. - * @param exception For a local subscription and failed creation of a remote subscription protocol (before - * sending the subscribe request), any exception that caused the failure, otherwise null. - * @param defaultMsg A user-friendly error message. - * @see #createDefaultFailureMessage - */ - protected abstract void failed(GENASubscription subscription, UpnpResponse responseStatus, Exception exception, String defaultMsg); - - /** - * Called when a local or remote subscription was successfully established. - * - * @param subscription The successful subscription. - */ - protected abstract void established(GENASubscription subscription); - - /** - * Called when a local or remote subscription ended, either on user request or because of a failure. - * - * @param subscription The ended subscription instance. - * @param reason If the subscription ended regularly (through end()), this is null. - * @param responseStatus For a remote subscription, if the cause implies a remopte response and it was - * received, this is it (e.g. renewal failure response). - */ - protected abstract void ended(GENASubscription subscription, CancelReason reason, UpnpResponse responseStatus); - - /** - * Called when an event for an established subscription has been received. - *

- * Use the {@link org.fourthline.cling.model.gena.GENASubscription#getCurrentValues()} method to obtain - * the evented state variable values. - *

- * - * @param subscription The established subscription with fresh state variable values. - */ - protected abstract void eventReceived(GENASubscription subscription); - - /** - * Called when a received event was out of sequence, indicating that events have been missed. - *

- * It's up to you if you want to react to missed events or if you (can) silently ignore them. - *

- * - * @param subscription The established subscription. - * @param numberOfMissedEvents The number of missed events. - */ - protected abstract void eventsMissed(GENASubscription subscription, int numberOfMissedEvents); - - /** - * Called when a received event message could not be parsed successfully. - *

- * This typically indicates a broken device which is not UPnP compliant. You can - * react to this failure in any way you like, for example, you could terminate - * the subscription or simply create an error report/log. - *

- *

- * The default implementation will log the exception at INFO level, and - * the invalid XML at FINE level. - *

- * - * @param remoteGENASubscription The established subscription. - * @param ex Call {@link org.fourthline.cling.model.UnsupportedDataException#getData()} to access the invalid XML. - */ - protected void invalidMessage(RemoteGENASubscription remoteGENASubscription, - UnsupportedDataException ex) { - Log.v(getClass().getName(), "Invalid event message received, causing: " + ex); - - Log.v(getClass().getName(), "------------------------------------------------------------------------------"); - Log.v(getClass().getName(), ex.getData() != null ? ex.getData().toString() : "null"); - Log.v(getClass().getName(), "------------------------------------------------------------------------------"); - - } - - @Override - public String toString() { - return "(SubscriptionCallback) " + getService(); - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/controlpoint/event/ExecuteAction.java b/yaacc/src/main/java/org/fourthline/cling/controlpoint/event/ExecuteAction.java deleted file mode 100644 index 65838bc9..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/controlpoint/event/ExecuteAction.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.controlpoint.event; - -import org.fourthline.cling.controlpoint.ActionCallback; - -/** - * @author Christian Bauer - */ -public class ExecuteAction { - - protected ActionCallback callback; - - public ExecuteAction(ActionCallback callback) { - this.callback = callback; - } - - public ActionCallback getCallback() { - return callback; - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/controlpoint/event/Search.java b/yaacc/src/main/java/org/fourthline/cling/controlpoint/event/Search.java deleted file mode 100644 index 70025239..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/controlpoint/event/Search.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.controlpoint.event; - -import org.fourthline.cling.model.message.header.MXHeader; -import org.fourthline.cling.model.message.header.STAllHeader; -import org.fourthline.cling.model.message.header.UpnpHeader; - -/** - * @author Christian Bauer - */ -public class Search { - - protected UpnpHeader searchType = new STAllHeader(); - protected int mxSeconds = MXHeader.DEFAULT_VALUE; - - public Search() { - } - - public Search(UpnpHeader searchType) { - this.searchType = searchType; - } - - public Search(UpnpHeader searchType, int mxSeconds) { - this.searchType = searchType; - this.mxSeconds = mxSeconds; - } - - public Search(int mxSeconds) { - this.mxSeconds = mxSeconds; - } - - public UpnpHeader getSearchType() { - return searchType; - } - - public int getMxSeconds() { - return mxSeconds; - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/model/DefaultServiceManager.java b/yaacc/src/main/java/org/fourthline/cling/model/DefaultServiceManager.java index 15fadcf3..cdf74453 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/DefaultServiceManager.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/DefaultServiceManager.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.meta.LocalService; import org.fourthline.cling.model.meta.StateVariable; @@ -72,7 +72,7 @@ public DefaultServiceManager(LocalService service, Class serviceClass) { protected void lock() { try { if (lock.tryLock(getLockTimeoutMillis(), TimeUnit.MILLISECONDS)) { - Log.v(getClass().getName(), "Acquired lock"); + YaaccLogger.v(getClass().getName(), "Acquired lock"); } else { throw new RuntimeException("Failed to acquire lock in milliseconds: " + getLockTimeoutMillis()); } @@ -83,7 +83,7 @@ protected void lock() { protected void unlock() { - Log.v(getClass().getName(), "Releasing lock"); + YaaccLogger.v(getClass().getName(), "Releasing lock"); lock.unlock(); } @@ -134,7 +134,7 @@ public Collection getCurrentState() throws Exception { try { Collection values = readInitialEventedStateVariableValues(); if (values != null) { - Log.v(getClass().getName(), "Obtained initial state variable values for event, skipping individual state variable accessors"); + YaaccLogger.v(getClass().getName(), "Obtained initial state variable values for event, skipping individual state variable accessors"); return values; } values = new ArrayList<>(); @@ -161,13 +161,13 @@ protected Collection getCurrentState(String[] variableNames) StateVariable stateVariable = getService().getStateVariable(variableName); if (stateVariable == null || !stateVariable.getEventDetails().isSendEvents()) { - Log.v(getClass().getName(), "Ignoring unknown or non-evented state variable: " + variableName); + YaaccLogger.v(getClass().getName(), "Ignoring unknown or non-evented state variable: " + variableName); continue; } StateVariableAccessor accessor = getService().getAccessor(stateVariable); if (accessor == null) { - Log.w(getClass().getName(), "Ignoring evented state variable without accessor: " + variableName); + YaaccLogger.w(getClass().getName(), "Ignoring evented state variable without accessor: " + variableName); continue; } values.add(accessor.read(stateVariable, getImplementation())); @@ -179,7 +179,7 @@ protected Collection getCurrentState(String[] variableNames) } protected void init() { - Log.v(getClass().getName(), "No service implementation instance available, initializing..."); + YaaccLogger.v(getClass().getName(), "No service implementation instance available, initializing..."); try { // The actual instance we ware going to use and hold a reference to (1:1 instance for manager) serviceImpl = createServiceInstance(); @@ -201,7 +201,7 @@ protected T createServiceInstance() throws Exception { // Use this constructor if possible return serviceClass.getConstructor(LocalService.class).newInstance(getService()); } catch (NoSuchMethodException ex) { - Log.v(getClass().getName(), "Creating new service implementation instance with no-arg constructor: " + serviceClass.getName()); + YaaccLogger.v(getClass().getName(), "Creating new service implementation instance with no-arg constructor: " + serviceClass.getName()); return serviceClass.newInstance(); } } @@ -210,10 +210,10 @@ protected PropertyChangeSupport createPropertyChangeSupport(T serviceImpl) throw Method m; if ((m = Reflections.getGetterMethod(serviceImpl.getClass(), "propertyChangeSupport")) != null && PropertyChangeSupport.class.isAssignableFrom(m.getReturnType())) { - Log.v(getClass().getName(), "Service implementation instance offers PropertyChangeSupport, using that: " + serviceImpl.getClass().getName()); + YaaccLogger.v(getClass().getName(), "Service implementation instance offers PropertyChangeSupport, using that: " + serviceImpl.getClass().getName()); return (PropertyChangeSupport) m.invoke(serviceImpl); } - Log.v(getClass().getName(), "Creating new PropertyChangeSupport for service implementation: " + serviceImpl.getClass().getName()); + YaaccLogger.v(getClass().getName(), "Creating new PropertyChangeSupport for service implementation: " + serviceImpl.getClass().getName()); return new PropertyChangeSupport(serviceImpl); } @@ -233,13 +233,13 @@ public String toString() { protected class DefaultPropertyChangeListener implements PropertyChangeListener { public void propertyChange(PropertyChangeEvent e) { - Log.v(getClass().getName(), "Property change event on local service: " + e.getPropertyName()); + YaaccLogger.v(getClass().getName(), "Property change event on local service: " + e.getPropertyName()); // Prevent recursion if (e.getPropertyName().equals(EVENTED_STATE_VARIABLES)) return; String[] variableNames = ModelUtil.fromCommaSeparatedList(e.getPropertyName()); - Log.v(getClass().getName(), "Changed variable names: " + Arrays.toString(variableNames)); + YaaccLogger.v(getClass().getName(), "Changed variable names: " + Arrays.toString(variableNames)); try { Collection currentValues = getCurrentState(variableNames); @@ -254,7 +254,7 @@ public void propertyChange(PropertyChangeEvent e) { } catch (Exception ex) { // TODO: Is it OK to only log this error? It means we keep running although we couldn't send events? - Log.w(getClass().getName(), + YaaccLogger.w(getClass().getName(), "Error reading state of service after state variable update event: " + Exceptions.unwrap(ex), ex ); diff --git a/yaacc/src/main/java/org/fourthline/cling/model/Namespace.java b/yaacc/src/main/java/org/fourthline/cling/model/Namespace.java index 73e6c621..29e1b201 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/Namespace.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/Namespace.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.meta.Device; import org.fourthline.cling.model.meta.Icon; @@ -158,12 +158,12 @@ public Resource[] getResources(Device device) throws ValidationException { Set resources = new HashSet<>(); List errors = new ArrayList<>(); - Log.v(getClass().getName(), "Discovering local resources of device graph"); + YaaccLogger.v(getClass().getName(), "Discovering local resources of device graph"); Resource[] discoveredResources = device.discoverResources(this); for (Resource resource : discoveredResources) { - Log.v(getClass().getName(), "Discovered: " + resource); + YaaccLogger.v(getClass().getName(), "Discovered: " + resource); if (!resources.add(resource)) { - Log.v(getClass().getName(), "Local resource already exists, queueing validation error"); + YaaccLogger.v(getClass().getName(), "Local resource already exists, queueing validation error"); errors.add(new ValidationError( getClass(), "resources", diff --git a/yaacc/src/main/java/org/fourthline/cling/model/VariableValue.java b/yaacc/src/main/java/org/fourthline/cling/model/VariableValue.java index 453c598b..5cfe97c1 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/VariableValue.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/VariableValue.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.types.Datatype; import org.fourthline.cling.model.types.InvalidValueException; @@ -95,7 +95,7 @@ protected void logInvalidXML(String s) { (cp >= 0x20 && cp <= 0xD7FF) || (cp >= 0xE000 && cp <= 0xFFFD) || (cp >= 0x10000 && cp <= 0x10FFFF))) { - Log.w(getClass().getName(), "Found invalid XML char code: " + cp); + YaaccLogger.w(getClass().getName(), "Found invalid XML char code: " + cp); } i += Character.charCount(cp); } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/action/AbstractActionExecutor.java b/yaacc/src/main/java/org/fourthline/cling/model/action/AbstractActionExecutor.java index be1ba367..71096b3a 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/action/AbstractActionExecutor.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/action/AbstractActionExecutor.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.action; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Command; import org.fourthline.cling.model.ServiceManager; @@ -56,7 +56,7 @@ public Map, StateVariableAccessor> getOutputArgumen */ public void execute(final ActionInvocation actionInvocation) { - Log.v(getClass().getName(), "Invoking on local service: " + actionInvocation); + YaaccLogger.v(getClass().getName(), "Invoking on local service: " + actionInvocation); final LocalService service = actionInvocation.getAction().getService(); @@ -82,19 +82,19 @@ public String toString() { } catch (ActionException ex) { - Log.v(getClass().getName(), "ActionException thrown by service, wrapping in invocation and returning: " + ex); - Log.v(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); + YaaccLogger.v(getClass().getName(), "ActionException thrown by service, wrapping in invocation and returning: " + ex); + YaaccLogger.v(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); actionInvocation.setFailure(ex); } catch (InterruptedException ex) { - Log.v(getClass().getName(), "InterruptedException thrown by service, wrapping in invocation and returning: " + ex); - Log.v(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); + YaaccLogger.v(getClass().getName(), "InterruptedException thrown by service, wrapping in invocation and returning: " + ex); + YaaccLogger.v(getClass().getName(), "Exception root cause: ", Exceptions.unwrap(ex)); actionInvocation.setFailure(new ActionCancelledException(ex)); } catch (Throwable t) { Throwable rootCause = Exceptions.unwrap(t); - Log.v(getClass().getName(), "Execution has thrown, wrapping root cause in ActionException and returning: " + t); - Log.v(getClass().getName(), "Exception root cause: ", rootCause); + YaaccLogger.v(getClass().getName(), "Execution has thrown, wrapping root cause in ActionException and returning: " + t); + YaaccLogger.v(getClass().getName(), "Exception root cause: ", rootCause); actionInvocation.setFailure( new ActionException( @@ -119,15 +119,15 @@ public String toString() { */ protected Object readOutputArgumentValues(Action action, Object instance) throws Exception { Object[] results = new Object[action.getOutputArguments().length]; - Log.v(getClass().getName(), "Attempting to retrieve output argument values using accessor: " + results.length); + YaaccLogger.v(getClass().getName(), "Attempting to retrieve output argument values using accessor: " + results.length); int i = 0; for (ActionArgument outputArgument : action.getOutputArguments()) { - Log.v(getClass().getName(), "Calling acccessor method for: " + outputArgument); + YaaccLogger.v(getClass().getName(), "Calling acccessor method for: " + outputArgument); StateVariableAccessor accessor = getOutputArgumentAccessors().get(outputArgument); if (accessor != null) { - Log.v(getClass().getName(), "Calling accessor to read output argument value: " + accessor); + YaaccLogger.v(getClass().getName(), "Calling accessor to read output argument value: " + accessor); results[i++] = accessor.read(instance); } else { throw new IllegalStateException("No accessor bound for: " + outputArgument); @@ -151,10 +151,10 @@ protected void setOutputArgumentValue(ActionInvocation actionInvoc if (result != null) { try { if (service.isStringConvertibleType(result)) { - Log.v(getClass().getName(), "Result of invocation matches convertible type, setting toString() single output argument value"); + YaaccLogger.v(getClass().getName(), "Result of invocation matches convertible type, setting toString() single output argument value"); actionInvocation.setOutput(new ActionArgumentValue(argument, result.toString())); } else { - Log.v(getClass().getName(), "Result of invocation is Object, setting single output argument value"); + YaaccLogger.v(getClass().getName(), "Result of invocation is Object, setting single output argument value"); actionInvocation.setOutput(new ActionArgumentValue(argument, result)); } } catch (InvalidValueException ex) { @@ -166,7 +166,7 @@ protected void setOutputArgumentValue(ActionInvocation actionInvoc } } else { - Log.v(getClass().getName(), "Result of invocation is null, not setting any output argument value(s)"); + YaaccLogger.v(getClass().getName(), "Result of invocation is null, not setting any output argument value(s)"); } } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/action/MethodActionExecutor.java b/yaacc/src/main/java/org/fourthline/cling/model/action/MethodActionExecutor.java index cb126cd0..e260b8c6 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/action/MethodActionExecutor.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/action/MethodActionExecutor.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.action; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.meta.ActionArgument; import org.fourthline.cling.model.meta.LocalService; @@ -67,31 +67,31 @@ protected void execute(ActionInvocation actionInvocation, Object s // Simple case: no output arguments if (!actionInvocation.getAction().hasOutputArguments()) { - Log.v(getClass().getName(), "Calling local service method with no output arguments: " + method); + YaaccLogger.v(getClass().getName(), "Calling local service method with no output arguments: " + method); Reflections.invoke(method, serviceImpl, inputArgumentValues); return; } boolean isVoid = method.getReturnType().equals(Void.TYPE); - Log.v(getClass().getName(), "Calling local service method with output arguments: " + method); + YaaccLogger.v(getClass().getName(), "Calling local service method with output arguments: " + method); Object result; boolean isArrayResultProcessed = true; if (isVoid) { - Log.v(getClass().getName(), "Action method is void, calling declared accessors(s) on service instance to retrieve ouput argument(s)"); + YaaccLogger.v(getClass().getName(), "Action method is void, calling declared accessors(s) on service instance to retrieve ouput argument(s)"); Reflections.invoke(method, serviceImpl, inputArgumentValues); result = readOutputArgumentValues(actionInvocation.getAction(), serviceImpl); } else if (isUseOutputArgumentAccessors(actionInvocation)) { - Log.v(getClass().getName(), "Action method is not void, calling declared accessor(s) on returned instance to retrieve ouput argument(s)"); + YaaccLogger.v(getClass().getName(), "Action method is not void, calling declared accessor(s) on returned instance to retrieve ouput argument(s)"); Object returnedInstance = Reflections.invoke(method, serviceImpl, inputArgumentValues); result = readOutputArgumentValues(actionInvocation.getAction(), returnedInstance); } else { - Log.v(getClass().getName(), "Action method is not void, using returned value as (single) output argument"); + YaaccLogger.v(getClass().getName(), "Action method is not void, using returned value as (single) output argument"); result = Reflections.invoke(method, serviceImpl, inputArgumentValues); isArrayResultProcessed = false; // We never want to process e.g. byte[] as individual variable values } @@ -100,7 +100,7 @@ protected void execute(ActionInvocation actionInvocation, Object s if (isArrayResultProcessed && result instanceof Object[]) { Object[] results = (Object[]) result; - Log.v(getClass().getName(), "Accessors returned Object[], setting output argument values: " + results.length); + YaaccLogger.v(getClass().getName(), "Accessors returned Object[], setting output argument values: " + results.length); for (int i = 0; i < outputArgs.length; i++) { setOutputArgumentValue(actionInvocation, outputArgs[i], results[i]); } @@ -156,12 +156,12 @@ protected Object[] createInputArgumentValues(ActionInvocation acti if (inputCallValueString.length() > 0 && service.isStringConvertibleType(methodParameterType) && !methodParameterType.isEnum()) { try { Constructor ctor = methodParameterType.getConstructor(String.class); - Log.v(getClass().getName(), "Creating new input argument value instance with String.class constructor of type: " + methodParameterType); + YaaccLogger.v(getClass().getName(), "Creating new input argument value instance with String.class constructor of type: " + methodParameterType); Object o = ctor.newInstance(inputCallValueString); values.add(i++, o); } catch (Exception ex) { - Log.w(getClass().getName(), "Error preparing action method call: " + method); - Log.w(getClass().getName(), "Can't convert input argument string to desired type of '" + argument.getName() + "': " + ex); + YaaccLogger.w(getClass().getName(), "Error preparing action method call: " + method); + YaaccLogger.w(getClass().getName(), "Can't convert input argument string to desired type of '" + argument.getName() + "': " + ex); throw new ActionException( ErrorCode.ARGUMENT_VALUE_INVALID, "Can't convert input argument string to desired type of '" + argument.getName() + "': " + ex ); @@ -176,7 +176,7 @@ protected Object[] createInputArgumentValues(ActionInvocation acti && RemoteClientInfo.class.isAssignableFrom(method.getParameterTypes()[method.getParameterTypes().length - 1])) { if (actionInvocation instanceof RemoteActionInvocation && ((RemoteActionInvocation) actionInvocation).getRemoteClientInfo() != null) { - Log.v(getClass().getName(), "Providing remote client info as last action method input argument: " + method); + YaaccLogger.v(getClass().getName(), "Providing remote client info as last action method input argument: " + method); values.add(i, ((RemoteActionInvocation) actionInvocation).getRemoteClientInfo()); } else { // Local call, no client info available diff --git a/yaacc/src/main/java/org/fourthline/cling/model/gena/LocalGENASubscription.java b/yaacc/src/main/java/org/fourthline/cling/model/gena/LocalGENASubscription.java index 145bedc6..00cd9357 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/gena/LocalGENASubscription.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/gena/LocalGENASubscription.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.gena; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.ServiceManager; import org.fourthline.cling.model.UserConstants; @@ -73,19 +73,19 @@ public LocalGENASubscription(LocalService service, setSubscriptionDuration(requestedDurationSeconds); - Log.v(getClass().getName(), "Reading initial state of local service at subscription time"); + YaaccLogger.v(getClass().getName(), "Reading initial state of local service at subscription time"); long currentTime = new Date().getTime(); this.currentValues.clear(); Collection values = getService().getManager().getCurrentState(); - Log.v(getClass().getName(), "Got evented state variable values: " + values.size()); + YaaccLogger.v(getClass().getName(), "Got evented state variable values: " + values.size()); for (StateVariableValue value : values) { this.currentValues.put(value.getStateVariable().getName(), value); - Log.v(getClass().getName(), "Read state variable value '" + value.getStateVariable().getName() + "': " + value.toString()); + YaaccLogger.v(getClass().getName(), "Read state variable value '" + value.getStateVariable().getName() + "': " + value.toString()); // Preserve "last sent" state for future moderation @@ -122,7 +122,7 @@ synchronized public void end(CancelReason reason) { try { getService().getManager().getPropertyChangeSupport().removePropertyChangeListener(this); } catch (Exception ex) { - Log.w(getClass().getName(), "Removal of local service property change listener failed: " + Exceptions.unwrap(ex)); + YaaccLogger.w(getClass().getName(), "Removal of local service property change listener failed: " + Exceptions.unwrap(ex)); } ended(reason); } @@ -134,7 +134,7 @@ synchronized public void end(CancelReason reason) { synchronized public void propertyChange(PropertyChangeEvent e) { if (!e.getPropertyName().equals(ServiceManager.EVENTED_STATE_VARIABLES)) return; - Log.v(getClass().getName(), "Eventing triggered, getting state for subscription: " + getSubscriptionId()); + YaaccLogger.v(getClass().getName(), "Eventing triggered, getting state for subscription: " + getSubscriptionId()); long currentTime = new Date().getTime(); @@ -145,7 +145,7 @@ synchronized public void propertyChange(PropertyChangeEvent e) { for (StateVariableValue newValue : newValues) { String name = newValue.getStateVariable().getName(); if (!excludedVariables.contains(name)) { - Log.v(getClass().getName(), "Adding state variable value to current values of event: " + newValue.getStateVariable() + " = " + newValue); + YaaccLogger.v(getClass().getName(), "Adding state variable value to current values of event: " + newValue.getStateVariable() + " = " + newValue); currentValues.put(newValue.getStateVariable().getName(), newValue); // Preserve "last sent" state for future moderation @@ -157,13 +157,13 @@ synchronized public void propertyChange(PropertyChangeEvent e) { } if (currentValues.size() > 0) { - Log.v(getClass().getName(), "Propagating new state variable values to subscription: " + this); + YaaccLogger.v(getClass().getName(), "Propagating new state variable values to subscription: " + this); // TODO: I'm not happy with this design, this dispatches to a separate thread which _then_ // is supposed to lock and read the values off this instance. That obviously doesn't work // so it's currently a hack in SendingEvent.java eventReceived(); } else { - Log.v(getClass().getName(), "No state variable values for event (all moderated out?), not triggering event"); + YaaccLogger.v(getClass().getName(), "No state variable values for event (all moderated out?), not triggering event"); } } @@ -186,13 +186,13 @@ synchronized protected Set moderateStateVariables(long currentTime, Coll if (stateVariable.getEventDetails().getEventMaximumRateMilliseconds() == 0 && stateVariable.getEventDetails().getEventMinimumDelta() == 0) { - Log.v(getClass().getName(), "Variable is not moderated: " + stateVariable); + YaaccLogger.v(getClass().getName(), "Variable is not moderated: " + stateVariable); continue; } // That should actually never happen, because we always "send" it as the initial state/event if (!lastSentTimestamp.containsKey(stateVariableName)) { - Log.v(getClass().getName(), "Variable is moderated but was never sent before: " + stateVariable); + YaaccLogger.v(getClass().getName(), "Variable is moderated but was never sent before: " + stateVariable); continue; } @@ -200,7 +200,7 @@ synchronized protected Set moderateStateVariables(long currentTime, Coll long timestampLastSent = lastSentTimestamp.get(stateVariableName); long timestampNextSend = timestampLastSent + (stateVariable.getEventDetails().getEventMaximumRateMilliseconds()); if (currentTime <= timestampNextSend) { - Log.v(getClass().getName(), "Excluding state variable with maximum rate: " + stateVariable); + YaaccLogger.v(getClass().getName(), "Excluding state variable with maximum rate: " + stateVariable); excludedVariables.add(stateVariableName); continue; } @@ -213,13 +213,13 @@ synchronized protected Set moderateStateVariables(long currentTime, Coll long minDelta = stateVariable.getEventDetails().getEventMinimumDelta(); if (newValue > oldValue && newValue - oldValue < minDelta) { - Log.v(getClass().getName(), "Excluding state variable with minimum delta: " + stateVariable); + YaaccLogger.v(getClass().getName(), "Excluding state variable with minimum delta: " + stateVariable); excludedVariables.add(stateVariableName); continue; } if (newValue < oldValue && oldValue - newValue < minDelta) { - Log.v(getClass().getName(), "Excluding state variable with minimum delta: " + stateVariable); + YaaccLogger.v(getClass().getName(), "Excluding state variable with minimum delta: " + stateVariable); excludedVariables.add(stateVariableName); } } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/message/UpnpHeaders.java b/yaacc/src/main/java/org/fourthline/cling/model/message/UpnpHeaders.java index 44c22cde..a9c861ad 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/message/UpnpHeaders.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/message/UpnpHeaders.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.message; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.message.header.UpnpHeader; import org.seamless.http.Headers; @@ -54,21 +54,21 @@ public UpnpHeaders(boolean normalizeHeaders) { protected void parseHeaders() { // This runs as late as possible and only when necessary (getter called and map is dirty) parsedHeaders = new LinkedHashMap<>(); - Log.v(getClass().getName(), "Parsing all HTTP headers for known UPnP headers: " + size()); + YaaccLogger.v(getClass().getName(), "Parsing all HTTP headers for known UPnP headers: " + size()); for (Entry> entry : entrySet()) { if (entry.getKey() == null) continue; // Oh yes, the JDK has 'null' HTTP headers UpnpHeader.Type type = UpnpHeader.Type.getByHttpName(entry.getKey()); if (type == null) { - Log.v(getClass().getName(), "Ignoring non-UPNP HTTP header: " + entry.getKey()); + YaaccLogger.v(getClass().getName(), "Ignoring non-UPNP HTTP header: " + entry.getKey()); continue; } for (String value : entry.getValue()) { UpnpHeader upnpHeader = UpnpHeader.newInstance(type, value); if (upnpHeader == null || upnpHeader.getValue() == null) { - Log.v(getClass().getName(), + YaaccLogger.v(getClass().getName(), "Ignoring known but irrelevant header (value violates the UDA specification?) '" + type.getHttpName() + "': " @@ -82,7 +82,7 @@ protected void parseHeaders() { } protected void addParsedValue(UpnpHeader.Type type, UpnpHeader value) { - Log.v(getClass().getName(), "Adding parsed header: " + value); + YaaccLogger.v(getClass().getName(), "Adding parsed header: " + value); List list = parsedHeaders.get(type); if (list == null) { list = new LinkedList<>(); @@ -168,23 +168,23 @@ public String getFirstHeaderString(UpnpHeader.Type type) { } public void log() { - Log.v(getClass().getName(), "############################ RAW HEADERS ###########################"); + YaaccLogger.v(getClass().getName(), "############################ RAW HEADERS ###########################"); for (Entry> entry : entrySet()) { - Log.v(getClass().getName(), "=== NAME : " + entry.getKey()); + YaaccLogger.v(getClass().getName(), "=== NAME : " + entry.getKey()); for (String v : entry.getValue()) { - Log.v(getClass().getName(), "VALUE: " + v); + YaaccLogger.v(getClass().getName(), "VALUE: " + v); } } if (parsedHeaders != null && parsedHeaders.size() > 0) { - Log.v(getClass().getName(), "########################## PARSED HEADERS ##########################"); + YaaccLogger.v(getClass().getName(), "########################## PARSED HEADERS ##########################"); for (Map.Entry> entry : parsedHeaders.entrySet()) { - Log.v(getClass().getName(), "=== TYPE: " + entry.getKey()); + YaaccLogger.v(getClass().getName(), "=== TYPE: " + entry.getKey()); for (UpnpHeader upnpHeader : entry.getValue()) { - Log.v(getClass().getName(), "HEADER: " + upnpHeader); + YaaccLogger.v(getClass().getName(), "HEADER: " + upnpHeader); } } } - Log.v(getClass().getName(), "####################################################################"); + YaaccLogger.v(getClass().getName(), "####################################################################"); } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/message/control/OutgoingActionRequestMessage.java b/yaacc/src/main/java/org/fourthline/cling/model/message/control/OutgoingActionRequestMessage.java index 6fa6a95e..694fe754 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/message/control/OutgoingActionRequestMessage.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/message/control/OutgoingActionRequestMessage.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.message.control; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.action.ActionInvocation; import org.fourthline.cling.model.action.RemoteActionInvocation; @@ -66,7 +66,7 @@ public OutgoingActionRequestMessage(Action action, UpnpRequest operation) { SoapActionHeader soapActionHeader; if (action instanceof QueryStateVariableAction) { - Log.v(getClass().getName(), "Adding magic control SOAP action header for state variable query action"); + YaaccLogger.v(getClass().getName(), "Adding magic control SOAP action header for state variable query action"); soapActionHeader = new SoapActionHeader( new SoapActionType( SoapActionType.MAGIC_CONTROL_NS, SoapActionType.MAGIC_CONTROL_TYPE, null, action.getName() @@ -87,7 +87,7 @@ public OutgoingActionRequestMessage(Action action, UpnpRequest operation) { if (getOperation().getMethod().equals(UpnpRequest.Method.POST)) { getHeaders().add(UpnpHeader.Type.SOAPACTION, soapActionHeader); - Log.v(getClass().getName(), "Added SOAP action header: " + soapActionHeader); + YaaccLogger.v(getClass().getName(), "Added SOAP action header: " + soapActionHeader); /* TODO: Finish the M-POST crap (or not) } else if (getOperation().getMethod().equals(UpnpRequest.Method.MPOST)) { diff --git a/yaacc/src/main/java/org/fourthline/cling/model/message/header/CallbackHeader.java b/yaacc/src/main/java/org/fourthline/cling/model/message/header/CallbackHeader.java index d434126b..3fb8a3de 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/message/header/CallbackHeader.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/message/header/CallbackHeader.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.message.header; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import java.net.MalformedURLException; import java.net.URISyntaxException; @@ -61,7 +61,7 @@ public void setString(String s) throws InvalidHeaderException { sp = sp.trim(); if (!sp.startsWith("http://")) { - Log.v(getClass().getName(), "Discarding non-http callback URL: " + sp); + YaaccLogger.v(getClass().getName(), "Discarding non-http callback URL: " + sp); continue; } @@ -79,7 +79,7 @@ On some platforms (Android...), a valid URL might not be a valid URI, so */ url.toURI(); } catch (URISyntaxException ex) { - Log.w(getClass().getName(), "Discarding callback URL, not a valid URI on this platform: " + url, ex); + YaaccLogger.w(getClass().getName(), "Discarding callback URL, not a valid URI on this platform: " + url, ex); continue; } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/message/header/UpnpHeader.java b/yaacc/src/main/java/org/fourthline/cling/model/message/header/UpnpHeader.java index 86265e5d..c1f254a7 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/message/header/UpnpHeader.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/message/header/UpnpHeader.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.message.header; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.seamless.util.Exceptions; @@ -59,18 +59,18 @@ public static UpnpHeader newInstance(UpnpHeader.Type type, String headerValue) { for (int i = 0; i < type.getHeaderTypes().length && upnpHeader == null; i++) { Class headerClass = type.getHeaderTypes()[i]; try { - //Log.v(UpnpHeader.class.getName(), "Trying to parse '" + type + "' with class: " + headerClass.getSimpleName()); + //YaaccLogger.v(UpnpHeader.class.getName(), "Trying to parse '" + type + "' with class: " + headerClass.getSimpleName()); upnpHeader = headerClass.newInstance(); if (headerValue != null) { upnpHeader.setString(headerValue); } } catch (InvalidHeaderException ex) { //This Exception is expected therefore no log is needed - // Log.v(UpnpHeader.class.getName(), "Invalid header value for tested type: " + headerClass.getSimpleName() + " - " + ex.getMessage()); + // YaaccLogger.v(UpnpHeader.class.getName(), "Invalid header value for tested type: " + headerClass.getSimpleName() + " - " + ex.getMessage()); upnpHeader = null; } catch (Exception ex) { - Log.w(UpnpHeader.class.getName(), "Error instantiating header of type '" + type + "' with value: " + headerValue); - Log.w(UpnpHeader.class.getName(), "Exception root cause: ", Exceptions.unwrap(ex)); + YaaccLogger.w(UpnpHeader.class.getName(), "Error instantiating header of type '" + type + "' with value: " + headerValue); + YaaccLogger.w(UpnpHeader.class.getName(), "Exception root cause: ", Exceptions.unwrap(ex)); } } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/meta/Action.java b/yaacc/src/main/java/org/fourthline/cling/model/meta/Action.java index a9b8f11f..239b8e3f 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/meta/Action.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/meta/Action.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.meta; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.ModelUtil; import org.fourthline.cling.model.Validatable; @@ -144,8 +144,8 @@ public List validate() { "Action without name of: " + getService() )); } else if (!ModelUtil.isValidUDAName(getName())) { - Log.w(getClass().getName(), "UPnP specification violation of: " + getService().getDevice()); - Log.w(getClass().getName(), "Invalid action name: " + this); + YaaccLogger.w(getClass().getName(), "UPnP specification violation of: " + getService().getDevice()); + YaaccLogger.w(getClass().getName(), "Invalid action name: " + this); } for (ActionArgument actionArgument : getArguments()) { @@ -167,12 +167,12 @@ public List validate() { // Check retval if (actionArgument.isReturnValue()) { if (actionArgument.getDirection() == ActionArgument.Direction.IN) { - Log.w(getClass().getName(), "UPnP specification violation of :" + getService().getDevice()); - Log.w(getClass().getName(), "Input argument can not have "); + YaaccLogger.w(getClass().getName(), "UPnP specification violation of :" + getService().getDevice()); + YaaccLogger.w(getClass().getName(), "Input argument can not have "); } else { if (retValueArgument != null) { - Log.w(getClass().getName(), "UPnP specification violation of: " + getService().getDevice()); - Log.w(getClass().getName(), "Only one argument of action '" + getName() + "' can be "); + YaaccLogger.w(getClass().getName(), "UPnP specification violation of: " + getService().getDevice()); + YaaccLogger.w(getClass().getName(), "Only one argument of action '" + getName() + "' can be "); } retValueArgument = actionArgument; retValueArgumentIndex = i; @@ -184,8 +184,8 @@ public List validate() { for (int j = 0; j < retValueArgumentIndex; j++) { ActionArgument a = getArguments()[j]; if (a.getDirection() == ActionArgument.Direction.OUT) { - Log.w(getClass().getName(), "UPnP specification violation of: " + getService().getDevice()); - Log.w(getClass().getName(), "Argument '" + retValueArgument.getName() + "' of action '" + getName() + "' is but not the first OUT argument"); + YaaccLogger.w(getClass().getName(), "UPnP specification violation of: " + getService().getDevice()); + YaaccLogger.w(getClass().getName(), "Argument '" + retValueArgument.getName() + "' of action '" + getName() + "' is but not the first OUT argument"); } } } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/meta/ActionArgument.java b/yaacc/src/main/java/org/fourthline/cling/model/meta/ActionArgument.java index 01248fcc..aafe9d2b 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/meta/ActionArgument.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/meta/ActionArgument.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.meta; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.ModelUtil; import org.fourthline.cling.model.Validatable; @@ -121,11 +121,11 @@ public List validate() { "Argument without name of: " + getAction() )); } else if (!ModelUtil.isValidUDAName(getName())) { - Log.w(getClass().getName(), "UPnP specification violation of: " + getAction().getService().getDevice()); - Log.w(getClass().getName(), "Invalid argument name: " + this); + YaaccLogger.w(getClass().getName(), "UPnP specification violation of: " + getAction().getService().getDevice()); + YaaccLogger.w(getClass().getName(), "Invalid argument name: " + this); } else if (getName().length() > 32) { - Log.w(getClass().getName(), "UPnP specification violation of: " + getAction().getService().getDevice()); - Log.w(getClass().getName(), "Argument name should be less than 32 characters: " + this); + YaaccLogger.w(getClass().getName(), "UPnP specification violation of: " + getAction().getService().getDevice()); + YaaccLogger.w(getClass().getName(), "Argument name should be less than 32 characters: " + this); } if (getDirection() == null) { diff --git a/yaacc/src/main/java/org/fourthline/cling/model/meta/Device.java b/yaacc/src/main/java/org/fourthline/cling/model/meta/Device.java index a4fabdc8..a011b618 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/meta/Device.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/meta/Device.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.meta; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Namespace; import org.fourthline.cling.model.Validatable; @@ -86,7 +86,7 @@ public Device(DI identity, UDAVersion version, DeviceType type, DeviceDetails de if (iconErrors.isEmpty()) { validIcons.add(icon); } else { - Log.w(getClass().getName(), "Discarding invalid '" + icon + "': " + iconErrors); + YaaccLogger.w(getClass().getName(), "Discarding invalid '" + icon + "': " + iconErrors); } } } @@ -119,7 +119,7 @@ public Device(DI identity, UDAVersion version, DeviceType type, DeviceDetails de if (errors.size() > 0) { for (ValidationError error : errors) { - Log.v(getClass().getName(), error.toString()); + YaaccLogger.v(getClass().getName(), error.toString()); } throw new ValidationException("Validation of device graph failed, call getErrors() on exception", errors); diff --git a/yaacc/src/main/java/org/fourthline/cling/model/meta/DeviceDetails.java b/yaacc/src/main/java/org/fourthline/cling/model/meta/DeviceDetails.java index db9f41b0..14be932e 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/meta/DeviceDetails.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/meta/DeviceDetails.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.meta; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Validatable; import org.fourthline.cling.model.ValidationError; @@ -203,12 +203,12 @@ public List validate() { if (getUpc() != null) { // This is broken in more than half of the devices I've tested, so let's not even bother with a warning if (getUpc().length() != 12) { - Log.v(getClass().getName(), "UPnP specification violation, UPC must be 12 digits: " + getUpc()); + YaaccLogger.v(getClass().getName(), "UPnP specification violation, UPC must be 12 digits: " + getUpc()); } else { try { Long.parseLong(getUpc()); } catch (NumberFormatException ex) { - Log.v(getClass().getName(), "UPnP specification violation, UPC must be 12 digits all-numeric: " + getUpc()); + YaaccLogger.v(getClass().getName(), "UPnP specification violation, UPC must be 12 digits all-numeric: " + getUpc()); } } } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/meta/Icon.java b/yaacc/src/main/java/org/fourthline/cling/model/meta/Icon.java index 3c043755..88ca7efb 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/meta/Icon.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/meta/Icon.java @@ -16,7 +16,7 @@ package org.fourthline.cling.model.meta; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Validatable; import org.fourthline.cling.model.ValidationError; @@ -157,20 +157,20 @@ public List validate() { List errors = new ArrayList<>(); if (getMimeType() == null) { - Log.w(getClass().getName(), "UPnP specification violation of: " + getDevice()); - Log.w(getClass().getName(), "Invalid icon, missing mime type: " + this); + YaaccLogger.w(getClass().getName(), "UPnP specification violation of: " + getDevice()); + YaaccLogger.w(getClass().getName(), "Invalid icon, missing mime type: " + this); } if (getWidth() == 0) { - Log.w(getClass().getName(), "UPnP specification violation of: " + getDevice()); - Log.w(getClass().getName(), "Invalid icon, missing width: " + this); + YaaccLogger.w(getClass().getName(), "UPnP specification violation of: " + getDevice()); + YaaccLogger.w(getClass().getName(), "Invalid icon, missing width: " + this); } if (getHeight() == 0) { - Log.w(getClass().getName(), "UPnP specification violation of: " + getDevice()); - Log.w(getClass().getName(), "Invalid icon, missing height: " + this); + YaaccLogger.w(getClass().getName(), "UPnP specification violation of: " + getDevice()); + YaaccLogger.w(getClass().getName(), "Invalid icon, missing height: " + this); } if (getDepth() == 0) { - Log.w(getClass().getName(), "UPnP specification violation of: " + getDevice()); - Log.w(getClass().getName(), "Invalid icon, missing bitmap depth: " + this); + YaaccLogger.w(getClass().getName(), "UPnP specification violation of: " + getDevice()); + YaaccLogger.w(getClass().getName(), "Invalid icon, missing bitmap depth: " + this); } if (getUri() == null) { diff --git a/yaacc/src/main/java/org/fourthline/cling/model/meta/Service.java b/yaacc/src/main/java/org/fourthline/cling/model/meta/Service.java index 4ee78006..c0403065 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/meta/Service.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/meta/Service.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.meta; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.ServiceReference; import org.fourthline.cling.model.ValidationError; @@ -188,9 +188,9 @@ public List validate() { List actionErrors = action.validate(); if (actionErrors.size() > 0) { actions.remove(action.getName()); // Remove it - Log.w(getClass().getName(), "Discarding invalid action of service '" + getServiceId() + "': " + action.getName()); + YaaccLogger.w(getClass().getName(), "Discarding invalid action of service '" + getServiceId() + "': " + action.getName()); for (ValidationError actionError : actionErrors) { - Log.w(getClass().getName(), "Invalid action '" + action.getName() + "': " + actionError); + YaaccLogger.w(getClass().getName(), "Invalid action '" + action.getName() + "': " + actionError); } } } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariable.java b/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariable.java index 650bfe95..6c74e446 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariable.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariable.java @@ -16,7 +16,7 @@ package org.fourthline.cling.model.meta; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.ModelUtil; import org.fourthline.cling.model.Validatable; @@ -82,8 +82,8 @@ public List validate() { "StateVariable without name of: " + getService() )); } else if (!ModelUtil.isValidUDAName(getName())) { - Log.w(getClass().getName(), "UPnP specification violation of: " + getService().getDevice()); - Log.w(getClass().getName(), "Invalid state variable name: " + this); + YaaccLogger.w(getClass().getName(), "UPnP specification violation of: " + getService().getDevice()); + YaaccLogger.w(getClass().getName(), "Invalid state variable name: " + this); } errors.addAll(getTypeDetails().validate()); diff --git a/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariableAllowedValueRange.java b/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariableAllowedValueRange.java index 6e09f3e6..7658c752 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariableAllowedValueRange.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariableAllowedValueRange.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.meta; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Validatable; import org.fourthline.cling.model.ValidationError; @@ -42,7 +42,7 @@ public StateVariableAllowedValueRange(long minimum, long maximum) { public StateVariableAllowedValueRange(long minimum, long maximum, long step) { if (minimum > maximum) { - Log.w(getClass().getName(), "UPnP specification violation, allowed value range minimum '" + minimum + YaaccLogger.w(getClass().getName(), "UPnP specification violation, allowed value range minimum '" + minimum + "' is greater than maximum '" + maximum + "', switching values."); this.minimum = maximum; this.maximum = minimum; diff --git a/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariableTypeDetails.java b/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariableTypeDetails.java index 636aeed8..a8b2dd9a 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariableTypeDetails.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/meta/StateVariableTypeDetails.java @@ -16,7 +16,7 @@ package org.fourthline.cling.model.meta; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Validatable; import org.fourthline.cling.model.ValidationError; @@ -115,12 +115,12 @@ public List validate() { for (String s : getAllowedValues()) { if (s.length() > 31) { - Log.w(getClass().getName(), "UPnP specification violation, allowed value string must be less than 32 chars: " + s); + YaaccLogger.w(getClass().getName(), "UPnP specification violation, allowed value string must be less than 32 chars: " + s); } } if (!foundDefaultInAllowedValues(defaultValue, allowedValues)) { - Log.w(getClass().getName(), "UPnP specification violation, allowed string values " + + YaaccLogger.w(getClass().getName(), "UPnP specification violation, allowed string values " + "don't contain default value: " + defaultValue ); } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/types/DeviceType.java b/yaacc/src/main/java/org/fourthline/cling/model/types/DeviceType.java index f6a995eb..dd73145d 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/types/DeviceType.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/types/DeviceType.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.types; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Constants; @@ -103,7 +103,7 @@ public static DeviceType valueOf(String s) throws InvalidValueException { // urn:schemas-upnp-org:device::1 matcher = Pattern.compile("urn:(" + Constants.REGEX_NAMESPACE + "):device::([0-9]+).*").matcher(s); if (matcher.matches() && matcher.groupCount() >= 2) { - Log.w(DeviceType.class.getName(), "UPnP specification violation, no device type token, defaulting to " + UNKNOWN + ": " + s); + YaaccLogger.w(DeviceType.class.getName(), "UPnP specification violation, no device type token, defaulting to " + UNKNOWN + ": " + s); return new DeviceType(matcher.group(1), UNKNOWN, Integer.valueOf(matcher.group(2))); } @@ -112,7 +112,7 @@ public static DeviceType valueOf(String s) throws InvalidValueException { matcher = Pattern.compile("urn:(" + Constants.REGEX_NAMESPACE + "):device:(.+?):([0-9]+).*").matcher(s); if (matcher.matches() && matcher.groupCount() >= 3) { String cleanToken = matcher.group(2).replaceAll("[^a-zA-Z_0-9\\-]", "-"); - Log.w(DeviceType.class.getName(), + YaaccLogger.w(DeviceType.class.getName(), "UPnP specification violation, replacing invalid device type token '" + matcher.group(2) + "' with: " diff --git a/yaacc/src/main/java/org/fourthline/cling/model/types/ServiceId.java b/yaacc/src/main/java/org/fourthline/cling/model/types/ServiceId.java index 110c4477..7bd0154b 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/types/ServiceId.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/types/ServiceId.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.types; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Constants; @@ -91,14 +91,14 @@ public static ServiceId valueOf(String s) throws InvalidValueException { // urn:upnp-org:serviceId: matcher = Pattern.compile("urn:(" + Constants.REGEX_NAMESPACE + "):serviceId:").matcher(s); if (matcher.matches() && matcher.groupCount() >= 1) { - Log.w(ServiceId.class.getName(), "UPnP specification violation, no service ID token, defaulting to " + UNKNOWN + ": " + s); + YaaccLogger.w(ServiceId.class.getName(), "UPnP specification violation, no service ID token, defaulting to " + UNKNOWN + ": " + s); return new ServiceId(matcher.group(1), UNKNOWN); } // TODO: UPNP VIOLATION: PS Audio Bridge has invalid service IDs String tokens[] = s.split("[:]"); if (tokens.length == 4) { - Log.w(ServiceId.class.getName(), "UPnP specification violation, trying a simple colon-split of: " + s); + YaaccLogger.w(ServiceId.class.getName(), "UPnP specification violation, trying a simple colon-split of: " + s); return new ServiceId(tokens[1], tokens[3]); } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/types/ServiceType.java b/yaacc/src/main/java/org/fourthline/cling/model/types/ServiceType.java index b44f3ca4..9a251231 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/types/ServiceType.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/types/ServiceType.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.types; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Constants; @@ -115,7 +115,7 @@ public static ServiceType valueOf(String s) throws InvalidValueException { matcher = Pattern.compile("urn:(" + Constants.REGEX_NAMESPACE + "):service:(.+?):([0-9]+).*").matcher(s); if (matcher.matches() && matcher.groupCount() >= 3) { String cleanToken = matcher.group(2).replaceAll("[^a-zA-Z_0-9\\-]", "-"); - Log.w(ServiceType.class.getName(), + YaaccLogger.w(ServiceType.class.getName(), "UPnP specification violation, replacing invalid service type token '" + matcher.group(2) + "' with: " @@ -129,7 +129,7 @@ public static ServiceType valueOf(String s) throws InvalidValueException { matcher = Pattern.compile("urn:(" + Constants.REGEX_NAMESPACE + "):serviceId:(.+?):([0-9]+).*").matcher(s); if (matcher.matches() && matcher.groupCount() >= 3) { String cleanToken = matcher.group(2).replaceAll("[^a-zA-Z_0-9\\-]", "-"); - Log.w(ServiceType.class.getName(), + YaaccLogger.w(ServiceType.class.getName(), "UPnP specification violation, replacing invalid service type token '" + matcher.group(2) + "' with: " diff --git a/yaacc/src/main/java/org/fourthline/cling/model/types/UDAServiceId.java b/yaacc/src/main/java/org/fourthline/cling/model/types/UDAServiceId.java index 1e321257..afc41df8 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/types/UDAServiceId.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/types/UDAServiceId.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.types; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Constants; @@ -60,7 +60,7 @@ public static UDAServiceId valueOf(String s) throws InvalidValueException { // TODO: UPNP VIOLATION: Handle garbage sent by Eyecon Android app matcher = Pattern.compile("urn:upnp-orgerviceId:urnchemas-upnp-orgervice:(" + Constants.REGEX_ID + ")").matcher(s); if (matcher.matches()) { - Log.w(UDAServiceId.class.getName(), "UPnP specification violation, recovering from Eyecon garbage: " + s); + YaaccLogger.w(UDAServiceId.class.getName(), "UPnP specification violation, recovering from Eyecon garbage: " + s); return new UDAServiceId(matcher.group(1)); } @@ -69,7 +69,7 @@ public static UDAServiceId valueOf(String s) throws InvalidValueException { "ConnectionManager".equals(s) || "RenderingControl".equals(s) || "AVTransport".equals(s)) { - Log.w(UDAServiceId.class.getName(), "UPnP specification violation, fixing broken Service ID: " + s); + YaaccLogger.w(UDAServiceId.class.getName(), "UPnP specification violation, fixing broken Service ID: " + s); return new UDAServiceId(s); } diff --git a/yaacc/src/main/java/org/fourthline/cling/model/types/UnsignedVariableInteger.java b/yaacc/src/main/java/org/fourthline/cling/model/types/UnsignedVariableInteger.java index 47792b32..ce14a7c6 100644 --- a/yaacc/src/main/java/org/fourthline/cling/model/types/UnsignedVariableInteger.java +++ b/yaacc/src/main/java/org/fourthline/cling/model/types/UnsignedVariableInteger.java @@ -15,7 +15,7 @@ package org.fourthline.cling.model.types; -import android.util.Log; +import de.yaacc.util.YaaccLogger; /** * A crude solution for unsigned "non-negative" types in UPnP, not usable for any arithmetic. @@ -54,7 +54,7 @@ public UnsignedVariableInteger(String s) throws NumberFormatException { if (s.startsWith("-")) { // Don't throw exception, just cut it! // TODO: UPNP VIOLATION: Twonky Player returns "-1" as the track number - Log.w(getClass().getName(), "Invalid negative integer value '" + s + "', assuming value 0!"); + YaaccLogger.w(getClass().getName(), "Invalid negative integer value '" + s + "', assuming value 0!"); s = "0"; } setValue(Long.parseLong(s.trim())); diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/ProtocolCreationException.java b/yaacc/src/main/java/org/fourthline/cling/protocol/ProtocolCreationException.java deleted file mode 100644 index 7db6b21a..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/ProtocolCreationException.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.protocol; - -/** - * Recoverable error, thrown when no protocol is available to handle a UPnP message. - * - * @author Christian Bauer - */ -public class ProtocolCreationException extends Exception { - - public ProtocolCreationException(String s) { - super(s); - } - - public ProtocolCreationException(String s, Throwable throwable) { - super(s, throwable); - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/ProtocolFactory.java b/yaacc/src/main/java/org/fourthline/cling/protocol/ProtocolFactory.java deleted file mode 100644 index 22f81d95..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/ProtocolFactory.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.protocol; - -import org.fourthline.cling.UpnpService; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.LocalDevice; -import org.fourthline.cling.model.gena.LocalGENASubscription; -import org.fourthline.cling.model.gena.RemoteGENASubscription; -import org.fourthline.cling.model.message.IncomingDatagramMessage; -import org.fourthline.cling.model.message.StreamRequestMessage; -import org.fourthline.cling.model.message.header.UpnpHeader; -import org.fourthline.cling.protocol.async.SendingNotificationAlive; -import org.fourthline.cling.protocol.async.SendingNotificationByebye; -import org.fourthline.cling.protocol.async.SendingSearch; -import org.fourthline.cling.protocol.sync.SendingAction; -import org.fourthline.cling.protocol.sync.SendingEvent; -import org.fourthline.cling.protocol.sync.SendingRenewal; -import org.fourthline.cling.protocol.sync.SendingSubscribe; -import org.fourthline.cling.protocol.sync.SendingUnsubscribe; - -import java.net.URL; - -/** - * Factory for UPnP protocols, the core implementation of the UPnP specification. - *

- * This factory creates an executable protocol either based on the received UPnP messsage, or - * on local device/search/service metadata). A protocol is an aspect of the UPnP specification, - * you can override individual protocols to customize the behavior of the UPnP stack. - *

- *

- * An implementation has to be thread-safe. - *

- * - * @author Christian Bauer - */ -public interface ProtocolFactory { - - public UpnpService getUpnpService(); - - /** - * Creates a {@link org.fourthline.cling.protocol.async.ReceivingNotification}, - * {@link org.fourthline.cling.protocol.async.ReceivingSearch}, - * or {@link org.fourthline.cling.protocol.async.ReceivingSearchResponse} protocol. - * - * @param message The incoming message, either {@link org.fourthline.cling.model.message.UpnpRequest} or - * {@link org.fourthline.cling.model.message.UpnpResponse}. - * @return The appropriate protocol that handles the messages or null if the message should be dropped. - * @throws ProtocolCreationException If no protocol could be found for the message. - */ - public ReceivingAsync createReceivingAsync(IncomingDatagramMessage message) throws ProtocolCreationException; - - /** - * Creates a {@link org.fourthline.cling.protocol.sync.ReceivingRetrieval}, - * {@link org.fourthline.cling.protocol.sync.ReceivingAction}, - * {@link org.fourthline.cling.protocol.sync.ReceivingSubscribe}, - * {@link org.fourthline.cling.protocol.sync.ReceivingUnsubscribe}, or - * {@link org.fourthline.cling.protocol.sync.ReceivingEvent} protocol. - * - * @param requestMessage The incoming message, examime {@link org.fourthline.cling.model.message.UpnpRequest.Method} - * to determine the protocol. - * @return The appropriate protocol that handles the messages. - * @throws ProtocolCreationException If no protocol could be found for the message. - */ - public ReceivingSync createReceivingSync(StreamRequestMessage requestMessage) throws ProtocolCreationException; - - /** - * Called by the {@link org.fourthline.cling.registry.Registry}, creates a protocol for announcing local devices. - */ - public SendingNotificationAlive createSendingNotificationAlive(LocalDevice localDevice); - - /** - * Called by the {@link org.fourthline.cling.registry.Registry}, creates a protocol for announcing local devices. - */ - public SendingNotificationByebye createSendingNotificationByebye(LocalDevice localDevice); - - /** - * Called by the {@link org.fourthline.cling.controlpoint.ControlPoint}, creates a protocol for a multicast search. - */ - public SendingSearch createSendingSearch(UpnpHeader searchTarget, int mxSeconds); - - /** - * Called by the {@link org.fourthline.cling.controlpoint.ControlPoint}, creates a protocol for executing an action. - */ - public SendingAction createSendingAction(ActionInvocation actionInvocation, URL controlURL); - - /** - * Called by the {@link org.fourthline.cling.controlpoint.ControlPoint}, creates a protocol for GENA subscription. - */ - public SendingSubscribe createSendingSubscribe(RemoteGENASubscription subscription) throws ProtocolCreationException; - - /** - * Called by the {@link org.fourthline.cling.controlpoint.ControlPoint}, creates a protocol for GENA renewal. - */ - public SendingRenewal createSendingRenewal(RemoteGENASubscription subscription); - - /** - * Called by the {@link org.fourthline.cling.controlpoint.ControlPoint}, creates a protocol for GENA unsubscription. - */ - public SendingUnsubscribe createSendingUnsubscribe(RemoteGENASubscription subscription); - - /** - * Called by the {@link org.fourthline.cling.model.gena.GENASubscription}, creates a protocol for sending GENA events. - */ - public SendingEvent createSendingEvent(LocalGENASubscription subscription); -} diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/async/ReceivingSearchResponse.java b/yaacc/src/main/java/org/fourthline/cling/protocol/async/ReceivingSearchResponse.java deleted file mode 100644 index 5644eee1..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/async/ReceivingSearchResponse.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.protocol.async; - -import android.util.Log; - -import org.fourthline.cling.UpnpService; -import org.fourthline.cling.model.ValidationError; -import org.fourthline.cling.model.ValidationException; -import org.fourthline.cling.model.message.IncomingDatagramMessage; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.model.message.discovery.IncomingSearchResponse; -import org.fourthline.cling.model.meta.RemoteDevice; -import org.fourthline.cling.model.meta.RemoteDeviceIdentity; -import org.fourthline.cling.model.types.UDN; -import org.fourthline.cling.protocol.ReceivingAsync; -import org.fourthline.cling.protocol.RetrieveRemoteDescriptors; -import org.fourthline.cling.transport.RouterException; - -/** - * Handles reception of search response messages. - *

- * This protocol implementation is basically the same as - * the {@link org.fourthline.cling.protocol.async.ReceivingNotification} protocol for - * an ALIVE message. - *

- * - * @author Christian Bauer - */ -public class ReceivingSearchResponse extends ReceivingAsync { - - - public ReceivingSearchResponse(UpnpService upnpService, IncomingDatagramMessage inputMessage) { - super(upnpService, new IncomingSearchResponse(inputMessage)); - } - - protected void execute() throws RouterException { - - if (!getInputMessage().isSearchResponseMessage()) { - Log.v(getClass().getName(), "Ignoring invalid search response message: " + getInputMessage()); - return; - } - - UDN udn = getInputMessage().getRootDeviceUDN(); - if (udn == null) { - Log.v(getClass().getName(), "Ignoring search response message without UDN: " + getInputMessage()); - return; - } - - RemoteDeviceIdentity rdIdentity = new RemoteDeviceIdentity(getInputMessage()); - Log.v(getClass().getName(), "Received device search response: " + rdIdentity); - - if (getUpnpService().getRegistry().update(rdIdentity)) { - Log.v(getClass().getName(), "Remote device was already known: " + udn); - return; - } - - RemoteDevice rd; - try { - rd = new RemoteDevice(rdIdentity); - } catch (ValidationException ex) { - Log.w(getClass().getName(), "Validation errors of device during discovery: " + rdIdentity); - for (ValidationError validationError : ex.getErrors()) { - Log.w(getClass().getName(), validationError.toString()); - } - return; - } - - if (rdIdentity.getDescriptorURL() == null) { - Log.v(getClass().getName(), "Ignoring message without location URL header: " + getInputMessage()); - return; - } - - if (rdIdentity.getMaxAgeSeconds() == null) { - Log.v(getClass().getName(), "Ignoring message without max-age header: " + getInputMessage()); - return; - } - - // Unfortunately, we always have to retrieve the descriptor because at this point we - // have no idea if it's a root or embedded device - getUpnpService().getConfiguration().getAsyncProtocolExecutor().execute( - new RetrieveRemoteDescriptors(getUpnpService(), rd) - ); - - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingNotificationAlive.java b/yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingNotificationAlive.java deleted file mode 100644 index 321e7a26..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/protocol/async/SendingNotificationAlive.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.protocol.async; - -import android.util.Log; - -import org.fourthline.cling.UpnpService; -import org.fourthline.cling.model.meta.LocalDevice; -import org.fourthline.cling.model.types.NotificationSubtype; -import org.fourthline.cling.transport.RouterException; - -/** - * Sending ALIVE notification messages for a registered local device. - * - * @author Christian Bauer - */ -public class SendingNotificationAlive extends SendingNotification { - - - public SendingNotificationAlive(UpnpService upnpService, LocalDevice device) { - super(upnpService, device); - } - - @Override - protected void execute() throws RouterException { - Log.v(getClass().getName(), "Sending alive messages (" + getBulkRepeat() + " times) for: " + getDevice()); - super.execute(); - } - - protected NotificationSubtype getNotificationSubtype() { - return NotificationSubtype.ALIVE; - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/DefaultRegistryListener.java b/yaacc/src/main/java/org/fourthline/cling/registry/DefaultRegistryListener.java deleted file mode 100644 index f05f15dd..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/registry/DefaultRegistryListener.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.registry; - -import org.fourthline.cling.model.meta.Device; -import org.fourthline.cling.model.meta.LocalDevice; -import org.fourthline.cling.model.meta.RemoteDevice; - -/** - * Convenience class, provides empty implementations of all methods. - *

- * Also unifies local and remote device additions and removals with - * {@link #deviceAdded(Registry, org.fourthline.cling.model.meta.Device)} and - * {@link #deviceRemoved(Registry, org.fourthline.cling.model.meta.Device)} methods. - *

- * - * @author Christian Bauer - */ -public class DefaultRegistryListener implements RegistryListener { - - public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device) { - - } - - public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex) { - - } - - /** - * Calls the {@link #deviceAdded(Registry, org.fourthline.cling.model.meta.Device)} method. - * - * @param registry The Cling registry of all devices and services know to the local UPnP stack. - * @param device A validated and hydrated device metadata graph, with complete service metadata. - */ - public void remoteDeviceAdded(Registry registry, RemoteDevice device) { - deviceAdded(registry, device); - } - - public void remoteDeviceUpdated(Registry registry, RemoteDevice device) { - - } - - /** - * Calls the {@link #deviceRemoved(Registry, org.fourthline.cling.model.meta.Device)} method. - * - * @param registry The Cling registry of all devices and services know to the local UPnP stack. - * @param device A validated and hydrated device metadata graph, with complete service metadata. - */ - public void remoteDeviceRemoved(Registry registry, RemoteDevice device) { - deviceRemoved(registry, device); - } - - /** - * Calls the {@link #deviceAdded(Registry, org.fourthline.cling.model.meta.Device)} method. - * - * @param registry The Cling registry of all devices and services know to the local UPnP stack. - * @param device The local device added to the {@link org.fourthline.cling.registry.Registry}. - */ - public void localDeviceAdded(Registry registry, LocalDevice device) { - deviceAdded(registry, device); - } - - /** - * Calls the {@link #deviceRemoved(Registry, org.fourthline.cling.model.meta.Device)} method. - * - * @param registry The Cling registry of all devices and services know to the local UPnP stack. - * @param device The local device removed from the {@link org.fourthline.cling.registry.Registry}. - */ - public void localDeviceRemoved(Registry registry, LocalDevice device) { - deviceRemoved(registry, device); - } - - public void deviceAdded(Registry registry, Device device) { - - } - - public void deviceRemoved(Registry registry, Device device) { - - } - - public void beforeShutdown(Registry registry) { - - } - - public void afterShutdown() { - - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/RegistryListener.java b/yaacc/src/main/java/org/fourthline/cling/registry/RegistryListener.java deleted file mode 100644 index f6d37b0e..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/registry/RegistryListener.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.registry; - -import org.fourthline.cling.model.meta.LocalDevice; -import org.fourthline.cling.model.meta.RemoteDevice; - -/** - * Notification of discovered device additions, removals, updates. - *

- * Add an instance of this interface to the registry to be notified when a device is - * discovered on your UPnP network, or when it is updated, or when it disappears. - *

- *

- * Implementations will be called concurrently by several threads, they should be thread-safe. - *

- *

- * Listener methods are called in a separate thread, so you can execute - * expensive procedures without spawning a new thread. The {@link #beforeShutdown(Registry)} - * and {@link #afterShutdown()} methods are however called in the thread that is stopping - * the registry and should not be blocking, unless you want to delay the shutdown procedure. - *

- * - * @author Christian Bauer - */ -public interface RegistryListener { - - /** - * Called as soon as possible after a device has been discovered. - *

- * This method will be called after SSDP notification datagrams of a new alive - * UPnP device have been received and processed. The announced device XML descriptor - * will be retrieved and parsed. The given {@link org.fourthline.cling.model.meta.RemoteDevice} metadata - * is validated and partial {@link org.fourthline.cling.model.meta.Service} metadata is available. The - * services are unhydrated, they have no actions or state variable metadata because the - * service descriptors of the device model have not been retrieved at this point. - *

- *

- * You typically do not use this method on a regular machine, this is an optimization - * for slower UPnP hosts (such as Android handsets). - *

- * - * @param registry The Cling registry of all devices and services know to the local UPnP stack. - * @param device A validated and hydrated device metadata graph, with anemic service metadata. - */ - public void remoteDeviceDiscoveryStarted(Registry registry, RemoteDevice device); - - /** - * Called when service metadata couldn't be initialized. - *

- * If you override the {@link #remoteDeviceDiscoveryStarted(Registry, org.fourthline.cling.model.meta.RemoteDevice)} - * method, you might want to override this method as well. - *

- * - * @param registry The Cling registry of all devices and services know to the local UPnP stack. - * @param device A validated and hydrated device metadata graph, with anemic service metadata. - * @param ex The reason why service metadata could not be initialized, or null if service - * descriptors couldn't be retrieved at all. - */ - public void remoteDeviceDiscoveryFailed(Registry registry, RemoteDevice device, Exception ex); - - /** - * Called when complete metadata of a newly discovered device is available. - * - * @param registry The Cling registry of all devices and services know to the local UPnP stack. - * @param device A validated and hydrated device metadata graph, with complete service metadata. - */ - public void remoteDeviceAdded(Registry registry, RemoteDevice device); - - /** - * Called when a discovered device's expiration timestamp is updated. - *

- * This is a signal that a device is still alive and you typically don't have to react to this - * event. You will be notified when a device disappears through timeout. - *

- * - * @param registry The Cling registry of all devices and services know to the local UPnP stack. - * @param device A validated and hydrated device metadata graph, with complete service metadata. - */ - public void remoteDeviceUpdated(Registry registry, RemoteDevice device); - - /** - * Called when a previously discovered device disappears. - *

- * This method will also be called when a discovered device did not update its expiration timeout - * and has been been removed automatically by the local registry. This method will not be called - * when the UPnP stack is shutting down. - *

- * - * @param registry The Cling registry of all devices and services know to the local UPnP stack. - * @param device A validated and hydrated device metadata graph, with complete service metadata. - */ - public void remoteDeviceRemoved(Registry registry, RemoteDevice device); - - /** - * Called after you add your own device to the {@link org.fourthline.cling.registry.Registry}. - * - * @param registry The Cling registry of all devices and services know to the local UPnP stack. - * @param device The local device added to the {@link org.fourthline.cling.registry.Registry}. - */ - public void localDeviceAdded(Registry registry, LocalDevice device); - - /** - * Called after you remove your own device from the {@link org.fourthline.cling.registry.Registry}. - *

- * This method will not be called when the UPnP stack is shutting down. - *

- * @param registry The Cling registry of all devices and services know to the local UPnP stack. - * @param device The local device removed from the {@link org.fourthline.cling.registry.Registry}. - */ - public void localDeviceRemoved(Registry registry, LocalDevice device); - - /** - * Called after registry maintenance stops but before the registry is cleared. - *

- * This method should typically not block, it executes in the thread that shuts down the UPnP stack. - *

- * @param registry The Cling registry of all devices and services know to the local UPnP stack. - */ - public void beforeShutdown(Registry registry); - - /** - * Called after the registry has been cleared on shutdown. - *

- * This method should typically not block, it executes in the thread that shuts down the UPnP stack. - *

- */ - public void afterShutdown(); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/event/After.java b/yaacc/src/main/java/org/fourthline/cling/registry/event/After.java deleted file mode 100644 index 43448908..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/registry/event/After.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.registry.event; - -import jakarta.inject.Qualifier; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * @author Christian Bauer - */ -@Qualifier -@Target({FIELD, PARAMETER}) -@Retention(RUNTIME) -public @interface After { -} diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/event/Before.java b/yaacc/src/main/java/org/fourthline/cling/registry/event/Before.java deleted file mode 100644 index 3c6950a5..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/registry/event/Before.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.registry.event; - -import jakarta.inject.Qualifier; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * @author Christian Bauer - */ -@Qualifier -@Target({FIELD, PARAMETER}) -@Retention(RUNTIME) -public @interface Before { -} diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/event/DeviceDiscovery.java b/yaacc/src/main/java/org/fourthline/cling/registry/event/DeviceDiscovery.java deleted file mode 100644 index d80f8293..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/registry/event/DeviceDiscovery.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.registry.event; - -import org.fourthline.cling.model.meta.Device; - -/** - * An observable event for CDI containers. - * - * @author Christian Bauer - */ -public class DeviceDiscovery { - - protected D device; - - public DeviceDiscovery(D device) { - this.device = device; - } - - public D getDevice() { - return device; - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/event/FailedRemoteDeviceDiscovery.java b/yaacc/src/main/java/org/fourthline/cling/registry/event/FailedRemoteDeviceDiscovery.java deleted file mode 100644 index 0e35de94..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/registry/event/FailedRemoteDeviceDiscovery.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.registry.event; - -import org.fourthline.cling.model.meta.RemoteDevice; - -/** - * @author Christian Bauer - */ -public class FailedRemoteDeviceDiscovery extends DeviceDiscovery { - - protected Exception exception; - - public FailedRemoteDeviceDiscovery(RemoteDevice device, Exception ex) { - super(device); - this.exception = ex; - } - - public Exception getException() { - return exception; - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/event/LocalDeviceDiscovery.java b/yaacc/src/main/java/org/fourthline/cling/registry/event/LocalDeviceDiscovery.java deleted file mode 100644 index 54cc895b..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/registry/event/LocalDeviceDiscovery.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.registry.event; - -import org.fourthline.cling.model.meta.LocalDevice; - -/** - * @author Christian Bauer - */ -public class LocalDeviceDiscovery extends DeviceDiscovery { - - public LocalDeviceDiscovery(LocalDevice device) { - super(device); - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/event/Phase.java b/yaacc/src/main/java/org/fourthline/cling/registry/event/Phase.java deleted file mode 100644 index 9f0bfeb4..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/registry/event/Phase.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.registry.event; - -import jakarta.enterprise.util.AnnotationLiteral; -import jakarta.inject.Qualifier; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.FIELD; -import static java.lang.annotation.ElementType.PARAMETER; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * @author Christian Bauer - */ - -public interface Phase { - - public static AnnotationLiteral ALIVE = new AnnotationLiteral() { - }; - - public static AnnotationLiteral COMPLETE = new AnnotationLiteral() { - }; - - public static AnnotationLiteral BYEBYE = new AnnotationLiteral() { - }; - - public static AnnotationLiteral UPDATED = new AnnotationLiteral() { - }; - - - @Qualifier - @Target({FIELD, PARAMETER}) - @Retention(RUNTIME) - public @interface Alive { - - } - - @Qualifier - @Target({FIELD, PARAMETER}) - @Retention(RUNTIME) - public @interface Complete { - - } - - @Qualifier - @Target({FIELD, PARAMETER}) - @Retention(RUNTIME) - public @interface Byebye { - - } - - @Qualifier - @Target({FIELD, PARAMETER}) - @Retention(RUNTIME) - public @interface Updated { - - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/event/RegistryShutdown.java b/yaacc/src/main/java/org/fourthline/cling/registry/event/RegistryShutdown.java deleted file mode 100644 index 92bf3bed..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/registry/event/RegistryShutdown.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.registry.event; - -/** - * @author Christian Bauer - */ -public class RegistryShutdown { -} diff --git a/yaacc/src/main/java/org/fourthline/cling/registry/event/RemoteDeviceDiscovery.java b/yaacc/src/main/java/org/fourthline/cling/registry/event/RemoteDeviceDiscovery.java deleted file mode 100644 index 491205d8..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/registry/event/RemoteDeviceDiscovery.java +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.registry.event; - -import org.fourthline.cling.model.meta.RemoteDevice; - -/** - * @author Christian Bauer - */ -public class RemoteDeviceDiscovery extends DeviceDiscovery { - - public RemoteDeviceDiscovery(RemoteDevice device) { - super(device); - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetDeviceCapabilities.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetDeviceCapabilities.java deleted file mode 100644 index b01ae4e7..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetDeviceCapabilities.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.avtransport.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.support.model.DeviceCapabilities; - -import java.util.logging.Logger; - -/** - * - * @author Christian Bauer - */ -public abstract class GetDeviceCapabilities extends ActionCallback { - - private static Logger log = Logger.getLogger(GetDeviceCapabilities.class.getName()); - - public GetDeviceCapabilities(Service service) { - this(new UnsignedIntegerFourBytes(0), service); - } - - public GetDeviceCapabilities(UnsignedIntegerFourBytes instanceId, Service service) { - super(new ActionInvocation(service.getAction("GetDeviceCapabilities"))); - getActionInvocation().setInput("InstanceID", instanceId); - } - - public void success(ActionInvocation invocation) { - DeviceCapabilities caps = new DeviceCapabilities(invocation.getOutputMap()); - received(invocation, caps); - } - - public abstract void received(ActionInvocation actionInvocation, DeviceCapabilities caps); - -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetPositionInfo.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetPositionInfo.java deleted file mode 100644 index a25e7846..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetPositionInfo.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.avtransport.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.support.model.PositionInfo; - -import java.util.logging.Logger; - -/** - * - * @author Christian Bauer - */ -public abstract class GetPositionInfo extends ActionCallback { - - private static Logger log = Logger.getLogger(GetPositionInfo.class.getName()); - - public GetPositionInfo(Service service) { - this(new UnsignedIntegerFourBytes(0), service); - } - - public GetPositionInfo(UnsignedIntegerFourBytes instanceId, Service service) { - super(new ActionInvocation(service.getAction("GetPositionInfo"))); - getActionInvocation().setInput("InstanceID", instanceId); - } - - public void success(ActionInvocation invocation) { - PositionInfo positionInfo = new PositionInfo(invocation.getOutputMap()); - received(invocation, positionInfo); - } - - public abstract void received(ActionInvocation invocation, PositionInfo positionInfo); - -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetTransportInfo.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetTransportInfo.java deleted file mode 100644 index 1194580c..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/GetTransportInfo.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.avtransport.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.support.model.TransportInfo; - -import java.util.logging.Logger; - -/** - * - * @author Christian Bauer - */ -public abstract class GetTransportInfo extends ActionCallback { - - private static Logger log = Logger.getLogger(GetTransportInfo.class.getName()); - - public GetTransportInfo(Service service) { - this(new UnsignedIntegerFourBytes(0), service); - } - - public GetTransportInfo(UnsignedIntegerFourBytes instanceId, Service service) { - super(new ActionInvocation(service.getAction("GetTransportInfo"))); - getActionInvocation().setInput("InstanceID", instanceId); - } - - public void success(ActionInvocation invocation) { - TransportInfo transportInfo = new TransportInfo(invocation.getOutputMap()); - received(invocation, transportInfo); - } - - public abstract void received(ActionInvocation invocation, TransportInfo transportInfo); - -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Next.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Next.java deleted file mode 100644 index 7ccf0f74..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Next.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.avtransport.callback; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; - -/** - * @author Christian Bauer - */ -public abstract class Next extends ActionCallback { - - - protected Next(ActionInvocation actionInvocation, ControlPoint controlPoint) { - super(actionInvocation, controlPoint); - } - - protected Next(ActionInvocation actionInvocation) { - super(actionInvocation); - } - - public Next(Service service) { - this(new UnsignedIntegerFourBytes(0), service); - } - - public Next(UnsignedIntegerFourBytes instanceId, Service service) { - super(new ActionInvocation(service.getAction("Next"))); - getActionInvocation().setInput("InstanceID", instanceId); - } - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Execution successful"); - } -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Pause.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Pause.java deleted file mode 100644 index c645f8ca..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Pause.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.avtransport.callback; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; - -/** - * @author Christian Bauer - */ -public abstract class Pause extends ActionCallback { - - - protected Pause(ActionInvocation actionInvocation, ControlPoint controlPoint) { - super(actionInvocation, controlPoint); - } - - protected Pause(ActionInvocation actionInvocation) { - super(actionInvocation); - } - - public Pause(Service service) { - this(new UnsignedIntegerFourBytes(0), service); - } - - public Pause(UnsignedIntegerFourBytes instanceId, Service service) { - super(new ActionInvocation(service.getAction("Pause"))); - getActionInvocation().setInput("InstanceID", instanceId); - } - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Execution successful"); - } -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Play.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Play.java deleted file mode 100644 index b3549508..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Play.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.avtransport.callback; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; - -/** - * @author Christian Bauer - */ -public abstract class Play extends ActionCallback { - - - public Play(Service service) { - this(new UnsignedIntegerFourBytes(0), service, "1"); - } - - public Play(Service service, String speed) { - this(new UnsignedIntegerFourBytes(0), service, speed); - } - - public Play(UnsignedIntegerFourBytes instanceId, Service service) { - this(instanceId, service, "1"); - } - - public Play(UnsignedIntegerFourBytes instanceId, Service service, String speed) { - super(new ActionInvocation(service.getAction("Play"))); - getActionInvocation().setInput("InstanceID", instanceId); - getActionInvocation().setInput("Speed", speed); - } - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Execution successful"); - } -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Previous.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Previous.java deleted file mode 100644 index 74a4e5e9..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Previous.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.avtransport.callback; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; - -/** - * @author Christian Bauer - */ -public abstract class Previous extends ActionCallback { - - - protected Previous(ActionInvocation actionInvocation, ControlPoint controlPoint) { - super(actionInvocation, controlPoint); - } - - protected Previous(ActionInvocation actionInvocation) { - super(actionInvocation); - } - - public Previous(Service service) { - this(new UnsignedIntegerFourBytes(0), service); - } - - public Previous(UnsignedIntegerFourBytes instanceId, Service service) { - super(new ActionInvocation(service.getAction("Previous"))); - getActionInvocation().setInput("InstanceID", instanceId); - } - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Execution successful"); - } -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Seek.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Seek.java deleted file mode 100644 index 17bac477..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Seek.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.avtransport.callback; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.support.model.SeekMode; - -/** - * @author Christian Bauer - */ -public abstract class Seek extends ActionCallback { - - - public Seek(Service service, String relativeTimeTarget) { - this(new UnsignedIntegerFourBytes(0), service, SeekMode.REL_TIME, relativeTimeTarget); - } - - public Seek(UnsignedIntegerFourBytes instanceId, Service service, String relativeTimeTarget) { - this(instanceId, service, SeekMode.REL_TIME, relativeTimeTarget); - } - - public Seek(Service service, SeekMode mode, String target) { - this(new UnsignedIntegerFourBytes(0), service, mode, target); - } - - public Seek(UnsignedIntegerFourBytes instanceId, Service service, SeekMode mode, String target) { - super(new ActionInvocation(service.getAction("Seek"))); - getActionInvocation().setInput("InstanceID", instanceId); - getActionInvocation().setInput("Unit", mode.name()); - getActionInvocation().setInput("Target", target); - } - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Execution successful"); - } -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/SetAVTransportURI.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/SetAVTransportURI.java deleted file mode 100644 index 84a8f82e..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/SetAVTransportURI.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.avtransport.callback; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; - -/** - * @author Christian Bauer - */ -public abstract class SetAVTransportURI extends ActionCallback { - - - public SetAVTransportURI(Service service, String uri) { - this(new UnsignedIntegerFourBytes(0), service, uri, null); - } - - public SetAVTransportURI(Service service, String uri, String metadata) { - this(new UnsignedIntegerFourBytes(0), service, uri, metadata); - } - - public SetAVTransportURI(UnsignedIntegerFourBytes instanceId, Service service, String uri) { - this(instanceId, service, uri, null); - } - - public SetAVTransportURI(UnsignedIntegerFourBytes instanceId, Service service, String uri, String metadata) { - super(new ActionInvocation(service.getAction("SetAVTransportURI"))); - Log.v(getClass().getName(), "Creating SetAVTransportURI action for URI: " + uri); - getActionInvocation().setInput("InstanceID", instanceId); - getActionInvocation().setInput("CurrentURI", uri); - getActionInvocation().setInput("CurrentURIMetaData", metadata); - } - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Execution successful"); - } -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/SetPlayMode.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/SetPlayMode.java deleted file mode 100644 index 1af9801c..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/SetPlayMode.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.avtransport.callback; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.support.model.PlayMode; - -/** - * @author Christian Bauer - */ -public abstract class SetPlayMode extends ActionCallback { - - - public SetPlayMode(Service service, PlayMode playMode) { - this(new UnsignedIntegerFourBytes(0), service, playMode); - } - - public SetPlayMode(UnsignedIntegerFourBytes instanceId, Service service, PlayMode playMode) { - super(new ActionInvocation(service.getAction("SetPlayMode"))); - getActionInvocation().setInput("InstanceID", instanceId); - getActionInvocation().setInput("NewPlayMode", playMode.toString()); - } - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Execution successful"); - } -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Stop.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Stop.java deleted file mode 100644 index 599d0dde..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/callback/Stop.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.avtransport.callback; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; - -/** - * @author Christian Bauer - */ -public abstract class Stop extends ActionCallback { - - - public Stop(Service service) { - this(new UnsignedIntegerFourBytes(0), service); - } - - public Stop(UnsignedIntegerFourBytes instanceId, Service service) { - super(new ActionInvocation(service.getAction("Stop"))); - getActionInvocation().setInput("InstanceID", instanceId); - } - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Execution successful"); - } -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/AVTransportService.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/AVTransportService.java index 326fcabe..bea05610 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/AVTransportService.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/AVTransportService.java @@ -15,7 +15,7 @@ package org.fourthline.cling.support.avtransport.impl; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.types.ErrorCode; import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; @@ -290,13 +290,13 @@ protected AVTransportStateMachine findStateMachine(UnsignedIntegerFourBytes inst long id = instanceId.getValue(); AVTransportStateMachine stateMachine = stateMachines.get(id); if (stateMachine == null && id == 0 && createDefaultTransport) { - Log.v(getClass().getName(), "Creating default transport instance with ID '0'"); + YaaccLogger.v(getClass().getName(), "Creating default transport instance with ID '0'"); stateMachine = createStateMachine(instanceId); stateMachines.put(id, stateMachine); } else if (stateMachine == null) { throw new AVTransportException(AVTransportErrorCode.INVALID_INSTANCE_ID); } - Log.v(getClass().getName(), "Found transport control with ID '" + id + "'"); + YaaccLogger.v(getClass().getName(), "Found transport control with ID '" + id + "'"); return stateMachine; } } diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/NoMediaPresent.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/NoMediaPresent.java index 1c8ecdb7..d197727a 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/NoMediaPresent.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/NoMediaPresent.java @@ -15,7 +15,7 @@ package org.fourthline.cling.support.avtransport.impl.state; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.avtransport.lastchange.AVTransportVariable; import org.fourthline.cling.support.model.AVTransport; @@ -36,7 +36,7 @@ public NoMediaPresent(T transport) { } public void onEntry() { - Log.v(getClass().getName(), "Setting transport state to NO_MEDIA_PRESENT"); + YaaccLogger.v(getClass().getName(), "Setting transport state to NO_MEDIA_PRESENT"); getTransport().setTransportInfo( new TransportInfo( TransportState.NO_MEDIA_PRESENT, diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/PausedPlay.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/PausedPlay.java index 15257838..d00b7983 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/PausedPlay.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/PausedPlay.java @@ -15,7 +15,7 @@ package org.fourthline.cling.support.avtransport.impl.state; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.avtransport.lastchange.AVTransportVariable; import org.fourthline.cling.support.model.AVTransport; @@ -36,7 +36,7 @@ public PausedPlay(T transport) { } public void onEntry() { - Log.v(getClass().getName(), "Setting transport state to PAUSED_PLAYBACK"); + YaaccLogger.v(getClass().getName(), "Setting transport state to PAUSED_PLAYBACK"); getTransport().setTransportInfo( new TransportInfo( TransportState.PAUSED_PLAYBACK, diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/Playing.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/Playing.java index e35d8409..20df1fdb 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/Playing.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/Playing.java @@ -15,7 +15,7 @@ package org.fourthline.cling.support.avtransport.impl.state; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.avtransport.lastchange.AVTransportVariable; import org.fourthline.cling.support.model.AVTransport; @@ -37,7 +37,7 @@ public Playing(T transport) { } public void onEntry() { - Log.v(getClass().getName(), "Setting transport state to PLAYING"); + YaaccLogger.v(getClass().getName(), "Setting transport state to PLAYING"); getTransport().setTransportInfo( new TransportInfo( TransportState.PLAYING, diff --git a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/Stopped.java b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/Stopped.java index 9c54b576..ca4dab31 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/Stopped.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/avtransport/impl/state/Stopped.java @@ -15,7 +15,7 @@ package org.fourthline.cling.support.avtransport.impl.state; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.support.avtransport.lastchange.AVTransportVariable; import org.fourthline.cling.support.model.AVTransport; @@ -37,7 +37,7 @@ public Stopped(T transport) { } public void onEntry() { - Log.v(getClass().getName(), "Setting transport state to STOPPED"); + YaaccLogger.v(getClass().getName(), "Setting transport state to STOPPED"); getTransport().setTransportInfo( new TransportInfo( TransportState.STOPPED, diff --git a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/AbstractPeeringConnectionManagerService.java b/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/AbstractPeeringConnectionManagerService.java deleted file mode 100644 index cc6573b4..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/AbstractPeeringConnectionManagerService.java +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.connectionmanager; - -import android.util.Log; - -import org.fourthline.cling.binding.annotations.UpnpAction; -import org.fourthline.cling.binding.annotations.UpnpInputArgument; -import org.fourthline.cling.binding.annotations.UpnpOutputArgument; -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.ServiceReference; -import org.fourthline.cling.model.action.ActionException; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.ErrorCode; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.model.types.csv.CSV; -import org.fourthline.cling.support.connectionmanager.callback.ConnectionComplete; -import org.fourthline.cling.support.connectionmanager.callback.PrepareForConnection; -import org.fourthline.cling.support.model.ConnectionInfo; -import org.fourthline.cling.support.model.ProtocolInfo; -import org.fourthline.cling.support.model.ProtocolInfos; - -import java.beans.PropertyChangeSupport; - -/** - * Support for setup and teardown of an arbitrary number of connections with a manager peer. - * - * @author Christian Bauer - * @author Alessio Gaeta - */ -public abstract class AbstractPeeringConnectionManagerService extends ConnectionManagerService { - - - protected AbstractPeeringConnectionManagerService(ConnectionInfo... activeConnections) { - super(activeConnections); - } - - protected AbstractPeeringConnectionManagerService(ProtocolInfos sourceProtocolInfo, ProtocolInfos sinkProtocolInfo, - ConnectionInfo... activeConnections) { - super(sourceProtocolInfo, sinkProtocolInfo, activeConnections); - } - - protected AbstractPeeringConnectionManagerService(PropertyChangeSupport propertyChangeSupport, - ProtocolInfos sourceProtocolInfo, ProtocolInfos sinkProtocolInfo, - ConnectionInfo... activeConnections) { - super(propertyChangeSupport, sourceProtocolInfo, sinkProtocolInfo, activeConnections); - } - - synchronized protected int getNewConnectionId() { - int currentHighestID = -1; - for (Integer key : activeConnections.keySet()) { - if (key > currentHighestID) currentHighestID = key; - } - return ++currentHighestID; - } - - synchronized protected void storeConnection(ConnectionInfo info) { - CSV oldConnectionIDs = getCurrentConnectionIDs(); - activeConnections.put(info.getConnectionID(), info); - Log.v(getClass().getName(), "Connection stored, firing event: " + info.getConnectionID()); - CSV newConnectionIDs = getCurrentConnectionIDs(); - getPropertyChangeSupport().firePropertyChange("CurrentConnectionIDs", oldConnectionIDs, newConnectionIDs); - } - - synchronized protected void removeConnection(int connectionID) { - CSV oldConnectionIDs = getCurrentConnectionIDs(); - activeConnections.remove(connectionID); - Log.v(getClass().getName(), "Connection removed, firing event: " + connectionID); - CSV newConnectionIDs = getCurrentConnectionIDs(); - getPropertyChangeSupport().firePropertyChange("CurrentConnectionIDs", oldConnectionIDs, newConnectionIDs); - } - - @UpnpAction(out = { - @UpnpOutputArgument(name = "ConnectionID", stateVariable = "A_ARG_TYPE_ConnectionID", getterName = "getConnectionID"), - @UpnpOutputArgument(name = "AVTransportID", stateVariable = "A_ARG_TYPE_AVTransportID", getterName = "getAvTransportID"), - @UpnpOutputArgument(name = "RcsID", stateVariable = "A_ARG_TYPE_RcsID", getterName = "getRcsID") - }) - synchronized public ConnectionInfo prepareForConnection( - @UpnpInputArgument(name = "RemoteProtocolInfo", stateVariable = "A_ARG_TYPE_ProtocolInfo") ProtocolInfo remoteProtocolInfo, - @UpnpInputArgument(name = "PeerConnectionManager", stateVariable = "A_ARG_TYPE_ConnectionManager") ServiceReference peerConnectionManager, - @UpnpInputArgument(name = "PeerConnectionID", stateVariable = "A_ARG_TYPE_ConnectionID") int peerConnectionId, - @UpnpInputArgument(name = "Direction", stateVariable = "A_ARG_TYPE_Direction") String direction) - throws ActionException { - - int connectionId = getNewConnectionId(); - - ConnectionInfo.Direction dir; - try { - dir = ConnectionInfo.Direction.valueOf(direction); - } catch (Exception ex) { - throw new ConnectionManagerException(ErrorCode.ARGUMENT_VALUE_INVALID, "Unsupported direction: " + direction); - } - - Log.v(getClass().getName(), "Preparing for connection with local new ID " + connectionId + " and peer connection ID: " + peerConnectionId); - - ConnectionInfo newConnectionInfo = createConnection( - connectionId, - peerConnectionId, - peerConnectionManager, - dir, - remoteProtocolInfo - ); - - storeConnection(newConnectionInfo); - - return newConnectionInfo; - } - - @UpnpAction - synchronized public void connectionComplete(@UpnpInputArgument(name = "ConnectionID", stateVariable = "A_ARG_TYPE_ConnectionID") int connectionID) - throws ActionException { - ConnectionInfo info = getCurrentConnectionInfo(connectionID); - Log.v(getClass().getName(), "Closing connection ID " + connectionID); - closeConnection(info); - removeConnection(connectionID); - } - - /** - * Generate a new local connection identifier, prepare the peer, store connection details. - * - * @return -1 if the {@link #peerFailure(org.fourthline.cling.model.action.ActionInvocation, org.fourthline.cling.model.message.UpnpResponse, String)} - * method had to be called, otherwise the local identifier of the established connection. - */ - synchronized public int createConnectionWithPeer(final ServiceReference localServiceReference, - final ControlPoint controlPoint, - final Service peerService, - final ProtocolInfo protInfo, - final ConnectionInfo.Direction direction) { - - // It is important that you synchronize the whole procedure, starting with getNewConnectionID(), - // then preparing the connection on the peer, then storeConnection() - - final int localConnectionID = getNewConnectionId(); - - Log.v(getClass().getName(), "Creating new connection ID " + localConnectionID + " with peer: " + peerService); - final boolean[] failed = new boolean[1]; - new PrepareForConnection( - peerService, - controlPoint, - protInfo, - localServiceReference, - localConnectionID, - direction - ) { - @Override - public void received(ActionInvocation invocation, int peerConnectionID, int rcsID, int avTransportID) { - ConnectionInfo info = new ConnectionInfo( - localConnectionID, - rcsID, - avTransportID, - protInfo, - peerService.getReference(), - peerConnectionID, - direction.getOpposite(), // If I prepared you for output, then I do input - ConnectionInfo.Status.OK - ); - storeConnection(info); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - AbstractPeeringConnectionManagerService.this.peerFailure( - invocation, operation, defaultMsg - ); - failed[0] = true; - } - }.run(); // Synchronous execution! We "reserved" a new connection ID earlier! - - return failed[0] ? -1 : localConnectionID; - } - - /** - * Close the connection with the peer, remove the connection details. - */ - synchronized public void closeConnectionWithPeer(ControlPoint controlPoint, - Service peerService, - int connectionID) throws ActionException { - closeConnectionWithPeer(controlPoint, peerService, getCurrentConnectionInfo(connectionID)); - } - - /** - * Close the connection with the peer, remove the connection details. - */ - synchronized public void closeConnectionWithPeer(final ControlPoint controlPoint, - final Service peerService, - final ConnectionInfo connectionInfo) throws ActionException { - - // It is important that you synchronize the whole procedure - Log.v(getClass().getName(), "Closing connection ID " + connectionInfo.getConnectionID() + " with peer: " + peerService); - new ConnectionComplete( - peerService, - controlPoint, - connectionInfo.getPeerConnectionID() - ) { - - @Override - public void success(ActionInvocation invocation) { - removeConnection(connectionInfo.getConnectionID()); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - AbstractPeeringConnectionManagerService.this.peerFailure( - invocation, operation, defaultMsg - ); - } - }.run(); // Synchronous execution! - } - - protected abstract ConnectionInfo createConnection(int connectionID, - int peerConnectionId, ServiceReference peerConnectionManager, - ConnectionInfo.Direction direction, ProtocolInfo protocolInfo) throws ActionException; - - protected abstract void closeConnection(ConnectionInfo connectionInfo); - - /** - * Called when connection creation or closing with a peer failed. - *

- * This is the failure result of an action invocation on the peer's connection - * management service. The execution of the {@link #createConnectionWithPeer(org.fourthline.cling.model.ServiceReference, org.fourthline.cling.controlpoint.ControlPoint, org.fourthline.cling.model.meta.Service, org.fourthline.cling.support.model.ProtocolInfo, org.fourthline.cling.support.model.ConnectionInfo.Direction)} - * and {@link #closeConnectionWithPeer(org.fourthline.cling.controlpoint.ControlPoint, org.fourthline.cling.model.meta.Service, org.fourthline.cling.support.model.ConnectionInfo)} - * methods will block until this method completes handling any failure. - *

- * - * @param invocation The underlying action invocation of the remote connection manager service. - * @param operation The network message response if there was a response, or null. - * @param defaultFailureMessage A user-friendly error message generated from the invocation exception and response. - */ - protected abstract void peerFailure(ActionInvocation invocation, UpnpResponse operation, String defaultFailureMessage); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/ConnectionComplete.java b/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/ConnectionComplete.java deleted file mode 100644 index d4f812ad..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/ConnectionComplete.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.connectionmanager.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; - -/** - * @author Christian Bauer - */ -public abstract class ConnectionComplete extends ActionCallback { - - public ConnectionComplete(Service service, int connectionID) { - this(service, null, connectionID); - } - - protected ConnectionComplete(Service service, ControlPoint controlPoint, int connectionID) { - super(new ActionInvocation(service.getAction("ConnectionComplete")), controlPoint); - getActionInvocation().setInput("ConnectionID", connectionID); - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/PrepareForConnection.java b/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/PrepareForConnection.java deleted file mode 100644 index f5ae7ac9..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/connectionmanager/callback/PrepareForConnection.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.connectionmanager.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.ServiceReference; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.support.model.ConnectionInfo; -import org.fourthline.cling.support.model.ProtocolInfo; - -/** - * @author Alessio Gaeta - * @author Christian Bauer - */ -public abstract class PrepareForConnection extends ActionCallback { - - public PrepareForConnection(Service service, - ProtocolInfo remoteProtocolInfo, ServiceReference peerConnectionManager, - int peerConnectionID, ConnectionInfo.Direction direction) { - this(service, null, remoteProtocolInfo, peerConnectionManager, peerConnectionID, direction); - } - - public PrepareForConnection(Service service, ControlPoint controlPoint, - ProtocolInfo remoteProtocolInfo, ServiceReference peerConnectionManager, - int peerConnectionID, ConnectionInfo.Direction direction) { - super(new ActionInvocation(service.getAction("PrepareForConnection")), controlPoint); - - getActionInvocation().setInput("RemoteProtocolInfo", remoteProtocolInfo.toString()); - getActionInvocation().setInput("PeerConnectionManager", peerConnectionManager.toString()); - getActionInvocation().setInput("PeerConnectionID", peerConnectionID); - getActionInvocation().setInput("Direction", direction.toString()); - } - - @Override - public void success(ActionInvocation invocation) { - received( - invocation, - (Integer)invocation.getOutput("ConnectionID").getValue(), - (Integer)invocation.getOutput("RcsID").getValue(), - (Integer)invocation.getOutput("AVTransportID").getValue() - ); - } - - public abstract void received(ActionInvocation invocation, int connectionID, int rcsID, int avTransportID); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/DIDLParser.java b/yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/DIDLParser.java index f3977932..efcaed79 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/DIDLParser.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/DIDLParser.java @@ -18,7 +18,7 @@ import static org.fourthline.cling.model.XMLUtil.appendNewElement; import static org.fourthline.cling.model.XMLUtil.appendNewElementIfNotNull; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.types.Datatype; import org.fourthline.cling.model.types.InvalidValueException; @@ -116,7 +116,7 @@ public DIDLContent parse(String xml) throws Exception { DIDLContent content = new DIDLContent(); createRootHandler(content, this); - Log.v(getClass().getName(), "Parsing DIDL XML content"); + YaaccLogger.v(getClass().getName(), "Parsing DIDL XML content"); parse(new InputSource(new StringReader(xml.replaceAll("&(?!amp;)", "&")))); return content; } @@ -204,7 +204,7 @@ protected Res createResource(Attributes attributes) { new ProtocolInfo(attributes.getValue("protocolInfo")) ); } catch (InvalidValueException ex) { - Log.w(getClass().getName(), "In DIDL content, invalid resource protocol info: " + Exceptions.unwrap(ex)); + YaaccLogger.w(getClass().getName(), "In DIDL content, invalid resource protocol info: " + Exceptions.unwrap(ex)); return null; } @@ -385,7 +385,7 @@ protected void generateContainer(Container container, Document descriptor, Eleme String title = container.getTitle(); if (title == null) { - Log.v(getClass().getName(), "Missing 'dc:title' element for container: " + container.getId()); + YaaccLogger.v(getClass().getName(), "Missing 'dc:title' element for container: " + container.getId()); title = UNKNOWN_TITLE; } @@ -466,7 +466,7 @@ protected void generateItem(Item item, Document descriptor, Element parent) { String title = item.getTitle(); if (title == null) { - Log.v(getClass().getName(), "Missing 'dc:title' element for item: " + item.getId()); + YaaccLogger.v(getClass().getName(), "Missing 'dc:title' element for item: " + item.getId()); title = UNKNOWN_TITLE; } @@ -587,7 +587,7 @@ protected void populateDescMetadata(Element descElement, DescMeta descMeta) { } } else { - Log.v(getClass().getName(), "Unknown desc metadata content, please override populateDescMetadata(): " + descMeta.getMetadata()); + YaaccLogger.v(getClass().getName(), "Unknown desc metadata content, please override populateDescMetadata(): " + descMeta.getMetadata()); } } @@ -626,9 +626,9 @@ protected String booleanToInt(boolean b) { */ public void debugXML(String s) { - Log.v(getClass().getName(), "-------------------------------------------------------------------------------------"); - Log.v(getClass().getName(), "\n" + s); - Log.v(getClass().getName(), "-------------------------------------------------------------------------------------"); + YaaccLogger.v(getClass().getName(), "-------------------------------------------------------------------------------------"); + YaaccLogger.v(getClass().getName(), "\n" + s); + YaaccLogger.v(getClass().getName(), "-------------------------------------------------------------------------------------"); } @@ -676,7 +676,7 @@ public void endElement(String uri, String localName, String qName) throws SAXExc WriteStatus.valueOf(getCharacters()) ); } catch (Exception ex) { - Log.v(getClass().getName(), "Ignoring invalid writeStatus value: " + getCharacters()); + YaaccLogger.v(getClass().getName(), "Ignoring invalid writeStatus value: " + getCharacters()); } } else if ("class".equals(localName)) { getInstance().setClazz( @@ -958,10 +958,10 @@ public void endElement(String uri, String localName, String qName) throws SAXExc protected boolean isLastElement(String uri, String localName, String qName) { if (DIDLContent.NAMESPACE_URI.equals(uri) && "container".equals(localName)) { if (getInstance().getTitle() == null) { - Log.w(getClass().getName(), "In DIDL content, missing 'dc:title' element for container: " + getInstance().getId()); + YaaccLogger.w(getClass().getName(), "In DIDL content, missing 'dc:title' element for container: " + getInstance().getId()); } if (getInstance().getClazz() == null) { - Log.w(getClass().getName(), "In DIDL content, missing 'upnp:class' element for container: " + getInstance().getId()); + YaaccLogger.w(getClass().getName(), "In DIDL content, missing 'upnp:class' element for container: " + getInstance().getId()); } return true; } @@ -1001,10 +1001,10 @@ public void startElement(String uri, String localName, String qName, Attributes protected boolean isLastElement(String uri, String localName, String qName) { if (DIDLContent.NAMESPACE_URI.equals(uri) && "item".equals(localName)) { if (getInstance().getTitle() == null) { - Log.w(getClass().getName(), "In DIDL content, missing 'dc:title' element for item: " + getInstance().getId()); + YaaccLogger.w(getClass().getName(), "In DIDL content, missing 'dc:title' element for item: " + getInstance().getId()); } if (getInstance().getClazz() == null) { - Log.w(getClass().getName(), "In DIDL content, missing 'upnp:class' element for item: " + getInstance().getId()); + YaaccLogger.w(getClass().getName(), "In DIDL content, missing 'upnp:class' element for item: " + getInstance().getId()); } return true; } diff --git a/yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/callback/GetSystemUpdateID.java b/yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/callback/GetSystemUpdateID.java deleted file mode 100644 index 71584b70..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/contentdirectory/callback/GetSystemUpdateID.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.contentdirectory.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionException; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.types.ErrorCode; - -/** - * - * @author Christian Bauer - */ -public abstract class GetSystemUpdateID extends ActionCallback { - - public GetSystemUpdateID(org.fourthline.cling.model.meta.Service service) { - super(new ActionInvocation(service.getAction("GetSystemUpdateID"))); - } - - public void success(ActionInvocation invocation) { - boolean ok = true; - long id = 0; - try { - id = Long.valueOf(invocation.getOutput("Id").getValue().toString()); // UnsignedIntegerFourBytes... - } catch (Exception ex) { - invocation.setFailure(new ActionException(ErrorCode.ACTION_FAILED, "Can't parse GetSystemUpdateID response: " + ex, ex)); - failure(invocation, null); - ok = false; - } - if (ok) received(invocation, id); - } - - public abstract void received(ActionInvocation invocation, long systemUpdateID); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/igd/PortMappingListener.java b/yaacc/src/main/java/org/fourthline/cling/support/igd/PortMappingListener.java deleted file mode 100644 index 21d84dcb..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/igd/PortMappingListener.java +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.igd; - -import android.util.Log; - -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.model.meta.Device; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.DeviceType; -import org.fourthline.cling.model.types.ServiceType; -import org.fourthline.cling.model.types.UDADeviceType; -import org.fourthline.cling.model.types.UDAServiceType; -import org.fourthline.cling.registry.DefaultRegistryListener; -import org.fourthline.cling.registry.Registry; -import org.fourthline.cling.support.igd.callback.PortMappingAdd; -import org.fourthline.cling.support.igd.callback.PortMappingDelete; -import org.fourthline.cling.support.model.PortMapping; - -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -/** - * Maintains UPnP port mappings on an InternetGatewayDevice automatically. - *

- * This listener will wait for discovered devices which support either - * {@code WANIPConnection} or the {@code WANPPPConnection} service. As soon as any such - * service is discovered, the desired port mapping will be created. When the UPnP service - * is shutting down, all previously established port mappings with all services will - * be deleted. - *

- *

- * The following listener maps external WAN TCP port 8123 to internal host 10.0.0.2: - *

- *
{@code
- * upnpService.getRegistry().addListener(
- * newPortMappingListener(newPortMapping(8123, "10.0.0.2",PortMapping.Protocol.TCP))
- * );}
- *

- * If all you need from the Cling UPnP stack is NAT port mapping, use the following idiom: - *

- *
{@code
- * UpnpService upnpService = new UpnpServiceImpl(
- *     new PortMappingListener(new PortMapping(8123, "10.0.0.2", PortMapping.Protocol.TCP))
- * );
- * 

- * upnpService.getControlPoint().search(new STAllHeader()); // Search for all devices - *

- * upnpService.shutdown(); // When you no longer need the port mapping - * }

- * - * @author Christian Bauer - */ -public class PortMappingListener extends DefaultRegistryListener { - - - public static final DeviceType IGD_DEVICE_TYPE = new UDADeviceType("InternetGatewayDevice", 1); - public static final DeviceType CONNECTION_DEVICE_TYPE = new UDADeviceType("WANConnectionDevice", 1); - - public static final ServiceType IP_SERVICE_TYPE = new UDAServiceType("WANIPConnection", 1); - public static final ServiceType PPP_SERVICE_TYPE = new UDAServiceType("WANPPPConnection", 1); - - protected PortMapping[] portMappings; - - // The key of the map is Service and equality is object identity, this is by-design - protected Map> activePortMappings = new HashMap<>(); - - public PortMappingListener(PortMapping portMapping) { - this(new PortMapping[]{portMapping}); - } - - public PortMappingListener(PortMapping[] portMappings) { - this.portMappings = portMappings; - } - - @Override - synchronized public void deviceAdded(Registry registry, Device device) { - - Service connectionService; - if ((connectionService = discoverConnectionService(device)) == null) return; - - Log.v(getClass().getName(), "Activating port mappings on: " + connectionService); - - final List activeForService = new ArrayList<>(); - for (final PortMapping pm : portMappings) { - new PortMappingAdd(connectionService, registry.getUpnpService().getControlPoint(), pm) { - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Port mapping added: " + pm); - activeForService.add(pm); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - handleFailureMessage("Failed to add port mapping: " + pm); - handleFailureMessage("Reason: " + defaultMsg); - } - }.run(); // Synchronous! - } - - activePortMappings.put(connectionService, activeForService); - } - - @Override - synchronized public void deviceRemoved(Registry registry, Device device) { - for (Service service : device.findServices()) { - Iterator>> it = activePortMappings.entrySet().iterator(); - while (it.hasNext()) { - Map.Entry> activeEntry = it.next(); - if (!activeEntry.getKey().equals(service)) continue; - - if (activeEntry.getValue().size() > 0) - handleFailureMessage("Device disappeared, couldn't delete port mappings: " + activeEntry.getValue().size()); - - it.remove(); - } - } - } - - @Override - synchronized public void beforeShutdown(Registry registry) { - for (Map.Entry> activeEntry : activePortMappings.entrySet()) { - - final Iterator it = activeEntry.getValue().iterator(); - while (it.hasNext()) { - final PortMapping pm = it.next(); - Log.v(getClass().getName(), "Trying to delete port mapping on IGD: " + pm); - new PortMappingDelete(activeEntry.getKey(), registry.getUpnpService().getControlPoint(), pm) { - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Port mapping deleted: " + pm); - it.remove(); - } - - @Override - public void failure(ActionInvocation invocation, UpnpResponse operation, String defaultMsg) { - handleFailureMessage("Failed to delete port mapping: " + pm); - handleFailureMessage("Reason: " + defaultMsg); - } - - }.run(); // Synchronous! - } - } - } - - protected Service discoverConnectionService(Device device) { - if (!device.getType().equals(IGD_DEVICE_TYPE)) { - return null; - } - - Device[] connectionDevices = device.findDevices(CONNECTION_DEVICE_TYPE); - if (connectionDevices.length == 0) { - Log.v(getClass().getName(), "IGD doesn't support '" + CONNECTION_DEVICE_TYPE + "': " + device); - return null; - } - - Device connectionDevice = connectionDevices[0]; - Log.v(getClass().getName(), "Using first discovered WAN connection device: " + connectionDevice); - - Service ipConnectionService = connectionDevice.findService(IP_SERVICE_TYPE); - Service pppConnectionService = connectionDevice.findService(PPP_SERVICE_TYPE); - - if (ipConnectionService == null && pppConnectionService == null) { - Log.v(getClass().getName(), "IGD doesn't support IP or PPP WAN connection service: " + device); - } - - return ipConnectionService != null ? ipConnectionService : pppConnectionService; - } - - protected void handleFailureMessage(String s) { - Log.w(getClass().getName(), s); - } - -} - diff --git a/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/GetExternalIP.java b/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/GetExternalIP.java deleted file mode 100644 index 08df5c5a..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/GetExternalIP.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.igd.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; - -/** - * @author Christian Bauer - */ -public abstract class GetExternalIP extends ActionCallback { - - public GetExternalIP(Service service) { - super(new ActionInvocation(service.getAction("GetExternalIPAddress"))); - } - - @Override - public void success(ActionInvocation invocation) { - success((String)invocation.getOutput("NewExternalIPAddress").getValue()); - } - - protected abstract void success(String externalIPAddress); -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/GetStatusInfo.java b/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/GetStatusInfo.java deleted file mode 100644 index 3a497a79..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/GetStatusInfo.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.igd.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionException; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.ErrorCode; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.support.model.Connection; - -/** - * @author Christian Bauer - */ -public abstract class GetStatusInfo extends ActionCallback { - - public GetStatusInfo(Service service) { - super(new ActionInvocation(service.getAction("GetStatusInfo"))); - } - - @Override - public void success(ActionInvocation invocation) { - - try { - Connection.Status status = - Connection.Status.valueOf(invocation.getOutput("NewConnectionStatus").getValue().toString()); - - Connection.Error lastError = - Connection.Error.valueOf(invocation.getOutput("NewLastConnectionError").getValue().toString()); - - success(new Connection.StatusInfo(status, (UnsignedIntegerFourBytes) invocation.getOutput("NewUptime").getValue(), lastError)); - - } catch (Exception ex) { - invocation.setFailure( - new ActionException( - ErrorCode.ARGUMENT_VALUE_INVALID, - "Invalid status or last error string: " + ex, - ex - ) - ); - failure(invocation, null); - } - } - - protected abstract void success(Connection.StatusInfo statusInfo); -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/PortMappingAdd.java b/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/PortMappingAdd.java deleted file mode 100644 index d61ccca5..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/PortMappingAdd.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.igd.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.support.model.PortMapping; - -/** - * @author Christian Bauer - */ -public abstract class PortMappingAdd extends ActionCallback { - - final protected PortMapping portMapping; - - public PortMappingAdd(Service service, PortMapping portMapping) { - this(service, null, portMapping); - } - - protected PortMappingAdd(Service service, ControlPoint controlPoint, PortMapping portMapping) { - super(new ActionInvocation(service.getAction("AddPortMapping")), controlPoint); - - this.portMapping = portMapping; - - getActionInvocation().setInput("NewExternalPort", portMapping.getExternalPort()); - getActionInvocation().setInput("NewProtocol", portMapping.getProtocol()); - getActionInvocation().setInput("NewInternalClient", portMapping.getInternalClient()); - getActionInvocation().setInput("NewInternalPort", portMapping.getInternalPort()); - getActionInvocation().setInput("NewLeaseDuration", portMapping.getLeaseDurationSeconds()); - getActionInvocation().setInput("NewEnabled", portMapping.isEnabled()); - if (portMapping.hasRemoteHost()) - getActionInvocation().setInput("NewRemoteHost", portMapping.getRemoteHost()); - if (portMapping.hasDescription()) - getActionInvocation().setInput("NewPortMappingDescription", portMapping.getDescription()); - - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/PortMappingDelete.java b/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/PortMappingDelete.java deleted file mode 100644 index d6cc73c2..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/PortMappingDelete.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.igd.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.support.model.PortMapping; - -/** - * @author Christian Bauer - */ -public abstract class PortMappingDelete extends ActionCallback { - - final protected PortMapping portMapping; - - public PortMappingDelete(Service service, PortMapping portMapping) { - this(service, null, portMapping); - } - - protected PortMappingDelete(Service service, ControlPoint controlPoint, PortMapping portMapping) { - super(new ActionInvocation(service.getAction("DeletePortMapping")), controlPoint); - - this.portMapping = portMapping; - - getActionInvocation().setInput("NewExternalPort", portMapping.getExternalPort()); - getActionInvocation().setInput("NewProtocol", portMapping.getProtocol()); - if (portMapping.hasRemoteHost()) - getActionInvocation().setInput("NewRemoteHost", portMapping.getRemoteHost()); - - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/PortMappingEntryGet.java b/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/PortMappingEntryGet.java deleted file mode 100644 index 82ca2e91..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/igd/callback/PortMappingEntryGet.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.fourthline.cling.support.igd.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.controlpoint.ControlPoint; -import org.fourthline.cling.model.action.ActionArgumentValue; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerTwoBytes; -import org.fourthline.cling.support.model.PortMapping; - -import java.util.Map; - -public abstract class PortMappingEntryGet extends ActionCallback { - - public PortMappingEntryGet(Service service, long index) { - this(service, null, index); - } - - protected PortMappingEntryGet(Service service, ControlPoint controlPoint, long index) { - super(new ActionInvocation(service.getAction("GetGenericPortMappingEntry")), controlPoint); - - getActionInvocation().setInput("NewPortMappingIndex", new UnsignedIntegerTwoBytes(index)); - } - - @Override - public void success(ActionInvocation invocation) { - - Map> outputMap = invocation.getOutputMap(); - success(new PortMapping(outputMap)); - } - - protected abstract void success(PortMapping portMapping); -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/lastchange/EventedValueURI.java b/yaacc/src/main/java/org/fourthline/cling/support/lastchange/EventedValueURI.java index 7ac9cfdf..3d320144 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/lastchange/EventedValueURI.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/lastchange/EventedValueURI.java @@ -15,7 +15,7 @@ package org.fourthline.cling.support.lastchange; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.types.Datatype; import org.fourthline.cling.model.types.InvalidValueException; @@ -45,7 +45,7 @@ protected URI valueOf(String s) throws InvalidValueException { // to parse whatever devices give us, like the Roku which sends "unknown url". return super.valueOf(s); } catch (InvalidValueException ex) { - Log.v(getClass().getName(), "Ignoring invalid URI in evented value '" + s + "': " + Exceptions.unwrap(ex)); + YaaccLogger.v(getClass().getName(), "Ignoring invalid URI in evented value '" + s + "': " + Exceptions.unwrap(ex)); return null; } } diff --git a/yaacc/src/main/java/org/fourthline/cling/support/lastchange/LastChangeParser.java b/yaacc/src/main/java/org/fourthline/cling/support/lastchange/LastChangeParser.java index 9d94a0e5..9ef36b87 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/lastchange/LastChangeParser.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/lastchange/LastChangeParser.java @@ -17,7 +17,7 @@ import static org.fourthline.cling.model.XMLUtil.appendNewElement; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.XMLUtil; import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; @@ -104,18 +104,18 @@ public Event parse(String xml) throws Exception { new RootHandler(event, this); - Log.v(getClass().getName(), "Parsing 'LastChange' event XML content"); - Log.v(getClass().getName(), "===================================== 'LastChange' BEGIN ============================================"); - Log.v(getClass().getName(), xml); - Log.v(getClass().getName(), "====================================== 'LastChange' END ============================================"); + YaaccLogger.v(getClass().getName(), "Parsing 'LastChange' event XML content"); + YaaccLogger.v(getClass().getName(), "===================================== 'LastChange' BEGIN ============================================"); + YaaccLogger.v(getClass().getName(), xml); + YaaccLogger.v(getClass().getName(), "====================================== 'LastChange' END ============================================"); parse(new InputSource(new StringReader(xml))); - Log.v(getClass().getName(), "Parsed event with instances IDs: " + event.getInstanceIDs().size()); + YaaccLogger.v(getClass().getName(), "Parsed event with instances IDs: " + event.getInstanceIDs().size()); for (InstanceID instanceID : event.getInstanceIDs()) { - Log.v(getClass().getName(), "InstanceID '" + instanceID.getId() + "' has values: " + instanceID.getValues().size()); + YaaccLogger.v(getClass().getName(), "InstanceID '" + instanceID.getId() + "' has values: " + instanceID.getValues().size()); for (EventedValue eventedValue : instanceID.getValues()) { - Log.v(getClass().getName(), eventedValue.getName() + " => " + eventedValue.getValue()); + YaaccLogger.v(getClass().getName(), eventedValue.getName() + " => " + eventedValue.getValue()); } } @@ -223,7 +223,7 @@ public void startElement(String uri, String localName, String qName, final Attri getInstance().getValues().add(esv); } catch (Exception ex) { // Don't exit, just log a warning - Log.w(getClass().getName(), "Error reading event XML, ignoring value: " + Exceptions.unwrap(ex)); + YaaccLogger.w(getClass().getName(), "Error reading event XML, ignoring value: " + Exceptions.unwrap(ex)); } } diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/AddMessage.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/AddMessage.java deleted file mode 100644 index 4d5d45f9..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/AddMessage.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.support.messagebox.model.Message; -import org.seamless.util.MimeType; - -/** - * @author Christian Bauer - */ -public abstract class AddMessage extends ActionCallback { - - final protected MimeType mimeType = MimeType.valueOf("text/xml;charset=\"utf-8\""); - - public AddMessage(Service service, Message message) { - super(new ActionInvocation(service.getAction("AddMessage"))); - - getActionInvocation().setInput("MessageID", Integer.toString(message.getId())); - getActionInvocation().setInput("MessageType", mimeType.toString()); - getActionInvocation().setInput("Message", message.toString()); - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/RemoveMessage.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/RemoveMessage.java deleted file mode 100644 index b94661a9..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/RemoveMessage.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.support.messagebox.model.Message; - -/** - * ATTENTION: My Samsung TV does not implement this! - * - * @author Christian Bauer - */ -public abstract class RemoveMessage extends ActionCallback { - - public RemoveMessage(Service service, Message message) { - this(service, message.getId()); - } - - public RemoveMessage(Service service, int id) { - super(new ActionInvocation(service.getAction("RemoveMessage"))); - getActionInvocation().setInput("MessageID", id); - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/DateTime.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/DateTime.java deleted file mode 100644 index 07801780..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/DateTime.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox.model; - -import org.fourthline.cling.support.messagebox.parser.MessageElement; - -import java.text.SimpleDateFormat; -import java.util.Date; - -/** - * @author Christian Bauer - */ -public class DateTime implements ElementAppender { - - final private String date; - final private String time; - - public DateTime() { - this(getCurrentDate(), getCurrentTime()); - } - - public DateTime(String date, String time) { - this.date = date; - this.time = time; - } - - public String getDate() { - return date; - } - - public String getTime() { - return time; - } - - public void appendMessageElements(MessageElement parent) { - parent.createChild("Date").setContent(getDate()); - parent.createChild("Time").setContent(getTime()); - } - - public static String getCurrentDate() { - SimpleDateFormat fmt = new SimpleDateFormat("yyyy-MM-dd"); - return fmt.format(new Date()); - } - - public static String getCurrentTime() { - SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss"); - return fmt.format(new Date()); - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/ElementAppender.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/ElementAppender.java deleted file mode 100644 index 9f5d63b6..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/ElementAppender.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox.model; - -import org.fourthline.cling.support.messagebox.parser.MessageElement; - -/** - * @author Christian Bauer - */ -public interface ElementAppender { - - public void appendMessageElements(MessageElement parent); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/Message.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/Message.java deleted file mode 100644 index 1bc6335f..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/Message.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox.model; - -import org.fourthline.cling.support.messagebox.parser.MessageDOM; -import org.fourthline.cling.support.messagebox.parser.MessageDOMParser; -import org.fourthline.cling.support.messagebox.parser.MessageElement; -import org.seamless.xml.ParserException; - -import java.util.Random; - -/** - * https://sourceforge.net/apps/mediawiki/samygo/index.php?title=MessageBoxService_request_format - * - * @author Christian Bauer - */ -public abstract class Message implements ElementAppender { - - final protected Random randomGenerator = new Random(); - - public enum Category { - SMS("SMS"), - INCOMING_CALL("Incoming Call"), - SCHEDULE_REMINDER("Schedule Reminder"); - - public String text; - - Category(String text) { - this.text = text; - } - } - - public enum DisplayType { - - MINIMUM("Minimum"), - MAXIMUM("Maximum"); - - public String text; - - DisplayType(String text) { - this.text = text; - } - } - - private final int id; - private final Category category; - private DisplayType displayType; - - public Message(Category category, DisplayType displayType) { - this(0, category, displayType); - } - - public Message(int id, Category category, DisplayType displayType) { - if (id == 0) id = randomGenerator.nextInt(Integer.MAX_VALUE); - this.id = id; - this.category = category; - this.displayType = displayType; - } - - public int getId() { - return id; - } - - public Category getCategory() { - return category; - } - - public DisplayType getDisplayType() { - return displayType; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - Message message = (Message) o; - - if (id != message.id) return false; - - return true; - } - - @Override - public int hashCode() { - return id; - } - - @Override - public String toString() { - try { - MessageDOMParser mp = new MessageDOMParser(); - MessageDOM dom = mp.createDocument(); - - MessageElement root = dom.createRoot(mp.createXPath(), "Message"); - root.createChild("Category").setContent(getCategory().text); - root.createChild("DisplayType").setContent(getDisplayType().text); - appendMessageElements(root); - - String s = mp.print(dom, 0, false); - - // Cut the root element, what we send to the TV is not really XML, just - // random element soup which I'm sure the Samsung guys think is XML... - return s.replaceAll("", "") - .replaceAll("", ""); - - - } catch (ParserException ex) { - throw new RuntimeException(ex); - } - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/MessageIncomingCall.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/MessageIncomingCall.java deleted file mode 100644 index 59e3b3de..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/MessageIncomingCall.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox.model; - -import org.fourthline.cling.support.messagebox.parser.MessageElement; - -/** - * @author Christian Bauer - */ -public class MessageIncomingCall extends Message { - - final private DateTime callTime; - final private NumberName callee; - final private NumberName caller; - - public MessageIncomingCall(NumberName callee, NumberName caller) { - this(new DateTime(), callee, caller); - } - - public MessageIncomingCall(DateTime callTime, NumberName callee, NumberName caller) { - this(DisplayType.MAXIMUM, callTime, callee, caller); - } - - public MessageIncomingCall(DisplayType displayType, DateTime callTime, NumberName callee, NumberName caller) { - super(Category.INCOMING_CALL, displayType); - this.callTime = callTime; - this.callee = callee; - this.caller = caller; - } - - public DateTime getCallTime() { - return callTime; - } - - public NumberName getCallee() { - return callee; - } - - public NumberName getCaller() { - return caller; - } - - public void appendMessageElements(MessageElement parent) { - getCallTime().appendMessageElements(parent.createChild("CallTime")); - getCallee().appendMessageElements(parent.createChild("Callee")); - getCaller().appendMessageElements(parent.createChild("Caller")); - } - -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/MessageSMS.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/MessageSMS.java deleted file mode 100644 index 1a6cea0b..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/MessageSMS.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox.model; - -import org.fourthline.cling.support.messagebox.parser.MessageElement; - -/** - * Sender and body will only be displayed if display type is set to "Maximum". - * - * @author Christian Bauer - */ -public class MessageSMS extends Message { - - final private DateTime receiveTime; - final private NumberName receiver; - final private NumberName sender; - final private String body; - - public MessageSMS(NumberName receiver, NumberName sender, String body) { - this(new DateTime(), receiver, sender, body); - } - - public MessageSMS(DateTime receiveTime, NumberName receiver, NumberName sender, String body) { - this(Message.DisplayType.MAXIMUM, receiveTime, receiver, sender, body); - } - - public MessageSMS(DisplayType displayType, DateTime receiveTime, NumberName receiver, NumberName sender, String body) { - super(Message.Category.SMS, displayType); - this.receiveTime = receiveTime; - this.receiver = receiver; - this.sender = sender; - this.body = body; - } - - public DateTime getReceiveTime() { - return receiveTime; - } - - public NumberName getReceiver() { - return receiver; - } - - public NumberName getSender() { - return sender; - } - - public String getBody() { - return body; - } - - public void appendMessageElements(MessageElement parent) { - getReceiveTime().appendMessageElements(parent.createChild("ReceiveTime")); - getReceiver().appendMessageElements(parent.createChild("Receiver")); - getSender().appendMessageElements(parent.createChild("Sender")); - parent.createChild("Body").setContent(getBody()); - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/MessageScheduleReminder.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/MessageScheduleReminder.java deleted file mode 100644 index 6af2c146..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/MessageScheduleReminder.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox.model; - -import org.fourthline.cling.support.messagebox.parser.MessageElement; - -/** - * @author Christian Bauer - */ -public class MessageScheduleReminder extends Message { - - final private DateTime startTime; - final private NumberName owner; - final private String subject; - final private DateTime endTime; - final private String location; - final private String body; - - public MessageScheduleReminder(DateTime startTime, NumberName owner, String subject, - DateTime endTime, String location, String body) { - this(DisplayType.MAXIMUM, startTime, owner, subject, endTime, location, body); - } - - public MessageScheduleReminder(DisplayType displayType, DateTime startTime, NumberName owner, String subject, - DateTime endTime, String location, String body) { - super(Category.SCHEDULE_REMINDER, displayType); - this.startTime = startTime; - this.owner = owner; - this.subject = subject; - this.endTime = endTime; - this.location = location; - this.body = body; - } - - public DateTime getStartTime() { - return startTime; - } - - public NumberName getOwner() { - return owner; - } - - public String getSubject() { - return subject; - } - - public DateTime getEndTime() { - return endTime; - } - - public String getLocation() { - return location; - } - - public String getBody() { - return body; - } - - public void appendMessageElements(MessageElement parent) { - getStartTime().appendMessageElements(parent.createChild("StartTime")); - getOwner().appendMessageElements(parent.createChild("Owner")); - parent.createChild("Subject").setContent(getSubject()); - getEndTime().appendMessageElements(parent.createChild("EndTime")); - parent.createChild("Location").setContent(getLocation()); - parent.createChild("Body").setContent(getBody()); - } - -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/NumberName.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/NumberName.java deleted file mode 100644 index f7d6edb2..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/model/NumberName.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox.model; - -import org.fourthline.cling.support.messagebox.parser.MessageElement; - -/** - * @author Christian Bauer - */ -public class NumberName implements ElementAppender { - - private String number; - private String name; - - public NumberName(String number, String name) { - this.number = number; - this.name = name; - } - - public String getNumber() { - return number; - } - - public String getName() { - return name; - } - - public void appendMessageElements(MessageElement parent) { - parent.createChild("Number").setContent(getNumber()); - parent.createChild("Name").setContent(getName()); - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/parser/MessageDOM.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/parser/MessageDOM.java deleted file mode 100644 index d2319e25..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/parser/MessageDOM.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox.parser; - -import org.seamless.xml.DOM; -import org.w3c.dom.Document; - -import javax.xml.xpath.XPath; - -/** - * @author Christian Bauer - */ -public class MessageDOM extends DOM { - - public static final String NAMESPACE_URI = "urn:samsung-com:messagebox-1-0"; - - public MessageDOM(Document dom) { - super(dom); - } - - @Override - public String getRootElementNamespace() { - return NAMESPACE_URI; - } - - @Override - public MessageElement getRoot(XPath xPath) { - return new MessageElement(xPath, getW3CDocument().getDocumentElement()); - } - - @Override - public MessageDOM copy() { - return new MessageDOM((Document) getW3CDocument().cloneNode(true)); - } - - public MessageElement createRoot(XPath xpath, String element) { - super.createRoot(element); - return getRoot(xpath); - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/parser/MessageDOMParser.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/parser/MessageDOMParser.java deleted file mode 100644 index 3a316f39..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/parser/MessageDOMParser.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox.parser; - -import org.seamless.xml.DOMParser; -import org.seamless.xml.NamespaceContextMap; -import org.w3c.dom.Document; - -import javax.xml.xpath.XPath; - -/** - * @author Christian Bauer - */ -public class MessageDOMParser extends DOMParser { - - @Override - protected MessageDOM createDOM(Document document) { - return new MessageDOM(document); - } - - public NamespaceContextMap createDefaultNamespaceContext(String... optionalPrefixes) { - NamespaceContextMap ctx = new NamespaceContextMap() { - @Override - protected String getDefaultNamespaceURI() { - return MessageDOM.NAMESPACE_URI; - } - }; - for (String optionalPrefix : optionalPrefixes) { - ctx.put(optionalPrefix, MessageDOM.NAMESPACE_URI); - } - return ctx; - } - - public XPath createXPath() { - return super.createXPath(createDefaultNamespaceContext(MessageElement.XPATH_PREFIX)); - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/parser/MessageElement.java b/yaacc/src/main/java/org/fourthline/cling/support/messagebox/parser/MessageElement.java deleted file mode 100644 index 95ef11a2..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/messagebox/parser/MessageElement.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.messagebox.parser; - -import org.seamless.xml.DOMElement; -import org.w3c.dom.Element; - -import javax.xml.xpath.XPath; - -/** - * @author Christian Bauer - */ -public class MessageElement extends DOMElement { - - public static final String XPATH_PREFIX = "m"; - - public MessageElement(XPath xpath, Element element) { - super(xpath, element); - } - - @Override - protected String prefix(String localName) { - return XPATH_PREFIX + ":" + localName; - } - - @Override - protected Builder createParentBuilder(DOMElement el) { - return new Builder(el) { - @Override - public MessageElement build(Element element) { - return new MessageElement(getXpath(), element); - } - }; - } - - @Override - protected ArrayBuilder createChildBuilder(DOMElement el) { - return new ArrayBuilder(el) { - @Override - public MessageElement[] newChildrenArray(int length) { - return new MessageElement[length]; - } - - @Override - public MessageElement build(Element element) { - return new MessageElement(getXpath(), element); - } - }; - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/support/model/PositionInfo.java b/yaacc/src/main/java/org/fourthline/cling/support/model/PositionInfo.java index b97172e3..f0385244 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/model/PositionInfo.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/model/PositionInfo.java @@ -28,7 +28,7 @@ public class PositionInfo { private UnsignedIntegerFourBytes track = new UnsignedIntegerFourBytes(0); private String trackDuration = "00:00:00"; - private String trackMetaData = "NOT_IMPLEMENTED"; + private String trackMetaData = ""; private String trackURI = ""; private String relTime = "00:00:00"; private String absTime = "00:00:00"; // TODO: MORE VALUES IN DOMAIN! diff --git a/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/DLNAAttribute.java b/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/DLNAAttribute.java index 33c63ca6..9fbe845d 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/DLNAAttribute.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/DLNAAttribute.java @@ -14,7 +14,7 @@ */ package org.fourthline.cling.support.model.dlna; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.seamless.util.Exceptions; @@ -56,17 +56,17 @@ public static DLNAAttribute newInstance(DLNAAttribute.Type type, String attribut for (int i = 0; i < type.getAttributeTypes().length && attr == null; i++) { Class attributeClass = type.getAttributeTypes()[i]; try { - Log.v(DLNAAttribute.class.getName(), "Trying to parse DLNA '" + type + "' with class: " + attributeClass.getSimpleName()); + YaaccLogger.v(DLNAAttribute.class.getName(), "Trying to parse DLNA '" + type + "' with class: " + attributeClass.getSimpleName()); attr = attributeClass.newInstance(); if (attributeValue != null) { attr.setString(attributeValue, contentFormat); } } catch (InvalidDLNAProtocolAttributeException ex) { - Log.v(DLNAAttribute.class.getName(), "Invalid DLNA attribute value for tested type: " + attributeClass.getSimpleName() + " - " + ex.getMessage()); + YaaccLogger.v(DLNAAttribute.class.getName(), "Invalid DLNA attribute value for tested type: " + attributeClass.getSimpleName() + " - " + ex.getMessage()); attr = null; } catch (Exception ex) { - Log.e(DLNAAttribute.class.getName(), "Error instantiating DLNA attribute of type '" + type + "' with value: " + attributeValue); - Log.e(DLNAAttribute.class.getName(), "Exception root cause: ", Exceptions.unwrap(ex)); + YaaccLogger.e(DLNAAttribute.class.getName(), "Error instantiating DLNA attribute of type '" + type + "' with value: " + attributeValue); + YaaccLogger.e(DLNAAttribute.class.getName(), "Exception root cause: ", Exceptions.unwrap(ex)); } } return attr; diff --git a/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/message/DLNAHeaders.java b/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/message/DLNAHeaders.java index e11bf3d1..bfc0cca3 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/message/DLNAHeaders.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/message/DLNAHeaders.java @@ -15,7 +15,7 @@ package org.fourthline.cling.support.model.dlna.message; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.message.UpnpHeaders; import org.fourthline.cling.model.message.header.UpnpHeader; @@ -55,21 +55,21 @@ protected void parseHeaders() { // This runs as late as possible and only when necessary (getter called and map is dirty) parsedDLNAHeaders = new LinkedHashMap<>(); - Log.v(getClass().getName(), String.format("Parsing all HTTP headers for known UPnP headers: {0}", size())); + YaaccLogger.v(getClass().getName(), String.format("Parsing all HTTP headers for known UPnP headers: {0}", size())); for (Entry> entry : entrySet()) { if (entry.getKey() == null) continue; // Oh yes, the JDK has 'null' HTTP headers DLNAHeader.Type type = DLNAHeader.Type.getByHttpName(entry.getKey()); if (type == null) { - Log.v(getClass().getName(), String.format("Ignoring non-UPNP HTTP header: {0}", entry.getKey())); + YaaccLogger.v(getClass().getName(), String.format("Ignoring non-UPNP HTTP header: {0}", entry.getKey())); continue; } for (String value : entry.getValue()) { UpnpHeader upnpHeader = DLNAHeader.newInstance(type, value); if (upnpHeader == null || upnpHeader.getValue() == null) { - Log.v(getClass().getName(), String.format("Ignoring known but non-parsable header (value violates the UDA specification?) '{0}': {1}", new Object[]{type.getHttpName(), value})); + YaaccLogger.v(getClass().getName(), String.format("Ignoring known but non-parsable header (value violates the UDA specification?) '{0}': {1}", new Object[]{type.getHttpName(), value})); } else { addParsedValue(type, upnpHeader); } @@ -78,7 +78,7 @@ protected void parseHeaders() { } protected void addParsedValue(DLNAHeader.Type type, UpnpHeader value) { - Log.v(getClass().getName(), String.format("Adding parsed header: {0}", value)); + YaaccLogger.v(getClass().getName(), String.format("Adding parsed header: {0}", value)); List list = parsedDLNAHeaders.get(type); if (list == null) { list = new LinkedList<>(); @@ -163,15 +163,15 @@ public void log() { super.log(); if (parsedDLNAHeaders != null && parsedDLNAHeaders.size() > 0) { - Log.v(getClass().getName(), "########################## PARSED DLNA HEADERS ##########################"); + YaaccLogger.v(getClass().getName(), "########################## PARSED DLNA HEADERS ##########################"); for (Map.Entry> entry : parsedDLNAHeaders.entrySet()) { - Log.v(getClass().getName(), String.format("=== TYPE: {0}", entry.getKey())); + YaaccLogger.v(getClass().getName(), String.format("=== TYPE: {0}", entry.getKey())); for (UpnpHeader upnpHeader : entry.getValue()) { - Log.v(getClass().getName(), String.format("HEADER: {0}", upnpHeader)); + YaaccLogger.v(getClass().getName(), String.format("HEADER: {0}", upnpHeader)); } } } - Log.v(getClass().getName(), "####################################################################"); + YaaccLogger.v(getClass().getName(), "####################################################################"); } diff --git a/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/message/header/DLNAHeader.java b/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/message/header/DLNAHeader.java index 29eaa962..d938b73c 100644 --- a/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/message/header/DLNAHeader.java +++ b/yaacc/src/main/java/org/fourthline/cling/support/model/dlna/message/header/DLNAHeader.java @@ -15,7 +15,7 @@ package org.fourthline.cling.support.model.dlna.message.header; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.message.header.InvalidHeaderException; import org.fourthline.cling.model.message.header.UpnpHeader; @@ -57,17 +57,17 @@ public static DLNAHeader newInstance(DLNAHeader.Type type, String headerValue) { for (int i = 0; i < type.getHeaderTypes().length && upnpHeader == null; i++) { Class headerClass = type.getHeaderTypes()[i]; try { - Log.v(DLNAHeader.class.getName(), "Trying to parse '" + type + "' with class: " + headerClass.getSimpleName()); + YaaccLogger.v(DLNAHeader.class.getName(), "Trying to parse '" + type + "' with class: " + headerClass.getSimpleName()); upnpHeader = headerClass.newInstance(); if (headerValue != null) { upnpHeader.setString(headerValue); } } catch (InvalidHeaderException ex) { - Log.v(DLNAHeader.class.getName(), "Invalid header value for tested type: " + headerClass.getSimpleName() + " - " + ex.getMessage()); + YaaccLogger.v(DLNAHeader.class.getName(), "Invalid header value for tested type: " + headerClass.getSimpleName() + " - " + ex.getMessage()); upnpHeader = null; } catch (Exception ex) { - Log.e(DLNAHeader.class.getName(), "Error instantiating header of type '" + type + "' with value: " + headerValue); - Log.e(DLNAHeader.class.getName(), "Exception root cause: ", Exceptions.unwrap(ex)); + YaaccLogger.e(DLNAHeader.class.getName(), "Error instantiating header of type '" + type + "' with value: " + headerValue); + YaaccLogger.e(DLNAHeader.class.getName(), "Exception root cause: ", Exceptions.unwrap(ex)); } } diff --git a/yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/GetMute.java b/yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/GetMute.java deleted file mode 100644 index 33cee2b2..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/GetMute.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.renderingcontrol.callback; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.support.model.Channel; - -/** - * @author Christian Bauer - */ -public abstract class GetMute extends ActionCallback { - - - public GetMute(Service service) { - this(new UnsignedIntegerFourBytes(0), service); - } - - public GetMute(UnsignedIntegerFourBytes instanceId, Service service) { - super(new ActionInvocation(service.getAction("GetMute"))); - getActionInvocation().setInput("InstanceID", instanceId); - getActionInvocation().setInput("Channel", Channel.Master.toString()); - } - - public void success(ActionInvocation invocation) { - boolean currentMute = (Boolean) invocation.getOutput("CurrentMute").getValue(); - received(invocation, currentMute); - } - - public abstract void received(ActionInvocation actionInvocation, boolean currentMute); -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/SetMute.java b/yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/SetMute.java deleted file mode 100644 index 4f13bbc5..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/SetMute.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.renderingcontrol.callback; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.support.model.Channel; - -/** - * @author Christian Bauer - */ -public abstract class SetMute extends ActionCallback { - - - public SetMute(Service service, boolean desiredMute) { - this(new UnsignedIntegerFourBytes(0), service, desiredMute); - } - - public SetMute(UnsignedIntegerFourBytes instanceId, Service service, boolean desiredMute) { - super(new ActionInvocation(service.getAction("SetMute"))); - getActionInvocation().setInput("InstanceID", instanceId); - getActionInvocation().setInput("Channel", Channel.Master.toString()); - getActionInvocation().setInput("DesiredMute", desiredMute); - } - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Executed successfully"); - - } -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/SetVolume.java b/yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/SetVolume.java deleted file mode 100644 index 762cbf28..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/support/renderingcontrol/callback/SetVolume.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.support.renderingcontrol.callback; - -import android.util.Log; - -import org.fourthline.cling.controlpoint.ActionCallback; -import org.fourthline.cling.model.action.ActionInvocation; -import org.fourthline.cling.model.meta.Service; -import org.fourthline.cling.model.types.UnsignedIntegerFourBytes; -import org.fourthline.cling.model.types.UnsignedIntegerTwoBytes; -import org.fourthline.cling.support.model.Channel; - -/** - * @author Christian Bauer - */ -public abstract class SetVolume extends ActionCallback { - - - public SetVolume(Service service, long newVolume) { - this(new UnsignedIntegerFourBytes(0), service, newVolume); - } - - public SetVolume(UnsignedIntegerFourBytes instanceId, Service service, long newVolume) { - super(new ActionInvocation(service.getAction("SetVolume"))); - getActionInvocation().setInput("InstanceID", instanceId); - getActionInvocation().setInput("Channel", Channel.Master.toString()); - getActionInvocation().setInput("DesiredVolume", new UnsignedIntegerTwoBytes(newVolume)); - } - - @Override - public void success(ActionInvocation invocation) { - Log.v(getClass().getName(), "Executed successfully"); - - } -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/DisableRouter.java b/yaacc/src/main/java/org/fourthline/cling/transport/DisableRouter.java deleted file mode 100644 index cd2b4cf4..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/DisableRouter.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport; - -/** - * @author Christian Bauer - */ -public class DisableRouter { -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/EnableRouter.java b/yaacc/src/main/java/org/fourthline/cling/transport/EnableRouter.java deleted file mode 100644 index 9dc25ba2..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/EnableRouter.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport; - -/** - * @author Christian Bauer - */ -public class EnableRouter { -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/Router.java b/yaacc/src/main/java/org/fourthline/cling/transport/Router.java deleted file mode 100644 index 1619deab..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/Router.java +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport; - -import org.fourthline.cling.UpnpServiceConfiguration; -import org.fourthline.cling.model.NetworkAddress; -import org.fourthline.cling.model.message.IncomingDatagramMessage; -import org.fourthline.cling.model.message.OutgoingDatagramMessage; -import org.fourthline.cling.model.message.StreamRequestMessage; -import org.fourthline.cling.model.message.StreamResponseMessage; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.transport.spi.InitializationException; -import org.fourthline.cling.transport.spi.UpnpStream; - -import java.net.InetAddress; -import java.util.List; - -/** - * Interface of the network transport layer. - *

- * Encapsulates the transport layer and provides methods to the upper layers for - * sending UPnP stream (HTTP) {@link org.fourthline.cling.model.message.StreamRequestMessage}s, - * sending (UDP) datagram {@link org.fourthline.cling.model.message.OutgoingDatagramMessage}s, - * as well as broadcasting bytes to all LAN participants. - *

- *

- * A router also maintains listening sockets and services, for incoming UDP unicast/multicast - * {@link org.fourthline.cling.model.message.IncomingDatagramMessage} and TCP - * {@link org.fourthline.cling.transport.spi.UpnpStream}s. An implementation of this interface - * handles these messages, e.g. by selecting and executing the right protocol. - *

- *

- * An implementation must be thread-safe, and can be accessed concurrently. If the Router is - * disabled, it doesn't listen on the network for incoming messages and does not send outgoing - * messages. - *

- * - * @see org.fourthline.cling.protocol.ProtocolFactory - * - * @author Christian Bauer - */ -public interface Router { - - /** - * @return The configuration used by this router. - */ - public UpnpServiceConfiguration getConfiguration(); - - /** - * @return The protocol factory used by this router. - */ - public ProtocolFactory getProtocolFactory(); - - /** - * Starts all sockets and listening threads for datagrams and streams. - * - * @return true if the router was enabled. false if it's already running. - */ - boolean enable() throws RouterException; - - /** - * Unbinds all sockets and stops all listening threads for datagrams and streams. - * - * @return true if the router was disabled. false if it wasn't running. - */ - boolean disable() throws RouterException; - - /** - * Disables the router and releases all other resources. - */ - void shutdown() throws RouterException ; - - /** - * - * @return true if the router is currently enabled. - */ - boolean isEnabled() throws RouterException; - - /** - * Called by the {@link #enable()} method before it returns. - * - * @param ex The cause of the failure. - * @throws InitializationException if the exception was not recoverable. - */ - void handleStartFailure(InitializationException ex) throws InitializationException; - - /** - * @param preferredAddress A preferred stream server bound address or null. - * @return An empty list if no stream server is currently active, otherwise a single network - * address if the preferred address is active, or a list of all active bound - * stream servers. - */ - public List getActiveStreamServers(InetAddress preferredAddress) throws RouterException; - - /** - *

- * This method is called internally by the transport layer when a datagram, either unicast or - * multicast, has been received. An implementation of this interface has to handle the received - * message, e.g. selecting and executing a UPnP protocol. This method should not block until - * the execution completes, the calling thread should be free to handle the next reception as - * soon as possible. - *

- * @param msg The received datagram message. - */ - public void received(IncomingDatagramMessage msg); - - /** - *

- * This method is called internally by the transport layer when a TCP stream connection has - * been made and a response has to be returned to the sender. An implementation of this interface - * has to handle the received stream connection and return a response, e.g. selecting and executing - * a UPnP protocol. This method should not block until the execution completes, the calling thread - * should be free to process the next reception as soon as possible. Typically this means starting - * a new thread of execution in this method. - *

- * @param stream - */ - public void received(UpnpStream stream); - - /** - *

- * Call this method to send a UDP datagram message. - *

- * @param msg The UDP datagram message to send. - * @throws RouterException if a recoverable error, such as thread interruption, occurs. - */ - public void send(OutgoingDatagramMessage msg) throws RouterException; - - /** - *

- * Call this method to send a TCP (HTTP) stream message. - *

- * @param msg The TCP (HTTP) stream message to send. - * @return The response received from the server. - * @throws RouterException if a recoverable error, such as thread interruption, occurs. - */ - public StreamResponseMessage send(StreamRequestMessage msg) throws RouterException; - - /** - *

- * Call this method to broadcast a UDP message to all hosts on the network. - *

- * @param bytes The byte payload of the UDP datagram. - * @throws RouterException if a recoverable error, such as thread interruption, occurs. - */ - public void broadcast(byte[] bytes) throws RouterException; - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/RouterException.java b/yaacc/src/main/java/org/fourthline/cling/transport/RouterException.java deleted file mode 100644 index 1c7a47f5..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/RouterException.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ -package org.fourthline.cling.transport; - -/** - * Thrown by the {@link Router} if a non-fatal recoverable exception occurred. - *

- * This exception is thrown if the calling thread wasn't able to obtain - * exclusive read/write access on the router. - *

- *

- * This exception is also thrown when you interrupt the thread calling the - * router. In such a case, the cause of this is an InterruptedException. - *

- * - * @author Christian Bauer - */ -public class RouterException extends Exception { - - public RouterException() { - super(); - } - - public RouterException(String s) { - super(s); - } - - public RouterException(String s, Throwable throwable) { - super(s, throwable); - } - - public RouterException(Throwable throwable) { - super(throwable); - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/RouterImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/RouterImpl.java deleted file mode 100644 index 90c6dc00..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/RouterImpl.java +++ /dev/null @@ -1,520 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport; - -import android.util.Log; - -import org.fourthline.cling.UpnpServiceConfiguration; -import org.fourthline.cling.model.NetworkAddress; -import org.fourthline.cling.model.message.IncomingDatagramMessage; -import org.fourthline.cling.model.message.OutgoingDatagramMessage; -import org.fourthline.cling.model.message.StreamRequestMessage; -import org.fourthline.cling.model.message.StreamResponseMessage; -import org.fourthline.cling.protocol.ProtocolCreationException; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.protocol.ReceivingAsync; -import org.fourthline.cling.transport.spi.DatagramIO; -import org.fourthline.cling.transport.spi.InitializationException; -import org.fourthline.cling.transport.spi.MulticastReceiver; -import org.fourthline.cling.transport.spi.NetworkAddressFactory; -import org.fourthline.cling.transport.spi.NoNetworkException; -import org.fourthline.cling.transport.spi.StreamClient; -import org.fourthline.cling.transport.spi.StreamServer; -import org.fourthline.cling.transport.spi.UpnpStream; -import org.seamless.util.Exceptions; - -import java.net.BindException; -import java.net.DatagramPacket; -import java.net.Inet4Address; -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.enterprise.event.Observes; -import jakarta.enterprise.inject.Default; -import jakarta.inject.Inject; - -/** - * Default implementation of network message router. - *

- * Initializes and starts listening for data on the network when enabled. - *

- * - * @author Christian Bauer - */ -@ApplicationScoped -public class RouterImpl implements Router { - - - protected final Map multicastReceivers = new HashMap<>(); - protected final Map datagramIOs = new HashMap<>(); - protected final Map streamServers = new HashMap<>(); - protected UpnpServiceConfiguration configuration; - protected ProtocolFactory protocolFactory; - protected volatile boolean enabled; - protected ReentrantReadWriteLock routerLock = new ReentrantReadWriteLock(true); - protected Lock readLock = routerLock.readLock(); - protected Lock writeLock = routerLock.writeLock(); - // These are created/destroyed when the router is enabled/disabled - protected NetworkAddressFactory networkAddressFactory; - protected StreamClient streamClient; - - protected RouterImpl() { - } - - /** - * @param configuration The configuration used by this router. - * @param protocolFactory The protocol factory used by this router. - */ - @Inject - public RouterImpl(UpnpServiceConfiguration configuration, ProtocolFactory protocolFactory) { - Log.v(getClass().getName(), "Creating Router: " + getClass().getName()); - this.configuration = configuration; - this.protocolFactory = protocolFactory; - } - - public boolean enable(@Observes @Default EnableRouter event) throws RouterException { - return enable(); - } - - public boolean disable(@Observes @Default DisableRouter event) throws RouterException { - return disable(); - } - - public UpnpServiceConfiguration getConfiguration() { - return configuration; - } - - public ProtocolFactory getProtocolFactory() { - return protocolFactory; - } - - /** - * Initializes listening services: First an instance of {@link org.fourthline.cling.transport.spi.MulticastReceiver} - * is bound to each network interface. Then an instance of {@link org.fourthline.cling.transport.spi.DatagramIO} and - * {@link org.fourthline.cling.transport.spi.StreamServer} is bound to each bind address returned by the network - * address factory, respectively. There is only one instance of - * {@link org.fourthline.cling.transport.spi.StreamClient} created and managed by this router. - */ - @Override - public boolean enable() throws RouterException { - lock(writeLock); - try { - if (!enabled) { - try { - Log.v(getClass().getName(), "Starting networking services..."); - networkAddressFactory = getConfiguration().createNetworkAddressFactory(); - - startInterfaceBasedTransports(networkAddressFactory.getNetworkInterfaces()); - startAddressBasedTransports(networkAddressFactory.getBindAddresses()); - - // The transports possibly removed some unusable network interfaces/addresses - if (!networkAddressFactory.hasUsableNetwork()) { - throw new NoNetworkException( - "No usable network interface and/or addresses available, check the log for errors." - ); - } - - // Start the HTTP client last, we don't even have to try if there is no network - streamClient = getConfiguration().createStreamClient(); - - enabled = true; - return true; - } catch (InitializationException ex) { - handleStartFailure(ex); - } - } - return false; - } finally { - unlock(writeLock); - } - } - - @Override - public boolean disable() throws RouterException { - lock(writeLock); - try { - if (enabled) { - Log.v(getClass().getName(), "Disabling network services..."); - - if (streamClient != null) { - Log.v(getClass().getName(), "Stopping stream client connection management/pool"); - streamClient.stop(); - streamClient = null; - } - - for (Map.Entry entry : streamServers.entrySet()) { - Log.v(getClass().getName(), "Stopping stream server on address: " + entry.getKey()); - entry.getValue().stop(); - } - streamServers.clear(); - - for (Map.Entry entry : multicastReceivers.entrySet()) { - Log.v(getClass().getName(), "Stopping multicast receiver on interface: " + entry.getKey().getDisplayName()); - entry.getValue().stop(); - } - multicastReceivers.clear(); - - for (Map.Entry entry : datagramIOs.entrySet()) { - Log.v(getClass().getName(), "Stopping datagram I/O on address: " + entry.getKey()); - entry.getValue().stop(); - } - datagramIOs.clear(); - - networkAddressFactory = null; - enabled = false; - return true; - } - return false; - } finally { - unlock(writeLock); - } - } - - @Override - public void shutdown() throws RouterException { - disable(); - } - - @Override - public boolean isEnabled() { - return enabled; - } - - @Override - public void handleStartFailure(InitializationException ex) throws InitializationException { - if (ex instanceof NoNetworkException) { - Log.v(getClass().getName(), "Unable to initialize network router, no network found."); - } else { - Log.e(getClass().getName(), "Unable to initialize network router: " + ex); - Log.e(getClass().getName(), "Cause: " + Exceptions.unwrap(ex)); - } - } - - public List getActiveStreamServers(InetAddress preferredAddress) throws RouterException { - lock(readLock); - try { - if (enabled && streamServers.size() > 0) { - List streamServerAddresses = new ArrayList<>(); - - StreamServer preferredServer; - if (preferredAddress != null && - (preferredServer = streamServers.get(preferredAddress)) != null) { - streamServerAddresses.add( - new NetworkAddress( - preferredAddress, - preferredServer.getPort(), - networkAddressFactory.getHardwareAddress(preferredAddress) - - ) - ); - return streamServerAddresses; - } - - for (Map.Entry entry : streamServers.entrySet()) { - byte[] hardwareAddress = networkAddressFactory.getHardwareAddress(entry.getKey()); - streamServerAddresses.add( - new NetworkAddress(entry.getKey(), entry.getValue().getPort(), hardwareAddress) - ); - } - return streamServerAddresses; - } else { - return Collections.EMPTY_LIST; - } - } finally { - unlock(readLock); - } - } - - /** - * Obtains the asynchronous protocol {@code Executor} and runs the protocol created - * by the {@link org.fourthline.cling.protocol.ProtocolFactory} for the given message. - *

- * If the factory doesn't create a protocol, the message is dropped immediately without - * creating another thread or consuming further resources. This means we can filter the - * datagrams in the protocol factory and e.g. completely disable discovery or only - * allow notification message from some known services we'd like to work with. - *

- * - * @param msg The received datagram message. - */ - public void received(IncomingDatagramMessage msg) { - if (!enabled) { - Log.v(getClass().getName(), "Router disabled, ignoring incoming message: " + msg); - return; - } - try { - ReceivingAsync protocol = getProtocolFactory().createReceivingAsync(msg); - if (protocol == null) { - - Log.v(getClass().getName(), "No protocol, ignoring received message: " + msg); - return; - } - - Log.v(getClass().getName(), "Received asynchronous message: " + msg); - getConfiguration().getAsyncProtocolExecutor().execute(protocol); - } catch (ProtocolCreationException ex) { - Log.w(getClass().getName(), "Handling received datagram failed - " + Exceptions.unwrap(ex).toString()); - } - } - - /** - * Obtains the synchronous protocol {@code Executor} and runs the - * {@link org.fourthline.cling.transport.spi.UpnpStream} directly. - * - * @param stream The received {@link org.fourthline.cling.transport.spi.UpnpStream}. - */ - public void received(UpnpStream stream) { - if (!enabled) { - Log.v(getClass().getName(), "Router disabled, ignoring incoming: " + stream); - return; - } - Log.v(getClass().getName(), "Received synchronous stream: " + stream); - getConfiguration().getSyncProtocolExecutorService().execute(stream); - } - - /** - * Sends the UDP datagram on all bound {@link org.fourthline.cling.transport.spi.DatagramIO}s. - * - * @param msg The UDP datagram message to send. - */ - public void send(OutgoingDatagramMessage msg) throws RouterException { - lock(readLock); - try { - if (enabled) { - for (DatagramIO datagramIO : datagramIOs.values()) { - datagramIO.send(msg); - } - } else { - Log.v(getClass().getName(), "Router disabled, not sending datagram: " + msg); - } - } finally { - unlock(readLock); - } - } - - /** - * Sends the TCP stream request with the {@link org.fourthline.cling.transport.spi.StreamClient}. - * - * @param msg The TCP (HTTP) stream message to send. - * @return The return value of the {@link org.fourthline.cling.transport.spi.StreamClient#sendRequest(StreamRequestMessage)} - * method or null if no StreamClient is available. - */ - public StreamResponseMessage send(StreamRequestMessage msg) throws RouterException { - lock(readLock); - try { - if (enabled) { - if (streamClient == null) { - Log.v(getClass().getName(), "No StreamClient available, not sending: " + msg); - return null; - } - Log.v(getClass().getName(), "Sending via TCP unicast stream: " + msg); - try { - return streamClient.sendRequest(msg); - } catch (InterruptedException ex) { - throw new RouterException("Sending stream request was interrupted", ex); - } - } else { - Log.v(getClass().getName(), "Router disabled, not sending stream request: " + msg); - return null; - } - } finally { - unlock(readLock); - } - } - - /** - * Sends the given bytes as a broadcast on all bound {@link org.fourthline.cling.transport.spi.DatagramIO}s, - * using source port 9. - *

- * TODO: Support source port parameter - *

- * - * @param bytes The byte payload of the UDP datagram. - */ - public void broadcast(byte[] bytes) throws RouterException { - lock(readLock); - try { - if (enabled) { - for (Map.Entry entry : datagramIOs.entrySet()) { - InetAddress broadcast = networkAddressFactory.getBroadcastAddress(entry.getKey()); - if (broadcast != null) { - Log.v(getClass().getName(), "Sending UDP datagram to broadcast address: " + broadcast.getHostAddress()); - DatagramPacket packet = new DatagramPacket(bytes, bytes.length, broadcast, 9); - entry.getValue().send(packet); - } - } - } else { - Log.v(getClass().getName(), "Router disabled, not broadcasting bytes: " + bytes.length); - } - } finally { - unlock(readLock); - } - } - - protected void startInterfaceBasedTransports(Iterator interfaces) throws InitializationException { - while (interfaces.hasNext()) { - NetworkInterface networkInterface = interfaces.next(); - // We only have the MulticastReceiver as an interface-based transport - MulticastReceiver multicastReceiver = getConfiguration().createMulticastReceiver(networkAddressFactory); - if (multicastReceiver == null) { - Log.v(getClass().getName(), "Configuration did not create a MulticastReceiver for: " + networkInterface); - } else { - try { - Log.v(getClass().getName(), "Init multicast receiver on interface: " + networkInterface.getDisplayName()); - multicastReceiver.init( - networkInterface, - this, - networkAddressFactory, - getConfiguration().getDatagramProcessor() - ); - - multicastReceivers.put(networkInterface, multicastReceiver); - } catch (InitializationException ex) { - /* TODO: What are some recoverable exceptions for this? - log.warning( - "Ignoring network interface '" - + networkInterface.getDisplayName() - + "' init failure of MulticastReceiver: " + ex.toString()); - if (log.isLoggable(Level.FINE)) - log.log(Level.FINE, "Initialization exception root cause", Exceptions.unwrap(ex)); - log.warning("Removing unusable interface " + interface); - it.remove(); - continue; // Don't need to try anything else on this interface - */ - throw ex; - } - } - } - - for (Map.Entry entry : multicastReceivers.entrySet()) { - Log.v(getClass().getName(), "Starting multicast receiver on interface: " + entry.getKey().getDisplayName()); - getConfiguration().getMulticastReceiverExecutor().execute(entry.getValue()); - } - } - - protected void startAddressBasedTransports(Iterator addresses) throws InitializationException { - while (addresses.hasNext()) { - InetAddress address = addresses.next(); - - if (!(address instanceof Inet4Address)) { - continue; - } - // HTTP servers - StreamServer streamServer = getConfiguration().createStreamServer(protocolFactory, networkAddressFactory); - if (streamServer == null) { - Log.v(getClass().getName(), "Configuration did not create a StreamServer for: " + address); - } else { - try { - - Log.v(getClass().getName(), "Init stream server on address: " + address); - streamServer.init(address, this); - streamServers.put(address, streamServer); - } catch (InitializationException ex) { - // Try to recover - Throwable cause = Exceptions.unwrap(ex); - if (cause instanceof BindException) { - Log.w(getClass().getName(), "Failed to init StreamServer: " + cause); - Log.v(getClass().getName(), "Initialization exception root cause", cause); - Log.w(getClass().getName(), "Removing unusable address: " + address); - addresses.remove(); - continue; // Don't try anything else with this address - } - throw ex; - } - } - - // Datagram I/O - DatagramIO datagramIO = getConfiguration().createDatagramIO(networkAddressFactory); - if (datagramIO == null) { - Log.v(getClass().getName(), "Configuration did not create a StreamServer for: " + address); - } else { - try { - Log.v(getClass().getName(), "Init datagram I/O on address: " + address); - datagramIO.init(address, this, getConfiguration().getDatagramProcessor()); - datagramIOs.put(address, datagramIO); - } catch (InitializationException ex) { - /* TODO: What are some recoverable exceptions for this? - Throwable cause = Exceptions.unwrap(ex); - if (cause instanceof BindException) { - log.warning("Failed to init datagram I/O: " + cause); - if (log.isLoggable(Level.FINE)) - log.log(Level.FINE, "Initialization exception root cause", cause); - log.warning("Removing unusable address: " + address); - addresses.remove(); - continue; // Don't try anything else with this address - } - */ - throw ex; - } - } - } - - for (Map.Entry entry : streamServers.entrySet()) { - Log.v(getClass().getName(), "Starting stream server on address: " + entry.getKey()); - getConfiguration().getStreamServerExecutorService().execute(entry.getValue()); - } - - for (Map.Entry entry : datagramIOs.entrySet()) { - Log.v(getClass().getName(), "Starting datagram I/O on address: " + entry.getKey()); - getConfiguration().getDatagramIOExecutor().execute(entry.getValue()); - } - } - - protected void lock(Lock lock, int timeoutMilliseconds) throws RouterException { - try { - Log.v(getClass().getName(), "Trying to obtain lock with timeout milliseconds '" + timeoutMilliseconds + "': " + lock.getClass().getSimpleName()); - if (lock.tryLock(timeoutMilliseconds, TimeUnit.MILLISECONDS)) { - Log.v(getClass().getName(), "Acquired router lock: " + lock.getClass().getSimpleName()); - } else { - throw new RouterException( - "Router wasn't available exclusively after waiting " + timeoutMilliseconds + "ms, lock failed: " - + lock.getClass().getSimpleName() - ); - } - } catch (InterruptedException ex) { - throw new RouterException( - "Interruption while waiting for exclusive access: " + lock.getClass().getSimpleName(), ex - ); - } - } - - protected void lock(Lock lock) throws RouterException { - lock(lock, getLockTimeoutMillis()); - } - - protected void unlock(Lock lock) { - Log.v(getClass().getName(), "Releasing router lock: " + lock.getClass().getSimpleName()); - lock.unlock(); - } - - /** - * @return Defaults to 6 seconds, should be longer than it takes the router to be enabled/disabled. - */ - protected int getLockTimeoutMillis() { - return 6000; - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/DatagramIOConfigurationImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/DatagramIOConfigurationImpl.java deleted file mode 100644 index 9570d44e..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/DatagramIOConfigurationImpl.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.impl; - -import org.fourthline.cling.transport.spi.DatagramIOConfiguration; - -/** - * Settings for the default implementation. - * - * @author Christian Bauer - */ -public class DatagramIOConfigurationImpl implements DatagramIOConfiguration { - - private int timeToLive = 4; - private int maxDatagramBytes = 640; - - /** - * Defaults to TTL of '4' and maximum datagram size of 640 bytes (512 per UDA 1.0, 128 byte header). - */ - public DatagramIOConfigurationImpl() { - } - - public DatagramIOConfigurationImpl(int timeToLive, int maxDatagramBytes) { - this.timeToLive = timeToLive; - this.maxDatagramBytes = maxDatagramBytes; - } - - public int getTimeToLive() { - return timeToLive; - } - - public void setTimeToLive(int timeToLive) { - this.timeToLive = timeToLive; - } - - public int getMaxDatagramBytes() { - return maxDatagramBytes; - } - - public void setMaxDatagramBytes(int maxDatagramBytes) { - this.maxDatagramBytes = maxDatagramBytes; - } -} \ No newline at end of file diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/DatagramIOImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/DatagramIOImpl.java deleted file mode 100644 index 3a34587d..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/DatagramIOImpl.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.impl; - -import android.util.Log; - -import org.fourthline.cling.model.UnsupportedDataException; -import org.fourthline.cling.model.message.OutgoingDatagramMessage; -import org.fourthline.cling.transport.Router; -import org.fourthline.cling.transport.spi.DatagramIO; -import org.fourthline.cling.transport.spi.DatagramProcessor; -import org.fourthline.cling.transport.spi.InitializationException; - -import java.net.DatagramPacket; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.MulticastSocket; -import java.net.SocketException; - -/** - * Default implementation based on a single shared (receive/send) UDP MulticastSocket. - *

- * Although we do not receive multicast datagrams with this service, sending multicast - * datagrams with a configuration time-to-live requires a MulticastSocket. - *

- *

- * Thread-safety is guaranteed through synchronization of methods of this service and - * by the thread-safe underlying socket. - *

- * - * @author Christian Bauer - */ -public class DatagramIOImpl implements DatagramIO { - - - /* Implementation notes for unicast/multicast UDP: - - http://forums.sun.com/thread.jspa?threadID=771852 - http://mail.openjdk.java.net/pipermail/net-dev/2008-December/000497.html - https://jira.jboss.org/jira/browse/JGRP-978 - http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4701650 - - */ - - final protected DatagramIOConfigurationImpl configuration; - - protected Router router; - protected DatagramProcessor datagramProcessor; - - protected InetSocketAddress localAddress; - protected MulticastSocket socket; // For sending unicast & multicast, and reveiving unicast - - public DatagramIOImpl(DatagramIOConfigurationImpl configuration) { - this.configuration = configuration; - } - - public DatagramIOConfigurationImpl getConfiguration() { - return configuration; - } - - synchronized public void init(InetAddress bindAddress, Router router, DatagramProcessor datagramProcessor) throws InitializationException { - - this.router = router; - this.datagramProcessor = datagramProcessor; - - try { - - // TODO: UPNP VIOLATION: The spec does not prohibit using the 1900 port here again, however, the - // Netgear ReadyNAS miniDLNA implementation will no longer answer if it has to send search response - // back via UDP unicast to port 1900... so we use an ephemeral port - Log.v(getClass().getName(), "Creating bound socket (for datagram input/output) on: " + bindAddress); - localAddress = new InetSocketAddress(bindAddress, 0); - socket = new MulticastSocket(localAddress); - socket.setTimeToLive(configuration.getTimeToLive()); - socket.setReceiveBufferSize(262144); // Keep a backlog of incoming datagrams if we are not fast enough - } catch (Exception ex) { - throw new InitializationException("Could not initialize " + getClass().getSimpleName() + ": " + ex); - } - } - - synchronized public void stop() { - if (socket != null && !socket.isClosed()) { - socket.close(); - } - } - - public void run() { - Log.v(getClass().getName(), "Entering blocking receiving loop, listening for UDP datagrams on: " + socket.getLocalAddress()); - - while (true) { - - try { - byte[] buf = new byte[getConfiguration().getMaxDatagramBytes()]; - DatagramPacket datagram = new DatagramPacket(buf, buf.length); - - socket.receive(datagram); - - Log.v(getClass().getName(), - "UDP datagram received from: " - + datagram.getAddress().getHostAddress() - + ":" + datagram.getPort() - + " on: " + localAddress - ); - - - router.received(datagramProcessor.read(localAddress.getAddress(), datagram)); - - } catch (SocketException ex) { - Log.v(getClass().getName(), "Socket closed", ex); - break; - } catch (UnsupportedDataException ex) { - Log.v(getClass().getName(), "Could not read datagram: " + ex.getMessage(), ex); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - try { - if (!socket.isClosed()) { - Log.v(getClass().getName(), "Closing unicast socket"); - socket.close(); - } - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - synchronized public void send(OutgoingDatagramMessage message) { - Log.v(getClass().getName(), "Sending message from address: " + localAddress); - - DatagramPacket packet = datagramProcessor.write(message); - - Log.v(getClass().getName(), "Sending UDP datagram packet to: " + message.getDestinationAddress() + ":" + message.getDestinationPort()); - - - send(packet); - } - - synchronized public void send(DatagramPacket datagram) { - Log.v(getClass().getName(), "Sending message from address: " + localAddress); - - - try { - socket.send(datagram); - } catch (SocketException ex) { - Log.v(getClass().getName(), "Socket closed, aborting datagram send to: " + datagram.getAddress()); - } catch (RuntimeException ex) { - throw ex; - } catch (Exception ex) { - try { - Log.w(getClass().getName(), socket.getNetworkInterface() + " Exception sending datagram to: " + datagram.getAddress() + ": " + ex, ex); - } catch (SocketException se) { - Log.e(getClass().getName(), " Exception sending datagram to: " + datagram.getAddress() + ": " + ex, ex); - } - } - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/DatagramProcessorImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/DatagramProcessorImpl.java index e8c7b02b..6232f13e 100644 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/DatagramProcessorImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/transport/impl/DatagramProcessorImpl.java @@ -15,7 +15,7 @@ package org.fourthline.cling.transport.impl; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.UnsupportedDataException; import org.fourthline.cling.model.message.IncomingDatagramMessage; @@ -44,9 +44,9 @@ public IncomingDatagramMessage read(InetAddress receivedOnAddress, DatagramPacke try { - Log.v(getClass().getName(), "===================================== DATAGRAM BEGIN ============================================"); - Log.v(getClass().getName(), new String(datagram.getData(), "UTF-8")); - Log.v(getClass().getName(), "-===================================== DATAGRAM END ============================================="); + YaaccLogger.v(getClass().getName(), "===================================== DATAGRAM BEGIN ============================================"); + YaaccLogger.v(getClass().getName(), new String(datagram.getData(), "UTF-8")); + YaaccLogger.v(getClass().getName(), "-===================================== DATAGRAM END ============================================="); ByteArrayInputStream is = new ByteArrayInputStream(datagram.getData()); @@ -92,10 +92,10 @@ public DatagramPacket write(OutgoingDatagramMessage message) throws UnsupportedD messageData.append(message.getHeaders().toString()).append("\r\n"); - Log.v(getClass().getName(), "Writing message data for: " + message); - Log.v(getClass().getName(), "---------------------------------------------------------------------------------"); - Log.v(getClass().getName(), messageData.toString().substring(0, messageData.length() - 2)); // Don't print the blank lines - Log.v(getClass().getName(), "---------------------------------------------------------------------------------"); + YaaccLogger.v(getClass().getName(), "Writing message data for: " + message); + YaaccLogger.v(getClass().getName(), "---------------------------------------------------------------------------------"); + YaaccLogger.v(getClass().getName(), messageData.toString().substring(0, messageData.length() - 2)); // Don't print the blank lines + YaaccLogger.v(getClass().getName(), "---------------------------------------------------------------------------------"); try { @@ -103,7 +103,7 @@ public DatagramPacket write(OutgoingDatagramMessage message) throws UnsupportedD // TODO: Probably should look into escaping rules, too byte[] data = messageData.toString().getBytes("US-ASCII"); - Log.v(getClass().getName(), "Writing new datagram packet with " + data.length + " bytes for: " + message); + YaaccLogger.v(getClass().getName(), "Writing new datagram packet with " + data.length + " bytes for: " + message); return new DatagramPacket(data, data.length, message.getDestinationAddress(), message.getDestinationPort()); } catch (UnsupportedEncodingException ex) { diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/GENAEventProcessorImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/GENAEventProcessorImpl.java index 82daba20..ddec4e8c 100644 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/GENAEventProcessorImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/transport/impl/GENAEventProcessorImpl.java @@ -15,7 +15,7 @@ package org.fourthline.cling.transport.impl; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Constants; import org.fourthline.cling.model.UnsupportedDataException; @@ -54,7 +54,7 @@ protected DocumentBuilderFactory createDocumentBuilderFactory() throws FactoryCo } public void writeBody(OutgoingEventRequestMessage requestMessage) throws UnsupportedDataException { - Log.v(getClass().getName(), "Writing body of: " + requestMessage); + YaaccLogger.v(getClass().getName(), "Writing body of: " + requestMessage); try { @@ -67,9 +67,9 @@ public void writeBody(OutgoingEventRequestMessage requestMessage) throws Unsuppo requestMessage.setBody(UpnpMessage.BodyType.STRING, toString(d)); - Log.v(getClass().getName(), "===================================== GENA BODY BEGIN ============================================"); - Log.v(getClass().getName(), requestMessage.getBody().toString()); - Log.v(getClass().getName(), "====================================== GENA BODY END ============================================="); + YaaccLogger.v(getClass().getName(), "===================================== GENA BODY BEGIN ============================================"); + YaaccLogger.v(getClass().getName(), requestMessage.getBody().toString()); + YaaccLogger.v(getClass().getName(), "====================================== GENA BODY END ============================================="); } catch (Exception ex) { throw new UnsupportedDataException("Can't transform message payload: " + ex.getMessage(), ex); } @@ -77,10 +77,10 @@ public void writeBody(OutgoingEventRequestMessage requestMessage) throws Unsuppo public void readBody(IncomingEventRequestMessage requestMessage) throws UnsupportedDataException { - Log.v(getClass().getName(), "Reading body of: " + requestMessage); - Log.v(getClass().getName(), "===================================== GENA BODY BEGIN ============================================"); - Log.v(getClass().getName(), requestMessage.getBody() != null ? requestMessage.getBody().toString() : "null"); - Log.v(getClass().getName(), "-===================================== GENA BODY END ============================================"); + YaaccLogger.v(getClass().getName(), "Reading body of: " + requestMessage); + YaaccLogger.v(getClass().getName(), "===================================== GENA BODY BEGIN ============================================"); + YaaccLogger.v(getClass().getName(), requestMessage.getBody() != null ? requestMessage.getBody().toString() : "null"); + YaaccLogger.v(getClass().getName(), "-===================================== GENA BODY END ============================================"); String body = getMessageBody(requestMessage); @@ -160,7 +160,7 @@ protected void readProperties(Element propertysetElement, IncomingEventRequestMe String stateVariableName = getUnprefixedNodeName(propertyChild); for (StateVariable stateVariable : stateVariables) { if (stateVariable.getName().equals(stateVariableName)) { - Log.v(getClass().getName(), "Reading state variable value: " + stateVariableName); + YaaccLogger.v(getClass().getName(), "Reading state variable value: " + stateVariableName); String value = XMLUtil.getTextContent(propertyChild); message.getStateVariableValues().add( new StateVariableValue(stateVariable, value) @@ -201,7 +201,7 @@ protected String getUnprefixedNodeName(Node node) { } public void warning(SAXParseException e) throws SAXException { - Log.w(getClass().getName(), e.toString()); + YaaccLogger.w(getClass().getName(), e.toString()); } public void error(SAXParseException e) throws SAXException { diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/MulticastReceiverConfigurationImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/MulticastReceiverConfigurationImpl.java deleted file mode 100644 index e7f36f70..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/MulticastReceiverConfigurationImpl.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.impl; - -import org.fourthline.cling.transport.spi.MulticastReceiverConfiguration; - -import java.net.InetAddress; -import java.net.UnknownHostException; - -/** - * Settings for the default implementation. - * - * @author Christian Bauer - */ -public class MulticastReceiverConfigurationImpl implements MulticastReceiverConfiguration { - - private InetAddress group; - private int port; - private int maxDatagramBytes; - - public MulticastReceiverConfigurationImpl(InetAddress group, int port, int maxDatagramBytes) { - this.group = group; - this.port = port; - this.maxDatagramBytes = maxDatagramBytes; - } - - /** - * Defaults to maximum datagram size of 640 bytes (512 per UDA 1.0, 128 byte header). - */ - public MulticastReceiverConfigurationImpl(InetAddress group, int port) { - this(group, port, 640); - } - - public MulticastReceiverConfigurationImpl(String group, int port, int maxDatagramBytes) throws UnknownHostException { - this(InetAddress.getByName(group), port, maxDatagramBytes); - } - - /** - * Defaults to maximum datagram size of 640 bytes (512 per UDA 1.0, 128 byte header). - */ - public MulticastReceiverConfigurationImpl(String group, int port) throws UnknownHostException { - this(InetAddress.getByName(group), port, 640); - } - - public InetAddress getGroup() { - return group; - } - - public void setGroup(InetAddress group) { - this.group = group; - } - - public int getPort() { - return port; - } - - public void setPort(int port) { - this.port = port; - } - - public int getMaxDatagramBytes() { - return maxDatagramBytes; - } - - public void setMaxDatagramBytes(int maxDatagramBytes) { - this.maxDatagramBytes = maxDatagramBytes; - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/MulticastReceiverImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/MulticastReceiverImpl.java deleted file mode 100644 index a1fb8729..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/MulticastReceiverImpl.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.impl; - -import android.util.Log; - -import org.fourthline.cling.model.UnsupportedDataException; -import org.fourthline.cling.transport.Router; -import org.fourthline.cling.transport.spi.DatagramProcessor; -import org.fourthline.cling.transport.spi.InitializationException; -import org.fourthline.cling.transport.spi.MulticastReceiver; -import org.fourthline.cling.transport.spi.NetworkAddressFactory; - -import java.net.DatagramPacket; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.InetSocketAddress; -import java.net.MulticastSocket; -import java.net.NetworkInterface; -import java.net.SocketException; - -/** - * Default implementation based on a UDP MulticastSocket. - *

- * Thread-safety is guaranteed through synchronization of methods of this service and - * by the thread-safe underlying socket. - *

- * - * @author Christian Bauer - */ -public class MulticastReceiverImpl implements MulticastReceiver { - - final protected MulticastReceiverConfigurationImpl configuration; - - protected Router router; - protected NetworkAddressFactory networkAddressFactory; - protected DatagramProcessor datagramProcessor; - - protected NetworkInterface multicastInterface; - protected InetSocketAddress multicastAddress; - protected MulticastSocket socket; - - public MulticastReceiverImpl(MulticastReceiverConfigurationImpl configuration) { - this.configuration = configuration; - } - - public MulticastReceiverConfigurationImpl getConfiguration() { - return configuration; - } - - synchronized public void init(NetworkInterface networkInterface, - Router router, - NetworkAddressFactory networkAddressFactory, - DatagramProcessor datagramProcessor) throws InitializationException { - - this.router = router; - this.networkAddressFactory = networkAddressFactory; - this.datagramProcessor = datagramProcessor; - this.multicastInterface = networkInterface; - - try { - - Log.v(getClass().getName(), "Creating wildcard socket (for receiving multicast datagrams) on port: " + configuration.getPort()); - multicastAddress = new InetSocketAddress(configuration.getGroup(), configuration.getPort()); - - socket = new MulticastSocket(configuration.getPort()); - socket.setReuseAddress(true); - socket.setReceiveBufferSize(32768); // Keep a backlog of incoming datagrams if we are not fast enough - - Log.v(getClass().getName(), "Joining multicast group: " + multicastAddress + " on network interface: " + multicastInterface.getDisplayName()); - socket.joinGroup(multicastAddress, multicastInterface); - - } catch (Exception ex) { - throw new InitializationException("Could not initialize " + getClass().getSimpleName() + ": " + ex); - } - } - - synchronized public void stop() { - if (socket != null && !socket.isClosed()) { - try { - Log.v(getClass().getName(), "Leaving multicast group"); - socket.leaveGroup(multicastAddress, multicastInterface); - // Well this doesn't work and I have no idea why I get "java.net.SocketException: Can't assign requested address" - } catch (Exception ex) { - Log.v(getClass().getName(), "Could not leave multicast group: ", ex); - } - // So... just close it and ignore the log messages - socket.close(); - } - } - - public void run() { - - Log.v(getClass().getName(), "Entering blocking receiving loop, listening for UDP datagrams on: " + socket.getLocalAddress()); - while (true) { - - try { - byte[] buf = new byte[getConfiguration().getMaxDatagramBytes()]; - DatagramPacket datagram = new DatagramPacket(buf, buf.length); - - socket.receive(datagram); - - InetAddress receivedOnLocalAddress = - networkAddressFactory.getLocalAddress( - multicastInterface, - multicastAddress.getAddress() instanceof Inet6Address, - datagram.getAddress() - ); - - Log.v(getClass().getName(), - "UDP datagram received from: " + datagram.getAddress().getHostAddress() - + ":" + datagram.getPort() - + " on local interface: " + multicastInterface.getDisplayName() - + " and address: " + receivedOnLocalAddress.getHostAddress() - ); - - router.received(datagramProcessor.read(receivedOnLocalAddress, datagram)); - - } catch (SocketException ex) { - Log.v(getClass().getName(), "Socket closed", ex); - break; - } catch (UnsupportedDataException ex) { - Log.v(getClass().getName(), "Could not read datagram: " + ex.getMessage(), ex); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - try { - if (!socket.isClosed()) { - Log.v(getClass().getName(), "Closing multicast socket"); - socket.close(); - } - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - -} - diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/NetworkAddressFactoryImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/NetworkAddressFactoryImpl.java deleted file mode 100644 index 57b03a92..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/NetworkAddressFactoryImpl.java +++ /dev/null @@ -1,518 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.impl; - -import android.util.Log; - -import org.fourthline.cling.model.Constants; -import org.fourthline.cling.transport.spi.InitializationException; -import org.fourthline.cling.transport.spi.NetworkAddressFactory; -import org.fourthline.cling.transport.spi.NoNetworkException; -import org.seamless.util.Iterators; - -import java.net.Inet4Address; -import java.net.Inet6Address; -import java.net.InetAddress; -import java.net.InterfaceAddress; -import java.net.NetworkInterface; -import java.net.SocketException; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Enumeration; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -/** - * Default implementation of network interface and address configuration/discovery. - *

- * - * @author Christian Bauer - */ -public class NetworkAddressFactoryImpl implements NetworkAddressFactory { - - // Ephemeral port is the default - public static final int DEFAULT_TCP_HTTP_LISTEN_PORT = 0; - - - final protected Set useInterfaces = new HashSet<>(); - final protected Set useAddresses = new HashSet<>(); - - final protected List networkInterfaces = new ArrayList<>(); - final protected List bindAddresses = new ArrayList<>(); - - protected int streamListenPort; - - /** - * Defaults to an ephemeral port. - */ - public NetworkAddressFactoryImpl() throws InitializationException { - this(DEFAULT_TCP_HTTP_LISTEN_PORT); - } - - public NetworkAddressFactoryImpl(int streamListenPort) throws InitializationException { - - System.setProperty("java.net.preferIPv4Stack", "true"); - - String useInterfacesString = System.getProperty(SYSTEM_PROPERTY_NET_IFACES); - if (useInterfacesString != null) { - String[] userInterfacesStrings = useInterfacesString.split(","); - useInterfaces.addAll(Arrays.asList(userInterfacesStrings)); - } - - String useAddressesString = System.getProperty(SYSTEM_PROPERTY_NET_ADDRESSES); - if (useAddressesString != null) { - String[] useAddressesStrings = useAddressesString.split(","); - useAddresses.addAll(Arrays.asList(useAddressesStrings)); - } - - discoverNetworkInterfaces(); - discoverBindAddresses(); - - if ((networkInterfaces.size() == 0 || bindAddresses.size() == 0)) { - Log.w(getClass().getName(), "No usable network interface or addresses found"); - if (requiresNetworkInterface()) { - throw new NoNetworkException( - "Could not discover any usable network interfaces and/or addresses" - ); - } - } - - this.streamListenPort = streamListenPort; - } - - /** - * @return true (the default) if a MissingNetworkInterfaceException should be thrown - */ - protected boolean requiresNetworkInterface() { - return true; - } - - public void logInterfaceInformation() { - synchronized (networkInterfaces) { - if (networkInterfaces.isEmpty()) { - Log.v(getClass().getName(), "No network interface to display!"); - return; - } - for (NetworkInterface networkInterface : networkInterfaces) { - try { - logInterfaceInformation(networkInterface); - } catch (SocketException ex) { - Log.w(getClass().getName(), "Exception while logging network interface information", ex); - } - } - } - } - - public InetAddress getMulticastGroup() { - try { - return InetAddress.getByName(Constants.IPV4_UPNP_MULTICAST_GROUP); - } catch (UnknownHostException ex) { - throw new RuntimeException(ex); - } - } - - public int getMulticastPort() { - return Constants.UPNP_MULTICAST_PORT; - } - - public int getStreamListenPort() { - return streamListenPort; - } - - public Iterator getNetworkInterfaces() { - return new Iterators.Synchronized(networkInterfaces) { - @Override - protected void synchronizedRemove(int index) { - synchronized (networkInterfaces) { - networkInterfaces.remove(index); - } - } - }; - } - - public Iterator getBindAddresses() { - return new Iterators.Synchronized(bindAddresses) { - @Override - protected void synchronizedRemove(int index) { - synchronized (bindAddresses) { - bindAddresses.remove(index); - } - } - }; - } - - public boolean hasUsableNetwork() { - return networkInterfaces.size() > 0 && bindAddresses.size() > 0; - } - - public byte[] getHardwareAddress(InetAddress inetAddress) { - try { - NetworkInterface iface = NetworkInterface.getByInetAddress(inetAddress); - return iface != null ? iface.getHardwareAddress() : null; - } catch (Throwable ex) { - Log.w(getClass().getName(), "Cannot get hardware address for: " + inetAddress, ex); - // On Win32: java.lang.Error: IP Helper Library GetIpAddrTable function failed - - // On Android 4.0.3 NullPointerException with inetAddress != null - - // On Android "SocketException: No such device or address" when - // switching networks (mobile -> WiFi) - return null; - } - } - - public InetAddress getBroadcastAddress(InetAddress inetAddress) { - synchronized (networkInterfaces) { - for (NetworkInterface iface : networkInterfaces) { - for (InterfaceAddress interfaceAddress : getInterfaceAddresses(iface)) { - if (interfaceAddress != null && interfaceAddress.getAddress().equals(inetAddress)) { - return interfaceAddress.getBroadcast(); - } - } - } - } - return null; - } - - public Short getAddressNetworkPrefixLength(InetAddress inetAddress) { - synchronized (networkInterfaces) { - for (NetworkInterface iface : networkInterfaces) { - for (InterfaceAddress interfaceAddress : getInterfaceAddresses(iface)) { - if (interfaceAddress != null && interfaceAddress.getAddress().equals(inetAddress)) { - short prefix = interfaceAddress.getNetworkPrefixLength(); - if (prefix > 0 && prefix < 32) - return prefix; // some network cards return -1 - return null; - } - } - } - } - return null; - } - - public InetAddress getLocalAddress(NetworkInterface networkInterface, boolean isIPv6, InetAddress remoteAddress) { - - // First try to find a local IP that is in the same subnet as the remote IP - InetAddress localIPInSubnet = getBindAddressInSubnetOf(remoteAddress); - if (localIPInSubnet != null) return localIPInSubnet; - - // There are two reasons why we end up here: - // - // - Windows Vista returns a 64 or 128 CIDR prefix if you ask it for the network prefix length of an IPv4 address! - // - // - We are dealing with genuine IPv6 addresses - // - // - Something is really wrong on the LAN and we received a multicast datagram from a source we can't reach via IP - Log.v(getClass().getName(), "Could not find local bind address in same subnet as: " + remoteAddress.getHostAddress()); - - // Next, just take the given interface (which is really totally random) and get the first address that we like - for (InetAddress interfaceAddress : getInetAddresses(networkInterface)) { - if (isIPv6 && interfaceAddress instanceof Inet6Address) - return interfaceAddress; - if (!isIPv6 && interfaceAddress instanceof Inet4Address) - return interfaceAddress; - } - throw new IllegalStateException("Can't find any IPv4 or IPv6 address on interface: " + networkInterface.getDisplayName()); - } - - protected List getInterfaceAddresses(NetworkInterface networkInterface) { - return networkInterface.getInterfaceAddresses(); - } - - protected List getInetAddresses(NetworkInterface networkInterface) { - return Collections.list(networkInterface.getInetAddresses()); - } - - protected InetAddress getBindAddressInSubnetOf(InetAddress inetAddress) { - synchronized (networkInterfaces) { - for (NetworkInterface iface : networkInterfaces) { - for (InterfaceAddress ifaceAddress : getInterfaceAddresses(iface)) { - - synchronized (bindAddresses) { - if (ifaceAddress == null || !bindAddresses.contains(ifaceAddress.getAddress())) { - continue; - } - } - - if (isInSubnet( - inetAddress.getAddress(), - ifaceAddress.getAddress().getAddress(), - ifaceAddress.getNetworkPrefixLength()) - ) { - return ifaceAddress.getAddress(); - } - } - - } - } - return null; - } - - protected boolean isInSubnet(byte[] ip, byte[] network, short prefix) { - if (ip.length != network.length) { - return false; - } - - if (prefix / 8 > ip.length) { - return false; - } - - int i = 0; - while (prefix >= 8 && i < ip.length) { - if (ip[i] != network[i]) { - return false; - } - i++; - prefix -= 8; - } - if (i == ip.length) return true; - final byte mask = (byte) ~((1 << 8 - prefix) - 1); - - return (ip[i] & mask) == (network[i] & mask); - } - - protected void discoverNetworkInterfaces() throws InitializationException { - try { - - Enumeration interfaceEnumeration = NetworkInterface.getNetworkInterfaces(); - for (NetworkInterface iface : Collections.list(interfaceEnumeration)) { - //displayInterfaceInformation(iface); - - Log.v(getClass().getName(), "Analyzing network interface: " + iface.getDisplayName()); - if (isUsableNetworkInterface(iface)) { - Log.v(getClass().getName(), "Discovered usable network interface: " + iface.getDisplayName()); - synchronized (networkInterfaces) { - networkInterfaces.add(iface); - } - } else { - Log.v(getClass().getName(), "Ignoring non-usable network interface: " + iface.getDisplayName()); - } - } - - } catch (Exception ex) { - throw new InitializationException("Could not not analyze local network interfaces: " + ex, ex); - } - } - - /** - * Validation of every discovered network interface. - *

- * Override this method to customize which network interfaces are used. - *

- *

- * The given implementation ignores interfaces which are - *

- *
    - *
  • loopback (yes, we do not bind to lo0)
  • - *
  • down
  • - *
  • have no bound IP addresses
  • - *
  • named "vmnet*" (OS X VMWare does not properly stop interfaces when it quits)
  • - *
  • named "vnic*" (OS X Parallels interfaces should be ignored as well)
  • - *
  • named "vboxnet*" (OS X Virtual Box interfaces should be ignored as well)
  • - *
  • named "*virtual*" (VirtualBox interfaces, for example
  • - *
  • named "ppp*"
  • - *
- * - * @param iface The interface to validate. - * @return True if the given interface matches all validation criteria. - * @throws Exception If any validation test failed with an un-recoverable error. - */ - protected boolean isUsableNetworkInterface(NetworkInterface iface) throws Exception { - if (!iface.isUp()) { - Log.v(getClass().getName(), "Skipping network interface (down): " + iface.getDisplayName()); - return false; - } - - if (getInetAddresses(iface).size() == 0) { - Log.v(getClass().getName(), "Skipping network interface without bound IP addresses: " + iface.getDisplayName()); - return false; - } - - if (iface.getName().toLowerCase(Locale.ROOT).startsWith("vmnet") || - (iface.getDisplayName() != null && iface.getDisplayName().toLowerCase(Locale.ROOT).contains("vmnet"))) { - Log.v(getClass().getName(), "Skipping network interface (VMWare): " + iface.getDisplayName()); - return false; - } - - if (iface.getName().toLowerCase(Locale.ROOT).startsWith("vnic")) { - Log.v(getClass().getName(), "Skipping network interface (Parallels): " + iface.getDisplayName()); - return false; - } - - if (iface.getName().toLowerCase(Locale.ROOT).startsWith("vboxnet")) { - Log.v(getClass().getName(), "Skipping network interface (Virtual Box): " + iface.getDisplayName()); - return false; - } - - if (iface.getName().toLowerCase(Locale.ROOT).contains("virtual")) { - Log.v(getClass().getName(), "Skipping network interface (named '*virtual*'): " + iface.getDisplayName()); - return false; - } - - if (iface.getName().toLowerCase(Locale.ROOT).startsWith("ppp")) { - Log.v(getClass().getName(), "Skipping network interface (PPP): " + iface.getDisplayName()); - return false; - } - - if (iface.getName().toLowerCase(Locale.ROOT).startsWith("rmnet")) { - Log.v(getClass().getName(), "Skipping network interface (rmnet): " + iface.getDisplayName()); - return false; - } - - if (iface.isLoopback()) { - Log.v(getClass().getName(), "Skipping network interface (ignoring loopback): " + iface.getDisplayName()); - return false; - } - - if (useInterfaces.size() > 0 && !useInterfaces.contains(iface.getName())) { - Log.v(getClass().getName(), "Skipping unwanted network interface (-D" + SYSTEM_PROPERTY_NET_IFACES + "): " + iface.getName()); - return false; - } - - if (!iface.supportsMulticast()) - Log.w(getClass().getName(), "Network interface may not be multicast capable: " + iface.getDisplayName()); - - return true; - } - - protected void discoverBindAddresses() throws InitializationException { - try { - - synchronized (networkInterfaces) { - Iterator it = networkInterfaces.iterator(); - while (it.hasNext()) { - NetworkInterface networkInterface = it.next(); - - Log.v(getClass().getName(), "Discovering addresses of interface: " + networkInterface.getDisplayName()); - int usableAddresses = 0; - for (InetAddress inetAddress : getInetAddresses(networkInterface)) { - if (inetAddress == null) { - Log.w(getClass().getName(), "Network has a null address: " + networkInterface.getDisplayName()); - continue; - } - - if (isUsableAddress(networkInterface, inetAddress)) { - Log.v(getClass().getName(), "Discovered usable network interface address: " + inetAddress.getHostAddress()); - usableAddresses++; - synchronized (bindAddresses) { - bindAddresses.add(inetAddress); - } - } else { - Log.v(getClass().getName(), "Ignoring non-usable network interface address: " + inetAddress.getHostAddress()); - } - } - - if (usableAddresses == 0) { - Log.v(getClass().getName(), "Network interface has no usable addresses, removing: " + networkInterface.getDisplayName()); - it.remove(); - } - } - } - - } catch (Exception ex) { - throw new InitializationException("Could not not analyze local network interfaces: " + ex, ex); - } - } - - /** - * Validation of every discovered local address. - *

- * Override this method to customize which network addresses are used. - *

- *

- * The given implementation ignores addresses which are - *

- *
    - *
  • not IPv4
  • - *
  • the local loopback (yes, we ignore 127.0.0.1)
  • - *
- * - * @param networkInterface The interface to validate. - * @param address The address of this interface to validate. - * @return True if the given address matches all validation criteria. - */ - protected boolean isUsableAddress(NetworkInterface networkInterface, InetAddress address) { - if (!(address instanceof Inet4Address)) { - Log.v(getClass().getName(), "Skipping unsupported non-IPv4 address: " + address); - return false; - } - - if (address.isLoopbackAddress()) { - Log.v(getClass().getName(), "Skipping loopback address: " + address); - return false; - } - - if (useAddresses.size() > 0 && !useAddresses.contains(address.getHostAddress())) { - Log.v(getClass().getName(), "Skipping unwanted address: " + address); - return false; - } - - return true; - } - - protected void logInterfaceInformation(NetworkInterface networkInterface) throws SocketException { - Log.v(getClass().getName(), "---------------------------------------------------------------------------------"); - Log.v(getClass().getName(), String.format("Interface display name: %s", networkInterface.getDisplayName())); - if (networkInterface.getParent() != null) - Log.v(getClass().getName(), String.format("Parent Info: %s", networkInterface.getParent())); - Log.v(getClass().getName(), String.format("Name: %s", networkInterface.getName())); - - Enumeration inetAddresses = networkInterface.getInetAddresses(); - - for (InetAddress inetAddress : Collections.list(inetAddresses)) { - Log.v(getClass().getName(), String.format("InetAddress: %s", inetAddress)); - } - - List interfaceAddresses = networkInterface.getInterfaceAddresses(); - - for (InterfaceAddress interfaceAddress : interfaceAddresses) { - if (interfaceAddress == null) { - Log.v(getClass().getName(), "Skipping null InterfaceAddress!"); - continue; - } - Log.v(getClass().getName(), " Interface Address"); - Log.v(getClass().getName(), " Address: " + interfaceAddress.getAddress()); - Log.v(getClass().getName(), " Broadcast: " + interfaceAddress.getBroadcast()); - Log.v(getClass().getName(), " Prefix length: " + interfaceAddress.getNetworkPrefixLength()); - } - - Enumeration subIfs = networkInterface.getSubInterfaces(); - - for (NetworkInterface subIf : Collections.list(subIfs)) { - if (subIf == null) { - Log.v(getClass().getName(), "Skipping null NetworkInterface sub-interface"); - continue; - } - Log.v(getClass().getName(), String.format("\tSub Interface Display name: %s", subIf.getDisplayName())); - Log.v(getClass().getName(), String.format("\tSub Interface Name: %s", subIf.getName())); - } - Log.v(getClass().getName(), String.format("Up? %s", networkInterface.isUp())); - Log.v(getClass().getName(), String.format("Loopback? %s", networkInterface.isLoopback())); - Log.v(getClass().getName(), String.format("PointToPoint? %s", networkInterface.isPointToPoint())); - Log.v(getClass().getName(), String.format("Supports multicast? %s", networkInterface.supportsMulticast())); - Log.v(getClass().getName(), String.format("Virtual? %s", networkInterface.isVirtual())); - Log.v(getClass().getName(), String.format("Hardware address: %s", Arrays.toString(networkInterface.getHardwareAddress()))); - Log.v(getClass().getName(), String.format("MTU: %s", networkInterface.getMTU())); - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/PullGENAEventProcessorImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/PullGENAEventProcessorImpl.java index 3a788d5f..ff22ae9e 100644 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/PullGENAEventProcessorImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/transport/impl/PullGENAEventProcessorImpl.java @@ -15,7 +15,7 @@ package org.fourthline.cling.transport.impl; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.UnsupportedDataException; import org.fourthline.cling.model.message.gena.IncomingEventRequestMessage; @@ -43,10 +43,10 @@ public class PullGENAEventProcessorImpl extends GENAEventProcessorImpl { public void readBody(IncomingEventRequestMessage requestMessage) throws UnsupportedDataException { - Log.v(getClass().getName(), "Reading body of: " + requestMessage); - Log.v(getClass().getName(), "===================================== GENA BODY BEGIN ============================================"); - Log.v(getClass().getName(), requestMessage.getBody() != null ? requestMessage.getBody().toString() : null); - Log.v(getClass().getName(), "-===================================== GENA BODY END ============================================"); + YaaccLogger.v(getClass().getName(), "Reading body of: " + requestMessage); + YaaccLogger.v(getClass().getName(), "===================================== GENA BODY BEGIN ============================================"); + YaaccLogger.v(getClass().getName(), requestMessage.getBody() != null ? requestMessage.getBody().toString() : null); + YaaccLogger.v(getClass().getName(), "-===================================== GENA BODY END ============================================"); String body = getMessageBody(requestMessage); @@ -80,7 +80,7 @@ protected void readProperty(XmlPullParser xpp, IncomingEventRequestMessage messa String stateVariableName = xpp.getName(); for (StateVariable stateVariable : stateVariables) { if (stateVariable.getName().equals(stateVariableName)) { - Log.v(getClass().getName(), "Reading state variable value: " + stateVariableName); + YaaccLogger.v(getClass().getName(), "Reading state variable value: " + stateVariableName); String value = xpp.nextText(); message.getStateVariableValues().add(new StateVariableValue(stateVariable, value)); break; diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/PullSOAPActionProcessorImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/PullSOAPActionProcessorImpl.java index 3ed34281..5c28176c 100644 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/PullSOAPActionProcessorImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/transport/impl/PullSOAPActionProcessorImpl.java @@ -15,7 +15,7 @@ package org.fourthline.cling.transport.impl; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.UnsupportedDataException; import org.fourthline.cling.model.action.ActionArgumentValue; @@ -169,7 +169,7 @@ protected ActionArgumentValue[] readArgumentValues(XmlPullParser xpp, ActionArgu "Could not find argument '" + arg.getName() + "' node"); } - Log.v(getClass().getName(), "Reading action argument: " + arg.getName() + " value: " + value); + YaaccLogger.v(getClass().getName(), "Reading action argument: " + arg.getName() + " value: " + value); values[i] = createValue(arg, value); } return values; @@ -209,10 +209,10 @@ protected ActionException readFaultElement(XmlPullParser xpp) throws Exception { int numericCode = Integer.valueOf(errorCode); ErrorCode standardErrorCode = ErrorCode.getByCode(numericCode); if (standardErrorCode != null) { - Log.v(getClass().getName(), "Reading fault element: " + standardErrorCode.getCode() + " - " + errorDescription); + YaaccLogger.v(getClass().getName(), "Reading fault element: " + standardErrorCode.getCode() + " - " + errorDescription); return new ActionException(standardErrorCode, errorDescription, false); } else { - Log.v(getClass().getName(), "Reading fault element: " + numericCode + " - " + errorDescription); + YaaccLogger.v(getClass().getName(), "Reading fault element: " + numericCode + " - " + errorDescription); return new ActionException(numericCode, errorDescription); } } catch (NumberFormatException ex) { diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/RecoveringGENAEventProcessorImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/RecoveringGENAEventProcessorImpl.java index 3a794997..d655f488 100644 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/RecoveringGENAEventProcessorImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/transport/impl/RecoveringGENAEventProcessorImpl.java @@ -15,7 +15,7 @@ package org.fourthline.cling.transport.impl; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.UnsupportedDataException; import org.fourthline.cling.model.XMLUtil; @@ -61,7 +61,7 @@ public void readBody(IncomingEventRequestMessage requestMessage) throws Unsuppor if (!requestMessage.isBodyNonEmptyString()) throw ex; - Log.w(getClass().getName(), "Trying to recover from invalid GENA XML event: " + ex); + YaaccLogger.w(getClass().getName(), "Trying to recover from invalid GENA XML event: " + ex); // Some properties may have been read at this point, so reset the list requestMessage.getStateVariableValues().clear(); @@ -82,7 +82,7 @@ public void readBody(IncomingEventRequestMessage requestMessage) throws Unsuppor // Throw the initial exception containing unmodified XML throw ex; } - Log.w(getClass().getName(), "Partial read of GENA event properties (probably due to truncated XML)"); + YaaccLogger.w(getClass().getName(), "Partial read of GENA event properties (probably due to truncated XML)"); } } } diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/RecoveringSOAPActionProcessorImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/RecoveringSOAPActionProcessorImpl.java index e15fca9b..004c2851 100644 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/RecoveringSOAPActionProcessorImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/transport/impl/RecoveringSOAPActionProcessorImpl.java @@ -15,7 +15,7 @@ package org.fourthline.cling.transport.impl; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.UnsupportedDataException; import org.fourthline.cling.model.action.ActionInvocation; @@ -61,7 +61,7 @@ public void readBody(ActionRequestMessage requestMessage, ActionInvocation actio if (!requestMessage.isBodyNonEmptyString()) throw ex; - Log.w(getClass().getName(), "Trying to recover from invalid SOAP XML request: " + ex); + YaaccLogger.w(getClass().getName(), "Trying to recover from invalid SOAP XML request: " + ex); String body = getMessageBody(requestMessage); // TODO: UPNP VIOLATION: TwonkyMobile sends unencoded '&' in SetAVTransportURI action calls: @@ -87,7 +87,7 @@ public void readBody(ActionResponseMessage responseMsg, ActionInvocation actionI if (!responseMsg.isBodyNonEmptyString()) throw ex; - Log.w(getClass().getName(), "Trying to recover from invalid SOAP XML response: " + ex); + YaaccLogger.w(getClass().getName(), "Trying to recover from invalid SOAP XML response: " + ex); String body = getMessageBody(responseMsg); // TODO: UPNP VIOLATION: TwonkyMobile doesn't properly encode '&' diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/impl/SOAPActionProcessorImpl.java b/yaacc/src/main/java/org/fourthline/cling/transport/impl/SOAPActionProcessorImpl.java index 8416c04c..d3ec5ff5 100644 --- a/yaacc/src/main/java/org/fourthline/cling/transport/impl/SOAPActionProcessorImpl.java +++ b/yaacc/src/main/java/org/fourthline/cling/transport/impl/SOAPActionProcessorImpl.java @@ -15,7 +15,7 @@ package org.fourthline.cling.transport.impl; -import android.util.Log; +import de.yaacc.util.YaaccLogger; import org.fourthline.cling.model.Constants; import org.fourthline.cling.model.UnsupportedDataException; @@ -62,7 +62,7 @@ protected DocumentBuilderFactory createDocumentBuilderFactory() throws FactoryCo public void writeBody(ActionRequestMessage requestMessage, ActionInvocation actionInvocation) throws UnsupportedDataException { - Log.v(getClass().getName(), "Writing body of " + requestMessage + " for: " + actionInvocation); + YaaccLogger.v(getClass().getName(), "Writing body of " + requestMessage + " for: " + actionInvocation); try { @@ -73,9 +73,9 @@ public void writeBody(ActionRequestMessage requestMessage, ActionInvocation acti writeBodyRequest(d, body, requestMessage, actionInvocation); - Log.v(getClass().getName(), "===================================== SOAP BODY (Request) BEGIN ============================================"); - Log.v(getClass().getName(), requestMessage.getBodyString()); - Log.v(getClass().getName(), "-===================================== SOAP BODY END ============================================"); + YaaccLogger.v(getClass().getName(), "===================================== SOAP BODY (Request) BEGIN ============================================"); + YaaccLogger.v(getClass().getName(), requestMessage.getBodyString()); + YaaccLogger.v(getClass().getName(), "-===================================== SOAP BODY END ============================================"); } catch (Exception ex) { @@ -85,7 +85,7 @@ public void writeBody(ActionRequestMessage requestMessage, ActionInvocation acti public void writeBody(ActionResponseMessage responseMessage, ActionInvocation actionInvocation) throws UnsupportedDataException { - Log.v(getClass().getName(), "Writing body of " + responseMessage + " for: " + actionInvocation); + YaaccLogger.v(getClass().getName(), "Writing body of " + responseMessage + " for: " + actionInvocation); try { @@ -100,9 +100,9 @@ public void writeBody(ActionResponseMessage responseMessage, ActionInvocation ac writeBodyResponse(d, body, responseMessage, actionInvocation); } - Log.v(getClass().getName(), "===================================== SOAP BODY (Response) BEGIN ============================================"); - Log.v(getClass().getName(), responseMessage.getBodyString()); - Log.v(getClass().getName(), "-===================================== SOAP BODY END ============================================"); + YaaccLogger.v(getClass().getName(), "===================================== SOAP BODY (Response) BEGIN ============================================"); + YaaccLogger.v(getClass().getName(), responseMessage.getBodyString()); + YaaccLogger.v(getClass().getName(), "-===================================== SOAP BODY END ============================================"); } catch (Exception ex) { throw new UnsupportedDataException("Can't transform message payload: " + ex, ex); @@ -111,10 +111,10 @@ public void writeBody(ActionResponseMessage responseMessage, ActionInvocation ac public void readBody(ActionRequestMessage requestMessage, ActionInvocation actionInvocation) throws UnsupportedDataException { - Log.v(getClass().getName(), "Reading body of " + requestMessage + " for: " + actionInvocation); - Log.v(getClass().getName(), "===================================== SOAP BODY (Request) BEGIN ============================================"); - Log.v(getClass().getName(), requestMessage.getBodyString()); - Log.v(getClass().getName(), "-===================================== SOAP BODY END ============================================"); + YaaccLogger.v(getClass().getName(), "Reading body of " + requestMessage + " for: " + actionInvocation); + YaaccLogger.v(getClass().getName(), "===================================== SOAP BODY (Request) BEGIN ============================================"); + YaaccLogger.v(getClass().getName(), requestMessage.getBodyString()); + YaaccLogger.v(getClass().getName(), "-===================================== SOAP BODY END ============================================"); String body = getMessageBody(requestMessage); @@ -138,10 +138,10 @@ public void readBody(ActionRequestMessage requestMessage, ActionInvocation actio public void readBody(ActionResponseMessage responseMsg, ActionInvocation actionInvocation) throws UnsupportedDataException { - Log.v(getClass().getName(), "Reading body of " + responseMsg + " for: " + actionInvocation); - Log.v(getClass().getName(), "===================================== SOAP BODY (Response) BEGIN ============================================"); - Log.v(getClass().getName(), responseMsg.getBodyString()); - Log.v(getClass().getName(), "-===================================== SOAP BODY END ============================================"); + YaaccLogger.v(getClass().getName(), "Reading body of " + responseMsg + " for: " + actionInvocation); + YaaccLogger.v(getClass().getName(), "===================================== SOAP BODY (Response) BEGIN ============================================"); + YaaccLogger.v(getClass().getName(), responseMsg.getBodyString()); + YaaccLogger.v(getClass().getName(), "-===================================== SOAP BODY END ============================================"); String body = getMessageBody(responseMsg); @@ -269,7 +269,7 @@ protected Element writeActionRequestElement(Document d, ActionRequestMessage message, ActionInvocation actionInvocation) { - Log.v(getClass().getName(), "Writing action request element: " + actionInvocation.getAction().getName()); + YaaccLogger.v(getClass().getName(), "Writing action request element: " + actionInvocation.getAction().getName()); Element actionRequestElement = d.createElementNS( message.getActionNamespace(), @@ -285,7 +285,7 @@ protected Element readActionRequestElement(Element bodyElement, ActionInvocation actionInvocation) { NodeList bodyChildren = bodyElement.getChildNodes(); - Log.v(getClass().getName(), "Looking for action request element matching namespace:" + message.getActionNamespace()); + YaaccLogger.v(getClass().getName(), "Looking for action request element matching namespace:" + message.getActionNamespace()); for (int i = 0; i < bodyChildren.getLength(); i++) { Node bodyChild = bodyChildren.item(i); @@ -300,7 +300,7 @@ protected Element readActionRequestElement(Element bodyElement, throw new UnsupportedDataException( "Illegal or missing namespace on action request element: " + bodyChild ); - Log.v(getClass().getName(), "Reading action request element: " + unprefixedName); + YaaccLogger.v(getClass().getName(), "Reading action request element: " + unprefixedName); return (Element) bodyChild; } } @@ -316,7 +316,7 @@ protected Element writeActionResponseElement(Document d, ActionResponseMessage message, ActionInvocation actionInvocation) { - Log.v(getClass().getName(), "Writing action response element: " + actionInvocation.getAction().getName()); + YaaccLogger.v(getClass().getName(), "Writing action response element: " + actionInvocation.getAction().getName()); Element actionResponseElement = d.createElementNS( message.getActionNamespace(), "u:" + actionInvocation.getAction().getName() + "Response" @@ -336,11 +336,11 @@ protected Element readActionResponseElement(Element bodyElement, ActionInvocatio continue; if (getUnprefixedNodeName(bodyChild).equals(actionInvocation.getAction().getName() + "Response")) { - Log.v(getClass().getName(), "Reading action response element: " + getUnprefixedNodeName(bodyChild)); + YaaccLogger.v(getClass().getName(), "Reading action response element: " + getUnprefixedNodeName(bodyChild)); return (Element) bodyChild; } } - Log.v(getClass().getName(), "Could not read action response element"); + YaaccLogger.v(getClass().getName(), "Could not read action response element"); return null; } @@ -352,7 +352,7 @@ protected void writeActionInputArguments(Document d, for (ActionArgument argument : actionInvocation.getAction().getInputArguments()) { String value = actionInvocation.getInput(argument) != null ? actionInvocation.getInput(argument).toString() : ""; - Log.v(getClass().getName(), "Writing action input argument: " + argument.getName() + " value: " + value); + YaaccLogger.v(getClass().getName(), "Writing action input argument: " + argument.getName() + " value: " + value); XMLUtil.appendNewElement(d, actionRequestElement, argument.getName(), value); } } @@ -375,7 +375,7 @@ protected void writeActionOutputArguments(Document d, for (ActionArgument argument : actionInvocation.getAction().getOutputArguments()) { String value = actionInvocation.getOutput(argument) != null ? actionInvocation.getOutput(argument).toString() : ""; - Log.v(getClass().getName(), "Writing action output argument: " + argument.getName() + " value: " + value); + YaaccLogger.v(getClass().getName(), "Writing action output argument: " + argument.getName() + " value: " + value); XMLUtil.appendNewElement(d, actionResponseElement, argument.getName(), value); } } @@ -411,7 +411,7 @@ protected void writeFaultElement(Document d, Element bodyElement, ActionInvocati int errorCode = actionInvocation.getFailure().getErrorCode(); String errorDescription = actionInvocation.getFailure().getMessage(); - Log.v(getClass().getName(), "Writing fault element: " + errorCode + " - " + errorDescription); + YaaccLogger.v(getClass().getName(), "Writing fault element: " + errorCode + " - " + errorDescription); XMLUtil.appendNewElement(d, upnpErrorElement, "errorCode", Integer.toString(errorCode)); XMLUtil.appendNewElement(d, upnpErrorElement, "errorDescription", errorDescription); @@ -480,10 +480,10 @@ protected ActionException readFaultElement(Element bodyElement) { int numericCode = Integer.valueOf(errorCode); ErrorCode standardErrorCode = ErrorCode.getByCode(numericCode); if (standardErrorCode != null) { - Log.v(getClass().getName(), "Reading fault element: " + standardErrorCode.getCode() + " - " + errorDescription); + YaaccLogger.v(getClass().getName(), "Reading fault element: " + standardErrorCode.getCode() + " - " + errorDescription); return new ActionException(standardErrorCode, errorDescription, false); } else { - Log.v(getClass().getName(), "Reading fault element: " + numericCode + " - " + errorDescription); + YaaccLogger.v(getClass().getName(), "Reading fault element: " + numericCode + " - " + errorDescription); return new ActionException(numericCode, errorDescription); } } catch (NumberFormatException ex) { @@ -547,7 +547,7 @@ protected ActionArgumentValue[] readArgumentValues(NodeList nodeList, ActionArgu } String value = XMLUtil.getTextContent(node); - Log.v(getClass().getName(), "Reading action argument: " + arg.getName() + " value: " + value); + YaaccLogger.v(getClass().getName(), "Reading action argument: " + arg.getName() + " value: " + value); values[i] = createValue(arg, value); } return values; @@ -614,7 +614,7 @@ protected Node findActionArgumentNode(List nodes, ActionArgument arg) { } public void warning(SAXParseException e) throws SAXException { - Log.w(getClass().getName(), e.toString()); + YaaccLogger.w(getClass().getName(), e.toString()); } public void error(SAXParseException e) throws SAXException { diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/AbstractStreamClient.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/AbstractStreamClient.java deleted file mode 100644 index af2bccd5..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/AbstractStreamClient.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ -package org.fourthline.cling.transport.spi; - -import android.util.Log; - -import org.fourthline.cling.model.message.StreamRequestMessage; -import org.fourthline.cling.model.message.StreamResponseMessage; -import org.seamless.util.Exceptions; - -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -/** - * Implements the timeout/callback processing and unifies exception handling. - * - * @author Christian Bauer - */ -public abstract class AbstractStreamClient implements StreamClient { - - - @Override - public StreamResponseMessage sendRequest(StreamRequestMessage requestMessage) throws InterruptedException { - - - Log.v(getClass().getName(), "Preparing HTTP request: " + requestMessage); - Log.v(getClass().getName(), "HTTP body: " + requestMessage.getBodyString()); - - REQUEST request = createRequest(requestMessage); - if (request == null) - return null; - - Callable callable = createCallable(requestMessage, request); - - // We want to track how long it takes - long start = System.currentTimeMillis(); - - // Execute the request on a new thread - Future future = - getConfiguration().getRequestExecutorService().submit(callable); - - // Wait on the current thread for completion - try { - Log.v(getClass().getName(), - "Waiting " + getConfiguration().getTimeoutSeconds() - + " seconds for HTTP request to complete: " + requestMessage - ); - StreamResponseMessage response = - future.get(getConfiguration().getTimeoutSeconds(), TimeUnit.SECONDS); - - // Log a warning if it took too long - long elapsed = System.currentTimeMillis() - start; - - Log.v(getClass().getName(), "Got HTTP response in " + elapsed + "ms: " + requestMessage); - if (getConfiguration().getLogWarningSeconds() > 0 - && elapsed > getConfiguration().getLogWarningSeconds() * 1000) { - Log.w(getClass().getName(), "HTTP request took a long time (" + elapsed + "ms): " + requestMessage); - } - - return response; - - } catch (InterruptedException ex) { - - - Log.v(getClass().getName(), "Interruption, aborting request: " + requestMessage); - abort(request, "Interruption, aborting request: " + requestMessage); - throw new InterruptedException("HTTP request interrupted and aborted"); - - } catch (TimeoutException ex) { - - Log.v(getClass().getName(), - "Timeout of " + getConfiguration().getTimeoutSeconds() - + " seconds while waiting for HTTP request to complete, aborting: " + requestMessage - ); - abort(request, "Timeout of " + getConfiguration().getTimeoutSeconds() - + " seconds while waiting for HTTP request to complete, aborting: " + requestMessage); - - return null; - - } catch (ExecutionException ex) { - Throwable cause = ex.getCause(); - if (!logExecutionException(cause)) { - Log.w(getClass().getName(), "HTTP request failed: " + requestMessage, Exceptions.unwrap(cause)); - } - return null; - } finally { - onFinally(request); - } - } - - /** - * Create a proprietary representation of this request, log warnings and - * return null if creation fails. - */ - abstract protected REQUEST createRequest(StreamRequestMessage requestMessage); - - /** - * Create a callable procedure that will execute the request. - */ - abstract protected Callable createCallable(StreamRequestMessage requestMessage, - REQUEST request); - - /** - * Cancel and abort the request immediately, with the proprietary API. - */ - abstract protected boolean abort(REQUEST request, String reason); - - /** - * @return true if no more logging of this exception should be done. - */ - abstract protected boolean logExecutionException(Throwable t); - - protected void onFinally(REQUEST request) { - // Do nothing - } - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/AbstractStreamClientConfiguration.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/AbstractStreamClientConfiguration.java deleted file mode 100644 index 9eb93a02..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/AbstractStreamClientConfiguration.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ -package org.fourthline.cling.transport.spi; - -import org.fourthline.cling.model.ServerClientTokens; - -import java.util.concurrent.ExecutorService; - -/** - * @author Christian Bauer - */ -public abstract class AbstractStreamClientConfiguration implements StreamClientConfiguration { - - protected ExecutorService requestExecutorService; - protected int timeoutSeconds = 60; - protected int logWarningSeconds = 5; - - protected AbstractStreamClientConfiguration(ExecutorService requestExecutorService) { - this.requestExecutorService = requestExecutorService; - } - - protected AbstractStreamClientConfiguration(ExecutorService requestExecutorService, int timeoutSeconds) { - this.requestExecutorService = requestExecutorService; - this.timeoutSeconds = timeoutSeconds; - } - - protected AbstractStreamClientConfiguration(ExecutorService requestExecutorService, int timeoutSeconds, int logWarningSeconds) { - this.requestExecutorService = requestExecutorService; - this.timeoutSeconds = timeoutSeconds; - this.logWarningSeconds = logWarningSeconds; - } - - public ExecutorService getRequestExecutorService() { - return requestExecutorService; - } - - public void setRequestExecutorService(ExecutorService requestExecutorService) { - this.requestExecutorService = requestExecutorService; - } - - /** - * @return Configured value or default of 60 seconds. - */ - public int getTimeoutSeconds() { - return timeoutSeconds; - } - - public void setTimeoutSeconds(int timeoutSeconds) { - this.timeoutSeconds = timeoutSeconds; - } - - /** - * @return Configured value or default of 5 seconds. - */ - public int getLogWarningSeconds() { - return logWarningSeconds; - } - - public void setLogWarningSeconds(int logWarningSeconds) { - this.logWarningSeconds = logWarningSeconds; - } - - /** - * @return Defaults to string value of {@link org.fourthline.cling.model.ServerClientTokens}. - */ - public String getUserAgentValue(int majorVersion, int minorVersion) { - return new ServerClientTokens(majorVersion, minorVersion).toString(); - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/DatagramIO.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/DatagramIO.java deleted file mode 100644 index 1f90c6d7..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/DatagramIO.java +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.spi; - -import org.fourthline.cling.transport.Router; -import org.fourthline.cling.model.message.OutgoingDatagramMessage; - -import java.net.InetAddress; -import java.net.DatagramPacket; - -/** - * Service for receiving (unicast only) and sending UDP datagrams, one per bound IP address. - *

- * This service typically listens on a socket for UDP unicast datagrams, with - * an ephemeral port. - *

- *

- * This listening loop is started with the run() method, - * this service is Runnable. Any received datagram is then converted into an - * {@link org.fourthline.cling.model.message.IncomingDatagramMessage} and - * handled by the - * {@link org.fourthline.cling.transport.Router#received(org.fourthline.cling.model.message.IncomingDatagramMessage)} - * method. This conversion is the job of the {@link org.fourthline.cling.transport.spi.DatagramProcessor}. - *

- *

- * Clients of this service use it to send UDP datagrams, either to a unicast - * or multicast destination. Any {@link org.fourthline.cling.model.message.OutgoingDatagramMessage} can - * be converted and written into a datagram with the {@link org.fourthline.cling.transport.spi.DatagramProcessor}. - *

- *

- * An implementation has to be thread-safe. - *

- * - * @param The type of the service's configuration. - * - * @author Christian Bauer - */ -public interface DatagramIO extends Runnable { - - /** - * Configures the service and starts any listening sockets. - * - * @param bindAddress The address to bind any sockets on. - * @param router The router which handles received {@link org.fourthline.cling.model.message.IncomingDatagramMessage}s. - * @param datagramProcessor Reads and writes datagrams. - * @throws InitializationException If the service could not be initialized or started. - */ - public void init(InetAddress bindAddress, Router router, DatagramProcessor datagramProcessor) throws InitializationException; - - /** - * Stops the service, closes any listening sockets. - */ - public void stop(); - - /** - * @return This service's configuration. - */ - public C getConfiguration(); - - /** - * Sends a datagram after conversion with {@link org.fourthline.cling.transport.spi.DatagramProcessor#write(org.fourthline.cling.model.message.OutgoingDatagramMessage)}. - * - * @param message The message to send. - */ - public void send(OutgoingDatagramMessage message); - - /** - * The actual sending of a UDP datagram. - *

- * Recoverable errors should be logged, if appropriate only with debug level. Any - * non-recoverable errors should be thrown as RuntimeExceptions. - *

- * - * @param datagram The UDP datagram to send. - */ - public void send(DatagramPacket datagram); -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/DatagramIOConfiguration.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/DatagramIOConfiguration.java deleted file mode 100644 index cf7718a8..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/DatagramIOConfiguration.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.spi; - -/** - * Collection of typically needed configuration settings. - * - * @author Christian Bauer - */ -public interface DatagramIOConfiguration { - - /** - * @return The TTL of a UDP datagram sent to a multicast address. - */ - public int getTimeToLive(); - - /** - * @return The maximum buffer size of received UDP datagrams. - */ - public int getMaxDatagramBytes(); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/InitializationException.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/InitializationException.java deleted file mode 100644 index b583ec83..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/InitializationException.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.spi; - -/** - * Thrown by the transport layer implementation when service setup fails. - *

- * This exception typically indicates a configuration problem and it is not - * recoverable unless you can continue without the service that threw this - * exception. - *

- * - * @author Christian Bauer - */ -public class InitializationException extends RuntimeException { - - public InitializationException(String s) { - super(s); - } - - public InitializationException(String s, Throwable throwable) { - super(s, throwable); - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/MulticastReceiver.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/MulticastReceiver.java deleted file mode 100644 index ffa5ef45..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/MulticastReceiver.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.spi; - -import org.fourthline.cling.transport.Router; - -import java.net.NetworkInterface; - -/** - * Service for receiving multicast UDP datagrams, one per bound network interface. - *

- * This services typically listens on a socket for UDP datagrams, the socket has joined - * the configured multicast group. - *

- *

- * This listening loop is started with the run() method, - * this service is Runnable. Any received datagram is then converted into an - * {@link org.fourthline.cling.model.message.IncomingDatagramMessage} and - * handled by the - * {@link org.fourthline.cling.transport.Router#received(org.fourthline.cling.model.message.IncomingDatagramMessage)} - * method. This conversion is the job of the {@link org.fourthline.cling.transport.spi.DatagramProcessor}. - *

- *

- * An implementation has to be thread-safe. - *

- * - * @param The type of the service's configuration. - * - * @author Christian Bauer - */ -public interface MulticastReceiver extends Runnable { - - /** - * Configures the service and starts any listening sockets. - * - * @param networkInterface The network interface on which to join the multicast group on. - * @param router The router which handles received {@link org.fourthline.cling.model.message.IncomingDatagramMessage}s. - * @param networkAddressFactory The network address factory to use for local address lookup given a local interface and a remote address. - * @param datagramProcessor Reads and writes datagrams. - * @throws InitializationException If the service could not be initialized or started. - */ - public void init(NetworkInterface networkInterface, - Router router, - NetworkAddressFactory networkAddressFactory, - DatagramProcessor datagramProcessor) throws InitializationException; - - /** - * Stops the service, closes any listening sockets. - */ - public void stop(); - - /** - * @return This service's configuration. - */ - public C getConfiguration(); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/MulticastReceiverConfiguration.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/MulticastReceiverConfiguration.java deleted file mode 100644 index 2c1cff7e..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/MulticastReceiverConfiguration.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.spi; - -import java.net.InetAddress; - -/** - * Collection of typically needed configuration settings. - * - * @author Christian Bauer - */ -public interface MulticastReceiverConfiguration { - - /** - * @return The multicast group to join. - */ - public InetAddress getGroup(); - - /** - * @return The port to listen on. - */ - public int getPort(); - - /** - * @return The maximum buffer size of received UDP datagrams. - */ - public int getMaxDatagramBytes(); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/NetworkAddressFactory.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/NetworkAddressFactory.java deleted file mode 100644 index 88ee5126..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/NetworkAddressFactory.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.spi; - -import java.net.InetAddress; -import java.net.NetworkInterface; -import java.util.Iterator; - -/** - * Configuration utility for network interfaces and addresses. - *

- * An implementation has to be thread-safe. - *

- * - * @author Christian Bauer - */ -public interface NetworkAddressFactory { - - // An implementation can honor these if it wants (the default does) - public static final String SYSTEM_PROPERTY_NET_IFACES = "org.fourthline.cling.network.useInterfaces"; - public static final String SYSTEM_PROPERTY_NET_ADDRESSES = "org.fourthline.cling.network.useAddresses"; - - - /** - * @return The UDP multicast group to join. - */ - public InetAddress getMulticastGroup(); - - /** - * @return The UDP multicast port to listen on. - */ - public int getMulticastPort(); - - /** - * @return The TCP (HTTP) stream request port to listen on. - */ - public int getStreamListenPort(); - - /** - * The caller might remove() an interface if initialization fails. - * - * @return The local network interfaces on which multicast groups will be joined. - */ - public Iterator getNetworkInterfaces(); - - /** - * The caller might remove() an address if initialization fails. - * - * @return The local addresses of the network interfaces bound to - * sockets listening for unicast datagrams and TCP requests. - */ - public Iterator getBindAddresses(); - - /** - * @return true if there is at least one usable network interface and bind address. - */ - public boolean hasUsableNetwork(); - - /** - * @return The network prefix length of this address or null. - */ - public Short getAddressNetworkPrefixLength(InetAddress inetAddress); - - /** - * @param inetAddress An address of a local network interface. - * @return The MAC hardware address of the network interface or null if no - * hardware address could be obtained. - */ - public byte[] getHardwareAddress(InetAddress inetAddress); - - /** - * @param inetAddress An address of a local network interface. - * @return The broadcast address of the network (interface) or null if no - * broadcast address could be obtained. - */ - public InetAddress getBroadcastAddress(InetAddress inetAddress); - - /** - * Best-effort attempt finding a reachable local address for a given remote host. - *

- * This method is called whenever a multicast datagram has been received. We need to be - * able to communicate with the sender using UDP unicast and we need to tell the sender - * how we are reachable with TCP requests. We need a local address that is in the same - * subnet as the senders address, that is reachable from the senders point of view. - *

- * - * @param networkInterface The network interface to examine. - * @param isIPv6 True if the given remote address is an IPv6 address. - * @param remoteAddress The remote address for which to find a local address in the same subnet. - * @return A local address that is reachable from the given remote address. - * @throws IllegalStateException If no local address reachable by the remote address has been found. - */ - public InetAddress getLocalAddress(NetworkInterface networkInterface, - boolean isIPv6, - InetAddress remoteAddress) throws IllegalStateException; - - /** - * For debugging, logs all "usable" network interface(s) details with INFO level. - */ - public void logInterfaceInformation(); -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/NoNetworkException.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/NoNetworkException.java deleted file mode 100644 index 582f6545..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/NoNetworkException.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ -package org.fourthline.cling.transport.spi; - -/** - * Might be thrown by the constructor of {@link NetworkAddressFactory} and - * {@link org.fourthline.cling.transport.Router} if no usable - * network interfaces/addresses were discovered. - * - * @author Christian Bauer - */ -public class NoNetworkException extends InitializationException { - - public NoNetworkException(String s) { - super(s); - } -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamClient.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamClient.java deleted file mode 100644 index ca5e6168..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamClient.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.spi; - -import org.fourthline.cling.model.message.StreamRequestMessage; -import org.fourthline.cling.model.message.StreamResponseMessage; - -/** - * Service for sending TCP (HTTP) stream request messages. - * - *

- * An implementation has to be thread-safe. - * Its constructor may throw {@link org.fourthline.cling.transport.spi.InitializationException}. - *

- * - * @param The type of the service's configuration. - * - * @author Christian Bauer - */ -public interface StreamClient { - - /** - * Sends the given request via TCP (HTTP) and returns the response. - * - *

- * This method must implement expiration of timed out requests using the - * {@link StreamClientConfiguration} settings. When a request expires, a - * null response will be returned. - *

- *

- * This method will always try to complete execution without throwing an exception. It will - * return null if an error occurs, and optionally log any exception messages. - *

- *

- * The rules for logging are: - *

- *
    - *
  • If the caller interrupts the calling thread, log at FINE.
  • - *
  • If the request expires because the timeout has been reached, log at INFO level.
  • - *
  • If another error occurs, log at WARNING level
  • - *
- *

- * This method is required to add a Host HTTP header to the - * outgoing HTTP request, even if the given - * {@link org.fourthline.cling.model.message.StreamRequestMessage} does not contain such a header. - *

- *

- * This method will add the User-Agent HTTP header to the outgoing HTTP request if - * the given message did not already contain such a header. You can set this default value in your - * {@link StreamClientConfiguration}. - *

- * - * @param message The message to send. - * @return The response or null if no response has been received or an error occurred. - * @throws InterruptedException if you interrupt the calling thread. - */ - public StreamResponseMessage sendRequest(StreamRequestMessage message) throws InterruptedException; - - /** - * Stops the service, closes any connection pools etc. - */ - public void stop(); - - /** - * @return This service's configuration. - */ - public C getConfiguration(); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamClientConfiguration.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamClientConfiguration.java deleted file mode 100644 index 81c324fc..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamClientConfiguration.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.spi; - -import java.util.concurrent.ExecutorService; - -/** - * Collection of typically needed configuration settings. - * - * @author Christian Bauer - */ -public interface StreamClientConfiguration { - - /** - * Used to execute the actual HTTP request, the StreamClient waits on the "current" thread for - * completion or timeout. You probably want to use the same executor service for both, so usually - * this is {@link org.fourthline.cling.UpnpServiceConfiguration#getSyncProtocolExecutorService()}. - * - * @return The ExecutorService to use for actual sending of HTTP requests. - */ - public ExecutorService getRequestExecutorService(); - - /** - * @return The number of seconds to wait for a request to expire, spanning connect and data-reads. - */ - public int getTimeoutSeconds(); - - /** - * @return If the request completion takes longer than this, a warning will be logged (0 to disable) - */ - public int getLogWarningSeconds(); - - /** - * Used for outgoing HTTP requests if no other value was already set on messages. - * - * @param majorVersion The UPnP UDA major version. - * @param minorVersion The UPnP UDA minor version. - * @return The HTTP user agent value. - */ - public String getUserAgentValue(int majorVersion, int minorVersion); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamServer.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamServer.java deleted file mode 100644 index 11594c91..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamServer.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.spi; - -import org.fourthline.cling.transport.Router; - -import java.net.InetAddress; - -/** - * Service for receiving TCP (HTTP) streams, one per bound IP address. - *

- * This service typically listens on a socket for TCP connections. - *

- * This listening loop is started with the run() method, this service is - * Runnable. Then {@link Router#received(UpnpStream)} is called with a custom - * {@link UpnpStream}. This will start processing of the request and run() the - * {@link UpnpStream} (which is also Runnable) in a separate thread, - * freeing up the receiving thread immediately. - *

- *

- * The {@link UpnpStream} then creates a {@link org.fourthline.cling.model.message.StreamRequestMessage} - * and calls the {@link UpnpStream#process(org.fourthline.cling.model.message.StreamRequestMessage)} - * method. The {@link UpnpStream} then returns the response to the network client. - *

- *

- * In pseudo-code: - *

- *
- * MyStreamServer implements StreamServer {
- *      run() {
- *          while (not stopped) {
- *              Connection con = listenToSocketAndBlock();
- *              router.received( new MyUpnpStream(con) );
- *          }
- *      }
- * }
- *
- * MyUpnpStream(con) extends UpnpStream {
- *      run() {
- *          try {
- *              StreamRequestMessage request = // ... Read request
- *              StreamResponseMessage response = process(request);
- *              // ... Send response
- *              responseSent(response))
- *          } catch (Exception ex) {
- *              responseException(ex);
- *          }
- *      }
- * }
- * 
- *

- * An implementation has to be thread-safe. - *

- * - * @param The type of the service's configuration. - * - * @author Christian Bauer - */ -public interface StreamServer extends Runnable { - - /** - * Configures the service and starts any listening sockets. - * - * @param bindAddress The address to bind any sockets on. - * @param router The router which handles the incoming {@link org.fourthline.cling.transport.spi.UpnpStream}. - * @throws InitializationException If the service could not be initialized or started. - */ - public void init(InetAddress bindAddress, Router router) throws InitializationException; - - /** - * This method will be called potentially right after - * {@link #init(java.net.InetAddress, org.fourthline.cling.transport.Router)}, the - * actual assigned local port must be available before the server is started. - * - * @return The TCP port this service is listening on, e.g. the actual ephemeral port. - */ - public int getPort(); - - /** - * Stops the service, closes any listening sockets. - */ - public void stop(); - - /** - * @return This service's configuration. - */ - public C getConfiguration(); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamServerConfiguration.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamServerConfiguration.java deleted file mode 100644 index 9f742281..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/StreamServerConfiguration.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.spi; - -/** - * Collection of typically needed configuration settings. - * - * @author Christian Bauer - */ -public interface StreamServerConfiguration { - - /** - * @return The TCP port to listen on for HTTP requests. - */ - public int getListenPort(); - -} diff --git a/yaacc/src/main/java/org/fourthline/cling/transport/spi/UpnpStream.java b/yaacc/src/main/java/org/fourthline/cling/transport/spi/UpnpStream.java deleted file mode 100644 index 212e97eb..00000000 --- a/yaacc/src/main/java/org/fourthline/cling/transport/spi/UpnpStream.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright (C) 2013 4th Line GmbH, Switzerland - * - * The contents of this file are subject to the terms of either the GNU - * Lesser General Public License Version 2 or later ("LGPL") or the - * Common Development and Distribution License Version 1 or later - * ("CDDL") (collectively, the "License"). You may not use this file - * except in compliance with the License. See LICENSE.txt for more - * information. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. - */ - -package org.fourthline.cling.transport.spi; - -import android.util.Log; - -import org.fourthline.cling.model.message.StreamRequestMessage; -import org.fourthline.cling.model.message.StreamResponseMessage; -import org.fourthline.cling.model.message.UpnpResponse; -import org.fourthline.cling.protocol.ProtocolCreationException; -import org.fourthline.cling.protocol.ProtocolFactory; -import org.fourthline.cling.protocol.ReceivingSync; -import org.seamless.util.Exceptions; - -/** - * A runnable representation of a single HTTP request/response procedure. - *

- * Instantiated by the {@link StreamServer}, executed by the - * {@link org.fourthline.cling.transport.Router}. See the pseudo-code example - * in the documentation of {@link StreamServer}. An implementation's - * run() method has to call the {@link #process(org.fourthline.cling.model.message.StreamRequestMessage)}, - * {@link #responseSent(org.fourthline.cling.model.message.StreamResponseMessage)} and - * {@link #responseException(Throwable)} methods. - *

- *

- * An implementation does not have to be thread-safe. - *

- * - * @author Christian Bauer - */ -public abstract class UpnpStream implements Runnable { - - - protected final ProtocolFactory protocolFactory; - protected ReceivingSync syncProtocol; - - protected UpnpStream(ProtocolFactory protocolFactory) { - this.protocolFactory = protocolFactory; - } - - public ProtocolFactory getProtocolFactory() { - return protocolFactory; - } - - /** - * Selects a UPnP protocol, runs it within the calling thread, returns the response. - *

- * This method will return null if the UPnP protocol returned null. - * The HTTP response in this case is always 404 NOT FOUND. Any other (HTTP) error - * condition will be encapsulated in the returned response message and has to be - * passed to the HTTP client as it is. - *

- * - * @param requestMsg The TCP (HTTP) stream request message. - * @return The TCP (HTTP) stream response message, or null if a 404 should be send to the client. - */ - public StreamResponseMessage process(StreamRequestMessage requestMsg) { - Log.v(getClass().getName(), "Processing stream request message: " + requestMsg); - - try { - // Try to get a protocol implementation that matches the request message - syncProtocol = getProtocolFactory().createReceivingSync(requestMsg); - } catch (ProtocolCreationException ex) { - Log.w(getClass().getName(), "Processing stream request failed - " + Exceptions.unwrap(ex).toString()); - return new StreamResponseMessage(UpnpResponse.Status.NOT_IMPLEMENTED); - } - - // Run it - Log.v(getClass().getName(), "Running protocol for synchronous message processing: " + syncProtocol); - syncProtocol.run(); - - // ... then grab the response - StreamResponseMessage responseMsg = syncProtocol.getOutputMessage(); - - if (responseMsg == null) { - // That's ok, the caller is supposed to handle this properly (e.g. convert it to HTTP 404) - Log.v(getClass().getName(), "Protocol did not return any response message"); - return null; - } - Log.v(getClass().getName(), "Protocol returned response: " + responseMsg); - return responseMsg; - } - - /** - * Must be called by a subclass after the response has been successfully sent to the client. - * - * @param responseMessage The response message successfully sent to the client. - */ - protected void responseSent(StreamResponseMessage responseMessage) { - if (syncProtocol != null) - syncProtocol.responseSent(responseMessage); - } - - /** - * Must be called by a subclass if the response was not delivered to the client. - * - * @param t The reason why the response wasn't delivered. - */ - protected void responseException(Throwable t) { - if (syncProtocol != null) - syncProtocol.responseException(t); - } - - @Override - public String toString() { - return "(" + getClass().getSimpleName() + ")"; - } -} diff --git a/yaacc/src/main/java/org/seamless/statemachine/StateMachineInvocationHandler.java b/yaacc/src/main/java/org/seamless/statemachine/StateMachineInvocationHandler.java index 6c1fa31a..ec557db4 100644 --- a/yaacc/src/main/java/org/seamless/statemachine/StateMachineInvocationHandler.java +++ b/yaacc/src/main/java/org/seamless/statemachine/StateMachineInvocationHandler.java @@ -14,14 +14,14 @@ */ package org.seamless.statemachine; - import android.util.Log; - import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; + import de.yaacc.util.YaaccLogger; + /** * @author Christian Bauer */ @@ -40,7 +40,7 @@ public class StateMachineInvocationHandler implements InvocationHandler { Class[] constructorArgumentTypes, Object[] constructorArguments) { - Log.v(getClass().getName(), "Creating state machine with initial state: " + initialStateClass); + YaaccLogger.v(getClass().getName(), "Creating state machine with initial state: " + initialStateClass); this.initialStateClass = initialStateClass; @@ -54,7 +54,7 @@ public class StateMachineInvocationHandler implements InvocationHandler { .newInstance(constructorArguments) : stateClass.newInstance(); - Log.v(getClass().getName(), "Adding state instance: " + state.getClass().getName()); + YaaccLogger.v(getClass().getName(), "Adding state instance: " + state.getClass().getName()); stateObjects.put(stateClass, state); } catch (NoSuchMethodException ex) { @@ -93,7 +93,7 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl if (forcedState == null) { throw new TransitionException("Can't force to invalid state: " + args[0]); } - Log.v(getClass().getName(), "Forcing state machine into state: " + forcedState.getClass().getName()); + YaaccLogger.v(getClass().getName(), "Forcing state machine into state: " + forcedState.getClass().getName()); invokeExitMethod(currentState); currentState = forcedState; invokeEntryMethod(forcedState); @@ -101,13 +101,13 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl } Method signalMethod = getMethodOfCurrentState(method); - Log.v(getClass().getName(), "Invoking signal method of current state: " + signalMethod.toString()); + YaaccLogger.v(getClass().getName(), "Invoking signal method of current state: " + signalMethod.toString()); Object methodReturn = signalMethod.invoke(currentState, args); if (methodReturn != null && methodReturn instanceof Class) { Class nextStateClass = (Class) methodReturn; if (stateObjects.containsKey(nextStateClass)) { - Log.v(getClass().getName(), "Executing transition to next state: " + nextStateClass.getName()); + YaaccLogger.v(getClass().getName(), "Executing transition to next state: " + nextStateClass.getName()); invokeExitMethod(currentState); currentState = stateObjects.get(nextStateClass); invokeEntryMethod(currentState); @@ -131,12 +131,12 @@ private Method getMethodOfCurrentState(Method method) { } private void invokeEntryMethod(Object state) { - Log.v(getClass().getName(), "Trying to invoke entry method of state: " + state.getClass().getName()); + YaaccLogger.v(getClass().getName(), "Trying to invoke entry method of state: " + state.getClass().getName()); try { Method onEntryMethod = state.getClass().getMethod(METHOD_ON_ENTRY); onEntryMethod.invoke(state); } catch (NoSuchMethodException ex) { - Log.v(getClass().getName(), "No entry method found on state: " + state.getClass().getName()); + YaaccLogger.v(getClass().getName(), "No entry method found on state: " + state.getClass().getName()); // That's OK, just don't call it } catch (Exception ex) { throw new TransitionException( @@ -146,12 +146,12 @@ private void invokeEntryMethod(Object state) { } private void invokeExitMethod(Object state) { - Log.v(getClass().getName(), "Trying to invoking exit method of state: " + state.getClass().getName()); + YaaccLogger.v(getClass().getName(), "Trying to invoking exit method of state: " + state.getClass().getName()); try { Method onExitMethod = state.getClass().getMethod(METHOD_ON_EXIT); onExitMethod.invoke(state); } catch (NoSuchMethodException ex) { - Log.v(getClass().getName(), "No exit method found on state: " + state.getClass().getName()); + YaaccLogger.v(getClass().getName(), "No exit method found on state: " + state.getClass().getName()); // That's OK, just don't call it } catch (Exception ex) { throw new TransitionException( diff --git a/yaacc/src/main/res/drawable/ic_baseline_bookmark_24.xml b/yaacc/src/main/res/drawable/ic_baseline_bookmark_24.xml new file mode 100644 index 00000000..d728c8f4 --- /dev/null +++ b/yaacc/src/main/res/drawable/ic_baseline_bookmark_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/drawable/ic_baseline_bookmark_32.xml b/yaacc/src/main/res/drawable/ic_baseline_bookmark_32.xml new file mode 100644 index 00000000..c78827bf --- /dev/null +++ b/yaacc/src/main/res/drawable/ic_baseline_bookmark_32.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/drawable/ic_baseline_bookmark_48.xml b/yaacc/src/main/res/drawable/ic_baseline_bookmark_48.xml new file mode 100644 index 00000000..3c4fbc0d --- /dev/null +++ b/yaacc/src/main/res/drawable/ic_baseline_bookmark_48.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/drawable/ic_live_audio_stream.xml b/yaacc/src/main/res/drawable/ic_live_audio_stream.xml new file mode 100644 index 00000000..6733a13f --- /dev/null +++ b/yaacc/src/main/res/drawable/ic_live_audio_stream.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + diff --git a/yaacc/src/main/res/drawable/ic_live_video_stream.xml b/yaacc/src/main/res/drawable/ic_live_video_stream.xml new file mode 100644 index 00000000..d34112d3 --- /dev/null +++ b/yaacc/src/main/res/drawable/ic_live_video_stream.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/yaacc/src/main/res/drawable/ic_search_bookmark.xml b/yaacc/src/main/res/drawable/ic_search_bookmark.xml new file mode 100644 index 00000000..ecdda6ef --- /dev/null +++ b/yaacc/src/main/res/drawable/ic_search_bookmark.xml @@ -0,0 +1,13 @@ + + + + diff --git a/yaacc/src/main/res/drawable/outline_database_off_24.xml b/yaacc/src/main/res/drawable/outline_database_off_24.xml new file mode 100644 index 00000000..a6902803 --- /dev/null +++ b/yaacc/src/main/res/drawable/outline_database_off_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/drawable/outline_database_off_32.xml b/yaacc/src/main/res/drawable/outline_database_off_32.xml new file mode 100644 index 00000000..c51d97ac --- /dev/null +++ b/yaacc/src/main/res/drawable/outline_database_off_32.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/drawable/outline_database_off_48.xml b/yaacc/src/main/res/drawable/outline_database_off_48.xml new file mode 100644 index 00000000..5857846e --- /dev/null +++ b/yaacc/src/main/res/drawable/outline_database_off_48.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/drawable/outline_database_search_24.xml b/yaacc/src/main/res/drawable/outline_database_search_24.xml new file mode 100644 index 00000000..a8d9d4a3 --- /dev/null +++ b/yaacc/src/main/res/drawable/outline_database_search_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/drawable/outline_database_search_32.xml b/yaacc/src/main/res/drawable/outline_database_search_32.xml new file mode 100644 index 00000000..204b36f9 --- /dev/null +++ b/yaacc/src/main/res/drawable/outline_database_search_32.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/drawable/outline_database_search_48.xml b/yaacc/src/main/res/drawable/outline_database_search_48.xml new file mode 100644 index 00000000..26288077 --- /dev/null +++ b/yaacc/src/main/res/drawable/outline_database_search_48.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/yaacc/src/main/res/layout-land/activity_yaacc_upnp_server_control.xml b/yaacc/src/main/res/layout-land/activity_yaacc_upnp_server_control.xml index 0ee8e1e4..5d85d415 100644 --- a/yaacc/src/main/res/layout-land/activity_yaacc_upnp_server_control.xml +++ b/yaacc/src/main/res/layout-land/activity_yaacc_upnp_server_control.xml @@ -9,7 +9,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" - tools:context=".upnp.server.YaaccUpnpServerControlActivity"> + tools:context=".upnp.server.configuration.YaaccUpnpServerControlActivity"> + + -