├── settings.gradle
├── app
├── checkstyle_config.xml
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_add_black_24dp.png
│ │ │ │ ├── ic_add_white_48dp.png
│ │ │ │ ├── ic_list_white_36dp.png
│ │ │ │ ├── ic_sync_white_24dp.png
│ │ │ │ ├── ic_close_white_36dp.png
│ │ │ │ ├── ic_delete_white_48dp.png
│ │ │ │ ├── ic_pause_white_36dp.png
│ │ │ │ ├── ic_refresh_black_24dp.png
│ │ │ │ ├── ic_clear_all_black_24dp.png
│ │ │ │ ├── ic_settings_white_36dp.png
│ │ │ │ ├── ic_skip_next_white_36dp.png
│ │ │ │ ├── ic_fast_forward_white_36dp.png
│ │ │ │ ├── ic_fast_rewind_white_36dp.png
│ │ │ │ ├── ic_play_arrow_white_36dp.png
│ │ │ │ ├── ic_playlist_add_white_36dp.png
│ │ │ │ ├── ic_file_download_white_36dp.png
│ │ │ │ └── ic_sort_by_alpha_black_24dp.png
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_add_black_24dp.png
│ │ │ │ ├── ic_add_white_48dp.png
│ │ │ │ ├── ic_list_white_36dp.png
│ │ │ │ ├── ic_sync_white_24dp.png
│ │ │ │ ├── ic_close_white_36dp.png
│ │ │ │ ├── ic_delete_white_48dp.png
│ │ │ │ ├── ic_pause_white_36dp.png
│ │ │ │ ├── ic_refresh_black_24dp.png
│ │ │ │ ├── ic_clear_all_black_24dp.png
│ │ │ │ ├── ic_settings_white_36dp.png
│ │ │ │ ├── ic_skip_next_white_36dp.png
│ │ │ │ ├── ic_fast_forward_white_36dp.png
│ │ │ │ ├── ic_fast_rewind_white_36dp.png
│ │ │ │ ├── ic_play_arrow_white_36dp.png
│ │ │ │ ├── ic_playlist_add_white_36dp.png
│ │ │ │ ├── ic_file_download_white_36dp.png
│ │ │ │ └── ic_sort_by_alpha_black_24dp.png
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_add_black_24dp.png
│ │ │ │ ├── ic_add_white_48dp.png
│ │ │ │ ├── ic_close_white_36dp.png
│ │ │ │ ├── ic_delete_white_48dp.png
│ │ │ │ ├── ic_list_white_36dp.png
│ │ │ │ ├── ic_pause_white_36dp.png
│ │ │ │ ├── ic_sync_white_24dp.png
│ │ │ │ ├── ic_refresh_black_24dp.png
│ │ │ │ ├── ic_settings_white_36dp.png
│ │ │ │ ├── ic_clear_all_black_24dp.png
│ │ │ │ ├── ic_fast_rewind_white_36dp.png
│ │ │ │ ├── ic_play_arrow_white_36dp.png
│ │ │ │ ├── ic_skip_next_white_36dp.png
│ │ │ │ ├── ic_fast_forward_white_36dp.png
│ │ │ │ ├── ic_file_download_white_36dp.png
│ │ │ │ ├── ic_playlist_add_white_36dp.png
│ │ │ │ └── ic_sort_by_alpha_black_24dp.png
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.png
│ │ │ │ ├── ic_add_black_24dp.png
│ │ │ │ ├── ic_add_white_48dp.png
│ │ │ │ ├── ic_close_white_36dp.png
│ │ │ │ ├── ic_list_white_36dp.png
│ │ │ │ ├── ic_pause_white_36dp.png
│ │ │ │ ├── ic_sync_white_24dp.png
│ │ │ │ ├── ic_delete_white_48dp.png
│ │ │ │ ├── ic_refresh_black_24dp.png
│ │ │ │ ├── ic_clear_all_black_24dp.png
│ │ │ │ ├── ic_play_arrow_white_36dp.png
│ │ │ │ ├── ic_settings_white_36dp.png
│ │ │ │ ├── ic_skip_next_white_36dp.png
│ │ │ │ ├── ic_fast_forward_white_36dp.png
│ │ │ │ ├── ic_fast_rewind_white_36dp.png
│ │ │ │ ├── ic_playlist_add_white_36dp.png
│ │ │ │ ├── ic_file_download_white_36dp.png
│ │ │ │ └── ic_sort_by_alpha_black_24dp.png
│ │ │ ├── drawable-nodpi
│ │ │ │ └── widget_preview.png
│ │ │ ├── xml
│ │ │ │ ├── authenticator.xml
│ │ │ │ ├── syncadapter.xml
│ │ │ │ ├── widget_provider_info.xml
│ │ │ │ └── preferences.xml
│ │ │ ├── values
│ │ │ │ ├── attrs.xml
│ │ │ │ ├── dimen.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── themes.xml
│ │ │ ├── drawable
│ │ │ │ ├── player_background.xml
│ │ │ │ ├── player_image_background.xml
│ │ │ │ ├── clickable_image_button_bg.xml
│ │ │ │ ├── clickable_list_element.xml
│ │ │ │ ├── scrollbar_thumb.xml
│ │ │ │ ├── episode_progress_bar.xml
│ │ │ │ ├── episode_progress_bar_indeterminate.xml
│ │ │ │ └── playback_progress.xml
│ │ │ ├── layout
│ │ │ │ ├── subscribe_spinner_item.xml
│ │ │ │ ├── common_list.xml
│ │ │ │ ├── activity_main.xml
│ │ │ │ ├── search_splash.xml
│ │ │ │ ├── tabbed_frame.xml
│ │ │ │ ├── subscribe_dialog.xml
│ │ │ │ ├── history_list_element.xml
│ │ │ │ ├── search_list_element.xml
│ │ │ │ ├── player.xml
│ │ │ │ ├── podcast_list_element.xml
│ │ │ │ └── episode_list_element.xml
│ │ │ └── layout-land
│ │ │ │ └── activity_main.xml
│ │ ├── assets
│ │ │ └── databases
│ │ │ │ └── PodcastCatalogue.sqlite
│ │ └── java
│ │ │ └── com
│ │ │ └── einmalfel
│ │ │ └── podlisten
│ │ │ ├── support
│ │ │ ├── PredictiveAnimatiedLayoutManager.java
│ │ │ ├── StubAuthenticatorService.java
│ │ │ ├── UnitConverter.java
│ │ │ ├── PatchedTextView.java
│ │ │ ├── SnackbarController.java
│ │ │ ├── StubAuthenticator.java
│ │ │ └── RotationLayout.java
│ │ │ ├── EpisodeSyncService.java
│ │ │ ├── ScanActivity.java
│ │ │ ├── WidgetProvider.java
│ │ │ ├── BaseCursorRecyclerAdapter.java
│ │ │ ├── SearchAdapter.java
│ │ │ ├── RefreshMode.java
│ │ │ ├── PlayerLocalConnection.java
│ │ │ ├── PodcastListAdapter.java
│ │ │ ├── FeedHistoryAdapter.java
│ │ │ ├── PodlistenAccount.java
│ │ │ ├── ForegroundOperations.java
│ │ │ ├── ScrollTrackingBehaviour.java
│ │ │ ├── MediaButtonReceiver.java
│ │ │ ├── PodListenApp.java
│ │ │ ├── NewEpisodesFragment.java
│ │ │ ├── EpisodeListAdapter.java
│ │ │ ├── SearchElementHolder.java
│ │ │ ├── SubscriptionsFragment.java
│ │ │ ├── PreferencesFragment.java
│ │ │ ├── EpisodesSyncAdapter.java
│ │ │ ├── SearchActivity.java
│ │ │ ├── PlaylistFragment.java
│ │ │ ├── HistoryElementHolder.java
│ │ │ ├── FeedHistoryFragment.java
│ │ │ ├── PodcastViewHolder.java
│ │ │ ├── SyncState.java
│ │ │ ├── PodcastHelper.java
│ │ │ ├── ImageManager.java
│ │ │ ├── CatalogueFragment.java
│ │ │ └── PreferencesActivity.java
│ ├── release
│ │ └── java
│ │ │ └── com
│ │ │ └── einmalfel
│ │ │ └── podlisten
│ │ │ ├── DebuggableApp.java
│ │ │ ├── DebuggableService.java
│ │ │ └── DebuggableFragment.java
│ └── debug
│ │ └── java
│ │ └── com
│ │ └── einmalfel
│ │ └── podlisten
│ │ ├── DebuggableService.java
│ │ ├── DebuggableFragment.java
│ │ └── DebuggableApp.java
├── lint.xml
├── proguard-rules.pro
└── build.gradle
├── .gitignore
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── README.md
├── gradlew.bat
└── gradlew
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
--------------------------------------------------------------------------------
/app/checkstyle_config.xml:
--------------------------------------------------------------------------------
1 | ../checkstyle_config.xml
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | *.iml
4 | build/
5 | /.idea
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable-nodpi/widget_preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/drawable-nodpi/widget_preview.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_add_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_add_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_add_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_add_white_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_list_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_list_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_sync_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_sync_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_add_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_add_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_add_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_add_white_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_list_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_list_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_sync_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_sync_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_add_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_add_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_add_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_add_white_48dp.png
--------------------------------------------------------------------------------
/app/src/main/assets/databases/PodcastCatalogue.sqlite:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/assets/databases/PodcastCatalogue.sqlite
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_close_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_close_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_delete_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_delete_white_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_pause_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_pause_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_refresh_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_refresh_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_close_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_close_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_delete_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_delete_white_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_pause_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_pause_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_refresh_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_refresh_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_close_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_close_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_delete_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_delete_white_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_list_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_list_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_pause_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_pause_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_sync_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_sync_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_add_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_add_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_add_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_add_white_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_close_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_close_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_list_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_list_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_pause_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_pause_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_sync_white_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_sync_white_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_clear_all_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_clear_all_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_settings_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_settings_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_skip_next_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_skip_next_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_clear_all_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_clear_all_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_settings_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_settings_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_skip_next_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_skip_next_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_refresh_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_refresh_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_settings_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_settings_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_delete_white_48dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_delete_white_48dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_refresh_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_refresh_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_fast_forward_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_fast_forward_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_fast_rewind_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_fast_rewind_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_play_arrow_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_play_arrow_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_playlist_add_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_playlist_add_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_fast_forward_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_fast_forward_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_fast_rewind_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_fast_rewind_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_play_arrow_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_play_arrow_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_playlist_add_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_playlist_add_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_clear_all_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_clear_all_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_fast_rewind_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_fast_rewind_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_play_arrow_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_play_arrow_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_skip_next_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_skip_next_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_clear_all_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_clear_all_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_play_arrow_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_play_arrow_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_settings_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_settings_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_skip_next_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_skip_next_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_file_download_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_file_download_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_sort_by_alpha_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-hdpi/ic_sort_by_alpha_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_file_download_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_file_download_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_sort_by_alpha_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-mdpi/ic_sort_by_alpha_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_fast_forward_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_fast_forward_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_file_download_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_file_download_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_playlist_add_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_playlist_add_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_sort_by_alpha_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xhdpi/ic_sort_by_alpha_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_fast_forward_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_fast_forward_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_fast_rewind_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_fast_rewind_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_playlist_add_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_playlist_add_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_file_download_white_36dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_file_download_white_36dp.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_sort_by_alpha_black_24dp.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/einmalfel/PodListen/HEAD/app/src/main/res/mipmap-xxhdpi/ic_sort_by_alpha_black_24dp.png
--------------------------------------------------------------------------------
/app/src/main/res/xml/authenticator.xml:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/app/src/release/java/com/einmalfel/podlisten/DebuggableApp.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.app.Application;
4 |
5 | public class DebuggableApp extends Application {}
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/release/java/com/einmalfel/podlisten/DebuggableService.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.app.Service;
4 |
5 | public abstract class DebuggableService extends Service {}
6 |
--------------------------------------------------------------------------------
/app/src/release/java/com/einmalfel/podlisten/DebuggableFragment.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.support.v4.app.Fragment;
4 |
5 | public class DebuggableFragment extends Fragment {}
6 |
--------------------------------------------------------------------------------
/app/lint.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/player_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/player_image_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/clickable_image_button_bg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/clickable_list_element.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Sun Sep 10 16:38:30 MSK 2017
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.1-all.zip
7 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimen.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 15sp
4 | 11sp
5 | 13sp
6 | 13sp
7 |
8 |
--------------------------------------------------------------------------------
/app/src/debug/java/com/einmalfel/podlisten/DebuggableService.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.app.Service;
4 |
5 | public abstract class DebuggableService extends Service {
6 | @Override
7 | public void onDestroy() {
8 | super.onDestroy();
9 | PodListenApp.getInstance().refWatcher.watch(this);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/debug/java/com/einmalfel/podlisten/DebuggableFragment.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.support.v4.app.Fragment;
4 |
5 | public class DebuggableFragment extends Fragment {
6 | @Override
7 | public void onDestroy() {
8 | super.onDestroy();
9 | PodListenApp.getInstance().refWatcher.watch(this);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/syncadapter.xml:
--------------------------------------------------------------------------------
1 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/scrollbar_thumb.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/subscribe_spinner_item.xml:
--------------------------------------------------------------------------------
1 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/common_list.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/widget_provider_info.xml:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/support/PredictiveAnimatiedLayoutManager.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten.support;
2 |
3 | import android.content.Context;
4 | import android.support.v7.widget.LinearLayoutManager;
5 |
6 |
7 | public class PredictiveAnimatiedLayoutManager extends LinearLayoutManager {
8 | public PredictiveAnimatiedLayoutManager(Context context) {
9 | super(context);
10 | }
11 |
12 | @Override
13 | public boolean supportsPredictiveItemAnimations() {
14 | return true;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/support/StubAuthenticatorService.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten.support;
2 |
3 | import android.app.Service;
4 | import android.content.Intent;
5 | import android.os.IBinder;
6 |
7 | public class StubAuthenticatorService extends Service {
8 | private StubAuthenticator authenticator;
9 |
10 | @Override
11 | public void onCreate() {
12 | authenticator = new StubAuthenticator(this);
13 | }
14 |
15 | @Override
16 | public IBinder onBind(Intent intent) {
17 | return authenticator.getIBinder();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/episode_progress_bar.xml:
--------------------------------------------------------------------------------
1 |
4 |
8 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/episode_progress_bar_indeterminate.xml:
--------------------------------------------------------------------------------
1 |
5 |
9 |
14 |
15 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
14 |
15 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/EpisodeSyncService.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.Intent;
4 | import android.os.IBinder;
5 |
6 |
7 | public class EpisodeSyncService extends DebuggableService {
8 | private static EpisodesSyncAdapter adapter;
9 |
10 | @Override
11 | public void onCreate() {
12 | super.onCreate();
13 | if (adapter == null) {
14 | adapter = new EpisodesSyncAdapter(getApplicationContext(), true);
15 | }
16 | }
17 |
18 | @Override
19 | public IBinder onBind(Intent intent) {
20 | return adapter.getSyncAdapterBinder();
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/ScanActivity.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.support.v7.app.AppCompatActivity;
4 |
5 | public class ScanActivity extends AppCompatActivity
6 | implements android.support.v7.widget.SearchView.OnQueryTextListener,
7 | SearchAdapter.SearchClickListener {
8 |
9 | @Override
10 | public boolean onQueryTextSubmit(String query) {
11 | return false;
12 | }
13 |
14 | @Override
15 | public boolean onQueryTextChange(String newText) {
16 | return false;
17 | }
18 |
19 | @Override
20 | public void onPodcastButtonTap(String rssUrl) {
21 |
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 | #ff303030
3 |
4 | #ff424242
5 |
6 | #ff212121
7 |
8 |
9 | #ff80cbc4
10 | #4080cbc4
11 | #2080cbc4
12 |
13 | #ffff8f00
14 | #40ff8f00
15 |
16 | #ffffffff
17 | #b3ffffff
18 | #4dffffff
19 |
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/WidgetProvider.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.appwidget.AppWidgetManager;
4 | import android.appwidget.AppWidgetProvider;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.support.annotation.NonNull;
8 |
9 | public class WidgetProvider extends AppWidgetProvider {
10 | private static String TAG = "WGP";
11 | private final WidgetHelper helper = WidgetHelper.getInstance();
12 |
13 | @Override
14 | public void onReceive(@NonNull Context context, @NonNull Intent intent) {
15 | if (!helper.processIntent(intent)) {
16 | super.onReceive(context, intent);
17 | }
18 | }
19 |
20 | public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
21 | helper.updateWidgetsFull(appWidgetIds);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/app/src/debug/java/com/einmalfel/podlisten/DebuggableApp.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.app.Application;
4 | import android.util.Log;
5 |
6 | import com.facebook.stetho.Stetho;
7 | import com.squareup.leakcanary.LeakCanary;
8 | import com.squareup.leakcanary.RefWatcher;
9 |
10 | public class DebuggableApp extends Application {
11 | private static final String TAG = "PLA";
12 |
13 | RefWatcher refWatcher;
14 |
15 | public void onCreate() {
16 | Log.i(TAG, "Initializing stetho...");
17 | Stetho.initialize(Stetho.newInitializerBuilder(this).enableDumpapp(Stetho
18 | .defaultDumperPluginsProvider(this)).enableWebKitInspector(Stetho
19 | .defaultInspectorModulesProvider(this)).build());
20 | Log.i(TAG, "Initializing leakcanary...");
21 | refWatcher = LeakCanary.install(this);
22 | super.onCreate();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/res/layout-land/activity_main.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
13 |
14 |
19 |
20 |
21 |
23 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/search_splash.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
17 |
18 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/playback_progress.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
12 |
13 |
14 |
15 | -
16 |
17 |
18 |
19 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/support/UnitConverter.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten.support;
2 |
3 | import android.util.DisplayMetrics;
4 | import android.util.TypedValue;
5 |
6 | import com.einmalfel.podlisten.PodListenApp;
7 |
8 | public class UnitConverter {
9 | private static UnitConverter instance;
10 | private final DisplayMetrics displayMetrics;
11 |
12 | private UnitConverter() {
13 | displayMetrics = PodListenApp.getContext().getResources().getDisplayMetrics();
14 | }
15 |
16 | public static UnitConverter getInstance() {
17 | if (instance == null) {
18 | synchronized (UnitConverter.class) {
19 | if (instance == null) {
20 | instance = new UnitConverter();
21 | }
22 | }
23 | }
24 | return instance;
25 | }
26 |
27 | public int spToPx(int dp) {
28 | return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, displayMetrics));
29 | }
30 |
31 | public int dpToPx(int dp) {
32 | return Math.round(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, displayMetrics));
33 | }
34 |
35 | public int pxToDp(int px) {
36 | return Math.round(px / (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT));
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/BaseCursorRecyclerAdapter.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.database.Cursor;
4 | import android.os.Handler;
5 | import android.os.Looper;
6 | import android.support.v7.widget.RecyclerView;
7 |
8 | import com.einmalfel.podlisten.thirdparty.CursorRecyclerAdapter;
9 |
10 | import java.util.HashSet;
11 | import java.util.Set;
12 |
13 | public abstract class BaseCursorRecyclerAdapter
14 | extends CursorRecyclerAdapter {
15 | protected final Set expandedElements = new HashSet<>(10);
16 |
17 | public BaseCursorRecyclerAdapter(Cursor cursor) {
18 | super(cursor);
19 | }
20 |
21 | void setExpanded(long id, boolean expanded, final int position) {
22 | if (!expandedElements.contains(id) && expanded) {
23 | expandedElements.add(id);
24 | new Handler(Looper.getMainLooper()).post(new Runnable() {
25 | @Override
26 | public void run() {
27 | notifyItemChanged(position);
28 | }
29 | });
30 | } else if (expandedElements.contains(id) && !expanded) {
31 | expandedElements.remove(id);
32 | new Handler(Looper.getMainLooper()).post(new Runnable() {
33 | @Override
34 | public void run() {
35 | notifyItemChanged(position);
36 | }
37 | });
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/support/PatchedTextView.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten.support;
2 |
3 | // silencing glitch in TextView
4 | // https://code.google.com/p/android/issues/detail?id=35466
5 |
6 |
7 | import android.content.Context;
8 | import android.support.v7.widget.AppCompatTextView;
9 | import android.util.AttributeSet;
10 |
11 |
12 | public class PatchedTextView extends AppCompatTextView {
13 | public PatchedTextView(Context context, AttributeSet attrs, int defStyle) {
14 | super(context, attrs, defStyle);
15 | }
16 |
17 | public PatchedTextView(Context context, AttributeSet attrs) {
18 | super(context, attrs);
19 | }
20 |
21 | public PatchedTextView(Context context) {
22 | super(context);
23 | }
24 |
25 | @Override
26 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
27 | try {
28 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
29 | } catch (ArrayIndexOutOfBoundsException toSuppress) {
30 | setText(getText().toString());
31 | super.onMeasure(widthMeasureSpec, heightMeasureSpec);
32 | }
33 | }
34 |
35 | @Override
36 | public void setGravity(int gravity) {
37 | try {
38 | super.setGravity(gravity);
39 | } catch (ArrayIndexOutOfBoundsException toSuppress) {
40 | setText(getText().toString());
41 | super.setGravity(gravity);
42 | }
43 | }
44 |
45 | @Override
46 | public void setText(CharSequence text, BufferType type) {
47 | try {
48 | super.setText(text, type);
49 | } catch (ArrayIndexOutOfBoundsException toSuppress) {
50 | setText(text.toString());
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/SearchAdapter.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.database.Cursor;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.LayoutInflater;
6 | import android.view.ViewGroup;
7 |
8 | class SearchAdapter extends BaseCursorRecyclerAdapter {
9 | public interface SearchClickListener {
10 | void onPodcastButtonTap(String rssUrl);
11 | }
12 |
13 | private final SearchClickListener listener;
14 |
15 | public SearchAdapter(SearchClickListener listener) {
16 | super(null);
17 | this.listener = listener;
18 | }
19 |
20 | @Override
21 | public void onBindViewHolderCursor(RecyclerView.ViewHolder holder, Cursor cursor) {
22 | long id = cursor.getLong(cursor.getColumnIndexOrThrow("_ID"));
23 | SearchElementHolder searchHolder = (SearchElementHolder) holder;
24 | searchHolder.bind(cursor.getString(cursor.getColumnIndexOrThrow("title")),
25 | cursor.getString(cursor.getColumnIndexOrThrow("description")),
26 | cursor.getString(cursor.getColumnIndexOrThrow("rss_url")),
27 | cursor.getString(cursor.getColumnIndexOrThrow("web_url")),
28 | cursor.getLong(cursor.getColumnIndexOrThrow("period")),
29 | id,
30 | expandedElements.contains(id));
31 | }
32 |
33 | @Override
34 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
35 | ViewGroup view = (ViewGroup) LayoutInflater.from(parent.getContext()).inflate(
36 | R.layout.search_list_element, parent, false);
37 | return new SearchElementHolder(view, listener, this);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
22 |
23 |
27 |
28 |
31 |
32 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | ########################## Logging
2 | -assumenosideeffects class android.util.Log {
3 | public static boolean isLoggable(java.lang.String, int);
4 | public static int v(...);
5 | }
6 |
7 | ########################## Support Library
8 | # prevent crashing caused by stripping Preference view constructor
9 | # https://code.google.com/p/android/issues/detail?id=183261
10 | -keep public class android.support.v7.preference.Preference { *; }
11 | -keep public class * extends android.support.v7.preference.Preference { *; }
12 |
13 | ########################## ACRA
14 | # Restore some Source file names and restore approximate line numbers in the stack traces,
15 | # otherwise the stack traces are pretty useless
16 | -keepattributes SourceFile,LineNumberTable
17 |
18 | # ACRA needs "annotations" so add this...
19 | # Note: This may already be defined in the default "proguard-android-optimize.txt"
20 | # file in the SDK. If it is, then you don't need to duplicate it. See your
21 | # "project.properties" file to get the path to the default "proguard-android-optimize.txt".
22 | -keepattributes *Annotation*
23 |
24 | -dontwarn android.app.Notification
25 |
26 | # keep this around for some enums that ACRA needs
27 | -keep class org.acra.ReportingInteractionMode {
28 | *;
29 | }
30 |
31 | -keepnames class org.acra.sender.HttpSender$** {
32 | *;
33 | }
34 |
35 | -keepnames class org.acra.ReportField {
36 | *;
37 | }
38 |
39 | # keep this otherwise it is removed by ProGuard
40 | -keep public class org.acra.ErrorReporter {
41 | public void addCustomData(java.lang.String,java.lang.String);
42 | public void putCustomData(java.lang.String,java.lang.String);
43 | public void removeCustomData(java.lang.String);
44 | }
45 |
46 | # keep this otherwise it is removed by ProGuard
47 | -keep public class org.acra.ErrorReporter {
48 | public void handleSilentException(java.lang.Throwable);
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/tabbed_frame.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
13 |
14 |
20 |
21 |
25 |
26 |
27 |
42 |
43 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/RefreshMode.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.Context;
4 | import android.support.annotation.NonNull;
5 |
6 | public enum RefreshMode {
7 | WEEK(R.string.refresh_mode_week),
8 | MONTH(R.string.refresh_mode_month),
9 | YEAR(R.string.refresh_mode_year),
10 | ALL(R.string.refresh_mode_all),
11 | NONE(R.string.refresh_mode_none),
12 | LAST(R.string.refresh_mode_last),
13 | LAST_2(R.string.refresh_mode_last_2),
14 | LAST_3(R.string.refresh_mode_last_3),
15 | LAST_4(R.string.refresh_mode_last_4),
16 | LAST_5(R.string.refresh_mode_last_5),
17 | LAST_10(R.string.refresh_mode_last_10),
18 | LAST_20(R.string.refresh_mode_last_20),
19 | LAST_50(R.string.refresh_mode_last_50),
20 | LAST_100(R.string.refresh_mode_last_100);
21 |
22 | private final int stringId;
23 |
24 |
25 | RefreshMode(int stringId) {
26 | this.stringId = stringId;
27 | }
28 |
29 | @NonNull
30 | public String toString(@NonNull Context context) {
31 | return context.getResources().getString(stringId);
32 | }
33 |
34 | /**
35 | * @return new episodes quantity limit
36 | */
37 | public int getCount() {
38 | switch (this) {
39 | case LAST_10:
40 | return 10;
41 | case LAST_20:
42 | return 20;
43 | case LAST_50:
44 | return 50;
45 | case LAST_100:
46 | return 100;
47 | default:
48 | return ordinal() < NONE.ordinal() ? Integer.MAX_VALUE : ordinal() - NONE.ordinal();
49 | }
50 | }
51 |
52 | /**
53 | * @return maximum age of new episode in milliseconds
54 | */
55 | public long getMaxAge() {
56 | final long dayMilliseconds = 1000 * 60 * 60 * 24L;
57 | switch (this) {
58 | case WEEK:
59 | return dayMilliseconds * 7;
60 | case MONTH:
61 | return dayMilliseconds * 30;
62 | case YEAR:
63 | return dayMilliseconds * 365;
64 | default:
65 | return Long.MAX_VALUE;
66 | }
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/PlayerLocalConnection.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.ComponentName;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.content.ServiceConnection;
7 | import android.os.IBinder;
8 |
9 | public class PlayerLocalConnection implements ServiceConnection {
10 | private static final String TAG = "PLC";
11 |
12 | public PlayerService service;
13 |
14 | private final PlayerService.PlayerStateListener listener;
15 | private final Context context = PodListenApp.getContext();
16 | private boolean clientExpectBind = false;
17 |
18 | public PlayerLocalConnection(PlayerService.PlayerStateListener listener) {
19 | this.listener = listener;
20 | }
21 |
22 | public synchronized void bind() {
23 | if (clientExpectBind || service != null) {
24 | return;
25 | }
26 | clientExpectBind = true;
27 | Intent intent = new Intent(context, PlayerService.class);
28 | context.startService(intent); //need this to make player service remain even if clients unbind
29 | context.bindService(intent, this, Context.BIND_AUTO_CREATE);
30 | }
31 |
32 | public synchronized void unbind() {
33 | if (clientExpectBind) {
34 | clientExpectBind = false;
35 | }
36 | if (service != null) {
37 | service.rmListener(listener);
38 | service = null;
39 | context.unbindService(this);
40 | }
41 | }
42 |
43 | // Callbacks below seem to be unpredictable. Both might not be called at all (e.g. in case of
44 | // frequent orientation changes) and might be called after owner calls unbind()
45 |
46 | @Override
47 | public void onServiceConnected(ComponentName name, IBinder service) {
48 | synchronized (this) {
49 | this.service = ((PlayerService.LocalBinder) service).getService();
50 | this.service.addListener(listener);
51 | if (!clientExpectBind) {
52 | unbind();
53 | }
54 | }
55 | }
56 |
57 | @Override
58 | public void onServiceDisconnected(ComponentName name) {}
59 | }
60 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/PodcastListAdapter.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 |
4 | import android.database.Cursor;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 |
9 | public class PodcastListAdapter extends BaseCursorRecyclerAdapter {
10 | public interface ItemClickListener {
11 | /**
12 | * @return true if event was consumed
13 | */
14 | boolean onLongTap(long id, String title);
15 |
16 | void onButtonTap(long id, String title);
17 | }
18 |
19 | private static final String TAG = "PLA";
20 | private final ItemClickListener listener;
21 |
22 | public PodcastListAdapter(Cursor cursor, ItemClickListener listener) {
23 | super(cursor);
24 | setHasStableIds(true);
25 | this.listener = listener;
26 | }
27 |
28 | @Override
29 | public void onBindViewHolderCursor(PodcastViewHolder holder, Cursor cursor) {
30 | long id = cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_ID));
31 | holder.bind(cursor.getInt(cursor.getColumnIndexOrThrow(Provider.K_PSTATE)),
32 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_PNAME)),
33 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_PDESCR)),
34 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_PFURL)),
35 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_PURL)),
36 | id,
37 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_PSDESCR)),
38 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_PERROR)),
39 | cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_PTSTAMP)),
40 | expandedElements.contains(id));
41 | }
42 |
43 | @Override
44 | public PodcastViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
45 | View view = LayoutInflater.from(parent.getContext())
46 | .inflate(R.layout.podcast_list_element, parent, false);
47 | return new PodcastViewHolder(view, listener, this);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/support/SnackbarController.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten.support;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.support.annotation.Nullable;
5 | import android.support.design.widget.Snackbar;
6 | import android.util.Log;
7 | import android.view.View;
8 |
9 | public class SnackbarController {
10 | private static final String TAG = "SCT";
11 |
12 | private final View parent;
13 | private final int color;
14 | private Snackbar snackbar;
15 | private Snackbar.Callback snackbarCallback;
16 |
17 | public SnackbarController(View parent, int color) {
18 | this.parent = parent;
19 | this.color = color;
20 | }
21 |
22 | public void showSnackbar(@NonNull String text, int duration, @Nullable String action,
23 | @Nullable final Snackbar.Callback callback) {
24 | if (snackbar == null || !snackbar.isShownOrQueued()) {
25 | // On some devices (e.g. Galaxy TAB 4 7.0) single snackbar instance cannot be showed
26 | // multiple times, so reinstantiate it
27 | if (snackbar != null) {
28 | snackbar.dismiss();
29 | }
30 | snackbar = Snackbar.make(parent, text, duration);
31 | snackbar.getView().setBackgroundColor(color);
32 | } else {
33 | // In genymotion (and therefore probably on some devices) snackbar queue glitches, so update
34 | // current snackbar instead of enqueuing it
35 | if (snackbarCallback != null) {
36 | Log.d(TAG, "Replacing snackbar with " + text + ". Emulating dismiss callback");
37 | snackbarCallback.onDismissed(snackbar, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
38 | }
39 | snackbar.setText(text);
40 | snackbar.setDuration(duration);
41 | }
42 | snackbar.setAction(action, callback == null ? null : new View.OnClickListener() {
43 | @Override
44 | public void onClick(View view) {
45 | callback.onDismissed(snackbar, Snackbar.Callback.DISMISS_EVENT_ACTION);
46 | }
47 | });
48 | snackbar.setCallback(callback);
49 | snackbarCallback = callback;
50 | snackbar.show();
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/FeedHistoryAdapter.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.database.Cursor;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.LayoutInflater;
6 | import android.view.ViewGroup;
7 |
8 | class FeedHistoryAdapter extends BaseCursorRecyclerAdapter {
9 | public interface HistoryEpisodeListener {
10 | void onEpisodeButtonTap(long id, int state);
11 | }
12 |
13 | static final String[] COLUMNS_NEEDED = new String[]{
14 | Provider.K_ENAME, Provider.K_EURL, Provider.K_EDATE, Provider.K_EDESCR, Provider.K_ESDESCR,
15 | Provider.K_ID, Provider.K_ESTATE, Provider.K_EPLAYED};
16 |
17 | private final HistoryEpisodeListener listener;
18 |
19 | public FeedHistoryAdapter(HistoryEpisodeListener listener) {
20 | super(null);
21 | this.listener = listener;
22 | }
23 |
24 | @Override
25 | public void onBindViewHolderCursor(RecyclerView.ViewHolder holder, Cursor cursor) {
26 | long id = cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_ID));
27 | HistoryElementHolder historyElementHolder = (HistoryElementHolder) holder;
28 | historyElementHolder.bind(cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_ENAME)),
29 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_EDESCR)),
30 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_ESDESCR)),
31 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_EURL)),
32 | cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_EDATE)),
33 | cursor.getInt(cursor.getColumnIndexOrThrow(Provider.K_ESTATE)),
34 | cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_EPLAYED)),
35 | id,
36 | expandedElements.contains(id));
37 | }
38 |
39 | @Override
40 | public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
41 | ViewGroup view = (ViewGroup) LayoutInflater.from(parent.getContext()).inflate(
42 | R.layout.history_list_element, parent, false);
43 | return new HistoryElementHolder(view, listener, this);
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/support/StubAuthenticator.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten.support;
2 |
3 | import android.accounts.AbstractAccountAuthenticator;
4 | import android.accounts.Account;
5 | import android.accounts.AccountAuthenticatorResponse;
6 | import android.accounts.NetworkErrorException;
7 | import android.content.Context;
8 | import android.os.Bundle;
9 |
10 |
11 | public class StubAuthenticator extends AbstractAccountAuthenticator {
12 | public StubAuthenticator(Context context) {
13 | super(context);
14 | }
15 |
16 | @Override
17 | public Bundle editProperties(AccountAuthenticatorResponse response, String accountType) {
18 | throw new UnsupportedOperationException();
19 | }
20 |
21 | @Override
22 | public Bundle addAccount(AccountAuthenticatorResponse response, String accountType,
23 | String authTokenType, String[] requiredFeatures,
24 | Bundle options) throws NetworkErrorException {
25 | return null;
26 | }
27 |
28 | @Override
29 | public Bundle confirmCredentials(AccountAuthenticatorResponse response, Account account,
30 | Bundle options) throws NetworkErrorException {
31 | return null;
32 | }
33 |
34 | @Override
35 | public Bundle getAuthToken(AccountAuthenticatorResponse response, Account account,
36 | String authTokenType,
37 | Bundle options) throws NetworkErrorException {
38 | throw new UnsupportedOperationException();
39 | }
40 |
41 | @Override
42 | public String getAuthTokenLabel(String authTokenType) {
43 | throw new UnsupportedOperationException();
44 | }
45 |
46 | @Override
47 | public Bundle updateCredentials(AccountAuthenticatorResponse response, Account account,
48 | String authTokenType,
49 | Bundle options) throws NetworkErrorException {
50 | throw new UnsupportedOperationException();
51 | }
52 |
53 | @Override
54 | public Bundle hasFeatures(AccountAuthenticatorResponse response, Account account,
55 | String[] features) throws NetworkErrorException {
56 | throw new UnsupportedOperationException();
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/PodlistenAccount.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.accounts.Account;
4 | import android.accounts.AccountManager;
5 | import android.content.ContentResolver;
6 | import android.content.Context;
7 | import android.os.Bundle;
8 | import android.support.annotation.NonNull;
9 |
10 | public class PodlistenAccount {
11 | private static PodlistenAccount instance;
12 |
13 | private final String appId;
14 | // cannot derive from Account - instance will be send to sync framework in form of parcel
15 | private final Account account;
16 |
17 | private PodlistenAccount(@NonNull Context context) {
18 | AccountManager accManager = (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);
19 | appId = context.getResources().getString(R.string.app_id);
20 | account = new Account(appId, appId);
21 | accManager.addAccountExplicitly(account, null, null);
22 | }
23 |
24 | public static PodlistenAccount getInstance(@NonNull Context context) {
25 | if (instance == null) {
26 | synchronized (PodlistenAccount.class) {
27 | if (instance == null) {
28 | instance = new PodlistenAccount(context);
29 | }
30 | }
31 | }
32 | return instance;
33 | }
34 |
35 | /**
36 | * @param feedId ID of feed to sync. If zero is given, will request sync for all feeds
37 | */
38 | void refresh(long feedId) {
39 | Bundle settingsBundle = new Bundle();
40 | settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
41 | settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
42 | settingsBundle.putBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, true);
43 | settingsBundle.putLong(EpisodesSyncAdapter.FEED_ID_EXTRA_OPTION, feedId);
44 | ContentResolver.requestSync(account, appId, settingsBundle);
45 | }
46 |
47 | void cancelRefresh() {
48 | ContentResolver.cancelSync(account, appId);
49 | }
50 |
51 | /**
52 | * @param pollPeriod sync interval in seconds. Pass zero to disable periodic sync
53 | */
54 | void setupSync(int pollPeriod) {
55 | if (pollPeriod == 0) {
56 | ContentResolver.removePeriodicSync(account, appId, Bundle.EMPTY);
57 | ContentResolver.setSyncAutomatically(account, appId, false);
58 | } else {
59 | ContentResolver.addPeriodicSync(account, appId, Bundle.EMPTY, pollPeriod);
60 | ContentResolver.setSyncAutomatically(account, appId, true);
61 | }
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://play.google.com/store/apps/details?id=com.einmalfel.podlisten) If you feel brave, consider [joining beta-testing program](https://play.google.com/apps/testing/com.einmalfel.podlisten).
2 |
3 | [](http://f-droid.org/repository/browse/?fdcategory=Multimedia&fdid=com.einmalfel.podlisten) N.B.: F-Droid processes updates with 1-2 day delay.
4 |
5 | Also, PodListen is available on Yandex Store
6 |
7 | ### App idea
8 |
9 | Android podcast app with simplified navigation and lightweight interface. It's intended to be convenient even if used on the run.
10 |
11 | ### Screenshots
12 |
13 | 

14 |
15 | ### Translation
16 |
17 | PodListen is ready for translation. If you are willing to contribute by translating the app to your language just make a pull-request with new or updated res/values-XX/strings.xml file.
18 |
19 | Alternatively, you can simply [download latest strings.xml](https://raw.githubusercontent.com/einmalfel/PodListen/master/app/src/main/res/values/strings.xml), translate it and email it to me.
20 |
21 | Currently, PodListen has Croatian, English, German, Russian and incomplete French localizations.
22 |
23 | ### License
24 |
25 | PodListen is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License (GPL) as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
26 |
27 | PodListen 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.
28 |
29 | See included copy of GNU GPLv3.0 for more details. Alternatively, you can find its text at http://www.gnu.org/licenses/gpl-3.0.txt
30 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/ForegroundOperations.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.app.IntentService;
4 | import android.content.ContentValues;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.support.annotation.NonNull;
8 | import android.util.Log;
9 |
10 |
11 | /**
12 | * This service handles operations that user expects to be done fast (like marking new eps leaving)
13 | */
14 | public class ForegroundOperations extends IntentService {
15 | private static final String TAG = "FGS";
16 |
17 | private static final String ACTION_SET_STATE = "com.einmalfel.podlisten.SET_STATE";
18 |
19 | private static final String EXTRA_EPISODE_STATE = "com.einmalfel.podlisten.EPISODE_STATE";
20 | private static final String EXTRA_EPISODE_FILTER = "com.einmalfel.podlisten.EPISODE_FILTER";
21 |
22 | public ForegroundOperations() {
23 | super("ForegroundOperations");
24 | setIntentRedelivery(true);
25 | }
26 |
27 | public static void startSetEpisodesState(@NonNull Context context, int state, int stateFilter) {
28 | Intent intent = new Intent(context, ForegroundOperations.class);
29 | intent.setAction(ACTION_SET_STATE);
30 | intent.putExtra(EXTRA_EPISODE_FILTER, stateFilter);
31 | intent.putExtra(EXTRA_EPISODE_STATE, state);
32 | context.startService(intent);
33 | }
34 |
35 | @Override
36 | protected void onHandleIntent(Intent intent) {
37 | if (intent != null) {
38 | final String action = intent.getAction();
39 | Log.i(TAG, "Processing " + action);
40 | switch (action) {
41 | case ACTION_SET_STATE:
42 | setEpisodesState(intent.getIntExtra(EXTRA_EPISODE_STATE, Provider.ESTATE_GONE),
43 | intent.getIntExtra(EXTRA_EPISODE_FILTER, Provider.ESTATE_GONE));
44 | break;
45 | default:
46 | Log.wtf(TAG, "Unexpected intent action: " + action);
47 | }
48 | Log.i(TAG, "Finished processing of " + action);
49 | }
50 | }
51 |
52 | private void setEpisodesState(int state, int stateFilter) {
53 | ContentValues cv = new ContentValues(1);
54 | cv.put(Provider.K_ESTATE, state);
55 | int result = getContentResolver().update(Provider.episodeUri,
56 | cv,
57 | Provider.K_ESTATE + " == " + stateFilter,
58 | null);
59 | Log.i(TAG, "Switched state from " + stateFilter + " to " + state + " for " + result + " eps");
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 | apply plugin: 'checkstyle'
3 |
4 | android {
5 | useLibrary 'org.apache.http.legacy'
6 | compileSdkVersion 26
7 | buildToolsVersion '27.0.1'
8 |
9 | defaultConfig {
10 | applicationId "com.einmalfel.podlisten"
11 | resValue "string", "app_id", applicationId
12 | minSdkVersion 16
13 | targetSdkVersion 23
14 | versionCode 1030600
15 | versionName "1.3.6"
16 | }
17 | buildTypes {
18 | release {
19 | debuggable false
20 | minifyEnabled true
21 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
22 | }
23 | debug {
24 | applicationIdSuffix '.debug'
25 | resValue "string", "app_id", defaultConfig.applicationId + applicationIdSuffix
26 | debuggable true
27 | jniDebuggable false
28 | renderscriptDebuggable false
29 | minifyEnabled false
30 | pseudoLocalesEnabled false
31 | }
32 | }
33 | packagingOptions {
34 | exclude 'META-INF/LICENSE.txt'
35 | exclude 'META-INF/NOTICE.txt'
36 | }
37 | compileOptions {
38 | sourceCompatibility JavaVersion.VERSION_1_7
39 | targetCompatibility JavaVersion.VERSION_1_7
40 | }
41 | lintOptions {
42 | warningsAsErrors true
43 | }
44 | }
45 |
46 | dependencies {
47 | compile fileTree(dir: 'libs', include: ['*.jar'])
48 | debugCompile 'com.facebook.stetho:stetho:1.5.0'
49 | debugCompile 'com.squareup.leakcanary:leakcanary-android:1.3.1'
50 | compile 'com.readystatesoftware.sqliteasset:sqliteassethelper:2.0.1'
51 | compile 'ch.acra:acra:4.9.2'
52 | compile 'com.nononsenseapps:filepicker:4.1.0'
53 | compile 'org.unbescape:unbescape:1.1.5.RELEASE'
54 | compile 'com.einmalfel:earl:1.2.0'
55 | compile 'com.android.support:design:26.1.0'
56 | compile 'com.android.support:recyclerview-v7:26.1.0'
57 | compile 'com.android.support:cardview-v7:26.1.0'
58 | compile 'com.android.support:support-annotations:27.0.0'
59 | compile 'com.android.support:support-v4:26.1.0'
60 | compile 'com.android.support:appcompat-v7:26.1.0'
61 | compile 'com.android.support:preference-v7:26.1.0'
62 | }
63 |
64 | task checkstyle(group: 'verification', type: Checkstyle) {
65 | configFile project.file("checkstyle_config.xml")
66 | source fileTree('src')
67 | include '**/*.java'
68 | exclude '**/thirdparty/**'
69 | classpath = files()
70 | project.tasks.check.dependsOn 'checkstyle'
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/ScrollTrackingBehaviour.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.Context;
4 | import android.support.design.widget.CoordinatorLayout;
5 | import android.support.design.widget.FloatingActionButton;
6 | import android.support.v4.view.ViewCompat;
7 | import android.support.v7.widget.LinearLayoutManager;
8 | import android.support.v7.widget.RecyclerView;
9 | import android.util.AttributeSet;
10 | import android.view.View;
11 |
12 | public class ScrollTrackingBehaviour extends FloatingActionButton.Behavior {
13 | private static final String TAG = "STB";
14 |
15 | /**
16 | * Need this constructor to reference ScrollTrackingBehaviour from xml
17 | */
18 | public ScrollTrackingBehaviour(Context context, AttributeSet attrs) {
19 | super();
20 | }
21 |
22 | @Override
23 | public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
24 | FloatingActionButton child, View directTargetChild,
25 | View target, int nestedScrollAxes) {
26 | if (target instanceof RecyclerView && nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL) {
27 | return true;
28 | } else {
29 | return super.onStartNestedScroll(
30 | coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
31 | }
32 | }
33 |
34 | @Override
35 | public void onNestedScroll(CoordinatorLayout coordinatorLayout, FloatingActionButton child,
36 | View target, int dxConsumed, int dyConsumed, int dxUnconsumed,
37 | int dyUnconsumed) {
38 | super.onNestedScroll(
39 | coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
40 |
41 | // hide FAB if scrolling down fast or if we are near the end of list
42 | if (dyConsumed < 0) {
43 | child.show();
44 | } else if (dyConsumed > 50 || approximatedVerticalScrollRatio((RecyclerView) target) > 0.75) {
45 | child.hide();
46 | }
47 | }
48 |
49 | /**
50 | * @return approximated equivalent of scrollPosition divided by scrollRange
51 | */
52 | float approximatedVerticalScrollRatio(RecyclerView recyclerView) {
53 | LinearLayoutManager linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
54 | int count = linearLayoutManager.getItemCount();
55 | int first = linearLayoutManager.findFirstCompletelyVisibleItemPosition();
56 | int last = linearLayoutManager.findLastCompletelyVisibleItemPosition();
57 | int visible = (last - first + 1);
58 | return (count == visible || count == 0) ? 0f : (float) first / (count - visible);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/MediaButtonReceiver.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.BroadcastReceiver;
4 | import android.content.ComponentName;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.support.v4.media.session.MediaSessionCompat;
8 | import android.util.Log;
9 | import android.view.KeyEvent;
10 |
11 | /**
12 | * Headset media button handler, controlled by PlayerService.
13 | * It subscribes to media button broadcast when PlayerService is started.
14 | */
15 | public class MediaButtonReceiver extends BroadcastReceiver {
16 | private static final String TAG = "MBR";
17 | private static PlayerService service = null;
18 | private static MediaSessionCompat session = null;
19 |
20 | public MediaButtonReceiver() {
21 | }
22 |
23 | static synchronized void setService(PlayerService playerService) {
24 | if (session == null && playerService != null) {
25 | ComponentName eventReceiver = new ComponentName(playerService.getPackageName(),
26 | MediaButtonReceiver.class.getName());
27 | session = new MediaSessionCompat(playerService, TAG, eventReceiver, null);
28 | session.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS);
29 | session.setCallback(new MediaSessionCompat.Callback() {
30 | @Override
31 | public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
32 | return handleButton(mediaButtonEvent) || super.onMediaButtonEvent(mediaButtonEvent);
33 | }
34 | });
35 | session.setActive(true);
36 | } else if (session != null && playerService == null) {
37 | session.release();
38 | session = null;
39 | }
40 | service = playerService;
41 | }
42 |
43 | @Override
44 | public void onReceive(Context context, Intent intent) {
45 | if (handleButton(intent)) {
46 | abortBroadcast();
47 | }
48 | }
49 |
50 | private static synchronized boolean handleButton(Intent intent) {
51 | KeyEvent ev = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
52 | if (service == null || ev.getAction() != KeyEvent.ACTION_DOWN || ev.getRepeatCount() != 0) {
53 | return false;
54 | }
55 | Log.d(TAG, "Processing media button: " + ev);
56 | switch (ev.getKeyCode()) {
57 | case KeyEvent.KEYCODE_MEDIA_PAUSE: // play and pause events could mean play_pause
58 | case KeyEvent.KEYCODE_MEDIA_PLAY:
59 | case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
60 | service.playPauseResume();
61 | break;
62 | case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
63 | case KeyEvent.KEYCODE_MEDIA_SKIP_FORWARD:
64 | case KeyEvent.KEYCODE_MEDIA_STEP_FORWARD:
65 | case KeyEvent.KEYCODE_MEDIA_NEXT:
66 | service.jumpForward();
67 | break;
68 | case KeyEvent.KEYCODE_MEDIA_REWIND:
69 | case KeyEvent.KEYCODE_MEDIA_SKIP_BACKWARD:
70 | case KeyEvent.KEYCODE_MEDIA_STEP_BACKWARD:
71 | case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
72 | service.jumpBackward();
73 | break;
74 | case KeyEvent.KEYCODE_MEDIA_STOP:
75 | service.stop();
76 | break;
77 | default:
78 | return false;
79 | }
80 | return true;
81 | }
82 |
83 | }
84 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/PodListenApp.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.annotation.SuppressLint;
4 | import android.app.Activity;
5 | import android.app.Application;
6 | import android.content.Context;
7 | import android.os.Bundle;
8 | import android.util.Log;
9 |
10 | import org.acra.ACRA;
11 | import org.acra.ReportField;
12 | import org.acra.ReportingInteractionMode;
13 | import org.acra.annotation.ReportsCrashes;
14 |
15 | @ReportsCrashes(
16 | mailTo = "einmalfel@gmail.com",
17 | logcatArguments = {"-t", "1000", "-v", "time", "*:D"},
18 | customReportContent = {
19 | ReportField.USER_CRASH_DATE, ReportField.AVAILABLE_MEM_SIZE, ReportField.TOTAL_MEM_SIZE,
20 | ReportField.BUILD, ReportField.BUILD_CONFIG, ReportField.DISPLAY,
21 | ReportField.CRASH_CONFIGURATION, ReportField.SHARED_PREFERENCES, ReportField.LOGCAT,
22 | ReportField.STACK_TRACE},
23 | mode = ReportingInteractionMode.DIALOG,
24 | resDialogText = R.string.crash_dialog_text,
25 | resDialogTitle = R.string.crash_dialog_title,
26 | resDialogOkToast = R.string.crash_dialog_ok_toast
27 | )
28 | public class PodListenApp extends DebuggableApp implements Application.ActivityLifecycleCallbacks {
29 | private static volatile PodListenApp instance;
30 |
31 | public static PodListenApp getInstance() {
32 | return instance;
33 | }
34 |
35 | public static Application getContext() {
36 | if (instance == null) {
37 | Log.wtf("APP", "Getting context before Application.onCreate()", new NullPointerException());
38 | }
39 | return instance;
40 | }
41 |
42 | @Override
43 | public void onCreate() {
44 | instance = this;
45 | ACRA.init(this);
46 | registerActivityLifecycleCallbacks(this);
47 | super.onCreate();
48 | }
49 |
50 | @Override
51 | public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
52 |
53 | @Override
54 | public void onActivityResumed(Activity activity) {
55 | Preferences.getInstance().setCurrentActivity(activity.getLocalClassName());
56 | }
57 |
58 | @Override
59 | public void onActivityStarted(Activity activity) {}
60 |
61 | @Override
62 | public void onActivityPaused(Activity activity) {
63 | Preferences.getInstance().setCurrentActivity(null);
64 | }
65 |
66 | @Override
67 | public void onActivityStopped(Activity activity) {}
68 |
69 | @Override
70 | public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
71 |
72 | @Override
73 | public void onActivityDestroyed(Activity activity) {}
74 |
75 | // when user decides to send report on his own, disable reporting via crash dialog, send report,
76 | // re-enable crash dialog.
77 | // using commit here to make sure we are not leaving temporary preference value
78 | @SuppressLint("ApplySharedPref")
79 | static void sendLogs() {
80 | ACRA.getACRASharedPreferences()
81 | .edit()
82 | .putBoolean(ACRA.PREF_ALWAYS_ACCEPT, Boolean.TRUE)
83 | .apply();
84 | try {
85 | ACRA.getErrorReporter().handleException(null, false);
86 | } finally {
87 | ACRA.getACRASharedPreferences()
88 | .edit()
89 | .putBoolean(ACRA.PREF_ALWAYS_ACCEPT, Boolean.FALSE)
90 | .commit();
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/NewEpisodesFragment.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 |
4 | import android.content.ContentValues;
5 | import android.database.Cursor;
6 | import android.os.Bundle;
7 | import android.support.v4.app.LoaderManager;
8 | import android.support.v4.content.CursorLoader;
9 | import android.support.v4.content.Loader;
10 | import android.support.v7.widget.DefaultItemAnimator;
11 | import android.support.v7.widget.RecyclerView;
12 | import android.view.LayoutInflater;
13 | import android.view.View;
14 | import android.view.ViewGroup;
15 |
16 | import com.einmalfel.podlisten.support.PredictiveAnimatiedLayoutManager;
17 |
18 |
19 | public class NewEpisodesFragment extends DebuggableFragment implements LoaderManager
20 | .LoaderCallbacks, EpisodeListAdapter.ItemClickListener {
21 | private MainActivity activity;
22 | private static final String TAG = "NEF";
23 | private static final MainActivity.Pages activityPage = MainActivity.Pages.NEW_EPISODES;
24 | private final EpisodeListAdapter adapter = new EpisodeListAdapter(null, this);
25 |
26 | @Override
27 | public void onDestroy() {
28 | adapter.swapCursor(null);
29 | super.onDestroy();
30 | }
31 |
32 | @Override
33 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
34 | savedInstanceState) {
35 | View layout = inflater.inflate(R.layout.common_list, container, false);
36 | RecyclerView rv = (RecyclerView) layout.findViewById(R.id.recycler_view);
37 | activity = (MainActivity) getActivity();
38 | rv.setLayoutManager(new PredictiveAnimatiedLayoutManager(activity));
39 | rv.setItemAnimator(new DefaultItemAnimator());
40 | activity.getSupportLoaderManager().initLoader(activityPage.ordinal(), null, this);
41 | rv.setAdapter(adapter);
42 | return layout;
43 | }
44 |
45 | @Override
46 | public boolean onLongTap(long id, String title, int state, String audioUrl, int downloaded) {
47 | activity.deleteEpisodeDialog(id, state, title);
48 | return true;
49 | }
50 |
51 | @Override
52 | public void onButtonTap(long id, String title, int state, String audioUrl, int downloaded) {
53 | ContentValues val = new ContentValues(1);
54 | val.put(Provider.K_ESTATE, Provider.ESTATE_IN_PLAYLIST);
55 | activity.getContentResolver().update(Provider.getUri(Provider.T_EPISODE, id), val, null, null);
56 | if (Preferences.getInstance().getAutoDownloadMode() == Preferences.AutoDownloadMode.PLAYLIST) {
57 | activity.sendBroadcast(DownloadReceiver.getUpdateQueueIntent(activity));
58 | }
59 | }
60 |
61 | @Override
62 | public Loader onCreateLoader(int id, Bundle args) {
63 | return new CursorLoader(activity,
64 | Provider.episodeJoinPodcastUri,
65 | EpisodeListAdapter.REQUIRED_DB_COLUMNS,
66 | Provider.K_ESTATE + " = " + Provider.ESTATE_NEW,
67 | null,
68 | Provider.K_EDATE);
69 | }
70 |
71 | @Override
72 | public void onLoadFinished(Loader loader, Cursor data) {
73 | activity.updateFab(data.getCount());
74 | adapter.swapCursor(data);
75 | }
76 |
77 | @Override
78 | public void onLoaderReset(Loader loader) {
79 | adapter.swapCursor(null);
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/preferences.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
8 |
12 |
16 |
20 |
24 |
28 |
29 |
31 |
35 |
39 |
44 |
45 |
46 |
51 |
52 |
54 |
58 |
62 |
63 |
67 |
71 |
72 |
73 |
74 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/EpisodeListAdapter.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.database.Cursor;
4 | import android.os.Handler;
5 | import android.os.Looper;
6 | import android.view.LayoutInflater;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 |
10 | public class EpisodeListAdapter extends BaseCursorRecyclerAdapter {
11 | public interface ItemClickListener {
12 | /**
13 | * @return true if event was consumed
14 | */
15 | boolean onLongTap(long id, String title, int state, String audioUrl, int downloaded);
16 |
17 | void onButtonTap(long id, String title, int state, String audioUrl, int downloaded);
18 | }
19 |
20 | private static final String TAG = "ELA";
21 | static final String[] REQUIRED_DB_COLUMNS = new String[]{
22 | Provider.K_EID, Provider.K_ENAME, Provider.K_EDESCR, Provider.K_EDFIN, Provider.K_ESIZE,
23 | Provider.K_ESTATE, Provider.K_PNAME, Provider.K_EPLAYED, Provider.K_ELENGTH, Provider.K_EDATE,
24 | Provider.K_EPID, Provider.K_ESDESCR, Provider.K_EERROR, Provider.K_EDID, Provider.K_EURL,
25 | Provider.K_EAURL};
26 | private final ItemClickListener listener;
27 | private long currentPlayingId = 0;
28 | private PlayerService.State currentState = PlayerService.State.STOPPED;
29 |
30 | public EpisodeListAdapter(Cursor cursor, ItemClickListener listener) {
31 | super(cursor);
32 | this.listener = listener;
33 | setHasStableIds(true);
34 | }
35 |
36 | void setCurrentIdState(long id, PlayerService.State state) {
37 | if (id != currentPlayingId || currentState != state) {
38 | currentPlayingId = id;
39 | currentState = state;
40 | new Handler(Looper.getMainLooper()).post(new Runnable() {
41 | @Override
42 | public void run() {
43 | notifyDataSetChanged();
44 | }
45 | });
46 | }
47 | }
48 |
49 | @Override
50 | public void onBindViewHolderCursor(EpisodeViewHolder holder, Cursor cursor) {
51 | long id = cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_ID));
52 | holder.bindEpisode(
53 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_ENAME)),
54 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_EDESCR)),
55 | id,
56 | cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_EPID)),
57 | cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_ESIZE)),
58 | cursor.getInt(cursor.getColumnIndexOrThrow(Provider.K_ESTATE)),
59 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_PNAME)),
60 | cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_EPLAYED)),
61 | cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_ELENGTH)),
62 | cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_EDATE)),
63 | cursor.getInt(cursor.getColumnIndexOrThrow(Provider.K_EDFIN)),
64 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_ESDESCR)),
65 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_EERROR)),
66 | currentPlayingId == id ? currentState : PlayerService.State.STOPPED,
67 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_EURL)),
68 | cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_EDID)),
69 | cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_EAURL)),
70 | expandedElements.contains(id));
71 | }
72 |
73 | @Override
74 | public EpisodeViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
75 | View view = LayoutInflater.from(parent.getContext())
76 | .inflate(R.layout.episode_list_element, parent, false);
77 | return new EpisodeViewHolder(parent.getContext(), view, listener, this);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/subscribe_dialog.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
13 |
14 |
19 |
20 |
22 |
23 |
24 |
25 |
34 |
35 |
42 |
43 |
50 |
51 |
61 |
62 |
70 |
71 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/SearchElementHolder.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.Context;
4 | import android.support.v7.widget.CardView;
5 | import android.support.v7.widget.RecyclerView;
6 | import android.text.TextUtils;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 | import android.widget.ImageButton;
10 | import android.widget.TextView;
11 |
12 | import com.einmalfel.podlisten.support.UnitConverter;
13 |
14 | class SearchElementHolder extends RecyclerView.ViewHolder {
15 |
16 | private final TextView titleView;
17 | private final TextView descriptionView;
18 | private final TextView urlView;
19 | private final TextView frequencyView;
20 | private final View dividerBottom;
21 | private final CardView cardView;
22 | private final Context context;
23 |
24 | private String rssUrl;
25 | private boolean expanded;
26 | private long id = -1;
27 |
28 | public SearchElementHolder(ViewGroup layout,
29 | final SearchAdapter.SearchClickListener listener,
30 | final SearchAdapter adapter) {
31 | super(layout);
32 | context = layout.getContext();
33 | titleView = (TextView) layout.findViewById(R.id.podcast_title);
34 | urlView = (TextView) layout.findViewById(R.id.podcast_url);
35 | frequencyView = (TextView) layout.findViewById(R.id.podcast_frequency);
36 | descriptionView = (TextView) layout.findViewById(R.id.podcast_description);
37 | dividerBottom = layout.findViewById(R.id.description_divider);
38 | cardView = (CardView) layout.findViewById(R.id.card);
39 | ImageButton addButton = (ImageButton) layout.findViewById(R.id.podcast_button);
40 | addButton.setOnClickListener(new View.OnClickListener() {
41 | @Override
42 | public void onClick(View view) {
43 | listener.onPodcastButtonTap(rssUrl);
44 | }
45 | });
46 | View relativeLayout = layout.findViewById(R.id.card_layout);
47 | relativeLayout.setOnClickListener(new View.OnClickListener() {
48 | @Override
49 | public void onClick(View view) {
50 | adapter.setExpanded(id, !expanded, getAdapterPosition());
51 | }
52 | });
53 |
54 | }
55 |
56 | void bind(String title, String description, String rssUrl, String url, long period, long id,
57 | boolean expanded) {
58 | if (this.id != id) {
59 | titleView.setText(title);
60 | descriptionView.setVisibility(TextUtils.isEmpty(description) ? View.GONE : View.VISIBLE);
61 | dividerBottom.setVisibility(TextUtils.isEmpty(description) ? View.GONE : View.VISIBLE);
62 | descriptionView.setText(description);
63 | urlView.setText(url != null && !url.isEmpty() ? url : rssUrl);
64 | double freq = 30d * 24 * 60 * 60 * 1000 / period;
65 | frequencyView.setText(context.getString(R.string.search_frequency, freq));
66 | this.rssUrl = rssUrl;
67 | this.id = id;
68 | }
69 |
70 | if (this.expanded != expanded) {
71 | if (expanded) {
72 | cardView.setCardElevation(UnitConverter.getInstance().dpToPx(8));
73 | } else {
74 | cardView.setCardElevation(UnitConverter.getInstance().dpToPx(2));
75 | }
76 | descriptionView.setSingleLine(!expanded);
77 | // without next line TextView still ellipsize first line when single line mode is turned off
78 | descriptionView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
79 | titleView.setSingleLine(!expanded);
80 | titleView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
81 | urlView.setSingleLine(!expanded);
82 | urlView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
83 | frequencyView.setSingleLine(!expanded);
84 | frequencyView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
85 | this.expanded = expanded;
86 | }
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/SubscriptionsFragment.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.DialogInterface;
4 | import android.database.Cursor;
5 | import android.os.Bundle;
6 | import android.support.v4.app.LoaderManager;
7 | import android.support.v4.content.CursorLoader;
8 | import android.support.v4.content.Loader;
9 | import android.support.v7.app.AlertDialog;
10 | import android.support.v7.widget.DefaultItemAnimator;
11 | import android.support.v7.widget.RecyclerView;
12 | import android.view.LayoutInflater;
13 | import android.view.View;
14 | import android.view.ViewGroup;
15 |
16 | import com.einmalfel.podlisten.support.PredictiveAnimatiedLayoutManager;
17 |
18 |
19 | public class SubscriptionsFragment extends DebuggableFragment implements
20 | LoaderManager.LoaderCallbacks, PodcastListAdapter.ItemClickListener {
21 | private MainActivity activity;
22 | private static final MainActivity.Pages activityPage = MainActivity.Pages.SUBSCRIPTIONS;
23 | private static final String TAG = "SSF";
24 | private final PodcastListAdapter adapter = new PodcastListAdapter(null, this);
25 | static final String[] projection = new String[]{
26 | Provider.K_ID, Provider.K_PNAME, Provider.K_PDESCR, Provider.K_PFURL, Provider.K_PSTATE,
27 | Provider.K_PURL, Provider.K_PTSTAMP, Provider.K_PERROR, Provider.K_PSDESCR};
28 |
29 | @Override
30 | public void onDestroy() {
31 | adapter.swapCursor(null);
32 | super.onDestroy();
33 | }
34 |
35 | @Override
36 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
37 | savedInstanceState) {
38 | View layout = inflater.inflate(R.layout.common_list, container, false);
39 | RecyclerView rv = (RecyclerView) layout.findViewById(R.id.recycler_view);
40 | activity = (MainActivity) getActivity();
41 | rv.setLayoutManager(new PredictiveAnimatiedLayoutManager(activity));
42 | rv.setItemAnimator(new DefaultItemAnimator());
43 | rv.setAdapter(adapter);
44 | activity.getSupportLoaderManager().initLoader(activityPage.ordinal(), null, this);
45 | return layout;
46 | }
47 |
48 | @Override
49 | public boolean onLongTap(final long podcatId, String title) {
50 | AlertDialog.Builder builder = new AlertDialog.Builder(activity);
51 | builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
52 | @Override
53 | public void onClick(DialogInterface dialog, int id) {
54 | ImageManager.getInstance().deleteImage(podcatId);
55 | activity.getContentResolver().delete(Provider.getUri(Provider.T_PODCAST, podcatId),
56 | null,
57 | null);
58 | BackgroundOperations.startCleanupEpisodes(getContext(), Provider.ESTATE_GONE);
59 | }
60 | });
61 | builder
62 | .setNegativeButton(R.string.cancel, null)
63 | .setTitle(title != null ? activity.getString(R.string.podcast_delete_question, title) :
64 | getString(R.string.podcast_delete_question_no_title))
65 | .create()
66 | .show();
67 | return true;
68 | }
69 |
70 | @Override
71 | public void onButtonTap(long id, String title) {
72 | FeedHistoryFragment historyFragment = FeedHistoryFragment.newInstance(id, title);
73 | historyFragment.show(getActivity().getSupportFragmentManager(), "history");
74 | }
75 |
76 | @Override
77 | public Loader onCreateLoader(int id, Bundle args) {
78 | return new CursorLoader(
79 | activity, Provider.podcastUri, projection, null, null, Provider.K_PATSTAMP + " DESC");
80 | }
81 |
82 | @Override
83 | public void onLoadFinished(Loader loader, Cursor data) {
84 | adapter.swapCursor(data);
85 | }
86 |
87 | @Override
88 | public void onLoaderReset(Loader loader) {
89 | adapter.swapCursor(null);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/PreferencesFragment.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.Intent;
4 | import android.database.Cursor;
5 | import android.net.Uri;
6 | import android.os.Bundle;
7 | import android.support.annotation.NonNull;
8 | import android.support.v7.preference.ListPreference;
9 | import android.support.v7.preference.Preference;
10 | import android.support.v7.preference.PreferenceFragmentCompat;
11 |
12 | import java.util.Set;
13 |
14 | public class PreferencesFragment extends PreferenceFragmentCompat {
15 | @Override
16 | public void onCreatePreferences(Bundle bundle, String screenKey) {
17 | addPreferencesFromResource(R.xml.preferences);
18 | }
19 |
20 | private static > void bindEnumToList(@NonNull ListPreference preference,
21 | @NonNull Class enumType) {
22 | int length = enumType.getEnumConstants().length;
23 | String[] entries = new String[length];
24 | String[] entryValues = new String[length];
25 | for (T value : enumType.getEnumConstants()) {
26 | int ordinal = value.ordinal();
27 | entries[ordinal] = value.toString();
28 | entryValues[ordinal] = Integer.toString(ordinal);
29 | }
30 | preference.setEntries(entries);
31 | preference.setEntryValues(entryValues);
32 | }
33 |
34 | @Override
35 | public void onCreate(Bundle savedInstanceState) {
36 | super.onCreate(savedInstanceState);
37 |
38 | ListPreference storageListPref = (ListPreference) findPreference(
39 | Preferences.Key.STORAGE_PATH.toString());
40 | Set storageOptions = Storage.getWritableStorages();
41 | String[] optionStrings = new String[storageOptions.size()];
42 | int index = 0;
43 | for (Storage storageOption : storageOptions) {
44 | optionStrings[index++] = storageOption.toString();
45 | }
46 | storageListPref.setEntries(optionStrings);
47 | storageListPref.setEntryValues(optionStrings);
48 |
49 | ListPreference refreshIntervalListPref = (ListPreference) findPreference(
50 | Preferences.Key.REFRESH_INTERVAL.toString());
51 | bindEnumToList(refreshIntervalListPref, Preferences.RefreshIntervalOption.class);
52 |
53 | ListPreference maxDownloadsListPref = (ListPreference) findPreference(
54 | Preferences.Key.MAX_DOWNLOADS.toString());
55 | bindEnumToList(maxDownloadsListPref, Preferences.MaxDownloadsOption.class);
56 |
57 | ListPreference autoDownloadListPref = (ListPreference) findPreference(
58 | Preferences.Key.AUTO_DOWNLOAD.toString());
59 | bindEnumToList(autoDownloadListPref, Preferences.AutoDownloadMode.class);
60 |
61 | ListPreference downloadNetworkListPref = (ListPreference) findPreference(
62 | Preferences.Key.DOWNLOAD_NETWORK.toString());
63 | bindEnumToList(downloadNetworkListPref, Preferences.DownloadNetwork.class);
64 |
65 | ListPreference onCompleteListPref = (ListPreference) findPreference(
66 | Preferences.Key.COMPLETE_ACTION.toString());
67 | bindEnumToList(onCompleteListPref, Preferences.CompleteAction.class);
68 |
69 | ListPreference jumpIntervalListPref = (ListPreference) findPreference(
70 | Preferences.Key.JUMP_INTERVAL.toString());
71 | bindEnumToList(jumpIntervalListPref, Preferences.JumpInterval.class);
72 |
73 | // if there is no mail app installed, disable send bug-report option
74 | Intent testEmailIntent = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", "", null));
75 | if (testEmailIntent.resolveActivity(getActivity().getPackageManager()) == null) {
76 | Preference sendBugReportPreference = findPreference("SEND_REPORT");
77 | sendBugReportPreference.setSummary(R.string.preferences_send_bug_report_summary_disabled);
78 | sendBugReportPreference.setEnabled(false);
79 | }
80 |
81 | Cursor cursor = getActivity().getContentResolver().query(
82 | Provider.podcastUri, null, null, null, null);
83 | if (cursor == null || cursor.getCount() == 0) {
84 | Preference opmlExportPreference = findPreference("OPML_EXPORT");
85 | opmlExportPreference.setSummary(R.string.preferences_opml_export_summary_disabled);
86 | opmlExportPreference.setEnabled(false);
87 | }
88 | if (cursor != null) {
89 | cursor.close();
90 | }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/history_list_element.xml:
--------------------------------------------------------------------------------
1 |
17 |
18 |
23 |
24 |
35 |
36 |
45 |
46 |
56 |
57 |
68 |
69 |
77 |
78 |
91 |
92 |
100 |
101 |
102 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/search_list_element.xml:
--------------------------------------------------------------------------------
1 |
17 |
18 |
23 |
24 |
35 |
36 |
45 |
46 |
56 |
57 |
69 |
70 |
78 |
79 |
92 |
93 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/EpisodesSyncAdapter.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.accounts.Account;
4 | import android.content.AbstractThreadedSyncAdapter;
5 | import android.content.ContentProviderClient;
6 | import android.content.ContentResolver;
7 | import android.content.Context;
8 | import android.content.SyncResult;
9 | import android.database.Cursor;
10 | import android.os.Bundle;
11 | import android.os.RemoteException;
12 | import android.util.Log;
13 |
14 | import java.util.Date;
15 | import java.util.concurrent.ExecutorService;
16 | import java.util.concurrent.Executors;
17 | import java.util.concurrent.TimeUnit;
18 |
19 | public class EpisodesSyncAdapter extends AbstractThreadedSyncAdapter {
20 | static final String FEED_ID_EXTRA_OPTION = "com.einmalfel.podlisten.FEED_ID";
21 |
22 | private static final String TAG = "SSA";
23 |
24 | private static final int WORKERS_NUMBER = 3;
25 |
26 | private static final int SYNC_TIMEOUT = 30 * 60; // [s]
27 |
28 | private static final String[] queryColumns = new String[]{
29 | Provider.K_ID, Provider.K_PFURL, Provider.K_PSTATE, Provider.K_PTSTAMP, Provider.K_PRMODE};
30 |
31 | public EpisodesSyncAdapter(Context context, boolean autoInitialize) {
32 | super(context, autoInitialize);
33 | }
34 |
35 | @Override
36 | public void onPerformSync(Account account, Bundle extras, String authority,
37 | ContentProviderClient provider, SyncResult syncResult) {
38 | Boolean manualSync = extras.getBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, false);
39 | Long requestedId = extras.getLong(FEED_ID_EXTRA_OPTION, 0);
40 | syncResult.tooManyRetries = extras.getBoolean(ContentResolver.SYNC_EXTRAS_DO_NOT_RETRY, false);
41 | SyncState syncState = new SyncState(getContext(), syncResult);
42 |
43 | Cursor cursor = null;
44 | try {
45 | cursor = provider.query(
46 | requestedId == 0 ? Provider.podcastUri : Provider.getUri(Provider.T_PODCAST, requestedId),
47 | queryColumns,
48 | null, null, null);
49 | } catch (RemoteException exception) {
50 | Log.e(TAG, "Failed to query podcast db", exception);
51 | }
52 | if (cursor == null) {
53 | syncResult.databaseError = true;
54 | syncState.error(getContext().getString(R.string.sync_database_error));
55 | return;
56 | }
57 | if (cursor.getCount() == 0) {
58 | Log.i(TAG, "No subscriptions, skipping sync");
59 | cursor.close();
60 | return;
61 | }
62 |
63 | syncState.start(cursor.getCount());
64 |
65 | ExecutorService executorService = Executors.newFixedThreadPool(WORKERS_NUMBER);
66 | while (cursor.moveToNext()) {
67 | long id = cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_ID));
68 | String url = cursor.getString(cursor.getColumnIndexOrThrow(Provider.K_PFURL));
69 | long feedTimestamp = cursor.getLong(cursor.getColumnIndexOrThrow(Provider.K_PTSTAMP));
70 | RefreshMode refreshMode = RefreshMode.values()[cursor.getInt(
71 | cursor.getColumnIndexOrThrow(Provider.K_PRMODE))];
72 |
73 | // If auto-sync is invoked more often then once in sync interval, it's sync retry and sync
74 | // adapter should process only feeds that failed to refresh on previous run.
75 | long syncPeriodMs = Preferences.getInstance().getRefreshInterval().periodSeconds * 1000;
76 | if (!manualSync && (new Date().getTime() - feedTimestamp < syncPeriodMs)) {
77 | Log.i(TAG, "Skipping feed refresh (syncing to often): " + id);
78 | syncState.signalFeedSuccess(null, 0);
79 | continue;
80 | }
81 |
82 | executorService.execute(new SyncWorker(id, url, provider, syncState, refreshMode));
83 | }
84 | cursor.close();
85 |
86 | executorService.shutdown();
87 | boolean workersDone = false;
88 | try {
89 | workersDone = executorService.awaitTermination(SYNC_TIMEOUT, TimeUnit.SECONDS);
90 | } catch (InterruptedException interrupt) {
91 | Thread.currentThread().interrupt();
92 | syncState.error(getContext().getString(R.string.sync_interrupted_by_system));
93 | // sync cancelled. Discard queue, try to interrupt workers and wait for them again
94 | executorService.shutdownNow();
95 | try {
96 | workersDone = executorService.awaitTermination(SYNC_TIMEOUT, TimeUnit.SECONDS);
97 | } catch (InterruptedException ignored) {
98 | Log.e(TAG, "Failed to interrupt workers");
99 | }
100 | }
101 | if (!workersDone) {
102 | Log.e(TAG, "Some of workers hanged during sync");
103 | } else {
104 | getContext().sendBroadcast(DownloadReceiver.getUpdateQueueIntent(getContext()));
105 | }
106 |
107 | syncState.stop();
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/SearchActivity.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.database.Cursor;
6 | import android.graphics.PorterDuff;
7 | import android.graphics.PorterDuffColorFilter;
8 | import android.os.Bundle;
9 | import android.support.v4.app.FragmentManager;
10 | import android.support.v4.content.ContextCompat;
11 | import android.support.v7.app.ActionBar;
12 | import android.support.v7.app.AppCompatActivity;
13 | import android.support.v7.widget.DefaultItemAnimator;
14 | import android.support.v7.widget.RecyclerView;
15 | import android.support.v7.widget.SearchView;
16 | import android.util.Log;
17 | import android.view.ViewGroup;
18 | import android.widget.ProgressBar;
19 |
20 | import com.einmalfel.podlisten.support.PredictiveAnimatiedLayoutManager;
21 |
22 | public class SearchActivity extends AppCompatActivity
23 | implements android.support.v7.widget.SearchView.OnQueryTextListener,
24 | SearchAdapter.SearchClickListener, CatalogueFragment.CatalogueListener {
25 |
26 | static final String RSS_URL_EXTRA = "rss_url";
27 | private static final String TAG = "SAC";
28 |
29 | private CatalogueFragment catalogue;
30 | private SearchAdapter adapter;
31 | private ProgressBar progressBar;
32 | private SearchView searchView;
33 |
34 | @Override
35 | public void onLoadProgress(int progress) {
36 | if (progress == 100) {
37 | setContentView(R.layout.common_list);
38 | RecyclerView rv = (RecyclerView) findViewById(R.id.recycler_view);
39 | rv.setLayoutManager(new PredictiveAnimatiedLayoutManager(SearchActivity.this));
40 | rv.setItemAnimator(new DefaultItemAnimator());
41 | rv.setAdapter(adapter);
42 | } else {
43 | progressBar.setIndeterminate(false);
44 | progressBar.setProgress(progress);
45 | }
46 | }
47 |
48 | @Override
49 | public void onQueryComplete(Cursor cursor) {
50 | adapter.swapCursor(cursor);
51 | }
52 |
53 | @Override
54 | protected void onCreate(Bundle savedInstanceState) {
55 | super.onCreate(savedInstanceState);
56 |
57 | setContentView(R.layout.search_splash);
58 | progressBar = (ProgressBar) findViewById(R.id.splash_progress);
59 | PorterDuffColorFilter progressFilter = new PorterDuffColorFilter(
60 | ContextCompat.getColor(this, R.color.accent_secondary), PorterDuff.Mode.MULTIPLY);
61 | progressBar.getProgressDrawable().setColorFilter(progressFilter);
62 | progressBar.getIndeterminateDrawable().setColorFilter(progressFilter);
63 |
64 | ActionBar actionBar = getSupportActionBar();
65 | if (actionBar != null) {
66 | actionBar.setDisplayHomeAsUpEnabled(true);
67 | searchView = new SearchView(this);
68 | searchView.setIconified(false);
69 | searchView.setOnQueryTextListener(this);
70 | searchView.requestFocus();
71 | actionBar.setCustomView(searchView, new ActionBar.LayoutParams(
72 | ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
73 | actionBar.setDisplayShowCustomEnabled(true);
74 | actionBar.setDisplayShowTitleEnabled(false);
75 | } else {
76 | Log.wtf(TAG, "Should never get here: failed to get action bar of search activity");
77 | }
78 |
79 | adapter = new SearchAdapter(this);
80 |
81 | FragmentManager fm = getSupportFragmentManager();
82 | catalogue = (CatalogueFragment) fm.findFragmentByTag("catalogue");
83 | if (catalogue == null) {
84 | catalogue = new CatalogueFragment();
85 | catalogue.setRetainInstance(true);
86 | fm.beginTransaction().add(catalogue, "catalogue").commit();
87 | } else {
88 | onLoadProgress(catalogue.getLoadProgress());
89 | }
90 | }
91 |
92 | @Override
93 | protected void onPause() {
94 | super.onPause();
95 | catalogue.setListener(null);
96 | }
97 |
98 | @Override
99 | protected void onResume() {
100 | super.onResume();
101 | catalogue.setListener(this);
102 | catalogue.query(searchView.getQuery().toString().split(" "));
103 | }
104 |
105 | @Override
106 | public boolean onQueryTextSubmit(String query) {
107 | return false;
108 | }
109 |
110 | @Override
111 | public boolean onQueryTextChange(String newText) {
112 | catalogue.query(newText.split(" "));
113 | return true;
114 | }
115 |
116 | @Override
117 | public void onPodcastButtonTap(String rssUrl) {
118 | Intent intent = new Intent();
119 | intent.putExtra(RSS_URL_EXTRA, rssUrl);
120 | setResult(Activity.RESULT_OK, intent);
121 | finish();
122 | }
123 |
124 | @Override
125 | protected void onNewIntent(Intent intent) {
126 | super.onNewIntent(intent);
127 | Log.e(TAG, intent.toString());
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/PlaylistFragment.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 |
4 | import android.database.Cursor;
5 | import android.os.Bundle;
6 | import android.support.v4.app.LoaderManager;
7 | import android.support.v4.content.CursorLoader;
8 | import android.support.v4.content.Loader;
9 | import android.support.v7.widget.DefaultItemAnimator;
10 | import android.support.v7.widget.RecyclerView;
11 | import android.util.Log;
12 | import android.view.LayoutInflater;
13 | import android.view.View;
14 | import android.view.ViewGroup;
15 |
16 | import com.einmalfel.podlisten.support.PredictiveAnimatiedLayoutManager;
17 |
18 |
19 | public class PlaylistFragment extends DebuggableFragment implements
20 | LoaderManager.LoaderCallbacks, EpisodeListAdapter.ItemClickListener,
21 | PlayerService.PlayerStateListener {
22 | private MainActivity activity;
23 | private static final String TAG = "PLF";
24 | private static final MainActivity.Pages activityPage = MainActivity.Pages.PLAYLIST;
25 | private final EpisodeListAdapter adapter = new EpisodeListAdapter(null, this);
26 | private PlayerLocalConnection conn;
27 | private RecyclerView rv;
28 |
29 | @Override
30 | public void onCreate(Bundle savedInstanceState) {
31 | super.onCreate(savedInstanceState);
32 | conn = new PlayerLocalConnection(this);
33 | }
34 |
35 | @Override
36 | public void onDestroy() {
37 | adapter.swapCursor(null);
38 | super.onDestroy();
39 | }
40 |
41 | @Override
42 | public void onPause() {
43 | super.onPause();
44 | conn.unbind();
45 | }
46 |
47 | @Override
48 | public void onResume() {
49 | super.onResume();
50 | conn.bind();
51 | }
52 |
53 | @Override
54 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle
55 | savedInstanceState) {
56 | View layout = inflater.inflate(R.layout.common_list, container, false);
57 | rv = (RecyclerView) layout.findViewById(R.id.recycler_view);
58 | activity = (MainActivity) getActivity();
59 | rv.setLayoutManager(new PredictiveAnimatiedLayoutManager(activity));
60 | rv.setItemAnimator(new DefaultItemAnimator());
61 | activity.getSupportLoaderManager().initLoader(activityPage.ordinal(), null, this);
62 | rv.setAdapter(adapter);
63 |
64 | return layout;
65 | }
66 |
67 | @Override
68 | public boolean onLongTap(long id, String title, int state, String audioUrl, int downloaded) {
69 | activity.deleteEpisodeDialog(id, state, title);
70 | return true;
71 | }
72 |
73 | @Override
74 | public void onButtonTap(long id, String title, int state, String audioUrl, int downloaded) {
75 | // episode button in playlist is enabled in two cases:
76 | // - episode is downloaded, button is used for play/pause
77 | // - episode isn't downloaded, isn't being download (downloadId == 0), button stats download
78 |
79 | if (downloaded != Provider.EDFIN_COMPLETE) {
80 | PodListenApp.getContext().sendBroadcast(DownloadReceiver.getDownloadEpisodeIntent(
81 | PodListenApp.getContext(), audioUrl, title, id));
82 | } else {
83 | if (conn.service != null) {
84 | if (id == conn.service.getEpisodeId()) {
85 | conn.service.playPauseResume();
86 | } else {
87 | conn.service.playEpisode(id);
88 | }
89 | }
90 | }
91 | }
92 |
93 | public void reloadList() {
94 | activity.getSupportLoaderManager().restartLoader(
95 | MainActivity.Pages.PLAYLIST.ordinal(), null, this);
96 | }
97 |
98 | @Override
99 | public Loader onCreateLoader(int id, Bundle args) {
100 | return new CursorLoader(activity,
101 | Provider.episodeJoinPodcastUri,
102 | EpisodeListAdapter.REQUIRED_DB_COLUMNS,
103 | Provider.K_ESTATE + " = " + Provider.ESTATE_IN_PLAYLIST,
104 | null,
105 | Preferences.getInstance().getSortingMode().toSql());
106 | }
107 |
108 | @Override
109 | public void onLoadFinished(Loader loader, Cursor data) {
110 | adapter.swapCursor(data);
111 | if (activity.pendingScrollId != 0) {
112 | for (int pos = 0; pos < adapter.getItemCount(); pos++) {
113 | if (adapter.getItemId(pos) == activity.pendingScrollId) {
114 | Log.d(TAG, "scrolling to " + pos + " id " + activity.pendingScrollId);
115 | rv.smoothScrollToPosition(pos);
116 | adapter.setExpanded(activity.pendingScrollId, true, pos);
117 | activity.pendingScrollId = 0;
118 | return;
119 | }
120 | }
121 | }
122 | }
123 |
124 | @Override
125 | public void onLoaderReset(Loader loader) {
126 | adapter.swapCursor(null);
127 | }
128 |
129 | @Override
130 | public void progressUpdate(int position, int max) {}
131 |
132 | @Override
133 | public void stateUpdate(PlayerService.State state, long episodeId) {
134 | adapter.setCurrentIdState(episodeId, state);
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/HistoryElementHolder.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.Context;
4 | import android.support.v4.content.ContextCompat;
5 | import android.support.v7.widget.CardView;
6 | import android.support.v7.widget.RecyclerView;
7 | import android.text.Html;
8 | import android.text.TextUtils;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 | import android.widget.ImageButton;
12 | import android.widget.TextView;
13 |
14 | import com.einmalfel.podlisten.support.UnitConverter;
15 |
16 | public class HistoryElementHolder extends RecyclerView.ViewHolder {
17 | private final TextView titleView;
18 | private final TextView urlView;
19 | private final TextView dateView;
20 | private final TextView descriptionView;
21 | private final View dividerBottom;
22 | private final CardView cardView;
23 | private final ImageButton button;
24 | private long id;
25 | private boolean expanded;
26 | private Context context = PodListenApp.getContext();
27 | private int state = -1;
28 |
29 | public HistoryElementHolder(
30 | ViewGroup layout, final FeedHistoryAdapter.HistoryEpisodeListener listener,
31 | final FeedHistoryAdapter adapter) {
32 | super(layout);
33 | titleView = (TextView) layout.findViewById(R.id.episode_title);
34 | urlView = (TextView) layout.findViewById(R.id.episode_url);
35 | dateView = (TextView) layout.findViewById(R.id.pub_date);
36 | descriptionView = (TextView) layout.findViewById(R.id.episode_description);
37 | dividerBottom = layout.findViewById(R.id.description_divider);
38 | cardView = (CardView) layout.findViewById(R.id.card);
39 | button = (ImageButton) layout.findViewById(R.id.episode_button);
40 | button.setOnClickListener(new View.OnClickListener() {
41 | @Override
42 | public void onClick(View view) {
43 | listener.onEpisodeButtonTap(id, state);
44 | }
45 | });
46 | View relativeLayout = layout.findViewById(R.id.card_layout);
47 | relativeLayout.setOnClickListener(new View.OnClickListener() {
48 | @Override
49 | public void onClick(View view) {
50 | adapter.setExpanded(id, !expanded, getAdapterPosition());
51 | }
52 | });
53 | }
54 |
55 | public void bind(String title, String description, String shortDescr, String url, long date,
56 | int state, long played, long id, boolean expanded) {
57 | if ((id != this.id || expanded != this.expanded) && !TextUtils.isEmpty(description)) {
58 | if (expanded) {
59 | descriptionView.setText(Html.fromHtml(description), TextView.BufferType.SPANNABLE);
60 | } else {
61 | descriptionView.setText(shortDescr, TextView.BufferType.NORMAL);
62 | }
63 | }
64 |
65 | if (id != this.id) {
66 | titleView.setText(title);
67 | urlView.setText(url);
68 | descriptionView.setVisibility(TextUtils.isEmpty(description) ? View.GONE : View.VISIBLE);
69 | dividerBottom.setVisibility(TextUtils.isEmpty(description) ? View.GONE : View.VISIBLE);
70 | dateView.setText(
71 | context.getString(R.string.episode_published, PodcastHelper.shortDateFormat(date)));
72 | this.id = id;
73 | }
74 |
75 | if (played == -1) {
76 | titleView.setTextColor(ContextCompat.getColor(context, R.color.text_bright));
77 | descriptionView.setTextColor(ContextCompat.getColor(context, R.color.text));
78 | dateView.setTextColor(ContextCompat.getColor(context, R.color.text));
79 | urlView.setTextColor(ContextCompat.getColor(context, R.color.accent_primary));
80 | urlView.setLinkTextColor(ContextCompat.getColor(context, R.color.accent_primary));
81 | } else {
82 | titleView.setTextColor(ContextCompat.getColor(context, R.color.text));
83 | descriptionView.setTextColor(ContextCompat.getColor(context, R.color.text_dim));
84 | dateView.setTextColor(ContextCompat.getColor(context, R.color.text_dim));
85 | urlView.setTextColor(ContextCompat.getColor(context, R.color.accent_primary_dim));
86 | urlView.setLinkTextColor(ContextCompat.getColor(context, R.color.accent_primary_dim));
87 | }
88 |
89 | if (this.state != state) {
90 | button.setImageResource(state == Provider.ESTATE_IN_PLAYLIST ? R.mipmap.ic_delete_white_48dp :
91 | R.mipmap.ic_add_white_48dp);
92 | this.state = state;
93 | }
94 |
95 | if (expanded != this.expanded) {
96 | cardView.setCardElevation(UnitConverter.getInstance().dpToPx(expanded ? 8 : 2));
97 | descriptionView.setSingleLine(!expanded);
98 | descriptionView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
99 | titleView.setSingleLine(!expanded);
100 | titleView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
101 | urlView.setSingleLine(!expanded);
102 | urlView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
103 | dateView.setSingleLine(!expanded);
104 | dateView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
105 | this.expanded = expanded;
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/player.xml:
--------------------------------------------------------------------------------
1 |
11 |
13 |
14 |
21 |
22 |
28 |
29 |
37 |
38 |
47 |
48 |
49 |
56 |
57 |
67 |
68 |
78 |
79 |
89 |
90 |
100 |
101 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/podcast_list_element.xml:
--------------------------------------------------------------------------------
1 |
17 |
18 |
22 |
23 |
36 |
37 |
49 |
50 |
61 |
62 |
69 |
70 |
82 |
83 |
94 |
95 |
103 |
104 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/FeedHistoryFragment.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.app.Dialog;
4 | import android.content.ContentValues;
5 | import android.database.Cursor;
6 | import android.os.Bundle;
7 | import android.support.annotation.NonNull;
8 | import android.support.annotation.Nullable;
9 | import android.support.v4.app.DialogFragment;
10 | import android.support.v4.app.LoaderManager;
11 | import android.support.v4.content.ContextCompat;
12 | import android.support.v4.content.CursorLoader;
13 | import android.support.v4.content.Loader;
14 | import android.support.v7.app.AlertDialog;
15 | import android.support.v7.widget.DefaultItemAnimator;
16 | import android.support.v7.widget.RecyclerView;
17 | import android.util.Log;
18 | import android.util.TypedValue;
19 | import android.view.Gravity;
20 | import android.view.LayoutInflater;
21 | import android.view.View;
22 | import android.view.ViewGroup;
23 | import android.widget.TextView;
24 |
25 | import com.einmalfel.podlisten.support.PredictiveAnimatiedLayoutManager;
26 | import com.einmalfel.podlisten.support.UnitConverter;
27 |
28 | public class FeedHistoryFragment extends DialogFragment implements
29 | LoaderManager.LoaderCallbacks, FeedHistoryAdapter.HistoryEpisodeListener {
30 | private static final String TAG = "FHF";
31 | private static final String ARG_PODCAST_ID = "Podcast_ID";
32 | private static final String ARG_PODCAST_TITLE = "Podcast_Title";
33 | private static final int LOADER_ID = 20;
34 |
35 | private long podcastId;
36 | private String podcastTitle;
37 | private FeedHistoryAdapter adapter = new FeedHistoryAdapter(this);
38 | private ViewGroup container;
39 |
40 | public FeedHistoryFragment() {
41 | }
42 |
43 | public static FeedHistoryFragment newInstance(long podcastId, String title) {
44 | FeedHistoryFragment fragment = new FeedHistoryFragment();
45 | Bundle args = new Bundle();
46 | args.putLong(ARG_PODCAST_ID, podcastId);
47 | args.putString(ARG_PODCAST_TITLE, title);
48 | fragment.setArguments(args);
49 | return fragment;
50 | }
51 |
52 | @Override
53 | public void onCreate(Bundle savedInstanceState) {
54 | super.onCreate(savedInstanceState);
55 | if (getArguments() != null) {
56 | podcastId = getArguments().getLong(ARG_PODCAST_ID);
57 | podcastTitle = getArguments().getString(ARG_PODCAST_TITLE);
58 | getLoaderManager().initLoader(LOADER_ID, null, this);
59 | } else {
60 | Log.wtf(TAG, "Empty fragment arguments");
61 | }
62 |
63 | }
64 |
65 | @Nullable
66 | @Override
67 | public View onCreateView(LayoutInflater inflater, ViewGroup container,
68 | Bundle savedInstanceState) {
69 | this.container = container;
70 | return null;
71 | }
72 |
73 | @NonNull
74 | @Override
75 | public Dialog onCreateDialog(Bundle savedInstanceState) {
76 | View layout = getActivity().getLayoutInflater().inflate(R.layout.common_list, container);
77 | RecyclerView rv = (RecyclerView) layout.findViewById(R.id.recycler_view);
78 | rv.setLayoutManager(new PredictiveAnimatiedLayoutManager(getContext()));
79 | rv.setItemAnimator(new DefaultItemAnimator());
80 | rv.setAdapter(adapter);
81 |
82 | TextView titleView = new TextView(getContext());
83 | int eightDpInPx = UnitConverter.getInstance().dpToPx(8);
84 | titleView.setText(podcastTitle);
85 | titleView.setPadding(eightDpInPx, eightDpInPx, eightDpInPx, eightDpInPx);
86 | titleView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16);
87 | titleView.setTextColor(ContextCompat.getColor(getContext(), R.color.text_bright));
88 | titleView.setSingleLine();
89 | titleView.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.background_contrast));
90 | titleView.setGravity(Gravity.CENTER);
91 |
92 | return new AlertDialog.Builder(getContext()).setView(layout).setCustomTitle(titleView).create();
93 | }
94 |
95 | @Override
96 | public void onEpisodeButtonTap(long id, int state) {
97 | if (state == Provider.ESTATE_IN_PLAYLIST) {
98 | ContentValues cv = new ContentValues(1);
99 | cv.put(Provider.K_ESTATE, Provider.ESTATE_GONE);
100 | getContext().getContentResolver().update(
101 | Provider.getUri(Provider.T_EPISODE, id), cv, null, null);
102 | BackgroundOperations.startCleanupEpisodes(getContext(), Provider.ESTATE_GONE);
103 | } else {
104 | ContentValues cv = new ContentValues(1);
105 | cv.put(Provider.K_ESTATE, Provider.ESTATE_IN_PLAYLIST);
106 | getContext().getContentResolver().update(
107 | Provider.getUri(Provider.T_EPISODE, id), cv, null, null);
108 | if (Preferences.getInstance().getAutoDownloadMode() != Preferences.AutoDownloadMode.NEVER) {
109 | getContext().sendBroadcast(DownloadReceiver.getUpdateQueueIntent(getContext()));
110 | }
111 | }
112 | }
113 |
114 | @Override
115 | public Loader onCreateLoader(int id, Bundle args) {
116 | return new CursorLoader(getContext(),
117 | Provider.episodeUri,
118 | FeedHistoryAdapter.COLUMNS_NEEDED,
119 | Provider.K_EPID + " == " + podcastId,
120 | null,
121 | Provider.K_EDATE + " DESC");
122 | }
123 |
124 | @Override
125 | public void onLoadFinished(Loader loader, Cursor data) {
126 | adapter.swapCursor(data);
127 | }
128 |
129 | @Override
130 | public void onLoaderReset(Loader loader) {
131 | adapter.swapCursor(null);
132 | }
133 | }
134 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/PodcastViewHolder.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.support.v4.content.ContextCompat;
6 | import android.support.v7.widget.CardView;
7 | import android.support.v7.widget.RecyclerView;
8 | import android.text.Html;
9 | import android.text.TextUtils;
10 | import android.text.format.DateUtils;
11 | import android.view.View;
12 | import android.widget.ImageButton;
13 | import android.widget.ImageView;
14 | import android.widget.RelativeLayout;
15 | import android.widget.TextView;
16 |
17 | import com.einmalfel.podlisten.support.PatchedTextView;
18 | import com.einmalfel.podlisten.support.UnitConverter;
19 |
20 | public class PodcastViewHolder extends RecyclerView.ViewHolder {
21 | private final PatchedTextView descriptionView;
22 | private final TextView titleView;
23 | private final TextView urlView;
24 | private final TextView statusView;
25 | private final ImageView imageView;
26 | private final Context context;
27 | private final View dividerBottom;
28 | private final CardView cardView;
29 | private final ImageButton button;
30 | private long id = 0;
31 | private boolean expanded = false;
32 | private String title;
33 |
34 | public PodcastViewHolder(View layout, final PodcastListAdapter.ItemClickListener listener,
35 | final PodcastListAdapter adapter) {
36 | super(layout);
37 | context = layout.getContext();
38 | titleView = (TextView) layout.findViewById(R.id.podcast_title);
39 | descriptionView = (PatchedTextView) layout.findViewById(R.id.podcast_description);
40 | dividerBottom = layout.findViewById(R.id.description_divider);
41 | urlView = (TextView) layout.findViewById(R.id.podcast_url);
42 | statusView = (TextView) layout.findViewById(R.id.podcast_status);
43 | imageView = (ImageView) layout.findViewById(R.id.podcast_image);
44 | cardView = (CardView) layout.findViewById(R.id.card);
45 | button = (ImageButton) layout.findViewById(R.id.podcast_button);
46 | button.setOnClickListener(new View.OnClickListener() {
47 | @Override
48 | public void onClick(View view) {
49 | listener.onButtonTap(id, title);
50 | }
51 | });
52 | RelativeLayout relativeLayout = (RelativeLayout) layout.findViewById(R.id.card_layout);
53 | relativeLayout.setOnClickListener(new View.OnClickListener() {
54 | @Override
55 | public void onClick(View view) {
56 | adapter.setExpanded(id, !expanded, getAdapterPosition());
57 | }
58 | });
59 | relativeLayout.setOnLongClickListener(new View.OnLongClickListener() {
60 | @Override
61 | public boolean onLongClick(View view) {
62 | return listener.onLongTap(id, title);
63 | }
64 | });
65 | }
66 |
67 | void bind(int state, String title, String description, String url, String podcastPage, long id,
68 | String shortDescr, String error, long timestamp, boolean expanded) {
69 | titleView.setText(title);
70 | titleView.setText(title == null ? context.getString(R.string.podcast_no_title) : title);
71 | urlView.setText(podcastPage == null ? url : podcastPage);
72 | if (state == Provider.PSTATE_LAST_REFRESH_FAILED) {
73 | statusView.setText(context.getString(R.string.podcast_refresh_failed, error));
74 | statusView.setTextColor(ContextCompat.getColor(context, R.color.accent_secondary));
75 | } else {
76 | statusView.setTextColor(ContextCompat.getColor(context, R.color.text));
77 | if (state == Provider.PSTATE_NEW) {
78 | statusView.setText(R.string.podcast_not_loaded_yet);
79 | } else {
80 | statusView.setText(context.getString(R.string.podcast_refresh_time,
81 | DateUtils.getRelativeTimeSpanString(timestamp)));
82 | }
83 | }
84 | if (state == Provider.PSTATE_NEW) {
85 | button.setEnabled(false);
86 | button.setColorFilter(MainActivity.disabledFilter);
87 | } else {
88 | button.setEnabled(true);
89 | button.setColorFilter(null);
90 | }
91 |
92 |
93 | if (description == null || description.isEmpty()) {
94 | descriptionView.setVisibility(View.GONE);
95 | dividerBottom.setVisibility(View.GONE);
96 | } else {
97 | if (expanded) {
98 | descriptionView.setText(Html.fromHtml(description), TextView.BufferType.SPANNABLE);
99 | } else {
100 | descriptionView.setText(shortDescr, TextView.BufferType.NORMAL);
101 | }
102 | descriptionView.setVisibility(View.VISIBLE);
103 | dividerBottom.setVisibility(View.VISIBLE);
104 | }
105 | if (this.expanded != expanded) {
106 | descriptionView.setSingleLine(!expanded);
107 | descriptionView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
108 | statusView.setSingleLine(!expanded);
109 | statusView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
110 | urlView.setSingleLine(!expanded);
111 | urlView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
112 | titleView.setMaxLines(expanded ? Integer.MAX_VALUE : 2);
113 | titleView.setEllipsize(expanded ? null : TextUtils.TruncateAt.END);
114 | cardView.setCardElevation(UnitConverter.getInstance().dpToPx(expanded ? 8 : 2));
115 | }
116 |
117 | Bitmap image = ImageManager.getInstance().getImage(id);
118 | if (image == null) {
119 | imageView.setImageResource(R.drawable.logo);
120 | } else {
121 | imageView.setImageBitmap(image);
122 | }
123 |
124 | this.id = id;
125 | this.expanded = expanded;
126 | this.title = title;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/SyncState.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.app.PendingIntent;
4 | import android.content.Context;
5 | import android.content.Intent;
6 | import android.content.SyncResult;
7 | import android.database.Cursor;
8 | import android.support.annotation.NonNull;
9 | import android.support.annotation.Nullable;
10 | import android.support.v4.app.NotificationCompat;
11 | import android.support.v4.app.NotificationManagerCompat;
12 |
13 | /**
14 | * This class keeps records on sync errors and manages sync notification
15 | */
16 | public class SyncState {
17 | private static final int NOTIFICATION_ID = 1;
18 | private final SyncResult syncResult;
19 | private final NotificationManagerCompat nm;
20 | private final NotificationCompat.Builder nb;
21 | private final Context context;
22 | private int maxFeeds = 0;
23 | private int errors = 0;
24 | private int parsed = 0;
25 | private int newEpisodes = 0;
26 | private boolean stopped = false;
27 |
28 | SyncState(@NonNull Context context, @NonNull SyncResult syncResult) {
29 | this.syncResult = syncResult;
30 | this.context = context;
31 | nm = NotificationManagerCompat.from(context);
32 | nb = new NotificationCompat.Builder(context);
33 | Intent intent = new Intent(context, MainActivity.class);
34 | intent.putExtra(MainActivity.PAGE_LAUNCH_OPTION, MainActivity.Pages.NEW_EPISODES.ordinal());
35 | PendingIntent pendingIntent = PendingIntent.getActivity(
36 | context, NOTIFICATION_ID, intent, PendingIntent.FLAG_UPDATE_CURRENT);
37 | nb.setSmallIcon(R.mipmap.ic_sync_white_24dp)
38 | .setCategory(NotificationCompat.CATEGORY_PROGRESS)
39 | .setPriority(NotificationCompat.PRIORITY_LOW)
40 | .setContentIntent(pendingIntent);
41 | }
42 |
43 | synchronized void start(int maxFeeds) {
44 | this.maxFeeds = maxFeeds;
45 | nb.setContentTitle(context.getString(R.string.sync_running))
46 | .setOngoing(true)
47 | .setAutoCancel(false)
48 | .setProgress(0, 0, true);
49 | updateNotification();
50 | }
51 |
52 | synchronized void error(String message) {
53 | nb.setOngoing(false)
54 | .setAutoCancel(true)
55 | .setProgress(0, 0, false)
56 | .setContentTitle(context.getString(R.string.sync_failed))
57 | .setContentText(message);
58 | updateNotification();
59 | stopped = true;
60 | }
61 |
62 | synchronized void stop() {
63 | // don't keep "refreshed" notification user if main activity is on screen
64 | String currentActivity = Preferences.getInstance().getCurrentActivity(true);
65 | if (MainActivity.class.getSimpleName().equals(currentActivity) && errors == 0) {
66 | stopped = true;
67 | nm.cancel(NOTIFICATION_ID);
68 | return;
69 | }
70 |
71 | Cursor cursor = context.getContentResolver().query(
72 | Provider.episodeUri,
73 | null,
74 | Provider.K_ESTATE + " == " + Provider.ESTATE_NEW,
75 | null,
76 | null,
77 | null);
78 | StringBuilder stringBuilder = new StringBuilder();
79 | if (cursor == null) {
80 | stringBuilder.append(context.getString(R.string.sync_database_error));
81 | } else {
82 | int count = cursor.getCount();
83 | cursor.close();
84 |
85 | String newEpisodesCount;
86 | if (count == newEpisodes) {
87 | newEpisodesCount = Integer.toString(newEpisodes);
88 | } else {
89 | newEpisodesCount = Integer.toString(count);
90 | if (newEpisodes > 0) {
91 | newEpisodesCount += "(+" + newEpisodes + ")";
92 | }
93 | }
94 | stringBuilder.append(context.getString(R.string.sync_new_episodes, newEpisodesCount));
95 |
96 | if (parsed > 0) {
97 | stringBuilder.append(", ")
98 | .append(context.getString(R.string.sync_feeds_synced, parsed));
99 | }
100 | if (errors > 0) {
101 | stringBuilder.append(", ")
102 | .append(context.getString(R.string.sync_feeds_failed, errors));
103 | }
104 | }
105 | nb.setOngoing(false)
106 | .setAutoCancel(true)
107 | .setProgress(0, 0, false)
108 | .setContentTitle(context.getString(R.string.sync_finished))
109 | .setContentText(stringBuilder);
110 | updateNotification();
111 | stopped = true;
112 | }
113 |
114 | private synchronized void updateProgress(String message) {
115 | nb.setProgress(maxFeeds, errors + parsed, false);
116 | nb.setContentText(message);
117 | updateNotification();
118 | }
119 |
120 |
121 | synchronized void signalParseError(String feedTitle) {
122 | syncResult.stats.numSkippedEntries++;
123 | errors++;
124 | updateProgress(context.getString(R.string.sync_feed_parsing_failed, feedTitle));
125 | }
126 |
127 | synchronized void signalDbError(String feedTitle) {
128 | syncResult.databaseError = true;
129 | errors++;
130 | updateProgress(context.getString(R.string.sync_feed_db_error, feedTitle));
131 | }
132 |
133 | synchronized void signalIoError(String feedTitle) {
134 | syncResult.stats.numIoExceptions++;
135 | errors++;
136 | updateProgress(context.getString(R.string.sync_feed_io_error, feedTitle));
137 | }
138 |
139 | synchronized void signalFeedSuccess(@Nullable String feedTitle, int episodesAdded) {
140 | syncResult.stats.numUpdates++;
141 | parsed++;
142 | newEpisodes += episodesAdded;
143 | if (feedTitle != null) {
144 | updateProgress(context.getString(R.string.sync_feed_synced, feedTitle));
145 | }
146 | }
147 |
148 | private synchronized void updateNotification() {
149 | if (!stopped) {
150 | nm.notify(NOTIFICATION_ID, nb.build());
151 | }
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/PodcastHelper.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 |
4 | import android.content.ContentResolver;
5 | import android.content.ContentValues;
6 | import android.content.Context;
7 | import android.database.Cursor;
8 | import android.support.annotation.NonNull;
9 | import android.support.annotation.Nullable;
10 | import android.support.design.widget.Snackbar;
11 | import android.text.format.DateUtils;
12 | import android.util.Log;
13 | import android.view.View;
14 |
15 | import java.io.IOException;
16 | import java.net.HttpURLConnection;
17 | import java.net.URL;
18 | import java.net.URLConnection;
19 | import java.text.DateFormat;
20 | import java.text.SimpleDateFormat;
21 | import java.util.Date;
22 | import java.util.Locale;
23 |
24 | /**
25 | * Helper class intended to do podcast-related stuff
26 | */
27 | public class PodcastHelper {
28 | private static final String TAG = "EPM";
29 | private static final int TIMEOUT_MS = 15000;
30 | private static final DateFormat formatYYYYMMDD = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
31 |
32 | private PodcastHelper() {
33 | throw new UnsupportedOperationException();
34 | }
35 |
36 | static URLConnection openConnectionWithTimeout(URL url) throws IOException {
37 | URLConnection result = url.openConnection();
38 | result.setConnectTimeout(TIMEOUT_MS);
39 | result.setReadTimeout(TIMEOUT_MS);
40 | if (result instanceof HttpURLConnection) {
41 | HttpURLConnection httpUrlConnection = (HttpURLConnection) result;
42 | httpUrlConnection.setInstanceFollowRedirects(true);
43 | if (httpUrlConnection.getResponseCode() == HttpURLConnection.HTTP_MOVED_PERM
44 | || httpUrlConnection.getResponseCode() == HttpURLConnection.HTTP_MOVED_TEMP) {
45 | httpUrlConnection.disconnect();
46 | URL newUrl = new URL(url, httpUrlConnection.getHeaderField("Location"));
47 | Log.d(TAG, "Following redirect from " + url + " to " + newUrl);
48 | return openConnectionWithTimeout(newUrl);
49 | }
50 | }
51 | return result;
52 | }
53 |
54 | public static long generateId(@NonNull String url) {
55 | return (long) url.hashCode() - Integer.MIN_VALUE;
56 | }
57 |
58 | @NonNull
59 | public static String shortDateFormat(long date) {
60 | if (new Date().getTime() - date > 6 * 24 * 60 * 60 * 1000) {
61 | return formatYYYYMMDD.format(date);
62 | } else {
63 | return DateUtils.getRelativeTimeSpanString(date).toString();
64 | }
65 | }
66 |
67 | @NonNull
68 | public static String shortFormatDurationMs(long milliseconds, @NonNull Context context) {
69 | long minutes = milliseconds / 60 / 1000;
70 | long hours = minutes / 60;
71 | return (hours > 0 ? hours + context.getString(R.string.hour_abbreviation) : "")
72 | + minutes % 60 + context.getString(R.string.minute_abbreviation);
73 | }
74 |
75 | /**
76 | * Based on http://stackoverflow.com/a/3758880/2015129
77 | */
78 | public static String humanReadableByteCount(long bytes, boolean si) {
79 | int unit = si ? 1000 : 1024;
80 | if (bytes < unit) {
81 | return bytes + "B";
82 | }
83 | int exp = (int) (Math.log(bytes) / Math.log(unit));
84 | String pre = (si ? "kMGTPE" : "KMGTPE").charAt(exp - 1) + (si ? "" : "i");
85 | return String.format(Locale.getDefault(), "%d%sB", (int) (bytes / Math.pow(unit, exp)), pre);
86 | }
87 |
88 | public static class SubscriptionNotInsertedException extends Throwable {
89 | }
90 |
91 | /**
92 | * Add subscription to podcasts table
93 | *
94 | * @param url url to subscribe
95 | * @return ID of podcast or zero if already subscribed
96 | * @throws SubscriptionNotInsertedException if failed to insert subscription into db
97 | */
98 | public static long addSubscription(String url, @NonNull RefreshMode refreshMode,
99 | @NonNull Context context)
100 | throws SubscriptionNotInsertedException {
101 | if (!url.toLowerCase(Locale.ROOT).matches("^\\w+://.*")) {
102 | url = "http://" + url;
103 | Log.w(TAG, "Feed download protocol defaults to http, new url: " + url);
104 | }
105 | ContentResolver resolver = context.getContentResolver();
106 | long id = generateId(url);
107 | Cursor cursor = resolver.query(
108 | Provider.getUri(Provider.T_PODCAST, id), null, null, null, null);
109 | int count = cursor.getCount();
110 | cursor.close();
111 | if (count == 1) {
112 | return 0;
113 | } else {
114 | ContentValues values = new ContentValues(5);
115 | values.put(Provider.K_PFURL, url);
116 | values.put(Provider.K_PRMODE, refreshMode.ordinal());
117 | values.put(Provider.K_ID, id);
118 | values.put(Provider.K_PSTATE, Provider.PSTATE_NEW);
119 | values.put(Provider.K_PTSTAMP, 0);
120 | values.put(Provider.K_PATSTAMP, new Date().getTime());
121 | if (resolver.insert(Provider.podcastUri, values) == null) {
122 | throw new SubscriptionNotInsertedException();
123 | } else {
124 | return id;
125 | }
126 | }
127 | }
128 |
129 | static long trySubscribe(@NonNull String url, @Nullable View container,
130 | @NonNull RefreshMode refreshMode, @NonNull Context context) {
131 | try {
132 | long result = addSubscription(url, refreshMode, context);
133 | if (result == 0 && container != null) {
134 | Snackbar.make(container,
135 | context.getString(R.string.podcast_already_subscribed, url),
136 | Snackbar.LENGTH_LONG)
137 | .show();
138 | }
139 | return result;
140 | } catch (PodcastHelper.SubscriptionNotInsertedException notInsertedException) {
141 | if (container != null) {
142 | Snackbar.make(container, R.string.podcast_subscribe_failed, Snackbar.LENGTH_LONG).show();
143 | }
144 | return 0;
145 | }
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/episode_list_element.xml:
--------------------------------------------------------------------------------
1 |
17 |
18 |
22 |
23 |
36 |
37 |
49 |
50 |
61 |
62 |
69 |
70 |
79 |
80 |
87 |
88 |
97 |
98 |
104 |
105 |
112 |
113 |
114 |
126 |
127 |
135 |
136 |
144 |
145 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/support/RotationLayout.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten.support;
2 |
3 | import android.content.Context;
4 | import android.content.res.TypedArray;
5 | import android.util.AttributeSet;
6 | import android.util.Log;
7 | import android.view.View;
8 | import android.view.ViewGroup;
9 |
10 | import com.einmalfel.podlisten.R;
11 |
12 | import java.util.ArrayList;
13 |
14 | /**
15 | * This layout correctly handles rotation of nested views.
16 | * Rotation is specified via layout_rotation attribute (available values are 0, 90, 180, 270),
17 | * which overrides view's rotation attribute.
18 | * Padding is supported, while layout_margins aren't.
19 | * Partially based on FrameLayout code.
20 | * TODO: extract into separate library
21 | */
22 | public class RotationLayout extends ViewGroup {
23 | private final ArrayList matchParentChildren = new ArrayList<>(1);
24 |
25 | public RotationLayout(Context context) {
26 | super(context);
27 | }
28 |
29 | public RotationLayout(Context context, AttributeSet attrs) {
30 | super(context, attrs);
31 | }
32 |
33 | public RotationLayout(Context context, AttributeSet attrs, int defStyle) {
34 | super(context, attrs, defStyle);
35 | }
36 |
37 | @Override
38 | protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
39 | boolean measureMatchParentChildren =
40 | MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY
41 | || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
42 | matchParentChildren.clear();
43 | int horizontalPadding = getPaddingLeft() + getPaddingRight();
44 | int verticalPadding = getPaddingTop() + getPaddingBottom();
45 | int maxHeight = 0;
46 | int maxWidth = 0;
47 | int childState = 0;
48 |
49 | for (int i = 0; i < getChildCount(); i++) {
50 | View child = getChildAt(i);
51 | if (child.getVisibility() != GONE) {
52 | LayoutParams lp = (LayoutParams) child.getLayoutParams();
53 | if (lp.layoutRotation == 90 || lp.layoutRotation == 270) {
54 | child.measure(
55 | getChildMeasureSpec(heightMeasureSpec, verticalPadding, lp.width),
56 | getChildMeasureSpec(widthMeasureSpec, horizontalPadding, lp.height));
57 | maxHeight = Math.max(maxHeight, child.getMeasuredWidth());
58 | maxWidth = Math.max(maxWidth, child.getMeasuredHeight());
59 | } else {
60 | child.measure(
61 | getChildMeasureSpec(widthMeasureSpec, horizontalPadding, lp.width),
62 | getChildMeasureSpec(heightMeasureSpec, verticalPadding, lp.height));
63 | maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
64 | maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
65 | }
66 | childState = combineMeasuredStates(childState, child.getMeasuredState());
67 | if (measureMatchParentChildren) {
68 | if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) {
69 | matchParentChildren.add(child);
70 | }
71 | }
72 |
73 | child.setPivotY(0);
74 | child.setPivotX(0);
75 | child.setRotation(lp.layoutRotation);
76 | if (lp.layoutRotation == 90) {
77 | child.setX(getPaddingLeft() + child.getMeasuredHeight());
78 | } else if (lp.layoutRotation == 180) {
79 | child.setX(getPaddingLeft() + child.getMeasuredWidth());
80 | child.setY(getPaddingTop() + child.getMeasuredHeight());
81 | } else if (lp.layoutRotation == 270) {
82 | child.setY(getPaddingTop() + child.getMeasuredWidth());
83 | }
84 | }
85 | }
86 |
87 | // Account for padding too
88 | maxWidth += horizontalPadding;
89 | maxHeight += verticalPadding;
90 |
91 | // Check against our minimum height and width
92 | maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
93 | maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
94 |
95 | setMeasuredDimension(
96 | resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
97 | resolveSizeAndState(maxHeight, heightMeasureSpec,
98 | childState << MEASURED_HEIGHT_STATE_SHIFT));
99 |
100 | // in case our layout didn't had exact size initially (wrap_content) and some of children don't
101 | // have it neither (match_parent), we need to remeasure those children now, when the layout
102 | // has already obtained its dimensions
103 | if (matchParentChildren.size() > 1) {
104 | for (int i = 0; i < matchParentChildren.size(); i++) {
105 | View child = matchParentChildren.get(i);
106 | LayoutParams lp = (LayoutParams) child.getLayoutParams();
107 | int childW;
108 | int childH;
109 | if (lp.width == LayoutParams.MATCH_PARENT) {
110 | childW = MeasureSpec
111 | .makeMeasureSpec(getMeasuredWidth() - horizontalPadding, MeasureSpec.EXACTLY);
112 | } else {
113 | childW = getChildMeasureSpec(widthMeasureSpec, horizontalPadding, lp.width);
114 | }
115 | if (lp.height == LayoutParams.MATCH_PARENT) {
116 | childH = MeasureSpec
117 | .makeMeasureSpec(getMeasuredHeight() - verticalPadding, MeasureSpec.EXACTLY);
118 | } else {
119 | childH = getChildMeasureSpec(heightMeasureSpec, verticalPadding, lp.height);
120 | }
121 | child.measure(childW, childH);
122 | }
123 | }
124 | }
125 |
126 | @Override
127 | protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
128 | for (int i = 0; i < getChildCount(); i++) {
129 | View child = getChildAt(i);
130 | if (child.getVisibility() != GONE) {
131 | child.layout(
132 | getPaddingLeft(),
133 | getPaddingTop(),
134 | getPaddingLeft() + child.getMeasuredWidth(),
135 | getPaddingTop() + child.getMeasuredHeight());
136 | }
137 | }
138 | }
139 |
140 | public static class LayoutParams extends ViewGroup.LayoutParams {
141 | public final int layoutRotation;
142 |
143 | public LayoutParams(int width, int height, int rotation) {
144 | this(new ViewGroup.LayoutParams(width, height), rotation);
145 | }
146 |
147 | public LayoutParams(ViewGroup.LayoutParams source, int rotation) {
148 | super(source);
149 | if (rotation != 0 && rotation != 90 && rotation != 180 && rotation != 270) {
150 | // layout inflater may suppress the exception, so scream in log
151 | String message =
152 | "Invalid layoutRotation value (" + rotation + "). Available values: 0, 90, 180, 270.";
153 | Log.e("RotationLayout", message);
154 | throw new IllegalArgumentException(message);
155 | }
156 | layoutRotation = rotation;
157 | }
158 | }
159 |
160 | protected LayoutParams generateDefaultLayoutParams() {
161 | return new LayoutParams(super.generateDefaultLayoutParams(), 0);
162 | }
163 |
164 | public LayoutParams generateLayoutParams(AttributeSet attrs) {
165 | TypedArray array = getContext().obtainStyledAttributes(
166 | attrs, R.styleable.RotationLayout);
167 | int rotation = array.getInt(R.styleable.RotationLayout_layout_rotation, 0);
168 | array.recycle();
169 | return new LayoutParams(new ViewGroup.LayoutParams(getContext(), attrs), rotation);
170 | }
171 |
172 | protected boolean checkLayoutParams(ViewGroup.LayoutParams params) {
173 | return params instanceof LayoutParams;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/ImageManager.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.app.Application;
4 | import android.content.Context;
5 | import android.graphics.Bitmap;
6 | import android.graphics.BitmapFactory;
7 | import android.graphics.Point;
8 | import android.support.annotation.NonNull;
9 | import android.support.annotation.Nullable;
10 | import android.util.Log;
11 | import android.util.LruCache;
12 | import android.view.WindowManager;
13 |
14 | import com.einmalfel.podlisten.support.UnitConverter;
15 |
16 | import java.io.File;
17 | import java.io.FileInputStream;
18 | import java.io.FileNotFoundException;
19 | import java.io.FileOutputStream;
20 | import java.io.IOException;
21 | import java.net.HttpURLConnection;
22 | import java.net.URL;
23 | import java.nio.channels.FileLock;
24 |
25 | /**
26 | * This class is in charge of downloading, storing and memory-caching images
27 | */
28 | public class ImageManager {
29 | private static final String TAG = "IMG";
30 | private static final int WIDTH_DP = 70;
31 | private static final int PAGES_TO_CACHE = 10;
32 | public static final String FAILED_TO_CLOSE_STREAM = "Failed to close stream";
33 | private final int widthPx;
34 | private static ImageManager instance;
35 |
36 | private final LruCache memoryCache;
37 |
38 | private ImageManager(@NonNull Application context) {
39 | widthPx = UnitConverter.getInstance().dpToPx(WIDTH_DP);
40 | Point displaySize = new Point();
41 | WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
42 | wm.getDefaultDisplay().getSize(displaySize);
43 | // to estimate how many images list page holds, assume images are square and sum of images
44 | // heights equals to a half of screen height
45 | int imagesPerPage = displaySize.y / 2 / widthPx;
46 | memoryCache = new LruCache<>(PAGES_TO_CACHE * imagesPerPage);
47 | }
48 |
49 | @NonNull
50 | public static ImageManager getInstance() {
51 | if (instance == null) {
52 | synchronized (ImageManager.class) {
53 | if (instance == null) {
54 | instance = new ImageManager(PodListenApp.getContext());
55 | }
56 | }
57 | }
58 | return instance;
59 | }
60 |
61 | @Nullable
62 | public Bitmap getImage(long id) {
63 | Bitmap result = memoryCache.get(id);
64 | if (result == null) {
65 | result = loadFromDisk(id);
66 | if (result != null) {
67 | memoryCache.put(id, result);
68 | }
69 | }
70 | return result;
71 | }
72 |
73 | public void deleteImage(long id) {
74 | File file = getImageFile(id, true);
75 | if (file != null && file.exists()) {
76 | if (!file.delete()) {
77 | Log.e(TAG, "Deletion of " + file.getAbsolutePath() + " failed");
78 | }
79 | }
80 | }
81 |
82 | // based on snippet from http://developer.android.com/training/displaying-bitmaps/load-bitmap.html
83 | private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth) {
84 | int inSampleSize = 1;
85 |
86 | if (options.outWidth > reqWidth) {
87 | final int halfWidth = options.outWidth / 2;
88 | // Calculate the largest inSampleSize value that is a power of 2 and keeps width larger than
89 | // requested
90 | while ((halfWidth / inSampleSize) > reqWidth) {
91 | inSampleSize *= 2;
92 | }
93 | }
94 |
95 | return inSampleSize;
96 | }
97 |
98 | public void download(long id, URL url) throws IOException {
99 |
100 | HttpURLConnection urlConnection = null;
101 | Bitmap bitmap = null;
102 | try {
103 | urlConnection = (HttpURLConnection) PodcastHelper.openConnectionWithTimeout(url);
104 | urlConnection.connect();
105 | BitmapFactory.Options options = new BitmapFactory.Options();
106 | options.inJustDecodeBounds = true;
107 | bitmap = BitmapFactory.decodeStream(urlConnection.getInputStream(), null, options);
108 | urlConnection.disconnect();
109 | urlConnection = (HttpURLConnection) PodcastHelper.openConnectionWithTimeout(url);
110 | options.inJustDecodeBounds = false;
111 | options.inSampleSize = calculateInSampleSize(options, widthPx);
112 | Log.d(TAG, "Downloading " + url + ". Sampling factor: " + options.inSampleSize);
113 | bitmap = BitmapFactory.decodeStream(urlConnection.getInputStream(), null, options);
114 | if (bitmap == null) {
115 | throw new IOException("Failed to load image from " + url);
116 | }
117 | } finally {
118 | if (urlConnection != null) {
119 | urlConnection.disconnect();
120 | }
121 | }
122 | Bitmap scaled = Bitmap.createScaledBitmap(
123 | bitmap, widthPx, bitmap.getHeight() * widthPx / bitmap.getWidth(), true);
124 |
125 | File file = getImageFile(id, false);
126 | if (file == null) {
127 | Log.e(TAG, "Image " + id + " download failed. No writable storage");
128 | return;
129 | }
130 | FileOutputStream stream = null;
131 | FileLock lock = null;
132 | try {
133 | stream = new FileOutputStream(file);
134 | lock = stream.getChannel().lock();
135 | scaled.compress(Bitmap.CompressFormat.PNG, 100, stream);
136 | Log.d(TAG, url.toString() + " written to " + file.getAbsolutePath());
137 | } catch (IOException exception) {
138 | Log.e(TAG, "Failed to read image " + id + "from flash", exception);
139 | } finally {
140 | bitmap.recycle();
141 | scaled.recycle();
142 | if (lock != null) {
143 | try {
144 | lock.release();
145 | } catch (IOException exception) {
146 | Log.wtf(TAG, FAILED_TO_CLOSE_STREAM, exception);
147 | }
148 | }
149 | if (stream != null) {
150 | try {
151 | stream.close();
152 | } catch (IOException exception) {
153 | Log.wtf(TAG, FAILED_TO_CLOSE_STREAM, exception);
154 | }
155 | }
156 | }
157 | }
158 |
159 | public boolean isDownloaded(long id) {
160 | File file = getImageFile(id, false);
161 | return file != null && file.exists();
162 | }
163 |
164 | @Nullable
165 | private File getImageFile(long id, boolean write) {
166 | Storage storage = Preferences.getInstance().getStorage();
167 | if (storage == null) {
168 | return null;
169 | }
170 | boolean isAvailable = write ? storage.isAvailableRw() : storage.isAvailableRead();
171 | return isAvailable ? new File(storage.getImagesDir(), id + ".png") : null;
172 | }
173 |
174 |
175 | @Nullable
176 | private Bitmap loadFromDisk(long id) {
177 | if (!isDownloaded(id)) {
178 | return null;
179 | }
180 | Log.d(TAG, "Loading " + id + " from sdcard. Cache size before " + getCacheSize());
181 | File file = getImageFile(id, false);
182 | FileInputStream stream = null;
183 | FileLock lock = null;
184 | try {
185 | stream = new FileInputStream(file);
186 | lock = stream.getChannel().lock(0, Long.MAX_VALUE, true);
187 | return BitmapFactory.decodeStream(stream);
188 | } catch (FileNotFoundException ignored) {
189 | return null; // it's normal if there is no file
190 | } catch (IOException exception) {
191 | Log.e(TAG, "Failed to read image " + id + "from flash", exception);
192 | return null;
193 | } finally {
194 | if (lock != null) {
195 | try {
196 | lock.release();
197 | } catch (IOException exception) {
198 | Log.wtf(TAG, FAILED_TO_CLOSE_STREAM, exception);
199 | }
200 | }
201 | if (stream != null) {
202 | try {
203 | stream.close();
204 | } catch (IOException exception) {
205 | Log.wtf(TAG, FAILED_TO_CLOSE_STREAM, exception);
206 | }
207 | }
208 | }
209 | }
210 |
211 | private int getCacheSize() {
212 | int size = 0;
213 | for (Bitmap b : memoryCache.snapshot().values()) {
214 | size += b.getByteCount(); // approximate value. TODO use getAllocatedByteCount on 4.4+
215 | }
216 | return size;
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/CatalogueFragment.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.ContentValues;
4 | import android.content.Context;
5 | import android.database.Cursor;
6 | import android.database.DatabaseUtils;
7 | import android.database.sqlite.SQLiteDatabase;
8 | import android.database.sqlite.SQLiteException;
9 | import android.os.Build;
10 | import android.os.Handler;
11 | import android.os.HandlerThread;
12 | import android.support.annotation.Nullable;
13 | import android.support.annotation.UiThread;
14 | import android.support.v4.app.Fragment;
15 | import android.text.TextUtils;
16 | import android.util.Log;
17 |
18 | import com.readystatesoftware.sqliteasset.SQLiteAssetHelper;
19 |
20 | import java.util.Locale;
21 |
22 | public class CatalogueFragment extends Fragment {
23 | interface CatalogueListener {
24 | void onLoadProgress(int progress);
25 |
26 | void onQueryComplete(Cursor cursor);
27 | }
28 |
29 | private class CatalogueHelper extends SQLiteAssetHelper {
30 | public CatalogueHelper(Context context) {
31 | super(context, "PodcastCatalogue.sqlite", null, 4);
32 | setForcedUpgrade();
33 | }
34 | }
35 |
36 | private class CatalogWorker extends HandlerThread {
37 | private Handler handler;
38 |
39 | public CatalogWorker() {
40 | super("CatalogWorker");
41 | }
42 |
43 | @Override
44 | public synchronized void start() {
45 | super.start();
46 | handler = new Handler(getLooper());
47 | }
48 |
49 | public synchronized Handler getHandler() {
50 | if (!isAlive()) {
51 | start();
52 | }
53 | return handler;
54 | }
55 | }
56 |
57 | static final String FTS_NAME = "catalogue_fts";
58 | static final String K_ID = "_ID";
59 | static final String K_DOCID = "docid";
60 | static final String K_RSS = "rss_url";
61 | static final String K_WEB = "web_url";
62 | static final String K_TITLE = "title";
63 | static final String K_DESCRIPTION = "description";
64 | static final String K_PERIOD = "period";
65 | static final String CAT_NAME = "catalogue";
66 | private static final String TAG = "PCT";
67 |
68 | private CatalogWorker worker;
69 | private CatalogueListener listener;
70 | private SQLiteDatabase db;
71 | private Context appContext;
72 | private int loadProgress;
73 |
74 | public CatalogueFragment() {
75 | super();
76 | }
77 |
78 | @Override
79 | public void onAttach(Context context) {
80 | super.onAttach(context);
81 | if (appContext == null) {
82 | appContext = context.getApplicationContext();
83 | worker = new CatalogWorker();
84 | worker.start();
85 | worker.getHandler().post(new Runnable() {
86 | @Override
87 | public void run() {
88 | initDb();
89 | }
90 | });
91 | }
92 | }
93 |
94 | @Override
95 | public void onDestroy() {
96 | super.onDestroy();
97 | Log.i(TAG, "Stopping handler thread");
98 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
99 | worker.quitSafely();
100 | } else {
101 | worker.quit();
102 | }
103 | }
104 |
105 | @UiThread
106 | public void setListener(CatalogueListener listener) {
107 | this.listener = listener;
108 | }
109 |
110 | public int getLoadProgress() {
111 | return loadProgress;
112 | }
113 |
114 | public void query(final String[] terms) {
115 | worker.getHandler().post(new Runnable() {
116 | @Override
117 | public void run() {
118 | String query;
119 | if (terms.length == 0 || (terms.length == 1 && terms[0].equals(""))) {
120 | query = "SELECT * FROM " + CAT_NAME + " WHERE title != ?";
121 | } else {
122 | query = "SELECT " + CAT_NAME + ".* FROM " + FTS_NAME + " JOIN " + CAT_NAME + " WHERE "
123 | + FTS_NAME + "." + K_DOCID + " == " + CAT_NAME + "." + K_ID + " AND " + FTS_NAME
124 | + " match ?";
125 |
126 | // optimization: don't sort results if number of terms is big (slow WHERE processing) or
127 | // all terms are short (i.e. a lots of results)
128 | int longest = 0;
129 | for (int i = 0; i < terms.length; i++) {
130 | if (terms[i].length() > longest) {
131 | longest = terms[i].length();
132 | }
133 | terms[i] = DatabaseUtils.sqlEscapeString(terms[i] + "*");
134 | }
135 | if (longest - (terms.length - 1) >= 3) { // minimal term size for sorting is 3 chars
136 | query += " ORDER BY length(offsets(" + FTS_NAME + ")) DESC";
137 | }
138 | }
139 |
140 | Cursor cursor = null;
141 | try {
142 | cursor = db.rawQuery(query, new String[]{TextUtils.join(" ", terms)});
143 | // need this to trigger SQLiteException if there are problems with this query
144 | cursor.getCount();
145 | sendQueryResult(cursor);
146 | } catch (SQLiteException exception) {
147 | if (cursor != null) {
148 | cursor.close();
149 | }
150 | Log.i(TAG, "User query couldn't be used for MATCH. Returning null");
151 | sendQueryResult(null);
152 | }
153 | }
154 | });
155 | }
156 |
157 | private void sendQueryResult(@Nullable final Cursor cursor) {
158 | new Handler(appContext.getMainLooper()).post(new Runnable() {
159 | @Override
160 | public void run() {
161 | if (listener != null) {
162 | listener.onQueryComplete(cursor);
163 | }
164 | }
165 | });
166 | }
167 |
168 | private void initDb() {
169 | db = new CatalogueHelper(appContext).getWritableDatabase();
170 | Cursor cursor = db.rawQuery("SELECT tbl_name FROM sqlite_master WHERE tbl_name == ?",
171 | new String[]{FTS_NAME});
172 | if (cursor.getCount() == 0) {
173 | reportProgress(0);
174 | Log.i(TAG, "Generating FTS DB");
175 | db.beginTransaction();
176 | try {
177 | db.execSQL("CREATE VIRTUAL TABLE IF NOT EXISTS " + FTS_NAME
178 | + " USING fts4(title, description, web_url)");
179 | Cursor catalogue = db.query(
180 | CAT_NAME,
181 | new String[]{K_ID, K_TITLE, K_DESCRIPTION, K_WEB},
182 | null, null, null, null, null);
183 | ContentValues cv = new ContentValues(4);
184 | int idId = catalogue.getColumnIndexOrThrow(K_ID);
185 | int titleId = catalogue.getColumnIndexOrThrow(K_TITLE);
186 | int descriptionId = catalogue.getColumnIndexOrThrow(K_DESCRIPTION);
187 | int webId = catalogue.getColumnIndexOrThrow(K_WEB);
188 | int recordsPerPercent = catalogue.getCount() / 90;
189 | while (catalogue.moveToNext()) {
190 | if (catalogue.getPosition() % recordsPerPercent == 0) {
191 | reportProgress(catalogue.getPosition() / recordsPerPercent);
192 | }
193 | cv.put(K_DOCID, catalogue.getLong(idId));
194 | cv.put(K_TITLE, catalogue.getString(titleId).toLowerCase(Locale.getDefault()));
195 | String description = catalogue.getString(descriptionId);
196 | if (description != null) {
197 | cv.put(K_DESCRIPTION, description.toLowerCase(Locale.getDefault()));
198 | }
199 | String webUrl = catalogue.getString(webId);
200 | if (webUrl != null) {
201 | cv.put(K_WEB, webUrl.toLowerCase(Locale.ROOT));
202 | }
203 | db.insert(FTS_NAME, null, cv);
204 | }
205 | reportProgress(91);
206 | catalogue.close();
207 | reportProgress(92);
208 | db.setTransactionSuccessful();
209 | reportProgress(98);
210 | } finally {
211 | db.endTransaction();
212 | reportProgress(99);
213 | }
214 | }
215 | cursor.close();
216 | reportProgress(100);
217 | }
218 |
219 | private void reportProgress(final int progress) {
220 | this.loadProgress = progress;
221 | Log.e(TAG, "Progress " + progress);
222 | new Handler(appContext.getMainLooper()).post(new Runnable() {
223 | @Override
224 | public void run() {
225 | if (listener != null) {
226 | listener.onLoadProgress(progress);
227 | }
228 | }
229 | });
230 | }
231 | }
232 |
233 |
234 |
--------------------------------------------------------------------------------
/app/src/main/java/com/einmalfel/podlisten/PreferencesActivity.java:
--------------------------------------------------------------------------------
1 | package com.einmalfel.podlisten;
2 |
3 | import android.content.ActivityNotFoundException;
4 | import android.content.DialogInterface;
5 | import android.content.Intent;
6 | import android.database.Cursor;
7 | import android.net.Uri;
8 | import android.os.Bundle;
9 | import android.os.Environment;
10 | import android.support.design.widget.Snackbar;
11 | import android.support.v7.app.ActionBar;
12 | import android.support.v7.app.AlertDialog;
13 | import android.support.v7.app.AppCompatActivity;
14 | import android.util.Log;
15 | import android.util.Xml;
16 |
17 | import org.xmlpull.v1.XmlPullParser;
18 | import org.xmlpull.v1.XmlSerializer;
19 |
20 | import java.io.File;
21 | import java.io.FileWriter;
22 | import java.io.IOException;
23 | import java.text.SimpleDateFormat;
24 | import java.util.Date;
25 | import java.util.Locale;
26 |
27 | public class PreferencesActivity extends AppCompatActivity {
28 | private static final String TAG = "PAC";
29 | private static final SimpleDateFormat RFC822DATEFORMAT
30 | = new SimpleDateFormat("EEE', 'dd' 'MMM' 'yyyy' 'HH:mm:ss' 'Z", Locale.US);
31 | private static final String[] directoryMimeTypes = new String[]{
32 | "application/x-directory",
33 | "resource/folder",
34 | "x-directory/normal",
35 | "inode/directory",
36 | "application/folder",
37 | "vnd.android.cursor.item/file"
38 | };
39 | private final Preferences preferences = Preferences.getInstance();
40 |
41 | @Override
42 | protected void onCreate(Bundle savedInstanceState) {
43 | super.onCreate(savedInstanceState);
44 |
45 | android.support.v4.app.FragmentManager fragmentManager = getSupportFragmentManager();
46 | if (fragmentManager.findFragmentById(android.R.id.content) == null) {
47 | fragmentManager.beginTransaction()
48 | .add(android.R.id.content, new PreferencesFragment())
49 | .commit();
50 | }
51 |
52 | setTitle(getString(R.string.preferences_title));
53 | ActionBar actionBar = getSupportActionBar();
54 | if (actionBar != null) {
55 | getSupportActionBar().setDisplayHomeAsUpEnabled(true);
56 | } else {
57 | Log.wtf(TAG, "Should never get here: failed to get action bar of preference activity");
58 | }
59 | }
60 |
61 | @Override
62 | protected void onNewIntent(Intent intent) {
63 | super.onNewIntent(intent);
64 | switch (intent.getAction()) {
65 | case "com.einmalfel.podlisten.SEND_BUG_REPORT":
66 | PodListenApp.sendLogs();
67 | break;
68 | case "com.einmalfel.podlisten.OPML_EXPORT":
69 | String state = Environment.getExternalStorageState();
70 | if (Environment.MEDIA_MOUNTED.equals(state) || Environment.MEDIA_UNKNOWN.equals(state)) {
71 | final File dir;
72 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
73 | dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS);
74 | } else {
75 | dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PODCASTS);
76 | }
77 | if (!dir.exists() && !dir.mkdirs()) {
78 | Log.e(TAG, "Directory for OPML export doesn't exist " + dir);
79 | }
80 | final File target = new File(dir, getString(R.string.opml_export_file_name));
81 | if (exportToOpml(target)) {
82 | AlertDialog.Builder builder = new AlertDialog.Builder(this);
83 | builder.setTitle(getString(R.string.opml_dialog_title))
84 | .setMessage(String.format(getString(R.string.opml_dialog_message), target))
85 | .setNegativeButton(R.string.opml_dialog_done, null);
86 |
87 | final Intent sendIntent = new Intent(Intent.ACTION_SEND);
88 | sendIntent.setType("file/*");
89 | sendIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(target));
90 | if (sendIntent.resolveActivity(getPackageManager()) != null) {
91 | builder.setPositiveButton(
92 | R.string.opml_dialog_send,
93 | new DialogInterface.OnClickListener() {
94 | @Override
95 | public void onClick(DialogInterface dialog, int which) {
96 | startActivity(sendIntent);
97 | }
98 | });
99 | }
100 |
101 | if (showDirectory(dir, true)) {
102 | builder.setNeutralButton(
103 | R.string.opml_dialog_FMapp,
104 | new DialogInterface.OnClickListener() {
105 | @Override
106 | public void onClick(DialogInterface dialog, int which) {
107 | showDirectory(dir, false);
108 | }
109 | });
110 | }
111 |
112 | builder.show();
113 | } else {
114 | Snackbar.make(findViewById(android.R.id.content),
115 | R.string.preferences_opml_export_failed,
116 | Snackbar.LENGTH_LONG).show();
117 | }
118 | }
119 | break;
120 | default:
121 | Log.e(TAG, "Unexpected intent received: " + intent);
122 | break;
123 | }
124 | }
125 |
126 | private boolean exportToOpml(File file) {
127 | XmlSerializer serializer = Xml.newSerializer();
128 | try {
129 | if (!file.exists() && !file.createNewFile()) {
130 | Log.e(TAG, "failed to create file for OPML export " + file);
131 | return false;
132 | }
133 | serializer.setOutput(new FileWriter(file));
134 | serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
135 | serializer.startDocument("UTF-8", false);
136 | serializer.startTag(XmlPullParser.NO_NAMESPACE, "opml");
137 | serializer.attribute(XmlPullParser.NO_NAMESPACE, "version", "1.0");
138 |
139 | serializer.startTag(XmlPullParser.NO_NAMESPACE, "head");
140 | serializer.startTag(XmlPullParser.NO_NAMESPACE, "title");
141 | serializer.text("Podlisten subscriptions");
142 | serializer.endTag(XmlPullParser.NO_NAMESPACE, "title");
143 | serializer.startTag(XmlPullParser.NO_NAMESPACE, "dateCreated");
144 | serializer.text(RFC822DATEFORMAT.format(new Date()));
145 | serializer.endTag(XmlPullParser.NO_NAMESPACE, "dateCreated");
146 | serializer.endTag(XmlPullParser.NO_NAMESPACE, "head");
147 |
148 | serializer.startTag(XmlPullParser.NO_NAMESPACE, "body");
149 |
150 | Cursor cursor = getContentResolver().query(
151 | Provider.podcastUri, new String[]{Provider.K_PNAME, Provider.K_PFURL}, null, null, null);
152 | if (cursor == null) {
153 | return false;
154 | }
155 | int urlInd = cursor.getColumnIndexOrThrow(Provider.K_PFURL);
156 | int titleInd = cursor.getColumnIndexOrThrow(Provider.K_PNAME);
157 | while (cursor.moveToNext()) {
158 | String url = cursor.getString(urlInd);
159 | String title = cursor.getString(titleInd);
160 | if (url == null || url.isEmpty()) {
161 | Log.w(TAG, "OPML export: Skipping " + title + " url " + url);
162 | continue;
163 | }
164 | serializer.startTag(XmlPullParser.NO_NAMESPACE, "outline");
165 | serializer.attribute(XmlPullParser.NO_NAMESPACE, "type", "rss");
166 | serializer.attribute(XmlPullParser.NO_NAMESPACE, "text", title == null ? "Unknown" : title);
167 | serializer.attribute(XmlPullParser.NO_NAMESPACE, "xmlUrl", url);
168 | serializer.endTag(XmlPullParser.NO_NAMESPACE, "outline");
169 | }
170 | cursor.close();
171 |
172 | serializer.endDocument();
173 | return true;
174 | } catch (IOException ioException) {
175 | Log.w(TAG, "OPML export failed", ioException);
176 | return false;
177 | }
178 | }
179 |
180 | private boolean showDirectory(File dir, boolean test) {
181 | Intent showFolderIntent = new Intent(Intent.ACTION_VIEW);
182 | for (String mimeType : directoryMimeTypes) {
183 | showFolderIntent.setDataAndType(Uri.fromFile(dir), mimeType);
184 | if (test) {
185 | if (showFolderIntent.resolveActivity(getPackageManager()) != null) {
186 | return true;
187 | }
188 | } else {
189 | try {
190 | startActivity(showFolderIntent);
191 | return true;
192 | } catch (ActivityNotFoundException ignored) {
193 | // it's ok, just try next mime type
194 | }
195 | }
196 | }
197 | return false;
198 | }
199 | }
200 |
--------------------------------------------------------------------------------