diff options
author | daniel oeh <daniel.oeh@gmail.com> | 2013-09-12 12:55:07 +0200 |
---|---|---|
committer | daniel oeh <daniel.oeh@gmail.com> | 2013-09-12 12:55:07 +0200 |
commit | de5c6a97df094a41419b8f61c91e03c0600bd9f1 (patch) | |
tree | 0c1073fda0016ab017195d6dd18edb002983e896 | |
parent | 2c79135d6a36fc14dc0d77a9e0d2a014cce0c99c (diff) | |
parent | e1fc800d088a4b150deb3aff072cda7af8da8ca7 (diff) | |
download | AntennaPod-de5c6a97df094a41419b8f61c91e03c0600bd9f1.zip |
Merge branch 'move-to-top' of git://github.com/TomHennen/AntennaPod into TomHennen-move-to-top
74 files changed, 5074 insertions, 557 deletions
diff --git a/.tx/config b/.tx/config index 3a05b8725..145e12a23 100644 --- a/.tx/config +++ b/.tx/config @@ -21,6 +21,7 @@ trans.ru-RU = res/values-ru/strings.xml trans.ru_RU = res/values-ru/strings.xml trans.uk_UA = res/values-uk-rUA/strings.xml trans.zh_CN = res/values-zh-rCN/strings.xml +trans.sv_SE = res/values-sv-rSE/strings.xml [antennapod.description] file_filter = description/<lang>.txt diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d99375a27..506607333 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="de.danoeh.antennapod" - android:versionCode="31" - android:versionName="0.9.7.4" > + android:versionCode="32" + android:versionName="0.9.7.5" > <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> @@ -39,303 +39,379 @@ <activity android:name=".activity.MainActivity" android:configChanges="keyboardHidden|orientation" - android:label="@string/app_name" > + android:label="@string/app_name"> <meta-data android:name="android.app.default_searchable" - android:value="de.danoeh.antennapod.activity.SearchActivity" /> + android:value="de.danoeh.antennapod.activity.SearchActivity"/> <meta-data android:name="android.app.searchable" - android:resource="@xml/searchable" /> + android:resource="@xml/searchable"/> + <intent-filter> - <action android:name="android.intent.action.MAIN" /> - <category android:name="android.intent.category.LAUNCHER" /> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> <activity android:name="de.danoeh.antennapod.activity.AddFeedActivity" android:configChanges="keyboardHidden|orientation" android:label="@string/add_new_feed_label" - android:windowSoftInputMode="adjustResize" > + android:windowSoftInputMode="adjustResize"> + <intent-filter> + <action android:name="android.intent.action.VIEW"/> + + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> + + <data android:scheme="http"/> + <data android:scheme="https"/> + <data android:host="*"/> + <data android:pathPattern=".*\\.xml"/> + <data android:pathPattern=".*\\.rss"/> + </intent-filter> + + <intent-filter> + <action android:name="android.intent.action.VIEW"/> + + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> + + <data android:scheme="http"/> + <data android:scheme="https"/> + <data android:host="feeds.feedburner.com"/> + <data android:host="feedproxy.google.com"/> + <data android:host="feeds2.feedburner.com"/> + <data android:host="feedsproxy.google.com"/> + </intent-filter> + <intent-filter> - <action android:name="android.intent.action.VIEW"/> - <category android:name="android.intent.category.DEFAULT"/> - <category android:name="android.intent.category.BROWSABLE"/> - <data android:scheme="http"/> - <data android:scheme="https"/> - <data android:host="*"/> - <data android:pathPattern=".*\\.xml"/> - <data android:pathPattern=".*\\.rss"/> - </intent-filter> - - <intent-filter> - <action android:name="android.intent.action.VIEW"/> - <category android:name="android.intent.category.DEFAULT"/> - <category android:name="android.intent.category.BROWSABLE"/> - <data android:scheme="http"/> - <data android:scheme="https"/> - <data android:host="feeds.feedburner.com"/> - <data android:host="feedproxy.google.com"/> - <data android:host="feeds2.feedburner.com"/> - <data android:host="feedsproxy.google.com"/> - </intent-filter> - - <intent-filter> - <action android:name="android.intent.action.VIEW"/> - <category android:name="android.intent.category.DEFAULT"/> - <category android:name="android.intent.category.BROWSABLE"/> - <data android:scheme="http"/> - <data android:scheme="https"/> - <data android:mimeType="text/xml"/> - <data android:mimeType="application/rss+xml"/> - <data android:mimeType="application/atom+xml"/> - <data android:mimeType="application/xml"/> - </intent-filter> + <action android:name="android.intent.action.VIEW"/> + + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> + + <data android:scheme="http"/> + <data android:scheme="https"/> + <data android:mimeType="text/xml"/> + <data android:mimeType="application/rss+xml"/> + <data android:mimeType="application/atom+xml"/> + <data android:mimeType="application/xml"/> + </intent-filter> <intent-filter> - <action android:name="android.intent.action.SEND" /> + <action android:name="android.intent.action.SEND"/> - <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.DEFAULT"/> - <data android:mimeType="text/plain" /> + <data android:mimeType="text/plain"/> </intent-filter> </activity> <activity android:name="de.danoeh.antennapod.activity.FeedItemlistActivity" - android:configChanges="orientation|screenSize" > + android:configChanges="orientation|screenSize"> <meta-data android:name="android.app.default_searchable" - android:value="de.danoeh.antennapod.activity.SearchActivity" /> + android:value="de.danoeh.antennapod.activity.SearchActivity"/> <meta-data android:name="android.app.searchable" - android:resource="@xml/searchable" /> + android:resource="@xml/searchable"/> </activity> <activity android:name="de.danoeh.antennapod.activity.ItemviewActivity" - android:configChanges="keyboard|orientation" /> + android:configChanges="keyboard|orientation"/> <activity android:name="de.danoeh.antennapod.activity.DownloadActivity" - android:label="@string/downloads_label" /> + android:label="@string/downloads_label"/> <activity android:name=".activity.AudioplayerActivity" - android:launchMode="singleTop" > + android:launchMode="singleTop"> <intent-filter> - <action android:name="android.intent.action.VIEW" /> + <action android:name="android.intent.action.VIEW"/> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.BROWSABLE" /> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> - <data android:scheme="file" /> - <data android:mimeType="audio/*" /> + <data android:scheme="file"/> + <data android:mimeType="audio/*"/> </intent-filter> </activity> <service android:name=".service.download.DownloadService" - android:enabled="true" /> + android:enabled="true"/> <service android:name="de.danoeh.antennapod.service.PlaybackService" - android:enabled="true" > + android:enabled="true"> + </service> + + <service + android:name=".service.GpodnetSyncService" + android:enabled="true"> </service> <activity android:name=".activity.PreferenceActivity" android:configChanges="keyboardHidden|orientation" - android:label="@string/settings_label" > + android:label="@string/settings_label"> </activity> <activity android:name=".activity.DownloadLogActivity" - android:label="@string/download_log_label" > + android:label="@string/download_log_label"> </activity> <receiver android:name=".receiver.MediaButtonReceiver" - android:exported="true" > + android:exported="true"> <intent-filter> - <action android:name="android.intent.action.MEDIA_BUTTON" /> + <action android:name="android.intent.action.MEDIA_BUTTON"/> </intent-filter> <intent-filter> - <action android:name="de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER" /> + <action android:name="de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER"/> </intent-filter> </receiver> - <activity android:name=".activity.FeedInfoActivity" > + <activity android:name=".activity.FeedInfoActivity"> </activity> <service android:name=".service.PlayerWidgetService" android:enabled="true" - android:exported="false" > + android:exported="false"> </service> - <receiver android:name=".receiver.PlayerWidget" > + <receiver android:name=".receiver.PlayerWidget"> <intent-filter> - <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> + <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> </intent-filter> <intent-filter> - <action android:name="de.danoeh.antennapod.FORCE_WIDGET_UPDATE" /> + <action android:name="de.danoeh.antennapod.FORCE_WIDGET_UPDATE"/> </intent-filter> <meta-data android:name="android.appwidget.provider" - android:resource="@xml/player_widget_info" /> + android:resource="@xml/player_widget_info"/> <intent-filter> - <action android:name="de.danoeh.antennapod.STOP_WIDGET_UPDATE" /> + <action android:name="de.danoeh.antennapod.STOP_WIDGET_UPDATE"/> </intent-filter> </receiver> - <receiver android:name=".receiver.FeedUpdateReceiver" > + <receiver android:name=".receiver.FeedUpdateReceiver"> <intent-filter> - <action android:name="de.danoeh.antennapod.feedupdatereceiver.refreshFeeds" /> + <action android:name="de.danoeh.antennapod.feedupdatereceiver.refreshFeeds"/> </intent-filter> </receiver> - <activity android:name=".activity.StorageErrorActivity" > + <activity android:name=".activity.StorageErrorActivity"> </activity> <activity android:name=".activity.FlattrAuthActivity" - android:label="@string/flattr_auth_label" > + android:label="@string/flattr_auth_label"> <intent-filter> - <action android:name=".activities.FlattrAuthActivity" /> + <action android:name=".activities.FlattrAuthActivity"/> - <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.DEFAULT"/> </intent-filter> <intent-filter> - <action android:name="android.intent.action.VIEW" /> + <action android:name="android.intent.action.VIEW"/> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.BROWSABLE" /> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> <data android:host="de.danoeh.antennapod" - android:scheme="flattr4j" /> + android:scheme="flattr4j"/> </intent-filter> </activity> <activity android:name=".activity.AboutActivity" - android:label="@string/about_pref" > + android:label="@string/about_pref"> </activity> <activity android:name=".activity.OpmlImportFromPathActivity" android:configChanges="keyboardHidden|orientation" - android:label="@string/opml_import_label" > + android:label="@string/opml_import_label"> </activity> <activity android:name=".activity.OpmlImportFromIntentActivity" android:configChanges="keyboardHidden|orientation" - android:label="@string/opml_import_label" > + android:label="@string/opml_import_label"> <intent-filter> - <action android:name="android.intent.action.VIEW" /> + <action android:name="android.intent.action.VIEW"/> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.BROWSABLE" /> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> <data android:host="*" android:mimeType="*/*" android:pathPattern=".*\\.opml" - android:scheme="file" /> + android:scheme="file"/> </intent-filter> <intent-filter> - <action android:name="android.intent.action.VIEW" /> + <action android:name="android.intent.action.VIEW"/> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.BROWSABLE" /> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> <data android:host="*" android:pathPattern=".*\\.opml" android:scheme="file" - android:mimeType="text/x-opml" /> + android:mimeType="text/x-opml"/> </intent-filter> </activity> <activity android:name=".activity.OpmlFeedChooserActivity" - android:label="@string/opml_import_label" > + android:label="@string/opml_import_label"> </activity> <activity android:name=".activity.SearchActivity" android:configChanges="keyboardHidden|orientation" android:label="@string/search_results_label" - android:launchMode="singleTop" > + android:launchMode="singleTop"> <intent-filter> - <action android:name="android.intent.action.SEARCH" /> + <action android:name="android.intent.action.SEARCH"/> </intent-filter> <meta-data android:name="android.app.searchable" - android:resource="@xml/searchable" /> + android:resource="@xml/searchable"/> </activity> <activity android:name=".activity.MiroGuideMainActivity" - android:label="@string/miro_guide_label" > + android:label="@string/miro_guide_label"> <meta-data android:name="android.app.default_searchable" - android:value="de.danoeh.antennapod.activity.MiroGuideSearchActivity" /> + android:value="de.danoeh.antennapod.activity.MiroGuideSearchActivity"/> <meta-data android:name="android.app.searchable" - android:resource="@xml/miroguide_searchable" /> + android:resource="@xml/miroguide_searchable"/> </activity> <activity android:name=".activity.MiroGuideSearchActivity" android:configChanges="keyboardHidden|orientation" - android:launchMode="singleTop" > + android:launchMode="singleTop"> <intent-filter> - <action android:name="android.intent.action.SEARCH" /> + <action android:name="android.intent.action.SEARCH"/> </intent-filter> <meta-data android:name="android.app.searchable" - android:resource="@xml/miroguide_searchable" /> + android:resource="@xml/miroguide_searchable"/> </activity> <activity android:name=".activity.MiroGuideCategoryActivity" - android:configChanges="keyboardHidden|orientation" > + android:configChanges="keyboardHidden|orientation"> </activity> <activity android:name=".activity.MiroGuideChannelViewActivity" android:configChanges="keyboard|orientation" - android:label="@string/miro_guide_label" > + android:label="@string/miro_guide_label"> </activity> <activity android:name=".activity.VideoplayerActivity" android:configChanges="keyboardHidden|orientation" - android:screenOrientation="landscape" > + android:screenOrientation="landscape"> <intent-filter> - <action android:name="android.intent.action.VIEW" /> + <action android:name="android.intent.action.VIEW"/> - <category android:name="android.intent.category.DEFAULT" /> - <category android:name="android.intent.category.BROWSABLE" /> + <category android:name="android.intent.category.DEFAULT"/> + <category android:name="android.intent.category.BROWSABLE"/> - <data android:scheme="file" /> - <data android:mimeType="video/*" /> + <data android:scheme="file"/> + <data android:mimeType="video/*"/> </intent-filter> </activity> <activity android:name=".activity.PlaybackHistoryActivity" - android:label="@string/playback_history_label" /> + android:label="@string/playback_history_label"/> <activity android:name=".activity.DirectoryChooserActivity" - android:label="@string/choose_data_directory" /> + android:label="@string/choose_data_directory"/> <activity android:name=".activity.OrganizeQueueActivity" android:configChanges="orientation" - android:label="@string/organize_queue_label" > + android:label="@string/organize_queue_label"> + </activity> + <activity + android:name=".activity.gpoddernet.GpodnetMainActivity" + android:configChanges="orientation" + android:label="@string/gpodnet_main_label"> + + <meta-data + android:name="android.app.default_searchable" + android:value="de.danoeh.antennapod.activity.gpoddernet.GpodnetSearchActivity"/> + <meta-data + android:name="android.app.searchable" + android:resource="@xml/gpodnet_searchable"/> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value="de.danoeh.antennapod.activity.AddFeedActivity" /> + </activity> + <activity + android:name=".activity.gpoddernet.GpodnetTagActivity" + android:configChanges="orientation"> + <meta-data + android:name="android.app.default_searchable" + android:value="de.danoeh.antennapod.activity.gpoddernet.GpodnetSearchActivity"/> + <meta-data + android:name="android.app.searchable" + android:resource="@xml/gpodnet_searchable"/> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value="de.danoeh.antennapod.activity.gpoddernet.GpodnetMainActivity" /> + </activity> + + <activity + android:name=".activity.gpoddernet.GpodnetSearchActivity" + android:configChanges="orientation" + android:label="@string/search_label" + android:launchMode="singleTop"> + <intent-filter> + <action android:name="android.intent.action.SEARCH"/> + </intent-filter> + <meta-data + android:name="android.app.searchable" + android:resource="@xml/gpodnet_searchable"/> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value="de.danoeh.antennapod.activity.gpoddernet.GpodnetMainActivity" /> + </activity> + + <activity + android:name=".activity.DefaultOnlineFeedViewActivity" + android:configChanges="orientation"/> + + <activity + android:name=".activity.gpoddernet.GpodnetAuthenticationActivity" + android:configChanges="orientation" + android:label="@string/gpodnet_auth_label" + android:screenOrientation="portrait"> + <intent-filter> + <action android:name=".activity.gpoddernet.GpodnetAuthenticationActivity"/> + <category android:name="android.intent.category.DEFAULT"/> + </intent-filter> + <meta-data + android:name="android.support.PARENT_ACTIVITY" + android:value="de.danoeh.antennapod.activity.PreferenceActivity" /> + </activity> + - <receiver android:name=".receiver.ConnectivityActionReceiver" > + <receiver android:name=".receiver.ConnectivityActionReceiver"> <intent-filter> - <action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> + <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/> </intent-filter> </receiver> - <receiver android:name=".receiver.AlarmUpdateReceiver" > + <receiver android:name=".receiver.AlarmUpdateReceiver"> <intent-filter> - <action android:name="android.intent.action.BOOT_COMPLETED" /> + <action android:name="android.intent.action.BOOT_COMPLETED"/> </intent-filter> <intent-filter> - <action android:name="android.intent.action.PACKAGE_REPLACED" /> + <action android:name="android.intent.action.PACKAGE_REPLACED"/> <data android:path="de.danoeh.antennapod" - android:scheme="package" /> + android:scheme="package"/> </intent-filter> </receiver> </application> diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9b28fb2..bfa26ee43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,14 @@ Change Log ========== +Version 0.9.7.5 +--------------- +* Reduced application startup time +* Reduced memory usage +* Added option to change the playback speed +* Added Swedish translation +* Several bugfixes and improvements + Version 0.9.7.4 --------------- * Episode cache size can now be set to unlimited diff --git a/assets/about.html b/assets/about.html index 84d94e39c..eb4853e64 100644 --- a/assets/about.html +++ b/assets/about.html @@ -40,7 +40,7 @@ <body> <div id="header" align="center"> <img src="logo.png" alt="Logo" width="100px" height="100px"/> - <p>AntennaPod, Version 0.9.7.4</p> + <p>AntennaPod, Version 0.9.7.5</p> <p>Copyright © 2012 Daniel Oeh</p> <p>Licensed under the MIT License <a href="LICENSE.html">(View)</a></p> </div> @@ -57,5 +57,7 @@ licensed under the Apache 2.0 license <h2>drag-sort-listview <a href="https://github.com/bauerca/drag-sort-listview">(Link)</a></h2> licensed under the Apache 2.0 license + <h2>Presto Client <a href="http://www.aocate.com/presto/">(Link)</a></h2> + licensed under the Apache 2.0 license </body> </html> diff --git a/build.gradle b/build.gradle index 8f2c0f684..92f15b779 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ dependencies { compile 'com.android.support:appcompat-v7:18.0.+' compile 'org.apache.commons:commons-lang3:3.1' - compile ('org.shredzone.flattr4j:flattr4j-core:2.4') { + compile ('org.shredzone.flattr4j:flattr4j-core:2.7') { exclude group: 'org.apache.httpcomponents', module: 'httpcore' exclude group: 'org.apache.httpcomponents', module: 'httpclient' exclude group: 'org.json', module: 'json' @@ -5,7 +5,7 @@ <groupId>de.danoeh</groupId> <artifactId>antennapod</artifactId> <packaging>apk</packaging> - <version>0.9.7.4</version> + <version>0.9.7.5</version> <name>AntennaPod</name> diff --git a/res/layout-v14/addfeed.xml b/res/layout-v14/addfeed.xml index 14d3a2a67..5760bea95 100644 --- a/res/layout-v14/addfeed.xml +++ b/res/layout-v14/addfeed.xml @@ -1,20 +1,21 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:orientation="vertical" > + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical"> <RelativeLayout android:id="@+id/footer" android:layout_width="fill_parent" android:layout_height="48dp" - android:layout_alignParentBottom="true" > + android:focusableInTouchMode="true" + android:layout_alignParentBottom="true"> <View android:layout_width="match_parent" android:layout_height="1dip" android:layout_alignParentTop="true" - android:background="?android:attr/dividerVertical" /> + android:background="?android:attr/dividerVertical"/> <View android:id="@+id/horizontal_divider" @@ -24,7 +25,7 @@ android:layout_centerHorizontal="true" android:layout_marginBottom="4dp" android:layout_marginTop="4dp" - android:background="?android:attr/dividerVertical" /> + android:background="?android:attr/dividerVertical"/> <Button android:id="@+id/butCancel" @@ -35,7 +36,7 @@ android:layout_alignParentTop="true" android:layout_toLeftOf="@id/horizontal_divider" android:background="?android:attr/selectableItemBackground" - android:text="@string/cancel_label" /> + android:text="@string/cancel_label"/> <Button android:id="@+id/butConfirm" @@ -46,7 +47,7 @@ android:layout_alignParentTop="true" android:layout_toRightOf="@id/horizontal_divider" android:background="?android:attr/selectableItemBackground" - android:text="@string/confirm_label" /> + android:text="@string/confirm_label"/> </RelativeLayout> <ScrollView @@ -54,21 +55,22 @@ android:layout_height="0dp" android:layout_above="@id/footer" android:layout_alignParentTop="true" - android:scrollbars="vertical" > + android:scrollbars="vertical"> <RelativeLayout android:layout_width="match_parent" - android:layout_height="wrap_content" > + android:layout_height="wrap_content"> <TextView android:id="@+id/txtvFeedurl" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" - android:layout_margin="8dp" - android:focusable="true" - android:focusableInTouchMode="true" - android:text="@string/txtvfeedurl_label" /> + android:layout_margin="16dp" + android:textSize="@dimen/text_size_large" + android:textColor="@color/bright_blue" + android:textStyle="italic" + android:text="@string/txtvfeedurl_label"/> <EditText android:id="@+id/etxtFeedurl" @@ -77,23 +79,35 @@ android:layout_below="@id/txtvFeedurl" android:layout_margin="8dp" android:hint="@string/feedurl_label" - android:inputType="textUri" /> + android:inputType="textUri"/> <TextView - android:id="@+id/txtvBrowseMiroguide" + android:id="@+id/txtvPodcastDirectories" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/etxtFeedurl" android:layout_margin="8dp" - android:text="@string/txtv_browse_miroguide_label" /> + android:textSize="@dimen/text_size_large" + android:textColor="@color/bright_blue" + android:textStyle="italic" + android:text="@string/podcastdirectories_label"/> + + <Button + android:id="@+id/butBrowseGpoddernet" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/txtvPodcastDirectories" + android:layout_margin="8dp" + android:text="@string/gpodnet_main_label"/> <Button android:id="@+id/butBrowseMiroguide" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@id/txtvBrowseMiroguide" + android:layout_below="@id/butBrowseGpoddernet" android:layout_margin="8dp" - android:text="@string/browse_miroguide_label" /> + android:text="@string/miro_guide_label"/> + <TextView android:id="@+id/txtvOpmlImport" @@ -101,17 +115,28 @@ android:layout_height="wrap_content" android:layout_below="@id/butBrowseMiroguide" android:layout_margin="8dp" - android:text="@string/opml_import_txtv_button_lable" /> + android:textSize="@dimen/text_size_large" + android:textColor="@color/bright_blue" + android:textStyle="italic" + android:text="@string/opml_import_label"/> + + <TextView + android:id="@+id/txtvOpmlImportExpl" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/txtvOpmlImport" + android:layout_margin="8dp" + android:text="@string/opml_import_txtv_button_lable"/> <Button android:id="@+id/butOpmlImport" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@id/txtvOpmlImport" + android:layout_below="@id/txtvOpmlImportExpl" android:layout_marginBottom="8dp" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" - android:text="@string/opml_import_label" /> + android:text="@string/opml_import_label"/> </RelativeLayout> </ScrollView> diff --git a/res/layout-v14/authentication_dialog.xml b/res/layout-v14/authentication_dialog.xml new file mode 100644 index 000000000..ed05dab1c --- /dev/null +++ b/res/layout-v14/authentication_dialog.xml @@ -0,0 +1,81 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" > + + <EditText + android:id="@+id/etxtUsername" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:layout_margin="16dp" + android:hint="@string/username_label"/> + + <EditText + android:id="@+id/etxtPassword" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:layout_margin="16dp" + android:inputType="textPassword" + android:hint="@string/password_label"/> + + <CheckBox + android:id="@+id/chkSaveUsernamePassword" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:text="@string/save_username_password_label"/> + </LinearLayout> + + <RelativeLayout + android:id="@+id/footer" + android:layout_width="fill_parent" + android:layout_height="48dp" > + + <View + android:layout_width="match_parent" + android:layout_height="1dip" + android:layout_alignParentTop="true" + android:background="?android:attr/dividerVertical" /> + + <View + android:id="@+id/horizontal_divider" + android:layout_width="1dip" + android:layout_height="fill_parent" + android:layout_alignParentTop="true" + android:layout_centerHorizontal="true" + android:layout_marginBottom="4dp" + android:layout_marginTop="4dp" + android:background="?android:attr/dividerVertical" /> + + <Button + android:id="@+id/butCancel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:layout_toLeftOf="@id/horizontal_divider" + android:background="?android:attr/selectableItemBackground" + android:text="@string/cancel_label" /> + + <Button + android:id="@+id/butConfirm" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentBottom="true" + android:layout_alignParentRight="true" + android:layout_alignParentTop="true" + android:layout_toRightOf="@id/horizontal_divider" + android:background="?android:attr/selectableItemBackground" + android:text="@string/confirm_label" /> + </RelativeLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/addfeed.xml b/res/layout/addfeed.xml index 39552976b..bb72a2be1 100644 --- a/res/layout/addfeed.xml +++ b/res/layout/addfeed.xml @@ -1,8 +1,8 @@ <?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="fill_parent" - android:layout_height="fill_parent" - android:orientation="vertical" > + android:layout_width="fill_parent" + android:layout_height="fill_parent" + android:orientation="vertical"> <LinearLayout android:id="@+id/footer" @@ -10,21 +10,21 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" - android:orientation="horizontal" > + android:orientation="horizontal"> <Button android:id="@+id/butConfirm" android:layout_width="0px" android:layout_height="wrap_content" android:layout_weight="1" - android:text="@string/confirm_label" /> + android:text="@string/confirm_label"/> <Button android:id="@+id/butCancel" android:layout_width="0px" android:layout_height="wrap_content" android:layout_weight="1" - android:text="@string/cancel_label" /> + android:text="@string/cancel_label"/> </LinearLayout> <ScrollView @@ -32,21 +32,22 @@ android:layout_height="0dp" android:layout_above="@id/footer" android:layout_alignParentTop="true" - android:scrollbars="vertical" > + android:scrollbars="vertical"> <RelativeLayout android:layout_width="match_parent" - android:layout_height="wrap_content" > + android:layout_height="wrap_content"> <TextView android:id="@+id/txtvFeedurl" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" - android:layout_margin="8dp" - android:focusable="true" - android:focusableInTouchMode="true" - android:text="@string/txtvfeedurl_label" /> + android:layout_margin="16dp" + android:textSize="@dimen/text_size_large" + android:textColor="@color/bright_blue" + android:textStyle="italic" + android:text="@string/txtvfeedurl_label"/> <EditText android:id="@+id/etxtFeedurl" @@ -55,23 +56,35 @@ android:layout_below="@id/txtvFeedurl" android:layout_margin="8dp" android:hint="@string/feedurl_label" - android:inputType="textUri" /> + android:inputType="textUri"/> <TextView - android:id="@+id/txtvBrowseMiroguide" + android:id="@+id/txtvPodcastDirectories" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_below="@id/etxtFeedurl" android:layout_margin="8dp" - android:text="@string/txtv_browse_miroguide_label" /> + android:textSize="@dimen/text_size_large" + android:textColor="@color/bright_blue" + android:textStyle="italic" + android:text="@string/podcastdirectories_label"/> + + <Button + android:id="@+id/butBrowseGpoddernet" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/txtvPodcastDirectories" + android:layout_margin="8dp" + android:text="@string/gpodnet_main_label"/> <Button android:id="@+id/butBrowseMiroguide" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@id/txtvBrowseMiroguide" + android:layout_below="@id/butBrowseGpoddernet" android:layout_margin="8dp" - android:text="@string/browse_miroguide_label" /> + android:text="@string/miro_guide_label"/> + <TextView android:id="@+id/txtvOpmlImport" @@ -79,17 +92,28 @@ android:layout_height="wrap_content" android:layout_below="@id/butBrowseMiroguide" android:layout_margin="8dp" - android:text="@string/opml_import_txtv_button_lable" /> + android:textSize="@dimen/text_size_large" + android:textColor="@color/bright_blue" + android:textStyle="italic" + android:text="@string/opml_import_label"/> + + <TextView + android:id="@+id/txtvOpmlImportExpl" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/txtvOpmlImport" + android:layout_margin="8dp" + android:text="@string/opml_import_txtv_button_lable"/> <Button android:id="@+id/butOpmlImport" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@id/txtvOpmlImport" + android:layout_below="@id/txtvOpmlImportExpl" android:layout_marginBottom="8dp" android:layout_marginLeft="8dp" android:layout_marginRight="8dp" - android:text="@string/opml_import_label" /> + android:text="@string/opml_import_label"/> </RelativeLayout> </ScrollView> diff --git a/res/layout/authentication_dialog.xml b/res/layout/authentication_dialog.xml new file mode 100644 index 000000000..82260eb43 --- /dev/null +++ b/res/layout/authentication_dialog.xml @@ -0,0 +1,62 @@ +<?xml version="1.0" encoding="utf-8"?> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <EditText + android:id="@+id/etxtUsername" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:layout_margin="16dp" + android:hint="@string/username_label"/> + + <EditText + android:id="@+id/etxtPassword" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:layout_margin="16dp" + android:inputType="textPassword" + android:hint="@string/password_label"/> + + <CheckBox + android:id="@+id/chkSaveUsernamePassword" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:text="@string/save_username_password_label"/> + + + </LinearLayout> + + <LinearLayout + style="@android:style/ButtonBar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/butConfirm" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginRight="8dp" + android:text="@string/confirm_label" + android:layout_weight="1"/> + + <Button + android:id="@+id/butCancel" + android:text="@string/cancel_label" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1"/> + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/gpodnet_main.xml b/res/layout/gpodnet_main.xml new file mode 100644 index 000000000..1017a6a65 --- /dev/null +++ b/res/layout/gpodnet_main.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/main_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + + <android.support.v4.view.ViewPager + android:id="@+id/viewpager" + android:layout_width="match_parent" + android:layout_height="0px" + android:layout_weight="1"> + <android.support.v4.view.PagerTabStrip + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top" /> + </android.support.v4.view.ViewPager> + +</LinearLayout> diff --git a/res/layout/gpodnet_podcast_list.xml b/res/layout/gpodnet_podcast_list.xml new file mode 100644 index 000000000..e69c2dbee --- /dev/null +++ b/res/layout/gpodnet_podcast_list.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> + +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <GridView + android:layout_width="match_parent" + android:layout_height="match_parent" + android:id="@+id/gridView" + android:stretchMode="columnWidth" + android:numColumns="auto_fit" + android:verticalSpacing="4dp" + android:horizontalSpacing="4dp" + android:gravity="center" + android:columnWidth="200dp" + tools:listitem="@layout/gpodnet_podcast_listitem"/> + + <ProgressBar + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/progressBar" + android:layout_gravity="center" + android:indeterminateOnly="true"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:id="@+id/txtvError" + android:layout_gravity="center" + android:visibility="gone" + android:textSize="@dimen/text_size_small"/> +</FrameLayout>
\ No newline at end of file diff --git a/res/layout/gpodnet_podcast_listitem.xml b/res/layout/gpodnet_podcast_listitem.xml new file mode 100644 index 000000000..f6ddb3bd8 --- /dev/null +++ b/res/layout/gpodnet_podcast_listitem.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <ImageView + android:id="@+id/imgvCover" + android:layout_width="@dimen/thumbnail_length_itemlist" + android:layout_height="@dimen/thumbnail_length_itemlist" + android:layout_alignParentLeft="true" + android:layout_centerVertical="true" + android:layout_marginRight="4dip" + android:adjustViewBounds="true" + android:cropToPadding="true" + android:scaleType="fitXY" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="@dimen/thumbnail_length_itemlist" + android:layout_centerVertical="true" + android:layout_toRightOf="@id/imgvCover" + android:layout_marginRight="8dp" + android:orientation="vertical" > + + <TextView + android:id="@+id/txtvTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:attr/textColorPrimary" + android:textSize="@dimen/text_size_small" /> + + <TextView + android:id="@+id/txtvDescription" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:maxLines="2" + android:ellipsize="end" + android:textColor="?android:attr/textColorTertiary" + android:textSize="@dimen/text_size_micro" /> + + </LinearLayout> +</RelativeLayout>
\ No newline at end of file diff --git a/res/layout/gpodnet_search.xml b/res/layout/gpodnet_search.xml new file mode 100644 index 000000000..deb9cffd6 --- /dev/null +++ b/res/layout/gpodnet_search.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + + <FrameLayout + android:id="@+id/searchListFragment" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/gpodnet_tag_activity.xml b/res/layout/gpodnet_tag_activity.xml new file mode 100644 index 000000000..01feb216f --- /dev/null +++ b/res/layout/gpodnet_tag_activity.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" > + + <FrameLayout + android:id="@+id/taglistFragment" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/gpodnetauth_activity.xml b/res/layout/gpodnetauth_activity.xml new file mode 100644 index 000000000..cd428fbdf --- /dev/null +++ b/res/layout/gpodnetauth_activity.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> + +<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> +<ViewFlipper + android:id="@+id/viewflipper" + android:layout_width="match_parent" + android:layout_height="match_parent"/> +</ScrollView>
\ No newline at end of file diff --git a/res/layout/gpodnetauth_credentials.xml b/res/layout/gpodnetauth_credentials.xml new file mode 100644 index 000000000..b66fc9414 --- /dev/null +++ b/res/layout/gpodnetauth_credentials.xml @@ -0,0 +1,85 @@ +<?xml version="1.0" encoding="utf-8"?> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@id/txtvTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_login_title" + android:layout_alignParentTop="true" + android:textSize="@dimen/text_size_large" + android:layout_margin="16dp" + android:textColor="@color/bright_blue" + android:textStyle="italic"/> + + <TextView + android:id="@id/txtvDescription" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_login_descr" + android:layout_below="@id/txtvTitle" + android:textSize="@dimen/text_size_medium" + android:textColor="?android:attr/textColorSecondary" + android:layout_margin="16dp"/> + + <EditText + android:id="@+id/etxtUsername" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/username_label" + android:layout_below="@id/txtvDescription" + android:layout_margin="8dp"/> + + <EditText + android:id="@+id/etxtPassword" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/password_label" + android:layout_below="@id/etxtUsername" + android:inputType="textPassword" + android:layout_margin="8dp"/> + + <Button + android:id="@+id/butLogin" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/etxtPassword" + android:layout_alignParentRight="true" + android:text="@string/gpodnetauth_login_butLabel" + android:layout_margin="8dp"/> + + <TextView + android:id="@+id/txtvError" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_below="@id/etxtPassword" + android:layout_alignParentLeft="true" + android:layout_toLeftOf="@id/butLogin" + android:textColor="@color/download_failed_red" + android:textSize="@dimen/text_size_small" + android:maxLines="2" + android:ellipsize="end" + android:gravity="center" + android:layout_margin="16dp"/> + + <ProgressBar + android:id="@+id/progBarLogin" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:layout_alignTop="@+id/butLogin" + android:layout_toLeftOf="@+id/butLogin"/> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="@dimen/text_size_medium" + android:textColor="?android:attr/textColorSecondary" + android:layout_margin="16dp" + android:text="@string/gpodnetauth_login_register" + android:autoLink="web" + android:layout_below="@id/butLogin"/> +</RelativeLayout>
\ No newline at end of file diff --git a/res/layout/gpodnetauth_device.xml b/res/layout/gpodnetauth_device.xml new file mode 100644 index 000000000..ac08cb5ad --- /dev/null +++ b/res/layout/gpodnetauth_device.xml @@ -0,0 +1,104 @@ +<?xml version="1.0" encoding="utf-8"?> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/txtvTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_device_title" + android:layout_alignParentTop="true" + android:textSize="@dimen/text_size_large" + android:layout_margin="16dp" + android:textColor="@color/bright_blue" + android:textStyle="italic"/> + + <TextView + android:id="@+id/txtvDescription" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_device_descr" + android:layout_below="@id/txtvTitle" + android:textSize="@dimen/text_size_medium" + android:textColor="?android:attr/textColorSecondary" + android:layout_margin="16dp"/> + + <EditText + android:id="@+id/etxtCaption" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/gpodnetauth_device_caption" + android:layout_below="@id/txtvDescription" + android:layout_margin="8dp"/> + + <EditText + android:id="@+id/etxtDeviceID" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/gpodnetauth_device_deviceID" + android:layout_below="@id/etxtCaption" + android:layout_margin="8dp"/> + + <Button + android:id="@+id/butCreateNewDevice" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="8dp" + android:layout_alignParentRight="true" + android:layout_below="@id/etxtDeviceID" + android:text="@string/gpodnetauth_device_butCreateNewDevice"/> + + <TextView + android:id="@+id/txtvError" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentLeft="true" + android:layout_below="@id/etxtCaption" + android:layout_alignBottom="@id/butCreateNewDevice" + android:textColor="@color/download_failed_red" + android:layout_margin="16dp" + android:textSize="@dimen/text_size_medium" + /> + + <ProgressBar + android:id="@+id/progbarCreateDevice" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignTop="@id/butCreateNewDevice" + android:layout_toLeftOf="@id/butCreateNewDevice" + android:textColor="@color/download_failed_red" + android:textSize="@dimen/text_size_medium" + android:visibility="gone" + /> + + <TextView + android:id="@+id/txtvChooseExistingDevice" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_device_chooseExistingDevice" + android:layout_below="@id/butCreateNewDevice" + android:textSize="@dimen/text_size_medium" + android:textColor="?android:attr/textColorSecondary" + android:layout_margin="16dp"/> + + <Button + android:id="@+id/butChooseExistingDevice" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_device_butChoose" + android:layout_below="@+id/spinnerChooseDevice" + android:layout_alignLeft="@+id/butCreateNewDevice" + android:layout_alignRight="@+id/butCreateNewDevice"/> + + <Spinner + android:id="@+id/spinnerChooseDevice" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_below="@id/txtvChooseExistingDevice" + android:layout_alignParentLeft="true" + android:layout_margin="8dp" + android:layout_alignParentRight="true"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/res/layout/gpodnetauth_finish.xml b/res/layout/gpodnetauth_finish.xml new file mode 100644 index 000000000..3b0b10d04 --- /dev/null +++ b/res/layout/gpodnetauth_finish.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/txtvTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_finish_title" + android:layout_alignParentTop="true" + android:textSize="@dimen/text_size_large" + android:layout_margin="16dp" + android:textColor="@color/bright_blue" + android:textStyle="italic"/> + + <TextView + android:id="@+id/txtvDescription" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="@string/gpodnetauth_finish_descr" + android:layout_below="@id/txtvTitle" + android:textSize="@dimen/text_size_medium" + android:textColor="?android:attr/textColorSecondary" + android:layout_margin="16dp"/> + + <Button + android:id="@+id/butSyncNow" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/txtvDescription" + android:layout_margin="16dp" + android:text="@string/gpodnetauth_finish_butsyncnow"/> + + <Button + android:id="@+id/butGoMainscreen" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/butSyncNow" + android:layout_margin="16dp" + android:text="@string/gpodnetauth_finish_butgomainscreen"/> + +</RelativeLayout>
\ No newline at end of file diff --git a/res/layout/itemdescription_listitem.xml b/res/layout/itemdescription_listitem.xml new file mode 100644 index 000000000..d6a3f6a16 --- /dev/null +++ b/res/layout/itemdescription_listitem.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@+id/txtvTitle" + android:layout_margin="8dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:lines="1" + android:ellipsize="end" + android:textColor="?android:attr/textColorPrimary" + android:textSize="@dimen/text_size_small"/> + + <TextView + android:id="@+id/txtvDescription" + android:layout_margin="8dp" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:lines="3" + android:ellipsize="end" + android:textColor="?android:attr/textColorTertiary" + android:textSize="@dimen/text_size_micro"/> +</LinearLayout>
\ No newline at end of file diff --git a/res/layout/onlinefeedview_header.xml b/res/layout/onlinefeedview_header.xml new file mode 100644 index 000000000..6bbecde1e --- /dev/null +++ b/res/layout/onlinefeedview_header.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?> + +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ImageView + android:id="@+id/imgvCover" + android:layout_width="@dimen/thumbnail_length_onlinefeedview" + android:layout_height="@dimen/thumbnail_length_onlinefeedview" + android:layout_alignParentLeft="true" + android:layout_alignParentTop="true" + android:layout_margin="4dp"/> + + <TextView + android:id="@+id/txtvTitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:gravity="center_vertical" + android:layout_alignTop="@id/imgvCover" + android:layout_toRightOf="@id/imgvCover" + android:layout_alignParentRight="true" + android:lines="1" + android:textColor="?android:attr/textColorPrimary" + android:textSize="@dimen/text_size_medium" + android:layout_margin="4dp"/> + + <TextView + android:id="@+id/txtvAuthor" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:layout_below="@id/txtvTitle" + android:layout_toRightOf="@id/imgvCover" + android:lines="1" + android:ellipsize="end" + android:textColor="?android:attr/textColorSecondary" + android:textSize="@dimen/text_size_small"/> + + <Button + android:id="@+id/butSubscribe" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="4dp" + android:text="@string/subscribe_label" + android:layout_below="@id/txtvAuthor" + android:layout_alignParentRight="true" + /> + + + <TextView + android:id="@+id/txtvDescription" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/butSubscribe" + android:maxLines="3" + android:ellipsize="end" + android:textColor="?android:attr/textColorTertiary" + android:textSize="@dimen/text_size_micro" + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:layout_margin="4dp"/> +</RelativeLayout>
\ No newline at end of file diff --git a/res/menu/feeditem.xml b/res/menu/feeditem.xml index 644b71081..505de1d4a 100644 --- a/res/menu/feeditem.xml +++ b/res/menu/feeditem.xml @@ -67,15 +67,5 @@ android:showAsAction="collapseActionView" android:title="@string/support_label"> </item> - <item - android:id="@+id/move_to_top_item" - android:showAsAction="collapseActionView" - android:title="@string/move_to_top_label"> - </item> - <item - android:id="@+id/move_to_bottom_item" - android:showAsAction="collapseActionView" - android:title="@string/move_to_bottom_label"> - </item> </menu>
\ No newline at end of file diff --git a/res/values/arrays.xml b/res/values/arrays.xml index 534b9df50..b0d6e66f1 100644 --- a/res/values/arrays.xml +++ b/res/values/arrays.xml @@ -81,7 +81,7 @@ <item>3.90</item> <item>4.00</item> </string-array> - + <string-array name="autodl_select_networks_default_entries"> <item>N/A</item> </string-array> @@ -96,6 +96,4 @@ <item>0</item> <item>1</item> </string-array> - - </resources>
\ No newline at end of file diff --git a/res/values/colors.xml b/res/values/colors.xml index 5cf84ec4a..7ce05bc13 100644 --- a/res/values/colors.xml +++ b/res/values/colors.xml @@ -28,7 +28,7 @@ <color name="status_playing">#E0EE5F52</color> <color name="overlay_dark">#262C31</color> <color name="overlay_light">#DDDDDD</color> - + <!-- Use Gingerbread-orange --> <color name="selection_background_color_dark">#FEBB20</color> <color name="selection_background_color_light">#FEBB20</color> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 3b7e7475a..8dea65a5b 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -12,4 +12,5 @@ <dimen name="text_size_large">22sp</dimen> <dimen name="status_indicator_width">36dp</dimen> <dimen name="thumbnail_length_itemlist">80dp</dimen> + <dimen name="thumbnail_length_onlinefeedview">110dp</dimen> </resources>
\ No newline at end of file diff --git a/res/values/ids.xml b/res/values/ids.xml index 4d393e675..c1d80d375 100644 --- a/res/values/ids.xml +++ b/res/values/ids.xml @@ -15,5 +15,11 @@ <item name="organize_queue_item" type="id"/> <item name="drag_handle" type="id"/> <item name="skip_episode_item" type="id"/> + <item name="move_to_top_item" type="id"/> + <item name="move_to_bottom_item" type="id"/> + <item name="image_disk_cache_key" type="id"/> + <item name="imageloader_key" type="id"/> + <item name="notification_gpodnet_sync_error" type="id"/> + <item name="notification_gpodnet_sync_autherror" type="id"/> </resources>
\ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 4ce85e91c..493122b1b 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -17,6 +17,8 @@ <string name="cancel_download_label">Cancel Download</string> <string name="download_log_label">Download log</string> <string name="playback_history_label">Playback history</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net login</string> <!-- Webview actions --> <string name="open_in_browser_label">Open in browser</string> @@ -47,12 +49,14 @@ <string name="processing_label">Processing</string> <string name="loading_label">Loading...</string> <string name="image_of_prefix">Image of:\u0020</string> + <string name="save_username_password_label">Save username and password</string> <string name="close_label">Close</string> <!-- 'Add Feed' Activity labels --> <string name="feedurl_label">Feed URL</string> - <string name="txtvfeedurl_label">Type in the URL of the Feed here:</string> + <string name="txtvfeedurl_label">Add Podcast by URL</string> + <string name="podcastdirectories_label">Podcast directories</string> <!-- Actions on feeds --> <string name="mark_all_read_label">Mark all as read</string> @@ -162,6 +166,8 @@ <string name="other_pref">Other</string> <string name="about_pref">About</string> <string name="queue_label">Queue</string> + <string name="services_label">Services</string> + <string name="flattr_label">Flattr</string> <string name="pref_pauseOnHeadsetDisconnect_sum">Pause playback when the headphones are disconnected</string> <string name="pref_followQueue_sum">Jump to next queue item when playback completes</string> <string name="playback_pref">Playback</string> @@ -195,9 +201,15 @@ <string name="pref_theme_title_light">Light</string> <string name="pref_theme_title_dark">Dark</string> <string name="pref_episode_cache_unlimited">Unlimited</string> - <string name="pref_update_interval_hours_plural">hours</string> - <string name="pref_update_interval_hours_singular">hour</string> - <string name="pref_update_interval_hours_manual">Manual</string> + <string name="pref_update_interval_hours_plural">hours</string> + <string name="pref_update_interval_hours_singular">hour</string> + <string name="pref_update_interval_hours_manual">Manual</string> + <string name="pref_gpodnet_authenticate_title">Login</string> + <string name="pref_gpodnet_authenticate_sum">Login with your gpodder.net account in order to sync your subscriptions.</string> + <string name="pref_gpodnet_logout_title">Logout</string> + <string name="pref_gpodnet_logout_toast">Logout was successful</string> + <string name="pref_gpodnet_setlogin_information_title">Change login information</string> + <string name="pref_gpodnet_setlogin_information_sum">Change the login information for your gpodder.net account.</string> <string name="pref_playback_speed_title">Playback Speeds</string> <string name="pref_playback_speed_sum">Customize the speeds available for variable speed audio playback</string> @@ -250,6 +262,37 @@ <string name="add_feed_label">Add feed</string> <string name="miro_feed_added">Feed is being added</string> + <!-- gpodder.net --> + <string name="gpodnet_taglist_header">CATEGORIES</string> + <string name="gpodnet_toplist_header">TOP PODCASTS</string> + <string name="gpodnet_suggestions_header">SUGGESTIONS</string> + <string name="gpodnet_search_hint">Search gpodder.net</string> + <string name="gpodnetauth_login_title">Login</string> + <string name="gpodnetauth_login_descr">Welcome to the gpodder.net login process. First, type in your login information:</string> + <string name="gpodnetauth_login_butLabel">Login</string> + <string name="gpodnetauth_login_register">If you do not have an account yet, you can create one here:\nhttps://gpodder.net/register/</string> + <string name="username_label">Username</string> + <string name="password_label">Password</string> + <string name="gpodnetauth_device_title">Device Selection</string> + <string name="gpodnetauth_device_descr">Create a new device to use for your gpodder.net account or choose an existing one:</string> + <string name="gpodnetauth_device_deviceID">Device ID</string> + <string name="gpodnetauth_device_caption">Caption</string> + <string name="gpodnetauth_device_butCreateNewDevice">Create new device</string> + <string name="gpodnetauth_device_chooseExistingDevice">Choose existring device:</string> + <string name="gpodnetauth_device_errorEmpty">Device ID must not be empty</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Device ID already in use</string> + + <string name="gpodnetauth_device_butChoose">Choose</string> + <string name="gpodnetauth_finish_title">Login successful!</string> + <string name="gpodnetauth_finish_descr">Congratulations! Your gpodder.net account is now linked with your device. AntennaPod will from now on automagically sync subscriptions on your device with your gpodder.net account.</string> + <string name="gpodnetauth_finish_butsyncnow">Start sync now</string> + <string name="gpodnetauth_finish_butgomainscreen">Go to main screen</string> + + <string name="gpodnetsync_auth_error_title">gpodder.net authentication error</string> + <string name="gpodnetsync_auth_error_descr">Wrong username or password</string> + <string name="gpodnetsync_error_title">gpodder.net sync error</string> + <string name="gpodnetsync_error_descr">An error occurred during syncing:\u0020</string> + <!-- Directory chooser --> <string name="selected_folder_label">Selected folder:</string> <string name="create_folder_label">Create folder</string> @@ -263,4 +306,8 @@ <string name="folder_not_empty_dialog_msg">The folder you have selected is not empty. Media downloads and other files will be placed directly in this folder. Continue anyway?</string> <string name="set_to_default_folder">Choose default folder</string> -</resources> + <!-- Online feed view --> + <string name="subscribe_label">Subscribe</string> + <string name="subscribed_label">Subscribed</string> + <string name="downloading_label">Downloading...</string> +</resources>
\ No newline at end of file diff --git a/res/xml/gpodnet_searchable.xml b/res/xml/gpodnet_searchable.xml new file mode 100644 index 000000000..d2c14d7f7 --- /dev/null +++ b/res/xml/gpodnet_searchable.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<searchable xmlns:android="http://schemas.android.com/apk/res/android" android:hint="@string/gpodnet_search_hint" android:label="@string/app_name" android:icon="@drawable/ic_launcher"> + + +</searchable>
\ No newline at end of file diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml index cf3219681..099ad27e9 100644 --- a/res/xml/preferences.xml +++ b/res/xml/preferences.xml @@ -1,73 +1,127 @@ <?xml version="1.0" encoding="utf-8"?> -<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" > +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <PreferenceCategory android:title="@string/user_interface_label"> - <CheckBoxPreference android:title="@string/pref_display_only_episodes_title" android:summary="@string/pref_display_only_episodes_sum" android:key="prefDisplayOnlyEpisodes"/> - <ListPreference android:entryValues="@array/theme_values" android:entries="@array/theme_options" android:title="@string/pref_set_theme_title" android:key="prefTheme" android:summary="@string/pref_set_theme_sum" android:defaultValue="0"/> - </PreferenceCategory><PreferenceCategory android:title="@string/playback_pref" > + <CheckBoxPreference + android:title="@string/pref_display_only_episodes_title" + android:summary="@string/pref_display_only_episodes_sum" + android:key="prefDisplayOnlyEpisodes"/> + <ListPreference + android:entryValues="@array/theme_values" + android:entries="@array/theme_options" + android:title="@string/pref_set_theme_title" + android:key="prefTheme" + android:summary="@string/pref_set_theme_sum" + android:defaultValue="0"/> + </PreferenceCategory> + <PreferenceCategory android:title="@string/playback_pref"> <CheckBoxPreference android:defaultValue="true" android:enabled="true" android:key="prefPauseOnHeadsetDisconnect" android:summary="@string/pref_pauseOnHeadsetDisconnect_sum" - android:title="@string/pref_pauseOnHeadsetDisconnect_title" /> + android:title="@string/pref_pauseOnHeadsetDisconnect_title"/> <CheckBoxPreference android:defaultValue="false" android:enabled="true" android:key="prefFollowQueue" android:summary="@string/pref_followQueue_sum" - android:title="@string/pref_followQueue_title" /> + android:title="@string/pref_followQueue_title"/> <Preference android:key="prefPlaybackSpeedLauncher" android:summary="@string/pref_playback_speed_sum" android:title="@string/pref_playback_speed_title" /> - </PreferenceCategory> - <PreferenceCategory android:title="@string/network_pref" > + <PreferenceCategory android:title="@string/network_pref"> <ListPreference android:defaultValue="0" android:entries="@array/update_intervall_values" android:entryValues="@array/update_intervall_values" android:key="prefAutoUpdateIntervall" android:summary="@string/pref_autoUpdateIntervall_sum" - android:title="@string/pref_autoUpdateIntervall_title" /> + android:title="@string/pref_autoUpdateIntervall_title"/> <CheckBoxPreference android:defaultValue="false" android:enabled="true" android:key="prefMobileUpdate" android:summary="@string/pref_mobileUpdate_sum" - android:title="@string/pref_mobileUpdate_title" /> - <ListPreference android:defaultValue="20" android:entries="@array/episode_cache_size_entries" android:key="prefEpisodeCacheSize" android:title="@string/pref_episode_cache_title" android:entryValues="@array/episode_cache_size_values"/><PreferenceScreen android:summary="@string/pref_automatic_download_sum" android:key="prefAutoDownloadSettings" android:title="@string/pref_automatic_download_title"> - <CheckBoxPreference android:key="prefEnableAutoDl" android:title="@string/pref_automatic_download_title" android:defaultValue="false"/><CheckBoxPreference android:key="prefEnableAutoDownloadWifiFilter" android:title="@string/pref_autodl_wifi_filter_title" android:summary="@string/pref_autodl_wifi_filter_sum"/> - + android:title="@string/pref_mobileUpdate_title"/> + <ListPreference + android:defaultValue="20" + android:entries="@array/episode_cache_size_entries" + android:key="prefEpisodeCacheSize" + android:title="@string/pref_episode_cache_title" + android:entryValues="@array/episode_cache_size_values"/> + <PreferenceScreen + android:summary="@string/pref_automatic_download_sum" + android:key="prefAutoDownloadSettings" + android:title="@string/pref_automatic_download_title"> + <CheckBoxPreference + android:key="prefEnableAutoDl" + android:title="@string/pref_automatic_download_title" + android:defaultValue="false"/> + <CheckBoxPreference + android:key="prefEnableAutoDownloadWifiFilter" + android:title="@string/pref_autodl_wifi_filter_title" + android:summary="@string/pref_autodl_wifi_filter_sum"/> + </PreferenceScreen> - + </PreferenceCategory> - <PreferenceCategory android:title="@string/flattr_settings_label" > + <PreferenceCategory android:title="@string/services_label"> + <PreferenceScreen + android:key="prefFlattrSettings" + android:title="@string/flattr_label"> + <PreferenceScreen + android:key="pref_flattr_authenticate" + android:summary="@string/pref_flattr_auth_sum" + android:title="@string/pref_flattr_auth_title"> + <intent android:action=".activities.FlattrAuthActivity"/> + </PreferenceScreen> + + <Preference + android:key="prefRevokeAccess" + android:summary="@string/pref_revokeAccess_sum" + android:title="@string/pref_revokeAccess_title"/> + </PreferenceScreen> <PreferenceScreen - android:key="pref_flattr_authenticate" - android:summary="@string/pref_flattr_auth_sum" - android:title="@string/pref_flattr_auth_title" > - <intent android:action=".activities.FlattrAuthActivity" /> + android:key="prefFlattrSettings" + android:title="@string/gpodnet_main_label"> + + <PreferenceScreen + android:key="pref_gpodnet_authenticate" + android:title="@string/pref_gpodnet_authenticate_title" + android:summary="@string/pref_gpodnet_authenticate_sum"> + <intent android:action=".activity.gpoddernet.GpodnetAuthenticationActivity"/> + </PreferenceScreen> + <Preference + android:key="pref_gpodnet_setlogin_information" + android:title="@string/pref_gpodnet_setlogin_information_title" + android:summary="@string/pref_gpodnet_setlogin_information_sum"/> + <Preference + android:key="pref_gpodnet_logout" + android:title="@string/pref_gpodnet_logout_title"/> </PreferenceScreen> - <Preference - android:key="prefRevokeAccess" - android:summary="@string/pref_revokeAccess_sum" - android:title="@string/pref_revokeAccess_title" /> </PreferenceCategory> - <PreferenceCategory android:title="@string/other_pref" > - <Preference android:title="@string/choose_data_directory" android:key="prefChooseDataDir"/><Preference + <PreferenceCategory android:title="@string/other_pref"> + <Preference + android:title="@string/choose_data_directory" + android:key="prefChooseDataDir"/> + <Preference android:key="prefFlattrThisApp" android:summary="@string/pref_flattr_this_app_sum" - android:title="@string/pref_flattr_this_app_title" > + android:title="@string/pref_flattr_this_app_title"> </Preference> - <Preference android:key="prefOpmlExport" android:title="@string/opml_export_label"/><Preference + <Preference + android:key="prefOpmlExport" + android:title="@string/opml_export_label"/> + <Preference android:key="prefAbout" - android:title="@string/about_pref" /> - - + android:title="@string/about_pref"/> + + </PreferenceCategory> </PreferenceScreen>
\ No newline at end of file diff --git a/src/de/danoeh/antennapod/AppConfig.java b/src/de/danoeh/antennapod/AppConfig.java index 6caea4127..e79eb64e8 100644 --- a/src/de/danoeh/antennapod/AppConfig.java +++ b/src/de/danoeh/antennapod/AppConfig.java @@ -3,4 +3,6 @@ package de.danoeh.antennapod; public final class AppConfig { /** Should be used for debug logging. */ public final static boolean DEBUG = true; + /** Should be used when setting User-Agent header for HTTP-requests. */ + public final static String USER_AGENT = "AntennaPod/0.9.8.0"; } diff --git a/src/de/danoeh/antennapod/activity/AddFeedActivity.java b/src/de/danoeh/antennapod/activity/AddFeedActivity.java index 4085fc8d2..ad1adfa6b 100644 --- a/src/de/danoeh/antennapod/activity/AddFeedActivity.java +++ b/src/de/danoeh/antennapod/activity/AddFeedActivity.java @@ -5,6 +5,7 @@ import java.util.Date; import android.support.v7.app.ActionBarActivity; import android.view.Menu; import android.view.MenuItem; +import de.danoeh.antennapod.activity.gpoddernet.GpodnetMainActivity; import org.apache.commons.lang3.StringUtils; import android.app.AlertDialog; @@ -37,6 +38,7 @@ public class AddFeedActivity extends ActionBarActivity { private EditText etxtFeedurl; private Button butBrowseMiroGuide; + private Button butBrowserGpoddernet; private Button butOpmlImport; private Button butConfirm; private Button butCancel; @@ -63,6 +65,7 @@ public class AddFeedActivity extends ActionBarActivity { } butBrowseMiroGuide = (Button) findViewById(R.id.butBrowseMiroguide); + butBrowserGpoddernet = (Button) findViewById(R.id.butBrowseGpoddernet); butOpmlImport = (Button) findViewById(R.id.butOpmlImport); butConfirm = (Button) findViewById(R.id.butConfirm); butCancel = (Button) findViewById(R.id.butCancel); @@ -75,6 +78,13 @@ public class AddFeedActivity extends ActionBarActivity { MiroGuideMainActivity.class)); } }); + butBrowserGpoddernet.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + startActivity(new Intent(AddFeedActivity.this, + GpodnetMainActivity.class)); + } + }); butOpmlImport.setOnClickListener(new OnClickListener() { diff --git a/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java b/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java new file mode 100644 index 000000000..bb56b1d12 --- /dev/null +++ b/src/de/danoeh/antennapod/activity/DefaultOnlineFeedViewActivity.java @@ -0,0 +1,164 @@ +package de.danoeh.antennapod.activity; + +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.TextView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.adapter.FeedItemlistDescriptionAdapter; +import de.danoeh.antennapod.asynctask.ImageDiskCache; +import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; +import de.danoeh.antennapod.feed.EventDistributor; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.DownloadRequestException; +import de.danoeh.antennapod.storage.DownloadRequester; + +import java.util.Date; +import java.util.List; + +/** + * Created by daniel on 24.08.13. + */ +public class DefaultOnlineFeedViewActivity extends OnlineFeedViewActivity { + + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED | EventDistributor.FEED_LIST_UPDATE; + private volatile List<Feed> feeds; + private Feed feed; + + private Button subscribeButton; + + @Override + protected void onCreate(Bundle arg0) { + super.onCreate(arg0); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + protected void loadData() { + super.loadData(); + feeds = DBReader.getFeedList(this); + } + + @Override + protected void showFeedInformation(final Feed feed) { + super.showFeedInformation(feed); + setContentView(R.layout.listview_activity); + + this.feed = feed; + EventDistributor.getInstance().register(listener); + ListView listView = (ListView) findViewById(R.id.listview); + LayoutInflater inflater = (LayoutInflater) + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View header = inflater.inflate(R.layout.onlinefeedview_header, null); + listView.addHeaderView(header); + + listView.setAdapter(new FeedItemlistDescriptionAdapter(this, 0, feed.getItems())); + + ImageView cover = (ImageView) header.findViewById(R.id.imgvCover); + TextView title = (TextView) header.findViewById(R.id.txtvTitle); + TextView author = (TextView) header.findViewById(R.id.txtvAuthor); + TextView description = (TextView) header.findViewById(R.id.txtvDescription); + subscribeButton = (Button) header.findViewById(R.id.butSubscribe); + + if (feed.getImage() != null) { + ImageDiskCache.getDefaultInstance().loadThumbnailBitmap(feed.getImage().getDownload_url(), cover, (int) getResources().getDimension( + R.dimen.thumbnail_length)); + } + title.setText(feed.getTitle()); + author.setText(feed.getAuthor()); + description.setText(feed.getDescription()); + + subscribeButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + DownloadRequester.getInstance().downloadFeed( + DefaultOnlineFeedViewActivity.this, + new Feed(feed.getDownload_url(), new Date(), feed + .getTitle())); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(DefaultOnlineFeedViewActivity.this, + e.getMessage()); + } + setSubscribeButtonState(feed); + } + }); + setSubscribeButtonState(feed); + + } + + private boolean feedInFeedlist(Feed feed) { + if (feeds == null || feed == null) + return false; + for (Feed f : feeds) { + if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) { + return true; + } + } + return false; + } + + private void setSubscribeButtonState(Feed feed) { + if (subscribeButton != null && feed != null) { + if (DownloadRequester.getInstance().isDownloadingFile(feed.getDownload_url())) { + subscribeButton.setEnabled(false); + subscribeButton.setText(R.string.downloading_label); + } else if (feedInFeedlist(feed)) { + subscribeButton.setEnabled(false); + subscribeButton.setText(R.string.subscribed_label); + } else { + subscribeButton.setEnabled(true); + subscribeButton.setText(R.string.subscribe_label); + } + } + } + + EventDistributor.EventListener listener = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((arg & EventDistributor.FEED_LIST_UPDATE) != 0) { + new AsyncTask<Void, Void, List<Feed>>() { + @Override + protected List<Feed> doInBackground(Void... params) { + return DBReader.getFeedList(DefaultOnlineFeedViewActivity.this); + } + + @Override + protected void onPostExecute(List<Feed> feeds) { + super.onPostExecute(feeds); + DefaultOnlineFeedViewActivity.this.feeds = feeds; + setSubscribeButtonState(feed); + } + }.execute(); + } else if ((arg & EVENTS) != 0) { + setSubscribeButtonState(feed); + } + } + }; + + @Override + protected void onStop() { + super.onStop(); + EventDistributor.getInstance().unregister(listener); + } +} + diff --git a/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index fbac7057d..84aa2d26b 100644 --- a/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -1,23 +1,15 @@ package de.danoeh.antennapod.activity; -import java.io.File; -import java.io.IOException; -import java.util.Date; - -import javax.xml.parsers.ParserConfigurationException; - -import android.support.v7.app.ActionBarActivity; -import org.xml.sax.SAXException; - import android.app.AlertDialog; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; import android.util.Log; import android.view.Gravity; import android.widget.LinearLayout; import android.widget.ProgressBar; - +import android.widget.RelativeLayout; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.Feed; @@ -25,7 +17,6 @@ import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.service.download.DownloadRequest; import de.danoeh.antennapod.service.download.DownloadStatus; import de.danoeh.antennapod.service.download.Downloader; -import de.danoeh.antennapod.service.download.DownloaderCallback; import de.danoeh.antennapod.service.download.HttpDownloader; import de.danoeh.antennapod.syndication.handler.FeedHandler; import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException; @@ -33,207 +24,238 @@ import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.FileNameGenerator; import de.danoeh.antennapod.util.StorageUtils; import de.danoeh.antennapod.util.URLChecker; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.io.IOException; +import java.util.Date; /** * Downloads a feed from a feed URL and parses it. Subclasses can display the * feed object that was parsed. This activity MUST be started with a given URL * or an Exception will be thrown. - * + * <p/> * If the feed cannot be downloaded or parsed, an error dialog will be displayed * and the activity will finish as soon as the error dialog is closed. */ public abstract class OnlineFeedViewActivity extends ActionBarActivity { - private static final String TAG = "OnlineFeedViewActivity"; - private static final String ARG_FEEDURL = "arg.feedurl"; - - public static final int RESULT_ERROR = 2; - - private Feed feed; - private Downloader downloader; - - @Override - protected void onCreate(Bundle arg0) { - setTheme(UserPreferences.getTheme()); - super.onCreate(arg0); - StorageUtils.checkStorageAvailability(this); - final String feedUrl = getIntent().getStringExtra(ARG_FEEDURL); - if (feedUrl == null) { - throw new IllegalArgumentException( - "Activity must be started with feedurl argument!"); - } - if (AppConfig.DEBUG) - Log.d(TAG, "Activity was started with url " + feedUrl); - setLoadingLayout(); - startFeedDownload(feedUrl); - } - - @Override - protected void onStop() { - super.onStop(); - if (downloader != null && !downloader.isFinished()) { - downloader.cancel(); - } - } - - private DownloaderCallback downloaderCallback = new DownloaderCallback() { - @Override - public void onDownloadCompleted(final Downloader downloader) { - runOnUiThread(new Runnable() { - - @Override - public void run() { - DownloadStatus status = downloader.getResult(); - if (status != null) { - if (!status.isCancelled()) { - if (status.isSuccessful()) { - parseFeed(); - } else { - String errorMsg = status.getReason().getErrorString( - OnlineFeedViewActivity.this); - if (errorMsg != null - && status.getReasonDetailed() != null) { - errorMsg += " (" - + status.getReasonDetailed() + ")"; - } - showErrorDialog(errorMsg); - } - } - } else { - Log.wtf(TAG, - "DownloadStatus returned by Downloader was null"); - finish(); - } - } - }); - - } - }; - - private void startFeedDownload(String url) { - if (AppConfig.DEBUG) - Log.d(TAG, "Starting feed download"); - url = URLChecker.prepareURL(url); - feed = new Feed(url, new Date()); - String fileUrl = new File(getExternalCacheDir(), - FileNameGenerator.generateFileName(feed.getDownload_url())) - .toString(); - feed.setFile_url(fileUrl); - DownloadRequest request = new DownloadRequest(feed.getFile_url(), - feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED); - /* TODO update - HttpDownloader httpDownloader = new HttpDownloader(downloaderCallback, - request); - httpDownloader.start(); - */ - } - - /** Displays a progress indicator. */ - private void setLoadingLayout() { - LinearLayout ll = new LinearLayout(this); - LinearLayout.LayoutParams llLayoutParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.MATCH_PARENT, - LinearLayout.LayoutParams.MATCH_PARENT); - - ProgressBar pb = new ProgressBar(this); - pb.setIndeterminate(true); - LinearLayout.LayoutParams pbLayoutParams = new LinearLayout.LayoutParams( - LinearLayout.LayoutParams.WRAP_CONTENT, - LinearLayout.LayoutParams.WRAP_CONTENT); - pbLayoutParams.gravity = Gravity.CENTER; - ll.addView(pb, pbLayoutParams); - addContentView(ll, llLayoutParams); - } - - private void parseFeed() { - if (feed == null || feed.getFile_url() == null) { - throw new IllegalStateException( - "feed must be non-null and downloaded when parseFeed is called"); - } - - if (AppConfig.DEBUG) - Log.d(TAG, "Parsing feed"); - - Thread thread = new Thread() { - - @Override - public void run() { - String reasonDetailed = ""; - boolean successful = false; - FeedHandler handler = new FeedHandler(); - try { - handler.parseFeed(feed); - successful = true; - } catch (SAXException e) { - e.printStackTrace(); - reasonDetailed = e.getMessage(); - } catch (IOException e) { - e.printStackTrace(); - reasonDetailed = e.getMessage(); - } catch (ParserConfigurationException e) { - e.printStackTrace(); - reasonDetailed = e.getMessage(); - } catch (UnsupportedFeedtypeException e) { - e.printStackTrace(); - reasonDetailed = e.getMessage(); - } finally { - boolean rc = new File(feed.getFile_url()).delete(); - if (AppConfig.DEBUG) - Log.d(TAG, "Deleted feed source file. Result: " + rc); - } - - if (successful) { - runOnUiThread(new Runnable() { - @Override - public void run() { - showFeedInformation(); - } - }); - } else { - final String errorMsg = - DownloadError.ERROR_PARSER_EXCEPTION.getErrorString( - OnlineFeedViewActivity.this) - + " (" + reasonDetailed + ")"; - runOnUiThread(new Runnable() { - - @Override - public void run() { - showErrorDialog(errorMsg); - } - }); - } - } - }; - thread.start(); - } - - /** Called when feed parsed successfully */ - protected void showFeedInformation() { - - } - - private void showErrorDialog(String errorMsg) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(R.string.error_label); - if (errorMsg != null) { - builder.setMessage(getString(R.string.error_msg_prefix) + errorMsg); - } else { - builder.setMessage(R.string.error_msg_prefix); - } - builder.setNeutralButton(android.R.string.ok, - new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }); - builder.setOnCancelListener(new OnCancelListener() { - @Override - public void onCancel(DialogInterface dialog) { - setResult(RESULT_ERROR); - finish(); - } - }); - } + private static final String TAG = "OnlineFeedViewActivity"; + public static final String ARG_FEEDURL = "arg.feedurl"; + + /** Optional argument: specify a title for the actionbar. */ + public static final String ARG_TITLE = "title"; + + public static final int RESULT_ERROR = 2; + + private Feed feed; + private Downloader downloader; + + @Override + protected void onCreate(Bundle arg0) { + setTheme(UserPreferences.getTheme()); + super.onCreate(arg0); + + if (getIntent() != null && getIntent().hasExtra(ARG_TITLE)) { + getSupportActionBar().setTitle(getIntent().getStringExtra(ARG_TITLE)); + } + + StorageUtils.checkStorageAvailability(this); + final String feedUrl = getIntent().getStringExtra(ARG_FEEDURL); + if (feedUrl == null) { + throw new IllegalArgumentException( + "Activity must be started with feedurl argument!"); + } + if (AppConfig.DEBUG) + Log.d(TAG, "Activity was started with url " + feedUrl); + setLoadingLayout(); + startFeedDownload(feedUrl); + } + + @Override + protected void onStop() { + super.onStop(); + if (downloader != null && !downloader.isFinished()) { + downloader.cancel(); + } + } + + + private void onDownloadCompleted(final Downloader downloader) { + runOnUiThread(new Runnable() { + + @Override + public void run() { + if (AppConfig.DEBUG) Log.d(TAG, "Download was completed"); + DownloadStatus status = downloader.getResult(); + if (status != null) { + if (!status.isCancelled()) { + if (status.isSuccessful()) { + parseFeed(); + } else { + String errorMsg = status.getReason().getErrorString( + OnlineFeedViewActivity.this); + if (errorMsg != null + && status.getReasonDetailed() != null) { + errorMsg += " (" + + status.getReasonDetailed() + ")"; + } + showErrorDialog(errorMsg); + } + } + } else { + Log.wtf(TAG, + "DownloadStatus returned by Downloader was null"); + finish(); + } + } + }); + + } + + private void startFeedDownload(String url) { + if (AppConfig.DEBUG) + Log.d(TAG, "Starting feed download"); + url = URLChecker.prepareURL(url); + feed = new Feed(url, new Date()); + String fileUrl = new File(getExternalCacheDir(), + FileNameGenerator.generateFileName(feed.getDownload_url())) + .toString(); + feed.setFile_url(fileUrl); + final DownloadRequest request = new DownloadRequest(feed.getFile_url(), + feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED); + downloader = new HttpDownloader( + request); + new Thread() { + @Override + public void run() { + loadData(); + downloader.call(); + onDownloadCompleted(downloader); + } + }.start(); + + + } + + /** + * Displays a progress indicator. + */ + private void setLoadingLayout() { + RelativeLayout rl = new RelativeLayout(this); + RelativeLayout.LayoutParams rlLayoutParams = new RelativeLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT); + + ProgressBar pb = new ProgressBar(this); + pb.setIndeterminate(true); + RelativeLayout.LayoutParams pbLayoutParams = new RelativeLayout.LayoutParams( + LinearLayout.LayoutParams.WRAP_CONTENT, + LinearLayout.LayoutParams.WRAP_CONTENT); + pbLayoutParams.addRule(RelativeLayout.CENTER_IN_PARENT); + rl.addView(pb, pbLayoutParams); + addContentView(rl, rlLayoutParams); + } + + private void parseFeed() { + if (feed == null || feed.getFile_url() == null) { + throw new IllegalStateException( + "feed must be non-null and downloaded when parseFeed is called"); + } + + if (AppConfig.DEBUG) + Log.d(TAG, "Parsing feed"); + + Thread thread = new Thread() { + + @Override + public void run() { + String reasonDetailed = ""; + boolean successful = false; + FeedHandler handler = new FeedHandler(); + try { + handler.parseFeed(feed); + successful = true; + } catch (SAXException e) { + e.printStackTrace(); + reasonDetailed = e.getMessage(); + } catch (IOException e) { + e.printStackTrace(); + reasonDetailed = e.getMessage(); + } catch (ParserConfigurationException e) { + e.printStackTrace(); + reasonDetailed = e.getMessage(); + } catch (UnsupportedFeedtypeException e) { + e.printStackTrace(); + reasonDetailed = e.getMessage(); + } finally { + boolean rc = new File(feed.getFile_url()).delete(); + if (AppConfig.DEBUG) + Log.d(TAG, "Deleted feed source file. Result: " + rc); + } + + if (successful) { + runOnUiThread(new Runnable() { + @Override + public void run() { + showFeedInformation(feed); + } + }); + } else { + final String errorMsg = + DownloadError.ERROR_PARSER_EXCEPTION.getErrorString( + OnlineFeedViewActivity.this) + + " (" + reasonDetailed + ")"; + runOnUiThread(new Runnable() { + + @Override + public void run() { + showErrorDialog(errorMsg); + } + }); + } + } + }; + thread.start(); + } + + /** + * Can be used to load data asynchronously. + * */ + protected void loadData() { + + } + + /** + * Called when feed parsed successfully + */ + protected void showFeedInformation(Feed feed) { + + } + + private void showErrorDialog(String errorMsg) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(R.string.error_label); + if (errorMsg != null) { + builder.setMessage(getString(R.string.error_msg_prefix) + errorMsg); + } else { + builder.setMessage(R.string.error_msg_prefix); + } + builder.setNeutralButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + setResult(RESULT_ERROR); + finish(); + } + }); + } } diff --git a/src/de/danoeh/antennapod/activity/PreferenceActivity.java b/src/de/danoeh/antennapod/activity/PreferenceActivity.java index 96471d06d..bae6c2e17 100644 --- a/src/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/src/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -1,10 +1,5 @@ package de.danoeh.antennapod.activity; -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - import android.content.Context; import android.content.Intent; import android.content.res.Resources.Theme; @@ -18,17 +13,24 @@ import android.preference.Preference.OnPreferenceChangeListener; import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceScreen; import android.util.Log; - import android.view.Menu; import android.view.MenuItem; +import android.widget.Toast; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.FlattrClickWorker; import de.danoeh.antennapod.asynctask.OpmlExportWorker; +import de.danoeh.antennapod.dialog.AuthenticationDialog; import de.danoeh.antennapod.dialog.VariableSpeedDialog; +import de.danoeh.antennapod.preferences.GpodnetPreferences; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.util.flattr.FlattrUtils; +import java.io.File; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + /** * The main preference activity */ @@ -43,7 +45,11 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; private static final String AUTO_DL_PREF_SCREEN = "prefAutoDownloadSettings"; private static final String PREF_PLAYBACK_SPEED_LAUNCHER = "prefPlaybackSpeedLauncher"; - + + private static final String PREF_GPODNET_LOGIN = "pref_gpodnet_authenticate"; + private static final String PREF_GPODNET_SETLOGIN_INFORMATION = "pref_gpodnet_setlogin_information"; + private static final String PREF_GPODNET_LOGOUT = "pref_gpodnet_logout"; + private CheckBoxPreference[] selectedNetworks; @SuppressWarnings("deprecation") @@ -56,9 +62,9 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { getActionBar().setDisplayHomeAsUpEnabled(true); } - addPreferencesFromResource(R.xml.preferences); - findPreference(PREF_FLATTR_THIS_APP).setOnPreferenceClickListener( - new OnPreferenceClickListener() { + addPreferencesFromResource(R.xml.preferences); + findPreference(PREF_FLATTR_THIS_APP).setOnPreferenceClickListener( + new OnPreferenceClickListener() { @Override public boolean onPreferenceClick(Preference preference) { @@ -166,11 +172,45 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { return true; } }); + findPreference(PREF_GPODNET_SETLOGIN_INFORMATION).setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + AuthenticationDialog dialog = new AuthenticationDialog(PreferenceActivity.this, + R.string.pref_gpodnet_setlogin_information_title, false, false, GpodnetPreferences.getUsername(), + null) { + + @Override + protected void onConfirmed(String username, String password, boolean saveUsernamePassword) { + GpodnetPreferences.setPassword(password); + } + }; + dialog.show(); + return true; + } + }); + findPreference(PREF_GPODNET_LOGOUT).setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + GpodnetPreferences.logout(); + Toast toast = Toast.makeText(PreferenceActivity.this, R.string.pref_gpodnet_logout_toast, Toast.LENGTH_SHORT); + toast.show(); + updateGpodnetPreferenceScreen(); + return true; + } + }); buildUpdateIntervalPreference(); buildAutodownloadSelectedNetworsPreference(); setSelectedNetworksEnabled(UserPreferences .isEnableAutodownloadWifiFilter()); + + } + + private void updateGpodnetPreferenceScreen() { + final boolean loggedIn = GpodnetPreferences.loggedIn(); + findPreference(PREF_GPODNET_LOGIN).setEnabled(!loggedIn); + findPreference(PREF_GPODNET_SETLOGIN_INFORMATION).setEnabled(loggedIn); + findPreference(PREF_GPODNET_LOGOUT).setEnabled(loggedIn); } private void buildUpdateIntervalPreference() { @@ -214,6 +254,7 @@ public class PreferenceActivity extends android.preference.PreferenceActivity { checkItemVisibility(); setEpisodeCacheSizeText(UserPreferences.getEpisodeCacheSize()); setDataFolderText(); + updateGpodnetPreferenceScreen(); } @SuppressWarnings("deprecation") diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetActivity.java new file mode 100644 index 000000000..08b37ae60 --- /dev/null +++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetActivity.java @@ -0,0 +1,44 @@ +package de.danoeh.antennapod.activity.gpoddernet; + +import android.app.SearchManager; +import android.content.Context; +import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.widget.SearchView; +import android.view.Menu; +import android.view.MenuItem; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.preferences.UserPreferences; + +/** + * Created by daniel on 23.08.13. + */ +public class GpodnetActivity extends ActionBarActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label) + .setIcon( + obtainStyledAttributes( + new int[]{R.attr.action_search}) + .getDrawable(0)), + MenuItem.SHOW_AS_ACTION_IF_ROOM); + MenuItemCompat.setActionView(menu.findItem(R.id.search_item), new SearchView(this)); + + SearchManager searchManager = + (SearchManager) getSystemService(Context.SEARCH_SERVICE); + SearchView searchView = (SearchView) MenuItemCompat.getActionView(menu.findItem(R.id.search_item)); + searchView.setIconifiedByDefault(true); + searchView.setSearchableInfo( + searchManager.getSearchableInfo(getComponentName())); + + return true; + } +} diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java new file mode 100644 index 000000000..d355a7826 --- /dev/null +++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetAuthenticationActivity.java @@ -0,0 +1,370 @@ +package de.danoeh.antennapod.activity.gpoddernet; + +import android.content.Context; +import android.content.Intent; +import android.content.res.Configuration; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.NavUtils; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.*; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.MainActivity; +import de.danoeh.antennapod.gpoddernet.GpodnetService; +import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.preferences.GpodnetPreferences; +import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.service.GpodnetSyncService; + +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Guides the user through the authentication process + * Step 1: Request username and password from user + * Step 2: Choose device from a list of available devices or create a new one + * Step 3: Choose from a list of actions + */ +public class GpodnetAuthenticationActivity extends ActionBarActivity { + private static final String TAG = "GpodnetAuthenticationActivity"; + + private static final String CURRENT_STEP = "current_step"; + + private ViewFlipper viewFlipper; + + private static final int STEP_DEFAULT = -1; + private static final int STEP_LOGIN = 0; + private static final int STEP_DEVICE = 1; + private static final int STEP_FINISH = 2; + + private int currentStep = -1; + + private GpodnetService service; + private volatile String username; + private volatile String password; + private volatile GpodnetDevice selectedDevice; + + View[] views; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setTheme(UserPreferences.getTheme()); + + setContentView(R.layout.gpodnetauth_activity); + service = new GpodnetService(); + + viewFlipper = (ViewFlipper) findViewById(R.id.viewflipper); + LayoutInflater inflater = (LayoutInflater) + getSystemService(Context.LAYOUT_INFLATER_SERVICE); + views = new View[]{ + inflater.inflate(R.layout.gpodnetauth_credentials, null), + inflater.inflate(R.layout.gpodnetauth_device, null), + inflater.inflate(R.layout.gpodnetauth_finish, null) + }; + for (View view : views) { + viewFlipper.addView(view); + } + advance(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (service != null) { + service.shutdown(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + } + + private void setupLoginView(View view) { + final EditText username = (EditText) view.findViewById(R.id.etxtUsername); + final EditText password = (EditText) view.findViewById(R.id.etxtPassword); + final Button login = (Button) view.findViewById(R.id.butLogin); + final TextView txtvError = (TextView) view.findViewById(R.id.txtvError); + final ProgressBar progressBar = (ProgressBar) view.findViewById(R.id.progBarLogin); + + login.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + + final String usernameStr = username.getText().toString(); + final String passwordStr = password.getText().toString(); + + if (AppConfig.DEBUG) Log.d(TAG, "Checking login credentials"); + new AsyncTask<GpodnetService, Void, Void>() { + + volatile Exception exception; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + login.setEnabled(false); + progressBar.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + login.setEnabled(true); + progressBar.setVisibility(View.GONE); + + if (exception == null) { + advance(); + } else { + txtvError.setText(exception.getMessage()); + txtvError.setVisibility(View.VISIBLE); + } + } + + @Override + protected Void doInBackground(GpodnetService... params) { + try { + params[0].authenticate(usernameStr, passwordStr); + GpodnetAuthenticationActivity.this.username = usernameStr; + GpodnetAuthenticationActivity.this.password = passwordStr; + } catch (GpodnetServiceException e) { + e.printStackTrace(); + exception = e; + } + return null; + } + }.execute(service); + } + }); + } + + private void setupDeviceView(View view) { + final EditText deviceID = (EditText) view.findViewById(R.id.etxtDeviceID); + final EditText caption = (EditText) view.findViewById(R.id.etxtCaption); + final Button createNewDevice = (Button) view.findViewById(R.id.butCreateNewDevice); + final Button chooseDevice = (Button) view.findViewById(R.id.butChooseExistingDevice); + final TextView txtvError = (TextView) view.findViewById(R.id.txtvError); + final ProgressBar progBarCreateDevice = (ProgressBar) view.findViewById(R.id.progbarCreateDevice); + final Spinner spinnerDevices = (Spinner) view.findViewById(R.id.spinnerChooseDevice); + + + // load device list + final AtomicReference<List<GpodnetDevice>> devices = new AtomicReference<List<GpodnetDevice>>(); + new AsyncTask<GpodnetService, Void, List<GpodnetDevice>>() { + + private volatile Exception exception; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + chooseDevice.setEnabled(false); + spinnerDevices.setEnabled(false); + createNewDevice.setEnabled(false); + } + + @Override + protected void onPostExecute(List<GpodnetDevice> gpodnetDevices) { + super.onPostExecute(gpodnetDevices); + if (gpodnetDevices != null) { + List<String> deviceNames = new ArrayList<String>(); + for (GpodnetDevice device : gpodnetDevices) { + deviceNames.add(device.getCaption()); + } + spinnerDevices.setAdapter(new ArrayAdapter<String>(GpodnetAuthenticationActivity.this, + android.R.layout.simple_spinner_dropdown_item, deviceNames)); + spinnerDevices.setEnabled(true); + if (!deviceNames.isEmpty()) { + chooseDevice.setEnabled(true); + } + devices.set(gpodnetDevices); + createNewDevice.setEnabled(true); + } + } + + @Override + protected List<GpodnetDevice> doInBackground(GpodnetService... params) { + try { + return params[0].getDevices(username); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + exception = e; + return null; + } + } + }.execute(service); + + + createNewDevice.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (checkDeviceIDText(deviceID, txtvError, devices.get())) { + final String deviceStr = deviceID.getText().toString(); + final String captionStr = caption.getText().toString(); + + new AsyncTask<GpodnetService, Void, GpodnetDevice>() { + + private volatile Exception exception; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + createNewDevice.setEnabled(false); + chooseDevice.setEnabled(false); + progBarCreateDevice.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + } + + @Override + protected void onPostExecute(GpodnetDevice result) { + super.onPostExecute(result); + createNewDevice.setEnabled(true); + chooseDevice.setEnabled(true); + progBarCreateDevice.setVisibility(View.GONE); + if (exception == null) { + selectedDevice = result; + advance(); + } else { + txtvError.setText(exception.getMessage()); + txtvError.setVisibility(View.VISIBLE); + } + } + + @Override + protected GpodnetDevice doInBackground(GpodnetService... params) { + try { + params[0].configureDevice(username, deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE); + return new GpodnetDevice(deviceStr, captionStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + exception = e; + } + return null; + } + }.execute(service); + } + } + }); + + deviceID.setText(generateDeviceID()); + chooseDevice.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + final int position = spinnerDevices.getSelectedItemPosition(); + selectedDevice = devices.get().get(position); + advance(); + } + }); + } + + + private String generateDeviceID() { + final int DEVICE_ID_LENGTH = 10; + StringBuilder buffer = new StringBuilder(DEVICE_ID_LENGTH); + SecureRandom random = new SecureRandom(); + for (int i = 0; i < DEVICE_ID_LENGTH; i++) { + buffer.append(random.nextInt(10)); + + } + return buffer.toString(); + } + + private boolean checkDeviceIDText(EditText deviceID, TextView txtvError, List<GpodnetDevice> devices) { + String text = deviceID.getText().toString(); + if (text.length() == 0) { + txtvError.setText(R.string.gpodnetauth_device_errorEmpty); + txtvError.setVisibility(View.VISIBLE); + return false; + } else { + if (devices != null) { + for (GpodnetDevice device : devices) { + if (device.getId().equals(text)) { + txtvError.setText(R.string.gpodnetauth_device_errorAlreadyUsed); + txtvError.setVisibility(View.VISIBLE); + return false; + } + } + txtvError.setVisibility(View.GONE); + return true; + } + return true; + } + + } + + private void setupFinishView(View view) { + final Button sync = (Button) view.findViewById(R.id.butSyncNow); + final Button back = (Button) view.findViewById(R.id.butGoMainscreen); + + sync.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + GpodnetSyncService.sendSyncIntent(GpodnetAuthenticationActivity.this); + finish(); + } + }); + back.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(GpodnetAuthenticationActivity.this, MainActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } + }); + } + + private void writeLoginCredentials() { + if (AppConfig.DEBUG) Log.d(TAG, "Writing login credentials"); + GpodnetPreferences.setUsername(username); + GpodnetPreferences.setPassword(password); + GpodnetPreferences.setDeviceID(selectedDevice.getId()); + } + + private void advance() { + if (currentStep < STEP_FINISH) { + + View view = views[currentStep + 1]; + if (currentStep == STEP_DEFAULT) { + setupLoginView(view); + } else if (currentStep == STEP_LOGIN) { + if (username == null || password == null) { + throw new IllegalStateException("Username and password must not be null here"); + } else { + setupDeviceView(view); + } + } else if (currentStep == STEP_DEVICE) { + if (selectedDevice == null) { + throw new IllegalStateException("Device must not be null here"); + } else { + writeLoginCredentials(); + setupFinishView(view); + } + } + if (currentStep != STEP_DEFAULT) { + viewFlipper.showNext(); + } + currentStep++; + } else { + finish(); + } + } +} diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetMainActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetMainActivity.java new file mode 100644 index 000000000..9535e9d32 --- /dev/null +++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetMainActivity.java @@ -0,0 +1,89 @@ +package de.danoeh.antennapod.activity.gpoddernet; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentStatePagerAdapter; +import android.support.v4.app.NavUtils; +import android.support.v4.view.ViewPager; +import android.view.MenuItem; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.fragment.gpodnet.PodcastTopListFragment; +import de.danoeh.antennapod.fragment.gpodnet.SuggestionListFragment; +import de.danoeh.antennapod.fragment.gpodnet.TagListFragment; +import de.danoeh.antennapod.preferences.GpodnetPreferences; + +/** + * Created by daniel on 22.08.13. + */ +public class GpodnetMainActivity extends GpodnetActivity { + private static final String TAG = "GPodnetMainActivity"; + + private static final int POS_TAGS = 0; + private static final int POS_TOPLIST = 1; + private static final int POS_SUGGESTIONS = 2; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.gpodnet_main); + ViewPager viewpager = (ViewPager) findViewById(R.id.viewpager); + viewpager.setAdapter(new PagerAdapter(getSupportFragmentManager())); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } + + private class PagerAdapter extends FragmentStatePagerAdapter { + + private static final int NUM_PAGES_LOGGED_OUT = 2; + private static final int NUM_PAGES_LOGGED_IN = 3; + private final int NUM_PAGES; + + public PagerAdapter(FragmentManager fm) { + super(fm); + NUM_PAGES = NUM_PAGES_LOGGED_OUT; + } + + @Override + public Fragment getItem(int i) { + switch (i) { + case POS_TAGS: + return new TagListFragment(); + case POS_TOPLIST: + return new PodcastTopListFragment(); + case POS_SUGGESTIONS: + return new SuggestionListFragment(); + default: + return null; + } + } + + @Override + public CharSequence getPageTitle(int position) { + switch (position) { + case POS_TAGS: + return getString(R.string.gpodnet_taglist_header); + case POS_TOPLIST: + return getString(R.string.gpodnet_toplist_header); + case POS_SUGGESTIONS: + return getString(R.string.gpodnet_suggestions_header); + default: + return super.getPageTitle(position); + } + } + + @Override + public int getCount() { + return NUM_PAGES; + } + } +} diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetSearchActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetSearchActivity.java new file mode 100644 index 000000000..199b45dc9 --- /dev/null +++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetSearchActivity.java @@ -0,0 +1,63 @@ +package de.danoeh.antennapod.activity.gpoddernet; + +import android.app.SearchManager; +import android.content.Intent; +import android.os.Bundle; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.NavUtils; +import android.view.MenuItem; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.fragment.gpodnet.SearchListFragment; +import org.apache.commons.lang3.StringUtils; + +/** + * Created by daniel on 23.08.13. + */ +public class GpodnetSearchActivity extends GpodnetActivity { + + private SearchListFragment searchFragment; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.gpodnet_search); + } + + @Override + protected void onResume() { + super.onResume(); + Intent intent = getIntent(); + if (StringUtils.equals(intent.getAction(), Intent.ACTION_SEARCH)) { + handleSearchRequest(intent.getStringExtra(SearchManager.QUERY)); + } + } + + @Override + protected void onNewIntent(Intent intent) { + setIntent(intent); + } + + private void handleSearchRequest(String query) { + getSupportActionBar().setSubtitle(getString(R.string.search_term_label) + query); + if (searchFragment == null) { + FragmentTransaction transaction = getSupportFragmentManager() + .beginTransaction(); + searchFragment = SearchListFragment.newInstance(query); + transaction.replace(R.id.searchListFragment, searchFragment); + transaction.commit(); + } else { + searchFragment.changeQuery(query); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetTagActivity.java b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetTagActivity.java new file mode 100644 index 000000000..f3922f7aa --- /dev/null +++ b/src/de/danoeh/antennapod/activity/gpoddernet/GpodnetTagActivity.java @@ -0,0 +1,64 @@ +package de.danoeh.antennapod.activity.gpoddernet; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.NavUtils; +import android.view.MenuItem; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.fragment.gpodnet.PodcastListFragment; +import de.danoeh.antennapod.fragment.gpodnet.SearchListFragment; +import de.danoeh.antennapod.gpoddernet.GpodnetService; +import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.gpoddernet.model.GpodnetTag; + +import java.util.List; + +/** + * Created by daniel on 23.08.13. + */ +public class GpodnetTagActivity extends GpodnetActivity{ + + private static final int PODCAST_COUNT = 50; + public static final String ARG_TAGNAME = "tagname"; + + private GpodnetTag tag; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.gpodnet_tag_activity); + + if (!getIntent().hasExtra(ARG_TAGNAME)) { + throw new IllegalArgumentException("No tagname argument"); + } + tag = new GpodnetTag(getIntent().getStringExtra(ARG_TAGNAME)); + getSupportActionBar().setTitle(tag.getName()); + + FragmentTransaction transaction = getSupportFragmentManager() + .beginTransaction(); + Fragment taglistFragment = new TaglistFragment(); + transaction.replace(R.id.taglistFragment, taglistFragment); + transaction.commit(); + } + + private class TaglistFragment extends PodcastListFragment { + + @Override + protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException { + return service.getPodcastsForTag(tag, PODCAST_COUNT); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + NavUtils.navigateUpFromSameTask(this); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java b/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java new file mode 100644 index 000000000..5fb204b26 --- /dev/null +++ b/src/de/danoeh/antennapod/adapter/FeedItemlistDescriptionAdapter.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.feed.FeedItem; + +import java.util.List; + +/** + * Created by daniel on 24.08.13. + */ +public class FeedItemlistDescriptionAdapter extends ArrayAdapter<FeedItem> { + + public FeedItemlistDescriptionAdapter(Context context, int resource, List<FeedItem> objects) { + super(context, resource, objects); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + + FeedItem item = getItem(position); + + // Inflate layout + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + convertView = inflater.inflate(R.layout.itemdescription_listitem, null); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.description = (TextView) convertView.findViewById(R.id.txtvDescription); + + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(item.getTitle()); + if (item.getDescription() != null) { + holder.description.setText(item.getDescription()); + } + + return convertView; + } + + static class Holder { + TextView title; + TextView description; + } +} diff --git a/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java b/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java new file mode 100644 index 000000000..795b17917 --- /dev/null +++ b/src/de/danoeh/antennapod/adapter/gpodnet/PodcastListAdapter.java @@ -0,0 +1,63 @@ +package de.danoeh.antennapod.adapter.gpodnet; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.TextView; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.asynctask.ImageDiskCache; +import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; + +import java.util.List; + +/** + * Adapter for displaying a list of GPodnetPodcast-Objects. + */ +public class PodcastListAdapter extends ArrayAdapter<GpodnetPodcast> { + private final ImageDiskCache diskCache; + private final int thumbnailLength; + + public PodcastListAdapter(Context context, int resource, List<GpodnetPodcast> objects) { + super(context, resource, objects); + diskCache = ImageDiskCache.getDefaultInstance(); + thumbnailLength = (int) context.getResources().getDimension(R.dimen.thumbnail_length); + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + + GpodnetPodcast podcast = getItem(position); + + // Inflate Layout + if (convertView == null) { + holder = new Holder(); + LayoutInflater inflater = (LayoutInflater) getContext() + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + + convertView = inflater.inflate(R.layout.gpodnet_podcast_listitem, null); + holder.title = (TextView) convertView.findViewById(R.id.txtvTitle); + holder.description = (TextView) convertView.findViewById(R.id.txtvDescription); + holder.image = (ImageView) convertView.findViewById(R.id.imgvCover); + + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + holder.title.setText(podcast.getTitle()); + holder.description.setText(podcast.getDescription()); + diskCache.loadThumbnailBitmap(podcast.getLogoUrl(), holder.image, thumbnailLength); + + return convertView; + } + + static class Holder { + TextView title; + TextView description; + ImageView image; + } +} diff --git a/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java b/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java index 7ba68ae22..cb8e4d292 100644 --- a/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java +++ b/src/de/danoeh/antennapod/asynctask/BitmapDecodeWorkerTask.java @@ -2,105 +2,115 @@ package de.danoeh.antennapod.asynctask; import android.content.res.TypedArray; import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.TransitionDrawable; import android.os.Handler; import android.util.Log; import android.widget.ImageView; import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.ImageLoader.ImageWorkerTaskResource; import de.danoeh.antennapod.util.BitmapDecoder; public class BitmapDecodeWorkerTask extends Thread { - protected int PREFERRED_LENGTH; - - /** Can be thumbnail or cover */ - protected int imageType; - - private static final String TAG = "BitmapDecodeWorkerTask"; - private ImageView target; - protected CachedBitmap cBitmap; - - protected ImageLoader.ImageWorkerTaskResource imageResource; - - private Handler handler; - - private final int defaultCoverResource; - - public BitmapDecodeWorkerTask(Handler handler, ImageView target, - ImageWorkerTaskResource imageResource, int length, int imageType) { - super(); - this.handler = handler; - this.target = target; - this.imageResource = imageResource; - this.PREFERRED_LENGTH = length; - this.imageType = imageType; - TypedArray res = target.getContext().obtainStyledAttributes( - new int[] { R.attr.default_cover }); - this.defaultCoverResource = res.getResourceId(0, 0); - res.recycle(); - } - - /** - * Should return true if tag of the imageview is still the same it was - * before the bitmap was decoded - */ - protected boolean tagsMatching(ImageView target) { - return target.getTag() == null - || target.getTag().equals(imageResource.getImageLoaderCacheKey()); - } - - protected void onPostExecute() { - // check if imageview is still supposed to display this image - if (tagsMatching(target) && cBitmap.getBitmap() != null) { - target.setImageBitmap(cBitmap.getBitmap()); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "Not displaying image"); - } - } - - @Override - public void run() { - cBitmap = new CachedBitmap(BitmapDecoder.decodeBitmapFromWorkerTaskResource( - PREFERRED_LENGTH, imageResource), PREFERRED_LENGTH); - if (cBitmap.getBitmap() != null) { - storeBitmapInCache(cBitmap); - } else { - Log.w(TAG, "Could not load bitmap. Using default image."); - cBitmap = new CachedBitmap(BitmapFactory.decodeResource( - target.getResources(), defaultCoverResource), - PREFERRED_LENGTH); - } - if (AppConfig.DEBUG) - Log.d(TAG, "Finished loading bitmaps"); - - endBackgroundTask(); - } - - protected final void endBackgroundTask() { - handler.post(new Runnable() { - - @Override - public void run() { - onPostExecute(); - } - - }); - } - - protected void onInvalidStream() { - cBitmap = new CachedBitmap(BitmapFactory.decodeResource( - target.getResources(), defaultCoverResource), PREFERRED_LENGTH); - } - - protected void storeBitmapInCache(CachedBitmap cb) { - ImageLoader loader = ImageLoader.getInstance(); - if (imageType == ImageLoader.IMAGE_TYPE_COVER) { - loader.addBitmapToCoverCache(imageResource.getImageLoaderCacheKey(), cb); - } else if (imageType == ImageLoader.IMAGE_TYPE_THUMBNAIL) { - loader.addBitmapToThumbnailCache(imageResource.getImageLoaderCacheKey(), cb); - } - } + protected int PREFERRED_LENGTH; + public static final int FADE_DURATION = 500; + + /** + * Can be thumbnail or cover + */ + protected int imageType; + + private static final String TAG = "BitmapDecodeWorkerTask"; + private ImageView target; + protected CachedBitmap cBitmap; + + protected ImageLoader.ImageWorkerTaskResource imageResource; + + private Handler handler; + + private final int defaultCoverResource; + + public BitmapDecodeWorkerTask(Handler handler, ImageView target, + ImageWorkerTaskResource imageResource, int length, int imageType) { + super(); + this.handler = handler; + this.target = target; + this.imageResource = imageResource; + this.PREFERRED_LENGTH = length; + this.imageType = imageType; + this.defaultCoverResource = android.R.color.transparent; + } + + /** + * Should return true if tag of the imageview is still the same it was + * before the bitmap was decoded + */ + protected boolean tagsMatching(ImageView target) { + return target.getTag(R.id.imageloader_key) == null + || target.getTag(R.id.imageloader_key).equals(imageResource.getImageLoaderCacheKey()); + } + + protected void onPostExecute() { + // check if imageview is still supposed to display this image + if (tagsMatching(target) && cBitmap.getBitmap() != null) { + Drawable[] drawables = new Drawable[]{ + PodcastApp.getInstance().getResources().getDrawable(android.R.color.transparent), + new BitmapDrawable(PodcastApp.getInstance().getResources(), cBitmap.getBitmap()) + }; + TransitionDrawable transitionDrawable = new TransitionDrawable(drawables); + target.setImageDrawable(transitionDrawable); + transitionDrawable.startTransition(FADE_DURATION); + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "Not displaying image"); + } + } + + @Override + public void run() { + cBitmap = new CachedBitmap(BitmapDecoder.decodeBitmapFromWorkerTaskResource( + PREFERRED_LENGTH, imageResource), PREFERRED_LENGTH); + if (cBitmap.getBitmap() != null) { + storeBitmapInCache(cBitmap); + } else { + Log.w(TAG, "Could not load bitmap. Using default image."); + cBitmap = new CachedBitmap(BitmapFactory.decodeResource( + target.getResources(), defaultCoverResource), + PREFERRED_LENGTH); + } + if (AppConfig.DEBUG) + Log.d(TAG, "Finished loading bitmaps"); + + endBackgroundTask(); + } + + protected final void endBackgroundTask() { + handler.post(new Runnable() { + + @Override + public void run() { + onPostExecute(); + } + + }); + } + + protected void onInvalidStream() { + cBitmap = new CachedBitmap(BitmapFactory.decodeResource( + target.getResources(), defaultCoverResource), PREFERRED_LENGTH); + } + + protected void storeBitmapInCache(CachedBitmap cb) { + ImageLoader loader = ImageLoader.getInstance(); + if (imageType == ImageLoader.IMAGE_TYPE_COVER) { + loader.addBitmapToCoverCache(imageResource.getImageLoaderCacheKey(), cb); + } else if (imageType == ImageLoader.IMAGE_TYPE_THUMBNAIL) { + loader.addBitmapToThumbnailCache(imageResource.getImageLoaderCacheKey(), cb); + } + } } diff --git a/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java b/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java new file mode 100644 index 000000000..f7f6b576f --- /dev/null +++ b/src/de/danoeh/antennapod/asynctask/ImageDiskCache.java @@ -0,0 +1,391 @@ +package de.danoeh.antennapod.asynctask; + +import android.os.Handler; +import android.util.Log; +import android.util.Pair; +import android.widget.ImageView; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.service.download.DownloadRequest; +import de.danoeh.antennapod.service.download.HttpDownloader; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; + +import java.io.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Provides local cache for storing downloaded image. An image disk cache downloads images and stores them as long + * as the cache is not full. Once the cache is full, the image disk cache will delete older images. + */ +public class ImageDiskCache { + private static final String TAG = "ImageDiskCache"; + + private static HashMap<String, ImageDiskCache> cacheSingletons = new HashMap<String, ImageDiskCache>(); + + /** + * Return a default instance of an ImageDiskCache. This cache will store data in the external cache folder. + */ + public static synchronized ImageDiskCache getDefaultInstance() { + final String DEFAULT_PATH = "imagecache"; + final long DEFAULT_MAX_CACHE_SIZE = 10 * 1024 * 1024; + + File cacheDir = PodcastApp.getInstance().getExternalCacheDir(); + if (cacheDir == null) { + return null; + } + return getInstance(new File(cacheDir, DEFAULT_PATH).getAbsolutePath(), DEFAULT_MAX_CACHE_SIZE); + } + + /** + * Return an instance of an ImageDiskCache that stores images in the specified folder. + */ + public static synchronized ImageDiskCache getInstance(String path, long maxCacheSize) { + if (path == null) { + throw new NullPointerException(); + } + if (cacheSingletons.containsKey(path)) { + return cacheSingletons.get(path); + } + + ImageDiskCache cache = cacheSingletons.get(path); + if (cache == null) { + cache = new ImageDiskCache(path, maxCacheSize); + cacheSingletons.put(new File(path).getAbsolutePath(), cache); + } + cacheSingletons.put(path, cache); + return cache; + } + + /** + * Filename - cache object mapping + */ + private static final String CACHE_FILE_NAME = "cachefile"; + private ExecutorService executor; + private ConcurrentHashMap<String, DiskCacheObject> diskCache; + private final long maxCacheSize; + private int cacheSize; + private final File cacheFolder; + private Handler handler; + + private ImageDiskCache(String path, long maxCacheSize) { + this.maxCacheSize = maxCacheSize; + this.cacheFolder = new File(path); + if (!cacheFolder.exists() && !cacheFolder.mkdir()) { + throw new IllegalArgumentException("Image disk cache could not create cache folder in: " + path); + } + + executor = Executors.newFixedThreadPool(Runtime.getRuntime() + .availableProcessors()); + handler = new Handler(); + } + + private synchronized void initCacheFolder() { + if (diskCache == null) { + if (AppConfig.DEBUG) Log.d(TAG, "Initializing cache folder"); + File cacheFile = new File(cacheFolder, CACHE_FILE_NAME); + if (cacheFile.exists()) { + try { + InputStream in = new FileInputStream(cacheFile); + BufferedInputStream buffer = new BufferedInputStream(in); + ObjectInputStream objectInput = new ObjectInputStream(buffer); + diskCache = (ConcurrentHashMap<String, DiskCacheObject>) objectInput.readObject(); + // calculate cache size + for (DiskCacheObject dco : diskCache.values()) { + cacheSize += dco.size; + } + deleteInvalidFiles(); + } catch (IOException e) { + e.printStackTrace(); + diskCache = new ConcurrentHashMap<String, DiskCacheObject>(); + } catch (ClassCastException e) { + e.printStackTrace(); + diskCache = new ConcurrentHashMap<String, DiskCacheObject>(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + diskCache = new ConcurrentHashMap<String, DiskCacheObject>(); + } + } else { + diskCache = new ConcurrentHashMap<String, DiskCacheObject>(); + } + } + } + + private List<File> getCacheFileList() { + Collection<DiskCacheObject> values = diskCache.values(); + List<File> files = new ArrayList<File>(); + for (DiskCacheObject dco : values) { + files.add(dco.getFile()); + } + files.add(new File(cacheFolder, CACHE_FILE_NAME)); + return files; + } + + private Pair<String, DiskCacheObject> getOldestCacheObject() { + Collection<String> keys = diskCache.keySet(); + DiskCacheObject oldest = null; + String oldestKey = null; + + for (String key : keys) { + + if (oldestKey == null) { + oldestKey = key; + oldest = diskCache.get(key); + } else { + DiskCacheObject dco = diskCache.get(key); + if (oldest.timestamp > dco.timestamp) { + oldestKey = key; + oldest = diskCache.get(key); + } + } + } + return new Pair<String, DiskCacheObject>(oldestKey, oldest); + } + + private synchronized void deleteCacheObject(String key, DiskCacheObject value) { + Log.i(TAG, "Deleting cached object: " + key); + diskCache.remove(key); + boolean result = value.getFile().delete(); + if (!result) { + Log.w(TAG, "Could not delete file " + value.fileUrl); + } + cacheSize -= value.size; + } + + private synchronized void deleteInvalidFiles() { + // delete files that are not stored inside the cache + File[] files = cacheFolder.listFiles(); + List<File> cacheFiles = getCacheFileList(); + for (File file : files) { + if (!cacheFiles.contains(file)) { + Log.i(TAG, "Deleting unused file: " + file.getAbsolutePath()); + boolean result = file.delete(); + if (!result) { + Log.w(TAG, "Could not delete file: " + file.getAbsolutePath()); + } + } + } + } + + private synchronized void cleanup() { + if (cacheSize > maxCacheSize) { + while (cacheSize > maxCacheSize) { + Pair<String, DiskCacheObject> oldest = getOldestCacheObject(); + deleteCacheObject(oldest.first, oldest.second); + } + } + } + + /** + * Loads a new image from the disk cache. If the image that the url points to has already been downloaded, the image will + * be loaded from the disk. Otherwise, the image will be downloaded first. + * The image will be stored in the thumbnail cache. + */ + public void loadThumbnailBitmap(final String url, final ImageView target, final int length) { + final ImageLoader il = ImageLoader.getInstance(); + target.setTag(R.id.image_disk_cache_key, url); + if (diskCache != null) { + DiskCacheObject dco = getFromCacheIfAvailable(url); + if (dco != null) { + il.loadThumbnailBitmap(dco.loadImage(), target, length); + return; + } + } + target.setImageResource(android.R.color.transparent); + executor.submit(new ImageDownloader(url) { + @Override + protected void onImageLoaded(DiskCacheObject diskCacheObject) { + final Object tag = target.getTag(R.id.image_disk_cache_key); + if (tag != null || StringUtils.equals((String) tag, url)) { + il.loadThumbnailBitmap(diskCacheObject.loadImage(), target, length); + } + } + }); + + } + + /** + * Loads a new image from the disk cache. If the image that the url points to has already been downloaded, the image will + * be loaded from the disk. Otherwise, the image will be downloaded first. + * The image will be stored in the cover cache. + */ + public void loadCoverBitmap(final String url, final ImageView target, final int length) { + final ImageLoader il = ImageLoader.getInstance(); + target.setTag(R.id.image_disk_cache_key, url); + if (diskCache != null) { + DiskCacheObject dco = getFromCacheIfAvailable(url); + if (dco != null) { + il.loadThumbnailBitmap(dco.loadImage(), target, length); + return; + } + } + target.setImageResource(android.R.color.transparent); + executor.submit(new ImageDownloader(url) { + @Override + protected void onImageLoaded(DiskCacheObject diskCacheObject) { + final Object tag = target.getTag(R.id.image_disk_cache_key); + if (tag != null || StringUtils.equals((String) tag, url)) { + il.loadCoverBitmap(diskCacheObject.loadImage(), target, length); + } + } + }); + } + + private synchronized void addToDiskCache(String url, DiskCacheObject obj) { + if (diskCache == null) { + initCacheFolder(); + } + if (AppConfig.DEBUG) Log.d(TAG, "Adding new image to disk cache: " + url); + diskCache.put(url, obj); + cacheSize += obj.size; + if (cacheSize > maxCacheSize) { + cleanup(); + } + saveCacheInfoFile(); + } + + private synchronized void saveCacheInfoFile() { + OutputStream out = null; + try { + out = new BufferedOutputStream(new FileOutputStream(new File(cacheFolder, CACHE_FILE_NAME))); + ObjectOutputStream objOut = new ObjectOutputStream(out); + objOut.writeObject(diskCache); + } catch (IOException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(out); + } + } + + private synchronized DiskCacheObject getFromCacheIfAvailable(String key) { + if (diskCache == null) { + initCacheFolder(); + } + DiskCacheObject dco = diskCache.get(key); + if (dco != null) { + dco.timestamp = System.currentTimeMillis(); + } + return dco; + } + + ConcurrentHashMap<String, File> runningDownloads = new ConcurrentHashMap<String, File>(); + + private abstract class ImageDownloader implements Runnable { + private String downloadUrl; + + public ImageDownloader(String downloadUrl) { + this.downloadUrl = downloadUrl; + } + + protected abstract void onImageLoaded(DiskCacheObject diskCacheObject); + + public void run() { + DiskCacheObject tmp = getFromCacheIfAvailable(downloadUrl); + if (tmp != null) { + onImageLoaded(tmp); + return; + } + + DiskCacheObject dco = null; + File newFile = new File(cacheFolder, Integer.toString(downloadUrl.hashCode())); + synchronized (ImageDiskCache.this) { + if (runningDownloads.containsKey(newFile.getAbsolutePath())) { + Log.d(TAG, "Download is already running: " + newFile.getAbsolutePath()); + return; + } else { + runningDownloads.put(newFile.getAbsolutePath(), newFile); + } + } + if (newFile.exists()) { + newFile.delete(); + } + + HttpDownloader result = downloadFile(newFile.getAbsolutePath(), downloadUrl); + if (result.getResult().isSuccessful()) { + long size = result.getDownloadRequest().getSoFar(); + + dco = new DiskCacheObject(newFile.getAbsolutePath(), size); + addToDiskCache(downloadUrl, dco); + if (AppConfig.DEBUG) Log.d(TAG, "Image was downloaded"); + } else { + Log.w(TAG, "Download of url " + downloadUrl + " failed. Reason: " + result.getResult().getReasonDetailed() + "(" + result.getResult().getReason() + ")"); + } + + if (dco != null) { + final DiskCacheObject dcoRef = dco; + handler.post(new Runnable() { + @Override + public void run() { + onImageLoaded(dcoRef); + } + }); + + } + runningDownloads.remove(newFile.getAbsolutePath()); + + } + + private HttpDownloader downloadFile(String destination, String source) { + DownloadRequest request = new DownloadRequest(destination, source, "", 0, 0); + HttpDownloader downloader = new HttpDownloader(request); + downloader.call(); + return downloader; + } + } + + private static class DiskCacheObject implements Serializable { + private final String fileUrl; + + /** + * Last usage of this image cache object. + */ + private long timestamp; + private final long size; + + public DiskCacheObject(String fileUrl, long size) { + if (fileUrl == null) { + throw new NullPointerException(); + } + this.fileUrl = fileUrl; + this.timestamp = System.currentTimeMillis(); + this.size = size; + } + + public File getFile() { + return new File(fileUrl); + } + + public ImageLoader.ImageWorkerTaskResource loadImage() { + return new ImageLoader.ImageWorkerTaskResource() { + + @Override + public InputStream openImageInputStream() { + try { + return new FileInputStream(getFile()); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + return null; + } + + @Override + public InputStream reopenImageInputStream(InputStream input) { + IOUtils.closeQuietly(input); + return openImageInputStream(); + } + + @Override + public String getImageLoaderCacheKey() { + return fileUrl; + } + }; + } + } +} diff --git a/src/de/danoeh/antennapod/asynctask/ImageLoader.java b/src/de/danoeh/antennapod/asynctask/ImageLoader.java index 45a99e704..a4a9bc823 100644 --- a/src/de/danoeh/antennapod/asynctask/ImageLoader.java +++ b/src/de/danoeh/antennapod/asynctask/ImageLoader.java @@ -66,7 +66,7 @@ public class ImageLoader { private ExecutorService createExecutor() { return Executors.newFixedThreadPool(Runtime.getRuntime() - .availableProcessors() + 1, new ThreadFactory() { + .availableProcessors(), new ThreadFactory() { @Override public Thread newThread(Runnable r) { @@ -106,7 +106,8 @@ public class ImageLoader { .getContext()); if (source != null && source.getImageLoaderCacheKey() != null) { - CachedBitmap cBitmap = getBitmapFromCoverCache(source.getImageLoaderCacheKey()); + target.setTag(R.id.imageloader_key, source.getImageLoaderCacheKey()); + CachedBitmap cBitmap = getBitmapFromCoverCache(source.getImageLoaderCacheKey()); if (cBitmap != null && cBitmap.getLength() >= length) { target.setImageBitmap(cBitmap.getBitmap()); } else { @@ -143,7 +144,8 @@ public class ImageLoader { .getContext()); if (source != null && source.getImageLoaderCacheKey() != null) { - CachedBitmap cBitmap = getBitmapFromThumbnailCache(source.getImageLoaderCacheKey()); + target.setTag(R.id.imageloader_key, source.getImageLoaderCacheKey()); + CachedBitmap cBitmap = getBitmapFromThumbnailCache(source.getImageLoaderCacheKey()); if (cBitmap != null && cBitmap.getLength() >= length) { target.setImageBitmap(cBitmap.getBitmap()); } else { @@ -195,11 +197,7 @@ public class ImageLoader { } private int getDefaultCoverResource(Context context) { - TypedArray res = context - .obtainStyledAttributes(new int[] { R.attr.default_cover }); - final int defaultCoverResource = res.getResourceId(0, 0); - res.recycle(); - return defaultCoverResource; + return android.R.color.transparent; } /** diff --git a/src/de/danoeh/antennapod/dialog/AuthenticationDialog.java b/src/de/danoeh/antennapod/dialog/AuthenticationDialog.java new file mode 100644 index 000000000..bdb2d68ba --- /dev/null +++ b/src/de/danoeh/antennapod/dialog/AuthenticationDialog.java @@ -0,0 +1,89 @@ +package de.danoeh.antennapod.dialog; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.View; +import android.view.Window; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import de.danoeh.antennapod.R; + +/** + * Displays a dialog with a username and password text field and an optional checkbox to save username and preferences. + */ +public abstract class AuthenticationDialog extends Dialog { + + private final int titleRes; + private final boolean enableUsernameField; + private final boolean showSaveCredentialsCheckbox; + private final String usernameInitialValue; + private final String passwordInitialValue; + + public AuthenticationDialog(Context context, int titleRes, boolean enableUsernameField, boolean showSaveCredentialsCheckbox, String usernameInitialValue, String passwordInitialValue) { + super(context); + this.titleRes = titleRes; + this.enableUsernameField = enableUsernameField; + this.showSaveCredentialsCheckbox = showSaveCredentialsCheckbox; + this.usernameInitialValue = usernameInitialValue; + this.passwordInitialValue = passwordInitialValue; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.authentication_dialog); + final EditText etxtUsername = (EditText) findViewById(R.id.etxtUsername); + final EditText etxtPassword = (EditText) findViewById(R.id.etxtPassword); + final CheckBox saveUsernamePassword = (CheckBox) findViewById(R.id.chkSaveUsernamePassword); + final Button butConfirm = (Button) findViewById(R.id.butConfirm); + final Button butCancel = (Button) findViewById(R.id.butCancel); + + if (titleRes != 0) { + setTitle(titleRes); + } else { + requestWindowFeature(Window.FEATURE_NO_TITLE); + } + etxtUsername.setEnabled(enableUsernameField); + if (showSaveCredentialsCheckbox) { + saveUsernamePassword.setVisibility(View.VISIBLE); + } else { + saveUsernamePassword.setVisibility(View.GONE); + } + if (usernameInitialValue != null) { + etxtUsername.setText(usernameInitialValue); + } + if (passwordInitialValue != null) { + etxtPassword.setText(passwordInitialValue); + } + setOnCancelListener(new OnCancelListener() { + @Override + public void onCancel(DialogInterface dialog) { + onCancelled(); + } + }); + butCancel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + cancel(); + } + }); + butConfirm.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + onConfirmed(etxtUsername.getText().toString(), + etxtPassword.getText().toString(), + showSaveCredentialsCheckbox && saveUsernamePassword.isChecked()); + dismiss(); + } + }); + } + + protected void onCancelled() { + + } + + protected abstract void onConfirmed(String username, String password, boolean saveUsernamePassword); +} diff --git a/src/de/danoeh/antennapod/fragment/EpisodesFragment.java b/src/de/danoeh/antennapod/fragment/EpisodesFragment.java index a99056c9a..6b228662f 100644 --- a/src/de/danoeh/antennapod/fragment/EpisodesFragment.java +++ b/src/de/danoeh/antennapod/fragment/EpisodesFragment.java @@ -222,6 +222,12 @@ public class EpisodesFragment extends Fragment { } }, selectedItem, false, QueueAccess.ItemListAccess(queue)); + // check to see if the item is in the queue, if so add queue menu items + int itemIndex = queue.indexOf(selectedItem); + if (itemIndex != -1) { + addQueueOnlyMenus(menu, itemIndex); + } + } else if (selectedGroupId == ExternalEpisodesListAdapter.GROUP_POS_QUEUE) { menu.add(Menu.NONE, R.id.organize_queue_item, Menu.NONE, R.string.organize_queue_label); @@ -237,6 +243,24 @@ public class EpisodesFragment extends Fragment { } } + /** + * Adds submenus to the ContextMenu if the item selected is in the queue. + * @param menu the ContextMenu to add the submenus to + * @param itemIndex the index of the selected item within the queue. + */ + private void addQueueOnlyMenus(ContextMenu menu, int itemIndex) { + if (itemIndex != 0) { + // don't add move to top if this item is already on the top + menu.add(Menu.NONE, R.id.move_to_top_item, Menu.NONE, getActivity() + .getString(R.string.move_to_top_label)); + } + if (itemIndex != queue.size() - 1) { + // don't add move to bottom if this item is already on the bottom + menu.add(Menu.NONE, R.id.move_to_bottom_item, Menu.NONE, getActivity() + .getString(R.string.move_to_bottom_label)); + } + } + @Override public boolean onContextItemSelected(android.view.MenuItem item) { boolean handled = false; @@ -249,7 +273,19 @@ public class EpisodesFragment extends Fragment { DownloadRequestErrorDialogCreator.newRequestErrorDialog( getActivity(), e.getMessage()); } - + if (!handled) { + // if it wasn't handled by the FeedItemMenuHandler it might be one of ours + switch (item.getItemId()) { + case R.id.move_to_top_item: + DBWriter.moveQueueItemToTop(getActivity(), selectedItem.getId(), true); + handled = true; + break; + case R.id.move_to_bottom_item: + DBWriter.moveQueueItemToBottom(getActivity(), selectedItem.getId(), true); + handled = true; + break; + } + } } else if (selectedGroupId == ExternalEpisodesListAdapter.GROUP_POS_QUEUE) { handled = true; switch (item.getItemId()) { diff --git a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java index 0dc29c415..6283a4b7f 100644 --- a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java +++ b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java @@ -1,7 +1,5 @@ package de.danoeh.antennapod.fragment; -import java.util.List; - import android.annotation.SuppressLint; import android.content.Context; import android.content.DialogInterface; @@ -14,7 +12,6 @@ import android.support.v7.view.ActionMode; import android.util.Log; import android.view.*; import android.widget.*; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.FeedItemlistActivity; @@ -29,6 +26,8 @@ import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.storage.FeedItemStatistics; import de.danoeh.antennapod.util.menuhandler.FeedMenuHandler; +import java.util.List; + public class FeedlistFragment extends Fragment implements ActionMode.Callback, AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener { @@ -244,11 +243,18 @@ public class FeedlistFragment extends Fragment implements return true; } + private boolean actionModeDestroyWorkaround = false; // TODO remove this workaround + @Override public void onDestroyActionMode(ActionMode mode) { - mActionMode = null; - selectedFeed = null; - fla.setSelectedItemIndex(FeedlistAdapter.SELECTION_NONE); + if (actionModeDestroyWorkaround) { + mActionMode = null; + selectedFeed = null; + fla.setSelectedItemIndex(FeedlistAdapter.SELECTION_NONE); + actionModeDestroyWorkaround = false; + } else { + actionModeDestroyWorkaround = true; + } } @Override diff --git a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java index fb086019c..282bb4d5c 100644 --- a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java +++ b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java @@ -60,6 +60,8 @@ public class ItemlistFragment extends ListFragment { /** Argument for FeeditemlistAdapter */ protected boolean showFeedtitle; + private AsyncTask<Long, Void, Feed> currentLoadTask; + public ItemlistFragment(boolean showFeedtitle) { super(); this.showFeedtitle = showFeedtitle; @@ -115,11 +117,21 @@ public class ItemlistFragment extends ListFragment { return inflater.inflate(R.layout.feeditemlist, container, false); } - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - loadData(); - } + @Override + public void onStart() { + super.onStart(); + EventDistributor.getInstance().register(contentUpdate); + loadData(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + EventDistributor.getInstance().unregister(contentUpdate); + if (currentLoadTask != null) { + currentLoadTask.cancel(true); + } + } protected void loadData() { final long feedId; @@ -168,6 +180,7 @@ public class ItemlistFragment extends ListFragment { } } }; + currentLoadTask = loadTask; loadTask.execute(feedId); } @@ -185,17 +198,6 @@ public class ItemlistFragment extends ListFragment { } @Override - public void onPause() { - super.onPause(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - EventDistributor.getInstance().unregister(contentUpdate); - } - - @Override public void onResume() { super.onResume(); getActivity().runOnUiThread(new Runnable() { @@ -206,7 +208,6 @@ public class ItemlistFragment extends ListFragment { } }); updateProgressBarVisibility(); - EventDistributor.getInstance().register(contentUpdate); } @Override diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java new file mode 100644 index 000000000..32e11e0ce --- /dev/null +++ b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java @@ -0,0 +1,120 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.GridView; +import android.widget.ProgressBar; +import android.widget.TextView; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.activity.DefaultOnlineFeedViewActivity; +import de.danoeh.antennapod.activity.OnlineFeedViewActivity; +import de.danoeh.antennapod.adapter.gpodnet.PodcastListAdapter; +import de.danoeh.antennapod.gpoddernet.GpodnetService; +import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; + +import java.util.List; + +/** + * Displays a list of GPodnetPodcast-Objects in a GridView + */ +public abstract class PodcastListFragment extends Fragment { + private static final String TAG = "PodcastListFragment"; + + private GridView gridView; + private ProgressBar progressBar; + private TextView txtvError; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + setRetainInstance(true); + View root = inflater.inflate(R.layout.gpodnet_podcast_list, container, false); + + gridView = (GridView) root.findViewById(R.id.gridView); + progressBar = (ProgressBar) root.findViewById(R.id.progressBar); + txtvError = (TextView) root.findViewById(R.id.txtvError); + + gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + onPodcastSelected((GpodnetPodcast) gridView.getAdapter().getItem(position)); + } + }); + + loadData(); + return root; + } + + protected void onPodcastSelected(GpodnetPodcast selection) { + if (AppConfig.DEBUG) Log.d(TAG, "Selected podcast: " + selection.toString()); + Intent intent = new Intent(getActivity(), DefaultOnlineFeedViewActivity.class); + intent.putExtra(OnlineFeedViewActivity.ARG_FEEDURL, selection.getUrl()); + intent.putExtra(DefaultOnlineFeedViewActivity.ARG_TITLE, getString(R.string.gpodnet_main_label)); + startActivity(intent); + } + + protected abstract List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException; + + protected final void loadData() { + AsyncTask<Void, Void, List<GpodnetPodcast>> loaderTask = new AsyncTask<Void, Void, List<GpodnetPodcast>>() { + volatile Exception exception = null; + + @Override + protected List<GpodnetPodcast> doInBackground(Void... params) { + GpodnetService service = null; + try { + service = new GpodnetService(); + return loadPodcastData(service); + } catch (GpodnetServiceException e) { + exception = e; + e.printStackTrace(); + return null; + } finally { + if (service != null) { + service.shutdown(); + } + } + } + + @Override + protected void onPostExecute(List<GpodnetPodcast> gpodnetPodcasts) { + super.onPostExecute(gpodnetPodcasts); + final Context context = getActivity(); + if (context != null && gpodnetPodcasts != null) { + PodcastListAdapter listAdapter = new PodcastListAdapter(context, 0, gpodnetPodcasts); + gridView.setAdapter(listAdapter); + listAdapter.notifyDataSetChanged(); + + progressBar.setVisibility(View.GONE); + gridView.setVisibility(View.VISIBLE); + } else if (context != null) { + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtvError.setText(getString(R.string.error_msg_prefix) + exception.getMessage()); + } + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + } + }; + + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + loaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + loaderTask.execute(); + } + } +} diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java new file mode 100644 index 000000000..7007d0b9a --- /dev/null +++ b/src/de/danoeh/antennapod/fragment/gpodnet/PodcastTopListFragment.java @@ -0,0 +1,22 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.gpoddernet.GpodnetService; +import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; + +import java.util.List; + +/** + * + */ +public class PodcastTopListFragment extends PodcastListFragment { + private static final String TAG = "PodcastTopListFragment"; + private static final int PODCAST_COUNT = 50; + + @Override + protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException { + return service.getPodcastToplist(PODCAST_COUNT); + } +} diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java new file mode 100644 index 000000000..322d13097 --- /dev/null +++ b/src/de/danoeh/antennapod/fragment/gpodnet/SearchListFragment.java @@ -0,0 +1,48 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import android.os.Bundle; +import de.danoeh.antennapod.gpoddernet.GpodnetService; +import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; + +import java.util.List; + +/** + * Created by daniel on 23.08.13. + */ +public class SearchListFragment extends PodcastListFragment { + private static final String ARG_QUERY = "query"; + + private String query; + + public static SearchListFragment newInstance(String query) { + SearchListFragment fragment = new SearchListFragment(); + Bundle args = new Bundle(); + args.putString(ARG_QUERY, query); + fragment.setArguments(args); + return fragment; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (getArguments() != null && getArguments().containsKey(ARG_QUERY)) { + this.query = getArguments().getString(ARG_QUERY); + } else { + this.query = ""; + } + } + + @Override + protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException { + return service.searchPodcasts(query, 0); + } + + public void changeQuery(String query) { + if (query == null) { + throw new NullPointerException(); + } + this.query = query; + loadData(); + } +} diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java new file mode 100644 index 000000000..45fe25580 --- /dev/null +++ b/src/de/danoeh/antennapod/fragment/gpodnet/SuggestionListFragment.java @@ -0,0 +1,26 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import de.danoeh.antennapod.gpoddernet.GpodnetService; +import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.preferences.GpodnetPreferences; + +import java.util.ArrayList; +import java.util.List; + +/** + * Displays suggestions from gpodder.net + */ +public class SuggestionListFragment extends PodcastListFragment { + private static final int SUGGESTIONS_COUNT = 50; + + @Override + protected List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException { + if (GpodnetPreferences.loggedIn()) { + service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); + return service.getSuggestions(SUGGESTIONS_COUNT); + } else { + return new ArrayList<GpodnetPodcast>(); + } + } +} diff --git a/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java new file mode 100644 index 000000000..3d63f2e58 --- /dev/null +++ b/src/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -0,0 +1,96 @@ +package de.danoeh.antennapod.fragment.gpodnet; + +import android.R; +import android.content.Context; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.TextView; +import de.danoeh.antennapod.activity.gpoddernet.GpodnetTagActivity; +import de.danoeh.antennapod.gpoddernet.GpodnetService; +import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.gpoddernet.model.GpodnetTag; + +import java.util.ArrayList; +import java.util.List; + +public class TagListFragment extends ListFragment { + private static final String TAG = "TagListFragment"; + private static final int COUNT = 50; + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + setRetainInstance(true); + + getListView().setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView<?> parent, View view, int position, long id) { + String selectedTag = (String) getListAdapter().getItem(position); + Intent intent = new Intent(getActivity(), GpodnetTagActivity.class); + intent.putExtra(GpodnetTagActivity.ARG_TAGNAME, selectedTag); + startActivity(intent); + } + }); + + loadData(); + } + + private void loadData() { + AsyncTask<Void, Void, List<GpodnetTag>> task = new AsyncTask<Void, Void, List<GpodnetTag>>() { + private Exception exception; + + @Override + protected List<GpodnetTag> doInBackground(Void... params) { + GpodnetService service = new GpodnetService(); + try { + return service.getTopTags(COUNT); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + exception = e; + return null; + } finally { + service.shutdown(); + } + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + setListShown(false); + } + + @Override + protected void onPostExecute(List<GpodnetTag> gpodnetTags) { + super.onPostExecute(gpodnetTags); + final Context context = getActivity(); + if (context != null) { + if (gpodnetTags != null) { + List<String> tagNames = new ArrayList<String>(); + for (GpodnetTag tag : gpodnetTags) { + tagNames.add(tag.getName()); + } + setListAdapter(new ArrayAdapter<String>(context, R.layout.simple_list_item_1, tagNames)); + setListShown(true); + } else if (exception != null) { + TextView txtvError = new TextView(getActivity()); + txtvError.setText(exception.getMessage()); + getListView().setEmptyView(txtvError); + } else { + setListShown(true); + } + } + } + }; + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + task.execute(); + } + } +} + diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java new file mode 100644 index 000000000..845a23823 --- /dev/null +++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetClient.java @@ -0,0 +1,35 @@ +package de.danoeh.antennapod.gpoddernet; + +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; + +/** + * HTTP client for the gpodder.net service. + */ +public class GpodnetClient extends DefaultHttpClient { + + private static SchemeRegistry prepareSchemeRegistry() { + SchemeRegistry sr = new SchemeRegistry(); + + Scheme http = new Scheme("http", + PlainSocketFactory.getSocketFactory(), 80); + sr.register(http); + Scheme https = new Scheme("https", + SSLSocketFactory.getSocketFactory(), 443); + sr.register(https); + + return sr; + } + + @Override + protected ClientConnectionManager createClientConnectionManager() { + return new ThreadSafeClientConnManager(new BasicHttpParams(), prepareSchemeRegistry()); + } + +} diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java new file mode 100644 index 000000000..7e0a34e0b --- /dev/null +++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetService.java @@ -0,0 +1,725 @@ +package de.danoeh.antennapod.gpoddernet; + +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.gpoddernet.model.*; +import org.apache.http.Header; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.auth.AuthenticationException; +import org.apache.http.auth.UsernamePasswordCredentials; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpRequestBase; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.auth.BasicScheme; +import org.apache.http.params.CoreProtocolPNames; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +/** + * Communicates with the gpodder.net service. + */ +public class GpodnetService { + + private static final String BASE_SCHEME = "https"; + private static final String BASE_HOST = "gpodder.net"; + + private GpodnetClient httpClient; + + public GpodnetService() { + httpClient = new GpodnetClient(); + httpClient.getParams().setParameter(CoreProtocolPNames.USER_AGENT, AppConfig.USER_AGENT); + } + + /** + * Returns the [count] most used tags. + */ + public List<GpodnetTag> getTopTags(int count) + throws GpodnetServiceException { + URI uri; + try { + uri = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/api/2/tags/%d.json", count), null); + } catch (URISyntaxException e1) { + e1.printStackTrace(); + throw new IllegalStateException(e1); + } + + HttpGet request = new HttpGet(uri); + String response = executeRequest(request); + try { + JSONArray jsonTagList = new JSONArray(response); + List<GpodnetTag> tagList = new ArrayList<GpodnetTag>( + jsonTagList.length()); + for (int i = 0; i < jsonTagList.length(); i++) { + JSONObject jObj = jsonTagList.getJSONObject(i); + String name = jObj.getString("tag"); + int usage = jObj.getInt("usage"); + tagList.add(new GpodnetTag(name, usage)); + } + return tagList; + } catch (JSONException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Returns the [count] most subscribed podcasts for the given tag. + * + * @throws IllegalArgumentException if tag is null + */ + public List<GpodnetPodcast> getPodcastsForTag(GpodnetTag tag, int count) + throws GpodnetServiceException { + if (tag == null) { + throw new IllegalArgumentException( + "Tag and title of tag must not be null"); + } + try { + URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/api/2/tag/%s/%d.json", tag.getName(), count), null); + HttpGet request = new HttpGet(uri); + String response = executeRequest(request); + + JSONArray jsonArray = new JSONArray(response); + return readPodcastListFromJSONArray(jsonArray); + + } catch (JSONException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + + } + } + + /** + * Returns the toplist of podcast. + * + * @param count of elements that should be returned. Must be in range 1..100. + * @throws IllegalArgumentException if count is out of range. + */ + public List<GpodnetPodcast> getPodcastToplist(int count) + throws GpodnetServiceException { + if (count < 1 || count > 100) { + throw new IllegalArgumentException("Count must be in range 1..100"); + } + try { + URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/toplist/%d.json", count), null); + HttpGet request = new HttpGet(uri); + String response = executeRequest(request); + + JSONArray jsonArray = new JSONArray(response); + return readPodcastListFromJSONArray(jsonArray); + + } catch (JSONException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + + } + } + + /** + * Returns a list of suggested podcasts for the user that is currently + * logged in. + * <p/> + * This method requires authentication. + * + * @param count The + * number of elements that should be returned. Must be in range + * 1..100. + * @throws IllegalArgumentException if count is out of range. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public List<GpodnetPodcast> getSuggestions(int count) throws GpodnetServiceException { + if (count < 1 || count > 100) { + throw new IllegalArgumentException("Count must be in range 1..100"); + } + try { + URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/suggestions/%d.json", count), null); + HttpGet request = new HttpGet(uri); + String response = executeRequest(request); + + JSONArray jsonArray = new JSONArray(response); + return readPodcastListFromJSONArray(jsonArray); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } catch (JSONException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Searches the podcast directory for a given string. + * + * @param query The search query + * @param scaledLogoSize The size of the logos that are returned by the search query. + * Must be in range 1..256. If the value is out of range, the + * default value defined by the gpodder.net API will be used. + */ + public List<GpodnetPodcast> searchPodcasts(String query, int scaledLogoSize) + throws GpodnetServiceException { + String parameters = (scaledLogoSize > 0 && scaledLogoSize <= 256) ? String + .format("q=%s&scale_logo=%d", query, scaledLogoSize) : String + .format("q=%s", query); + try { + URI uri = new URI(BASE_SCHEME, null, BASE_HOST, -1, "/search.json", + parameters, null); + System.out.println(uri.toASCIIString()); + HttpGet request = new HttpGet(uri); + String response = executeRequest(request); + + JSONArray jsonArray = new JSONArray(response); + return readPodcastListFromJSONArray(jsonArray); + + } catch (JSONException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + + } + } + + /** + * Returns all devices of a given user. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @throws IllegalArgumentException If username is null. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public List<GpodnetDevice> getDevices(String username) + throws GpodnetServiceException { + if (username == null) { + throw new IllegalArgumentException("Username must not be null"); + } + try { + URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/api/2/devices/%s.json", username), null); + HttpGet request = new HttpGet(uri); + String response = executeRequest(request); + JSONArray devicesArray = new JSONArray(response); + List<GpodnetDevice> result = readDeviceListFromJSONArray(devicesArray); + + return result; + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } catch (JSONException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Configures the device of a given user. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @param deviceId The ID of the device that should be configured. + * @throws IllegalArgumentException If username or deviceId is null. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public void configureDevice(String username, String deviceId, + String caption, GpodnetDevice.DeviceType type) + throws GpodnetServiceException { + if (username == null || deviceId == null) { + throw new IllegalArgumentException( + "Username and device ID must not be null"); + } + try { + URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/api/2/devices/%s/%s.json", username, deviceId), null); + HttpPost request = new HttpPost(uri); + if (caption != null || type != null) { + JSONObject jsonContent = new JSONObject(); + if (caption != null) { + jsonContent.put("caption", caption); + } + if (type != null) { + jsonContent.put("type", type.toString()); + } + StringEntity strEntity = new StringEntity( + jsonContent.toString(), "UTF-8"); + strEntity.setContentType("application/json"); + request.setEntity(strEntity); + } + executeRequest(request); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalArgumentException(e); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } catch (JSONException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Returns the subscriptions of a specific device. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @param deviceId The ID of the device whose subscriptions should be returned. + * @return A list of subscriptions in OPML format. + * @throws IllegalArgumentException If username or deviceId is null. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public String getSubscriptionsOfDevice(String username, String deviceId) + throws GpodnetServiceException { + if (username == null || deviceId == null) { + throw new IllegalArgumentException( + "Username and device ID must not be null"); + } + try { + URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/subscriptions/%s/%s.opml", username, deviceId), null); + HttpGet request = new HttpGet(uri); + String response = executeRequest(request); + return response; + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalArgumentException(e); + } + } + + /** + * Returns all subscriptions of a specific user. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @return A list of subscriptions in OPML format. + * @throws IllegalArgumentException If username is null. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public String getSubscriptionsOfUser(String username) + throws GpodnetServiceException { + if (username == null) { + throw new IllegalArgumentException("Username must not be null"); + } + try { + URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/subscriptions/%s.opml", username), null); + HttpGet request = new HttpGet(uri); + String response = executeRequest(request); + return response; + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalArgumentException(e); + } + } + + /** + * Uploads the subscriptions of a specific device. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @param deviceId The ID of the device whose subscriptions should be updated. + * @param subscriptions A list of feed URLs containing all subscriptions of the + * device. + * @throws IllegalArgumentException If username, deviceId or subscriptions is null. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public void uploadSubscriptions(String username, String deviceId, + List<String> subscriptions) throws GpodnetServiceException { + if (username == null || deviceId == null || subscriptions == null) { + throw new IllegalArgumentException( + "Username, device ID and subscriptions must not be null"); + } + try { + URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/subscriptions/%s/%s.txt", username, deviceId), null); + HttpPut request = new HttpPut(uri); + StringBuilder builder = new StringBuilder(); + for (String s : subscriptions) { + builder.append(s); + builder.append("\n"); + } + StringEntity entity = new StringEntity(builder.toString(), "UTF-8"); + request.setEntity(entity); + + executeRequest(request); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } + } + + /** + * Updates the subscription list of a specific device. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @param deviceId The ID of the device whose subscriptions should be updated. + * @param added Collection of feed URLs of added feeds. This Collection MUST NOT contain any duplicates + * @param removed Collection of feed URLs of removed feeds. This Collection MUST NOT contain any duplicates + * @return a GpodnetUploadChangesResponse. See {@link de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse} + * for details. + * @throws java.lang.IllegalArgumentException if username, deviceId, added or removed is null. + * @throws de.danoeh.antennapod.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there + * is an authentication error. + */ + public GpodnetUploadChangesResponse uploadChanges(String username, String deviceId, Collection<String> added, + Collection<String> removed) throws GpodnetServiceException { + if (username == null || deviceId == null || added == null || removed == null) { + throw new IllegalArgumentException( + "Username, device ID, added and removed must not be null"); + } + try { + URI uri = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/api/2/subscriptions/%s/%s.json", username, deviceId), null); + + final JSONObject requestObject = new JSONObject(); + requestObject.put("add", new JSONArray(added)); + requestObject.put("remove", new JSONArray(removed)); + + HttpPost request = new HttpPost(uri); + StringEntity entity = new StringEntity(requestObject.toString(), "UTF-8"); + request.setEntity(entity); + + final String response = executeRequest(request); + return GpodnetUploadChangesResponse.fromJSONObject(response); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } catch (JSONException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } + + } + + /** + * Returns all subscription changes of a specific device. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @param deviceId The ID of the device whose subscription changes should be + * downloaded. + * @param timestamp A timestamp that can be used to receive all changes since a + * specific point in time. + * @throws IllegalArgumentException If username or deviceId is null. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public GpodnetSubscriptionChange getSubscriptionChanges(String username, + String deviceId, long timestamp) throws GpodnetServiceException { + if (username == null || deviceId == null) { + throw new IllegalArgumentException( + "Username and device ID must not be null"); + } + String params = String.format("since=%d", timestamp); + String path = String.format("/api/2/subscriptions/%s/%s.json", + username, deviceId); + try { + URI uri = new URI(BASE_SCHEME, null, BASE_HOST, -1, path, params, + null); + HttpGet request = new HttpGet(uri); + + String response = executeRequest(request); + JSONObject changes = new JSONObject(response); + return readSubscriptionChangesFromJSONObject(changes); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } catch (JSONException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + + } + + /** + * Logs in a specific user. This method must be called if any of the methods + * that require authentication is used. + * + * @throws IllegalArgumentException If username or password is null. + */ + public void authenticate(String username, String password) + throws GpodnetServiceException { + if (username == null || password == null) { + throw new IllegalArgumentException( + "Username and password must not be null"); + } + URI uri; + try { + uri = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/api/2/auth/%s/login.json", username), null); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(); + } + HttpPost request = new HttpPost(uri); + executeRequestWithAuthentication(request, username, password); + } + + /** + * Shuts down the GpodnetService's HTTP client. The service will be shut down in a separate thread to avoid + * NetworkOnMainThreadExceptions. + */ + public void shutdown() { + new Thread() { + @Override + public void run() { + httpClient.getConnectionManager().shutdown(); + } + }.start(); + } + + private String executeRequest(HttpRequestBase request) + throws GpodnetServiceException { + if (request == null) { + throw new IllegalArgumentException("request must not be null"); + } + String responseString = null; + HttpResponse response = null; + try { + response = httpClient.execute(request); + checkStatusCode(response); + responseString = getStringFromEntity(response.getEntity()); + } catch (ClientProtocolException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } catch (IOException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } finally { + if (response != null) { + try { + response.getEntity().consumeContent(); + } catch (IOException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + } + return responseString; + } + + private String executeRequestWithAuthentication(HttpRequestBase request, + String username, String password) throws GpodnetServiceException { + if (request == null || username == null || password == null) { + throw new IllegalArgumentException( + "request and credentials must not be null"); + } + String result = null; + HttpResponse response = null; + try { + Header auth = new BasicScheme().authenticate( + new UsernamePasswordCredentials(username, password), + request); + request.addHeader(auth); + response = httpClient.execute(request); + checkStatusCode(response); + result = getStringFromEntity(response.getEntity()); + } catch (ClientProtocolException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } catch (IOException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } catch (AuthenticationException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } finally { + if (response != null) { + try { + response.getEntity().consumeContent(); + } catch (IOException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + } + return result; + } + + private String getStringFromEntity(HttpEntity entity) + throws GpodnetServiceException { + if (entity == null) { + throw new IllegalArgumentException("entity must not be null"); + } + ByteArrayOutputStream outputStream; + int contentLength = (int) entity.getContentLength(); + if (contentLength > 0) { + outputStream = new ByteArrayOutputStream(contentLength); + } else { + outputStream = new ByteArrayOutputStream(); + } + try { + byte[] buffer = new byte[8 * 1024]; + InputStream in = entity.getContent(); + int count; + while ((count = in.read(buffer)) > 0) { + outputStream.write(buffer, 0, count); + } + } catch (IOException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + // System.out.println(outputStream.toString()); + return outputStream.toString(); + } + + private void checkStatusCode(HttpResponse response) + throws GpodnetServiceException { + if (response == null) { + throw new IllegalArgumentException("response must not be null"); + } + int responseCode = response.getStatusLine().getStatusCode(); + if (responseCode != HttpStatus.SC_OK) { + if (responseCode == HttpStatus.SC_UNAUTHORIZED) { + throw new GpodnetServiceAuthenticationException("Wrong username or password"); + } else { + throw new GpodnetServiceBadStatusCodeException( + "Bad response code: " + responseCode, responseCode); + } + } + } + + private List<GpodnetPodcast> readPodcastListFromJSONArray(JSONArray array) + throws JSONException { + if (array == null) { + throw new IllegalArgumentException("array must not be null"); + } + List<GpodnetPodcast> result = new ArrayList<GpodnetPodcast>( + array.length()); + for (int i = 0; i < array.length(); i++) { + result.add(readPodcastFromJSONObject(array.getJSONObject(i))); + } + return result; + + } + + private GpodnetPodcast readPodcastFromJSONObject(JSONObject object) + throws JSONException { + String url = object.getString("url"); + + String title; + Object titleObj = object.opt("title"); + if (titleObj != null && titleObj instanceof String) { + title = (String) titleObj; + } else { + title = url; + } + + String description; + Object descriptionObj = object.opt("description"); + if (descriptionObj != null && descriptionObj instanceof String) { + description = (String) descriptionObj; + } else { + description = ""; + } + + int subscribers = object.getInt("subscribers"); + + Object logoUrlObj = object.opt("logo_url"); + String logoUrl = (logoUrlObj instanceof String) ? (String) logoUrlObj + : null; + if (logoUrl == null) { + Object scaledLogoUrl = object.opt("scaled_logo_url"); + if (scaledLogoUrl != null && scaledLogoUrl instanceof String) { + logoUrl = (String) scaledLogoUrl; + } + } + + String website = null; + Object websiteObj = object.opt("website"); + if (websiteObj != null && websiteObj instanceof String) { + website = (String) websiteObj; + } + String mygpoLink = object.getString("mygpo_link"); + return new GpodnetPodcast(url, title, description, subscribers, + logoUrl, website, mygpoLink); + } + + private List<GpodnetDevice> readDeviceListFromJSONArray(JSONArray array) + throws JSONException { + if (array == null) { + throw new IllegalArgumentException("array must not be null"); + } + List<GpodnetDevice> result = new ArrayList<GpodnetDevice>( + array.length()); + for (int i = 0; i < array.length(); i++) { + result.add(readDeviceFromJSONObject(array.getJSONObject(i))); + } + return result; + } + + private GpodnetDevice readDeviceFromJSONObject(JSONObject object) + throws JSONException { + String id = object.getString("id"); + String caption = object.getString("caption"); + String type = object.getString("type"); + int subscriptions = object.getInt("subscriptions"); + return new GpodnetDevice(id, caption, type, subscriptions); + } + + private GpodnetSubscriptionChange readSubscriptionChangesFromJSONObject( + JSONObject object) throws JSONException { + if (object == null) { + throw new IllegalArgumentException("object must not be null"); + } + List<String> added = new LinkedList<String>(); + JSONArray jsonAdded = object.getJSONArray("add"); + for (int i = 0; i < jsonAdded.length(); i++) { + added.add(jsonAdded.getString(i)); + } + + List<String> removed = new LinkedList<String>(); + JSONArray jsonRemoved = object.getJSONArray("remove"); + for (int i = 0; i < jsonRemoved.length(); i++) { + removed.add(jsonRemoved.getString(i)); + } + + long timestamp = object.getLong("timestamp"); + return new GpodnetSubscriptionChange(added, removed, timestamp); + } +} diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java new file mode 100644 index 000000000..3b0140826 --- /dev/null +++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceAuthenticationException.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.gpoddernet; + +public class GpodnetServiceAuthenticationException extends GpodnetServiceException { + + public GpodnetServiceAuthenticationException() { + super(); + } + + public GpodnetServiceAuthenticationException(String message, Throwable cause) { + super(message, cause); + } + + public GpodnetServiceAuthenticationException(String message) { + super(message); + } + + public GpodnetServiceAuthenticationException(Throwable cause) { + super(cause); + } + +} diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java new file mode 100644 index 000000000..a32e9357b --- /dev/null +++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceBadStatusCodeException.java @@ -0,0 +1,12 @@ +package de.danoeh.antennapod.gpoddernet; + +public class GpodnetServiceBadStatusCodeException extends GpodnetServiceException { + int statusCode; + + public GpodnetServiceBadStatusCodeException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + +} diff --git a/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java new file mode 100644 index 000000000..bdb394454 --- /dev/null +++ b/src/de/danoeh/antennapod/gpoddernet/GpodnetServiceException.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.gpoddernet; + +public class GpodnetServiceException extends Exception { + + public GpodnetServiceException() { + } + + public GpodnetServiceException(String message) { + super(message); + } + + public GpodnetServiceException(Throwable cause) { + super(cause); + } + + public GpodnetServiceException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java new file mode 100644 index 000000000..ae7199fcc --- /dev/null +++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetDevice.java @@ -0,0 +1,72 @@ +package de.danoeh.antennapod.gpoddernet.model; + +public class GpodnetDevice { + + private String id; + private String caption; + private DeviceType type; + private int subscriptions; + + public GpodnetDevice(String id, String caption, String type, + int subscriptions) { + if (id == null) { + throw new IllegalArgumentException("ID must not be null"); + } + + this.id = id; + this.caption = caption; + this.type = DeviceType.fromString(type); + this.subscriptions = subscriptions; + } + + @Override + public String toString() { + return "GpodnetDevice [id=" + id + ", caption=" + caption + ", type=" + + type + ", subscriptions=" + subscriptions + "]"; + } + + public static enum DeviceType { + DESKTOP, LAPTOP, MOBILE, SERVER, OTHER; + + static DeviceType fromString(String s) { + if (s == null) { + return OTHER; + } + + if (s.equals("desktop")) { + return DESKTOP; + } else if (s.equals("laptop")) { + return LAPTOP; + } else if (s.equals("mobile")) { + return MOBILE; + } else if (s.equals("server")) { + return SERVER; + } else { + return OTHER; + } + } + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + } + + public String getId() { + return id; + } + + public String getCaption() { + return caption; + } + + public DeviceType getType() { + return type; + } + + public int getSubscriptions() { + return subscriptions; + } + +} diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java new file mode 100644 index 000000000..aa01b66e2 --- /dev/null +++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetPodcast.java @@ -0,0 +1,64 @@ +package de.danoeh.antennapod.gpoddernet.model; + +public class GpodnetPodcast { + private String url; + private String title; + private String description; + private int subscribers; + private String logoUrl; + private String website; + private String mygpoLink; + + public GpodnetPodcast(String url, String title, String description, + int subscribers, String logoUrl, String website, String mygpoLink) { + if (url == null || title == null || description == null) { + throw new IllegalArgumentException( + "URL, title and description must not be null"); + } + + this.url = url; + this.title = title; + this.description = description; + this.subscribers = subscribers; + this.logoUrl = logoUrl; + this.website = website; + this.mygpoLink = mygpoLink; + } + + @Override + public String toString() { + return "GpodnetPodcast [url=" + url + ", title=" + title + + ", description=" + description + ", subscribers=" + + subscribers + ", logoUrl=" + logoUrl + ", website=" + website + + ", mygpoLink=" + mygpoLink + "]"; + } + + public String getUrl() { + return url; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public int getSubscribers() { + return subscribers; + } + + public String getLogoUrl() { + return logoUrl; + } + + public String getWebsite() { + return website; + } + + public String getMygpoLink() { + return mygpoLink; + } + +} diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java new file mode 100644 index 000000000..dccb53e5d --- /dev/null +++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetSubscriptionChange.java @@ -0,0 +1,40 @@ +package de.danoeh.antennapod.gpoddernet.model; + +import java.util.List; + +public class GpodnetSubscriptionChange { + private List<String> added; + private List<String> removed; + private long timestamp; + + public GpodnetSubscriptionChange(List<String> added, List<String> removed, + long timestamp) { + if (added == null || removed == null) { + throw new IllegalArgumentException( + "added and remove must not be null"); + } + this.added = added; + this.removed = removed; + this.timestamp = timestamp; + } + + @Override + public String toString() { + return "GpodnetSubscriptionChange [added=" + added.toString() + + ", removed=" + removed.toString() + ", timestamp=" + + timestamp + "]"; + } + + public List<String> getAdded() { + return added; + } + + public List<String> getRemoved() { + return removed; + } + + public long getTimestamp() { + return timestamp; + } + +} diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java new file mode 100644 index 000000000..e8a36a554 --- /dev/null +++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetTag.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.gpoddernet.model; + +import java.util.Comparator; + +public class GpodnetTag { + + private String name; + private int usage; + + public GpodnetTag(String name, int usage) { + if (name == null) { + throw new IllegalArgumentException("Name must not be null"); + } + + this.name = name; + this.usage = usage; + } + + public GpodnetTag(String name) { + super(); + this.name = name; + } + + @Override + public String toString() { + return "GpodnetTag [name=" + name + ", usage=" + usage + "]"; + } + + public String getName() { + return name; + } + + public int getUsage() { + return usage; + } + + public static class UsageComparator implements Comparator<GpodnetTag> { + + @Override + public int compare(GpodnetTag o1, GpodnetTag o2) { + return o1.usage - o2.usage; + } + + } + +} diff --git a/src/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java new file mode 100644 index 000000000..fee8c7d28 --- /dev/null +++ b/src/de/danoeh/antennapod/gpoddernet/model/GpodnetUploadChangesResponse.java @@ -0,0 +1,56 @@ +package de.danoeh.antennapod.gpoddernet.model; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * Object returned by {@link de.danoeh.antennapod.gpoddernet.GpodnetService} in uploadChanges method. + */ +public class GpodnetUploadChangesResponse { + + /** + * timestamp/ID that can be used for requesting changes since this upload. + */ + public final long timestamp; + + /** + * URLs that should be updated. The key of the map is the original URL, the value of the map + * is the sanitized URL. + */ + public final Map<String, String> updatedUrls; + + public GpodnetUploadChangesResponse(long timestamp, Map<String, String> updatedUrls) { + this.timestamp = timestamp; + this.updatedUrls = updatedUrls; + } + + /** + * Creates a new GpodnetUploadChangesResponse-object from a JSON object that was + * returned by an uploadChanges call. + * + * @throws org.json.JSONException If the method could not parse the JSONObject. + */ + public static GpodnetUploadChangesResponse fromJSONObject(String objectString) throws JSONException { + final JSONObject object = new JSONObject(objectString); + final long timestamp = object.getLong("timestamp"); + Map<String, String> updatedUrls = new HashMap<String, String>(); + JSONArray urls = object.getJSONArray("update_urls"); + for (int i = 0; i < urls.length(); i++) { + JSONArray urlPair = urls.getJSONArray(i); + updatedUrls.put(urlPair.getString(0), urlPair.getString(1)); + } + return new GpodnetUploadChangesResponse(timestamp, updatedUrls); + } + + @Override + public String toString() { + return "GpodnetUploadChangesResponse{" + + "timestamp=" + timestamp + + ", updatedUrls=" + updatedUrls + + '}'; + } +} diff --git a/src/de/danoeh/antennapod/preferences/GpodnetPreferences.java b/src/de/danoeh/antennapod/preferences/GpodnetPreferences.java new file mode 100644 index 000000000..44b0f3cc3 --- /dev/null +++ b/src/de/danoeh/antennapod/preferences/GpodnetPreferences.java @@ -0,0 +1,217 @@ +package de.danoeh.antennapod.preferences; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.PodcastApp; +import de.danoeh.antennapod.service.GpodnetSyncService; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Manages preferences for accessing gpodder.net service + */ +public class GpodnetPreferences { + + private static final String TAG = "GpodnetPreferences"; + + private static final String PREF_NAME = "gpodder.net"; + public static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username"; + public static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password"; + public static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID"; + + public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp"; + public static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added"; + public static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed"; + + private static String username; + private static String password; + private static String deviceID; + + private static ReentrantLock feedListLock = new ReentrantLock(); + private static Set<String> addedFeeds; + private static Set<String> removedFeeds; + + /** + * Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges. + */ + private static long lastSyncTimestamp; + + private static boolean preferencesLoaded = false; + + private static SharedPreferences getPreferences() { + return PodcastApp.getInstance().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } + + private static synchronized void ensurePreferencesLoaded() { + if (!preferencesLoaded) { + SharedPreferences prefs = getPreferences(); + username = prefs.getString(PREF_GPODNET_USERNAME, null); + password = prefs.getString(PREF_GPODNET_PASSWORD, null); + deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null); + lastSyncTimestamp = prefs.getLong(PREF_LAST_SYNC_TIMESTAMP, 0); + addedFeeds = readListFromString(prefs.getString(PREF_SYNC_ADDED, "")); + removedFeeds = readListFromString(prefs.getString(PREF_SYNC_REMOVED, "")); + + preferencesLoaded = true; + } + } + + private static void writePreference(String key, String value) { + SharedPreferences.Editor editor = getPreferences().edit(); + editor.putString(key, value); + editor.commit(); + } + + private static void writePreference(String key, long value) { + SharedPreferences.Editor editor = getPreferences().edit(); + editor.putLong(key, value); + editor.commit(); + } + + private static void writePreference(String key, Collection<String> value) { + SharedPreferences.Editor editor = getPreferences().edit(); + editor.putString(key, writeListToString(value)); + editor.commit(); + } + + public static String getUsername() { + ensurePreferencesLoaded(); + return username; + } + + public static void setUsername(String username) { + GpodnetPreferences.username = username; + writePreference(PREF_GPODNET_USERNAME, username); + } + + public static String getPassword() { + ensurePreferencesLoaded(); + return password; + } + + public static void setPassword(String password) { + GpodnetPreferences.password = password; + writePreference(PREF_GPODNET_PASSWORD, password); + } + + public static String getDeviceID() { + ensurePreferencesLoaded(); + return deviceID; + } + + public static void setDeviceID(String deviceID) { + GpodnetPreferences.deviceID = deviceID; + writePreference(PREF_GPODNET_DEVICEID, deviceID); + } + + public static long getLastSyncTimestamp() { + ensurePreferencesLoaded(); + return lastSyncTimestamp; + } + + public static void setLastSyncTimestamp(long lastSyncTimestamp) { + GpodnetPreferences.lastSyncTimestamp = lastSyncTimestamp; + writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp); + } + + public static void addAddedFeed(String feed) { + ensurePreferencesLoaded(); + feedListLock.lock(); + if (addedFeeds.add(feed)) { + writePreference(PREF_SYNC_ADDED, addedFeeds); + } + if (removedFeeds.remove(feed)) { + writePreference(PREF_SYNC_REMOVED, removedFeeds); + } + feedListLock.unlock(); + GpodnetSyncService.sendSyncIntent(PodcastApp.getInstance()); + } + + public static void addRemovedFeed(String feed) { + ensurePreferencesLoaded(); + feedListLock.lock(); + if (removedFeeds.add(feed)) { + writePreference(PREF_SYNC_REMOVED, removedFeeds); + } + if (addedFeeds.remove(feed)) { + writePreference(PREF_SYNC_ADDED, addedFeeds); + } + feedListLock.unlock(); + GpodnetSyncService.sendSyncIntent(PodcastApp.getInstance()); + } + + public static Set<String> getAddedFeedsCopy() { + ensurePreferencesLoaded(); + Set<String> copy = new HashSet<String>(); + feedListLock.lock(); + copy.addAll(addedFeeds); + feedListLock.unlock(); + return copy; + } + + public static void removeAddedFeeds(Collection<String> removed) { + ensurePreferencesLoaded(); + feedListLock.lock(); + addedFeeds.removeAll(removed); + writePreference(PREF_SYNC_ADDED, addedFeeds); + feedListLock.unlock(); + } + + public static Set<String> getRemovedFeedsCopy() { + ensurePreferencesLoaded(); + Set<String> copy = new HashSet<String>(); + feedListLock.lock(); + copy.addAll(removedFeeds); + feedListLock.unlock(); + return copy; + } + + public static void removeRemovedFeeds(Collection<String> removed) { + ensurePreferencesLoaded(); + removedFeeds.removeAll(removed); + writePreference(PREF_SYNC_REMOVED, removedFeeds); + + } + + /** + * Returns true if device ID, username and password have a non-null value + */ + public static boolean loggedIn() { + ensurePreferencesLoaded(); + return deviceID != null && username != null && password != null; + } + + public static synchronized void logout() { + if (AppConfig.DEBUG) Log.d(TAG, "Logout: Clearing preferences"); + setUsername(null); + setPassword(null); + setDeviceID(null); + addedFeeds.clear(); + writePreference(PREF_SYNC_ADDED, addedFeeds); + removedFeeds.clear(); + writePreference(PREF_SYNC_REMOVED, removedFeeds); + setLastSyncTimestamp(0); + } + + private static Set<String> readListFromString(String s) { + Set<String> result = new HashSet<String>(); + for (String item : s.split(" ")) { + result.add(item); + } + return result; + } + + private static String writeListToString(Collection<String> c) { + StringBuilder result = new StringBuilder(); + for (String item : c) { + result.append(item); + result.append(" "); + } + return result.toString().trim(); + } +} diff --git a/src/de/danoeh/antennapod/preferences/UserPreferences.java b/src/de/danoeh/antennapod/preferences/UserPreferences.java index 8ee03527a..0d07a7178 100644 --- a/src/de/danoeh/antennapod/preferences/UserPreferences.java +++ b/src/de/danoeh/antennapod/preferences/UserPreferences.java @@ -211,7 +211,8 @@ public class UserPreferences implements public static boolean isDisplayOnlyEpisodes() { instanceAvailable(); - return instance.displayOnlyEpisodes; + //return instance.displayOnlyEpisodes; + return false; } public static boolean isAutoDelete() { diff --git a/src/de/danoeh/antennapod/service/GpodnetSyncService.java b/src/de/danoeh/antennapod/service/GpodnetSyncService.java new file mode 100644 index 000000000..71e128b55 --- /dev/null +++ b/src/de/danoeh/antennapod/service/GpodnetSyncService.java @@ -0,0 +1,243 @@ +package de.danoeh.antennapod.service; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.gpoddernet.GpodnetService; +import de.danoeh.antennapod.gpoddernet.GpodnetServiceAuthenticationException; +import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.gpoddernet.model.GpodnetSubscriptionChange; +import de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse; +import de.danoeh.antennapod.preferences.GpodnetPreferences; +import de.danoeh.antennapod.storage.*; +import de.danoeh.antennapod.util.NetworkUtils; + +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +/** + * Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument. + * This class also provides static methods for starting the GpodnetSyncService. + */ +public class GpodnetSyncService extends Service { + private static final String TAG = "GpodnetSyncService"; + + private static final long WAIT_INTERVAL = 5000L; + + public static final String ARG_ACTION = "action"; + + public static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync"; + + private GpodnetService service; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null; + if (action != null && action.equals(ACTION_SYNC)) { + Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL)); + syncWaiterThread.restart(); + } else { + Log.e(TAG, "Received invalid intent: action argument is null or invalid"); + } + return START_FLAG_REDELIVERY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (AppConfig.DEBUG) Log.d(TAG, "onDestroy"); + syncWaiterThread.interrupt(); + + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private synchronized GpodnetService tryLogin() throws GpodnetServiceException { + if (service == null) { + service = new GpodnetService(); + service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); + } + return service; + } + + private synchronized void syncChanges() { + if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(this)) { + final long timestamp = GpodnetPreferences.getLastSyncTimestamp(); + try { + final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this); + GpodnetService service = tryLogin(); + + if (timestamp == 0) { + // first sync: download all subscriptions... + GpodnetSubscriptionChange changes = + service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), 0); + if (AppConfig.DEBUG) Log.d(TAG, "Downloaded subscription changes: " + changes); + processSubscriptionChanges(localSubscriptions, changes); + + // ... then upload all local subscriptions + if (AppConfig.DEBUG) Log.d(TAG, "Uploading subscription list: " + localSubscriptions); + GpodnetUploadChangesResponse uploadChangesResponse = + service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), localSubscriptions, new LinkedList<String>()); + if (AppConfig.DEBUG) Log.d(TAG, "Uploading changes response: " + uploadChangesResponse); + DBWriter.updateFeedDownloadURLs(GpodnetSyncService.this, uploadChangesResponse.updatedUrls).get(); + GpodnetPreferences.removeAddedFeeds(localSubscriptions); + GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy()); + GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp); + } else { + Set<String> added = GpodnetPreferences.getAddedFeedsCopy(); + Set<String> removed = GpodnetPreferences.getRemovedFeedsCopy(); + + // download remote changes first... + GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), timestamp); + if (AppConfig.DEBUG) Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); + processSubscriptionChanges(localSubscriptions, subscriptionChanges); + + // ... then upload changes local changes + if (AppConfig.DEBUG) Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s", + added.toString(), removed)); + GpodnetUploadChangesResponse uploadChangesResponse = service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), added, removed); + if (AppConfig.DEBUG) Log.d(TAG, "Upload subscriptions response: " + uploadChangesResponse); + + GpodnetPreferences.removeAddedFeeds(added); + GpodnetPreferences.removeRemovedFeeds(removed); + DBWriter.updateFeedDownloadURLs(GpodnetSyncService.this, uploadChangesResponse.updatedUrls).get(); + GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp); + } + clearErrorNotifications(); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + updateErrorNotification(e); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + stopSelf(); + } + + private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException { + for (String downloadUrl : changes.getAdded()) { + if (!localSubscriptions.contains(downloadUrl)) { + Feed feed = new Feed(downloadUrl, new Date()); + DownloadRequester.getInstance().downloadFeed(this, feed); + } + } + for (String downloadUrl : changes.getRemoved()) { + DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl); + } + } + + private void clearErrorNotifications() { + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(R.id.notification_gpodnet_sync_error); + nm.cancel(R.id.notification_gpodnet_sync_autherror); + } + + private void updateErrorNotification(GpodnetServiceException exception) { + if (AppConfig.DEBUG) Log.d(TAG, "Posting error notification"); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + final String title; + final String description; + final int id; + if (exception instanceof GpodnetServiceAuthenticationException) { + title = getString(R.string.gpodnetsync_auth_error_title); + description = getString(R.string.gpodnetsync_auth_error_descr); + id = R.id.notification_gpodnet_sync_autherror; + } else { + title = getString(R.string.gpodnetsync_error_title); + description = getString(R.string.gpodnetsync_error_descr) + exception.getMessage(); + id = R.id.notification_gpodnet_sync_error; + } + Notification notification = builder.setContentTitle(title) + .setContentText(description) + .setSmallIcon(R.drawable.stat_notify_sync_error) + .setAutoCancel(true) + .build(); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(id, notification); + } + + private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) { + @Override + public void onWaitCompleted() { + syncChanges(); + } + }; + + private abstract class WaiterThread { + private long waitInterval; + private Thread thread; + + private WaiterThread(long waitInterval) { + this.waitInterval = waitInterval; + reinit(); + } + + public abstract void onWaitCompleted(); + + public void exec() { + if (!thread.isAlive()) { + thread.start(); + } + } + + private void reinit() { + if (thread != null && thread.isAlive()) { + Log.d(TAG, "Interrupting waiter thread"); + thread.interrupt(); + } + thread = new Thread() { + @Override + public void run() { + try { + Thread.sleep(waitInterval); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (!isInterrupted()) { + synchronized (this) { + onWaitCompleted(); + } + } + } + }; + } + + public void restart() { + reinit(); + exec(); + } + + public void interrupt() { + if (thread != null && thread.isAlive()) { + thread.interrupt(); + } + } + } + + public static void sendSyncIntent(Context context) { + if (GpodnetPreferences.loggedIn()) { + Intent intent = new Intent(context, GpodnetSyncService.class); + intent.putExtra(ARG_ACTION, ACTION_SYNC); + context.startService(intent); + } + } +} diff --git a/src/de/danoeh/antennapod/storage/DBReader.java b/src/de/danoeh/antennapod/storage/DBReader.java index 28ab3d939..a5a4c8cd4 100644 --- a/src/de/danoeh/antennapod/storage/DBReader.java +++ b/src/de/danoeh/antennapod/storage/DBReader.java @@ -76,6 +76,27 @@ public final class DBReader { } /** + * Returns a list with the download URLs of all feeds. + * @param context A context that is used for opening the database connection. + * @return A list of Strings with the download URLs of all feeds. + * */ + public static List<String> getFeedListDownloadUrls(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + List<String> result = new ArrayList<String>(); + adapter.open(); + Cursor feeds = adapter.getFeedCursorDownloadUrls(); + if (feeds.moveToFirst()) { + do { + result.add(feeds.getString(1)); + } while (feeds.moveToNext()); + } + feeds.close(); + adapter.close(); + + return result; + } + + /** * Returns a list of 'expired Feeds', i.e. Feeds that have not been updated for a certain amount of time. * * @param context A context that is used for opening a database connection. diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java index 741699bdf..b9a1fd002 100644 --- a/src/de/danoeh/antennapod/storage/DBTasks.java +++ b/src/de/danoeh/antennapod/storage/DBTasks.java @@ -23,11 +23,13 @@ import de.danoeh.antennapod.feed.FeedImage; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.service.GpodnetSyncService; import de.danoeh.antennapod.service.PlaybackService; import de.danoeh.antennapod.service.download.DownloadStatus; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.NetworkUtils; import de.danoeh.antennapod.util.QueueAccess; +import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.util.exception.MediaFileNotFoundException; /** @@ -40,6 +42,39 @@ public final class DBTasks { } /** + * Removes the feed with the given download url. This method should NOT be executed on the GUI thread. + * @param context Used for accessing the db + * @param downloadUrl URL of the feed. + * */ + public static void removeFeedWithDownloadUrl(Context context, String downloadUrl) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getFeedCursorDownloadUrls(); + long feedID = 0; + if (cursor.moveToFirst()) { + do { + if (cursor.getString(1).equals(downloadUrl)) { + feedID = cursor.getLong(0); + } + } while (cursor.moveToNext()); + } + cursor.close(); + adapter.close(); + + if (feedID != 0) { + try { + DBWriter.deleteFeed(context, feedID).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } else { + Log.w(TAG, "removeFeedWithDownloadUrl: Could not find feed with url: " + downloadUrl); + } + } + + /** * Starts playback of a FeedMedia object's file. This method will build an Intent based on the given parameters to * start the {@link PlaybackService}. * @@ -110,6 +145,8 @@ public final class DBTasks { refreshFeeds(context, DBReader.getFeedList(context)); } isRefreshing.set(false); + + GpodnetSyncService.sendSyncIntent(context); } }.start(); } else { @@ -568,6 +605,7 @@ public final class DBTasks { Log.d(TAG, "Feed with title " + newFeed.getTitle() + " already exists. Syncing new with existing one."); + Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator()); savedFeed.setItems(DBReader.getFeedItemList(context, savedFeed)); if (savedFeed.compareWithOther(newFeed)) { if (AppConfig.DEBUG) @@ -585,7 +623,7 @@ public final class DBTasks { final int i = idx; item.setFeed(savedFeed); savedFeed.getItems().add(i, item); - DBWriter.markItemRead(context, item.getId(), false); + item.setRead(false); } else { oldItem.updateFromOther(item); } diff --git a/src/de/danoeh/antennapod/storage/DBWriter.java b/src/de/danoeh/antennapod/storage/DBWriter.java index 67b25b9ba..f4ba8c237 100644 --- a/src/de/danoeh/antennapod/storage/DBWriter.java +++ b/src/de/danoeh/antennapod/storage/DBWriter.java @@ -4,6 +4,7 @@ import java.io.File; import java.util.Date; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -17,7 +18,9 @@ import android.preference.PreferenceManager; import android.util.Log; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.feed.*; +import de.danoeh.antennapod.preferences.GpodnetPreferences; import de.danoeh.antennapod.preferences.PlaybackPreferences; +import de.danoeh.antennapod.service.GpodnetSyncService; import de.danoeh.antennapod.service.PlaybackService; import de.danoeh.antennapod.service.download.DownloadStatus; import de.danoeh.antennapod.util.QueueAccess; @@ -101,6 +104,8 @@ public class DBWriter { } if (AppConfig.DEBUG) Log.d(TAG, "Deleting File. Result: " + result); + EventDistributor.getInstance().sendQueueUpdateBroadcast(); + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); } } }); @@ -171,6 +176,8 @@ public class DBWriter { } adapter.removeFeed(feed); adapter.close(); + + GpodnetPreferences.addRemovedFeed(feed.getDownload_url()); EventDistributor.getInstance().sendFeedUpdateBroadcast(); } } @@ -685,6 +692,7 @@ public class DBWriter { adapter.setCompleteFeed(feed); adapter.close(); + GpodnetPreferences.addAddedFeed(feed.getDownload_url()); EventDistributor.getInstance().sendFeedUpdateBroadcast(); } }); @@ -787,6 +795,26 @@ public class DBWriter { }); } + /** + * Updates download URLs of feeds from a given Map. The key of the Map is the original URL of the feed + * and the value is the updated URL + * */ + public static Future<?> updateFeedDownloadURLs(final Context context, final Map<String, String> urls) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + for (String key : urls.keySet()) { + if (AppConfig.DEBUG) Log.d(TAG, "Replacing URL " + key + " with url " + urls.get(key)); + + adapter.setFeedDownloadUrl(key, urls.get(key)); + } + adapter.close(); + } + }); + } + private static boolean itemListContains(List<FeedItem> items, long itemId) { for (FeedItem item : items) { if (item.getId() == itemId) { diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java index bb743e2b1..6d41f6dfd 100644 --- a/src/de/danoeh/antennapod/storage/PodDBAdapter.java +++ b/src/de/danoeh/antennapod/storage/PodDBAdapter.java @@ -425,6 +425,15 @@ public class PodDBAdapter { db.endTransaction(); } + /** + * Updates the download URL of a Feed. + */ + public void setFeedDownloadUrl(String original, String updated) { + ContentValues values = new ContentValues(); + values.put(KEY_DOWNLOAD_URL, updated); + db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original}); + } + public void setFeedItemlist(List<FeedItem> items) { db.beginTransaction(); for (FeedItem item : items) { @@ -659,6 +668,10 @@ public class PodDBAdapter { return c; } + public final Cursor getFeedCursorDownloadUrls() { + return db.query(TABLE_NAME_FEEDS, new String[]{KEY_ID, KEY_DOWNLOAD_URL}, null, null, null, null, null); + } + public final Cursor getExpiredFeedsCursor(long expirationTime) { Cursor c = db.query(TABLE_NAME_FEEDS, null, "?<?", new String[]{ KEY_LASTUPDATE, String.valueOf(System.currentTimeMillis() - expirationTime)}, null, null, @@ -1010,7 +1023,7 @@ public class PodDBAdapter { " MAX(pubDate) AS latest_episode," + " COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," + " COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " + - " FROM FeedItems INNER JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" + + " FROM FeedItems LEFT JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" + " INNER JOIN Feeds ON Feeds.id = feed ORDER BY Feeds.title;"; public Cursor getFeedStatisticsCursor() { diff --git a/src/de/danoeh/antennapod/util/NetworkUtils.java b/src/de/danoeh/antennapod/util/NetworkUtils.java index de7b854cc..278f7ad7a 100644 --- a/src/de/danoeh/antennapod/util/NetworkUtils.java +++ b/src/de/danoeh/antennapod/util/NetworkUtils.java @@ -60,4 +60,10 @@ public class NetworkUtils { Log.d(TAG, "Network for auto-dl is not available"); return false; } + + public static boolean networkAvailable(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + return info != null && info.isConnected(); + } } diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java index 7856422ba..e99a733dc 100644 --- a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java +++ b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java @@ -132,7 +132,7 @@ public class FeedItemMenuHandler { false); break; case R.id.remove_item: - DBWriter.deleteFeedMediaOfItem(context, selectedItem.getId()); + DBWriter.deleteFeedMediaOfItem(context, selectedItem.getMedia().getId()); break; case R.id.cancel_download_item: requester.cancelDownload(context, selectedItem.getMedia()); @@ -153,12 +153,6 @@ public class FeedItemMenuHandler { DBTasks.playMedia(context, selectedItem.getMedia(), true, true, true); break; - case R.id.move_to_top_item: - DBWriter.moveQueueItemToTop(context, selectedItem.getId(), true); - break; - case R.id.move_to_bottom_item: - DBWriter.moveQueueItemToBottom(context, selectedItem.getId(), true); - break; case R.id.visit_website_item: Uri uri = Uri.parse(selectedItem.getLink()); context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); diff --git a/src/instrumentationTest/de/test/antennapod/gpodnet/GPodnetServiceTest.java b/src/instrumentationTest/de/test/antennapod/gpodnet/GPodnetServiceTest.java new file mode 100644 index 000000000..a96fc7aab --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/gpodnet/GPodnetServiceTest.java @@ -0,0 +1,114 @@ +package instrumentationTest.de.test.antennapod.gpodnet; + +import android.test.AndroidTestCase; +import android.util.Log; +import de.danoeh.antennapod.gpoddernet.GpodnetService; +import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.gpoddernet.model.GpodnetTag; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Test class for GpodnetService + */ +public class GPodnetServiceTest extends AndroidTestCase { + + private GpodnetService service; + + private static final String USER = ""; + private static final String PW = ""; + + @Override + protected void setUp() throws Exception { + super.setUp(); + service = new GpodnetService(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + private void authenticate() throws GpodnetServiceException { + service.authenticate(USER, PW); + } + + public void testUploadSubscription() throws GpodnetServiceException { + authenticate(); + ArrayList<String> l = new ArrayList<String>(); + l.add("http://bitsundso.de/feed"); + service.uploadSubscriptions(USER, "radio", l); + } + + public void testUploadSubscription2() throws GpodnetServiceException { + authenticate(); + ArrayList<String> l = new ArrayList<String>(); + l.add("http://bitsundso.de/feed"); + l.add("http://gamesundso.de/feed"); + service.uploadSubscriptions(USER, "radio", l); + } + + public void testUploadChanges() throws GpodnetServiceException { + authenticate(); + String[] URLS = {"http://bitsundso.de/feed", "http://gamesundso.de/feed", "http://cre.fm/feed/mp3/", "http://freakshow.fm/feed/m4a/"}; + List<String> subscriptions = Arrays.asList(URLS[0], URLS[1]); + List<String> removed = Arrays.asList(URLS[0]); + List<String> added = Arrays.asList(URLS[2], URLS[3]); + service.uploadSubscriptions(USER, "radio", subscriptions); + service.uploadChanges(USER, "radio", added, removed); + } + + public void testGetSubscriptionChanges() throws GpodnetServiceException { + authenticate(); + service.getSubscriptionChanges(USER, "radio", 1362322610L); + } + + public void testGetSubscriptionsOfUser() + throws GpodnetServiceException { + authenticate(); + service.getSubscriptionsOfUser(USER); + } + + public void testGetSubscriptionsOfDevice() + throws GpodnetServiceException { + authenticate(); + service.getSubscriptionsOfDevice(USER, "radio"); + } + + public void testConfigureDevices() throws GpodnetServiceException { + authenticate(); + service.configureDevice(USER, "foo", "This is an updated caption", + GpodnetDevice.DeviceType.LAPTOP); + } + + public void testGetDevices() throws GpodnetServiceException { + authenticate(); + service.getDevices(USER); + } + + public void testGetSuggestions() throws GpodnetServiceException { + authenticate(); + service.getSuggestions(10); + } + + public void testTags() throws GpodnetServiceException { + service.getTopTags(20); + } + + public void testPodcastForTags() throws GpodnetServiceException { + List<GpodnetTag> tags = service.getTopTags(20); + service.getPodcastsForTag(tags.get(1), + 10); + } + + public void testSearch() throws GpodnetServiceException { + service.searchPodcasts("linux", 64); + } + + public void testToplist() throws GpodnetServiceException { + service.getPodcastToplist(10); + } +} diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java index 6dd9ef92b..7631a5787 100644 --- a/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java +++ b/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java @@ -8,12 +8,14 @@ import de.danoeh.antennapod.feed.Feed; import de.danoeh.antennapod.feed.FeedItem; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.storage.PodDBAdapter; import java.io.File; import java.io.IOException; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; @@ -164,4 +166,87 @@ public class DBTasksTest extends InstrumentationTestCase { assertTrue(file.exists()); } } + + public void testUpdateFeedNewFeed() { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_ITEMS = 10; + + Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + for (int i = 0; i < NUM_ITEMS; i++) { + feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(), false, feed)); + } + Feed newFeed = DBTasks.updateFeed(context, feed); + + assertTrue(newFeed == feed); + assertTrue(feed.getId() != 0); + for (FeedItem item : feed.getItems()) { + assertFalse(item.isRead()); + assertTrue(item.getId() != 0); + } + } + + public void testUpdateFeedUpdatedFeed() { + final Context context = getInstrumentation().getTargetContext(); + final int NUM_ITEMS_OLD = 10; + final int NUM_ITEMS_NEW = 10; + + final Feed feed = new Feed("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + for (int i = 0; i < NUM_ITEMS_OLD; i++) { + feed.getItems().add(new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), true, feed)); + } + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + // ensure that objects have been saved in db, then reset + assertTrue(feed.getId() != 0); + final long feedID = feed.getId(); + feed.setId(0); + List<Long> itemIDs = new ArrayList<Long>(); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + itemIDs.add(item.getId()); + item.setId(0); + } + + for (int i = NUM_ITEMS_OLD; i < NUM_ITEMS_NEW + NUM_ITEMS_OLD; i++) { + feed.getItems().add(0, new FeedItem(0, "item " + i, "id " + i, "link " + i, new Date(i), true, feed)); + } + + final Feed newFeed = DBTasks.updateFeed(context, feed); + assertTrue(feed != newFeed); + + updatedFeedTest(newFeed, feedID, itemIDs, NUM_ITEMS_OLD, NUM_ITEMS_NEW); + + final Feed feedFromDB = DBReader.getFeed(context, newFeed.getId()); + assertNotNull(feedFromDB); + assertTrue(feedFromDB.getId() == newFeed.getId()); + updatedFeedTest(feedFromDB, feedID, itemIDs, NUM_ITEMS_OLD, NUM_ITEMS_NEW); + } + + private void updatedFeedTest(final Feed newFeed, long feedID, List<Long> itemIDs, final int NUM_ITEMS_OLD, final int NUM_ITEMS_NEW) { + assertTrue(newFeed.getId() == feedID); + assertTrue(newFeed.getItems().size() == NUM_ITEMS_NEW + NUM_ITEMS_OLD); + Collections.reverse(newFeed.getItems()); + Date lastDate = new Date(0); + for (int i = 0; i < NUM_ITEMS_OLD; i++) { + FeedItem item = newFeed.getItems().get(i); + assertTrue(item.getFeed() == newFeed); + assertTrue(item.getId() == itemIDs.get(i)); + assertTrue(item.isRead()); + assertTrue(item.getPubDate().getTime() >= lastDate.getTime()); + lastDate = item.getPubDate(); + } + for (int i = NUM_ITEMS_OLD; i < NUM_ITEMS_NEW + NUM_ITEMS_OLD; i++) { + FeedItem item = newFeed.getItems().get(i); + assertTrue(item.getFeed() == newFeed); + assertTrue(item.getId() != 0); + assertFalse(item.isRead()); + assertTrue(item.getPubDate().getTime() >= lastDate.getTime()); + lastDate = item.getPubDate(); + } + } } |