Merge branch 'gpoddernet' into develop

Conflicts:
	AndroidManifest.xml
	res/values/arrays.xml
	res/values/strings.xml
	res/xml/preferences.xml
	src/de/danoeh/antennapod/activity/PreferenceActivity.java
This commit is contained in:
daniel oeh 2013-09-05 15:24:50 +02:00
commit 02926a6e5f
62 changed files with 4862 additions and 475 deletions

View File

@ -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: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:host="*"/>
<data android:pathPattern=".*\\.xml"/>
<data android:pathPattern=".*\\.rss"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:mimeType="text/plain" />
<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>
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<category android:name="android.intent.category.DEFAULT"/>
<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>
<receiver android:name=".receiver.ConnectivityActionReceiver" >
<activity
android:name=".activity.gpoddernet.GpodnetSearchActivity"
android:configChanges="orientation"
android:label="@string/search_label"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<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">
<intent-filter>
<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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -15,5 +15,9 @@
<item name="organize_queue_item" type="id"/>
<item name="drag_handle" type="id"/>
<item name="skip_episode_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>

View File

@ -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>
@ -160,6 +164,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>
@ -193,9 +199,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>
@ -248,6 +260,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>
@ -261,4 +304,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>

View File

@ -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>

View File

@ -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" android:visibility="gone"/>-->
<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"/>
</PreferenceScreen>
</PreferenceCategory>
<PreferenceCategory android:title="@string/flattr_settings_label" >
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:key="pref_flattr_authenticate"
android:summary="@string/pref_flattr_auth_sum"
android:title="@string/pref_flattr_auth_title" >
<intent android:action=".activities.FlattrAuthActivity" />
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>
<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/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="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>
</PreferenceCategory>
<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>

View File

@ -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";
}

View File

@ -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() {

View File

@ -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);
}
}

View File

@ -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";
private static final String TAG = "OnlineFeedViewActivity";
public static final String ARG_FEEDURL = "arg.feedurl";
public static final int RESULT_ERROR = 2;
/** Optional argument: specify a title for the actionbar. */
public static final String ARG_TITLE = "title";
private Feed feed;
private Downloader downloader;
public static final int RESULT_ERROR = 2;
@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);
}
private Feed feed;
private Downloader downloader;
@Override
protected void onStop() {
super.onStop();
if (downloader != null && !downloader.isFinished()) {
downloader.cancel();
}
}
@Override
protected void onCreate(Bundle arg0) {
setTheme(UserPreferences.getTheme());
super.onCreate(arg0);
private DownloaderCallback downloaderCallback = new DownloaderCallback() {
@Override
public void onDownloadCompleted(final Downloader downloader) {
runOnUiThread(new Runnable() {
if (getIntent() != null && getIntent().hasExtra(ARG_TITLE)) {
getSupportActionBar().setTitle(getIntent().getStringExtra(ARG_TITLE));
}
@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();
}
}
});
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 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);
private void onDownloadCompleted(final Downloader downloader) {
runOnUiThread(new Runnable() {
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);
}
@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 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");
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();
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() {
/**
* 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);
@Override
public void run() {
showErrorDialog(errorMsg);
}
});
}
}
};
thread.start();
}
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);
}
/** Called when feed parsed successfully */
protected void showFeedInformation() {
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");
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() {
Thread thread = new Thread() {
@Override
public void onClick(DialogInterface dialog, int which) {
dialog.cancel();
}
});
builder.setOnCancelListener(new OnCancelListener() {
@Override
public void onCancel(DialogInterface dialog) {
setResult(RESULT_ERROR);
finish();
}
});
}
@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();
}
});
}
}

View File

@ -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")

View File

@ -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;
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
protected int PREFERRED_LENGTH;
public static final int FADE_DURATION = 500;
/** Can be thumbnail or cover */
protected int imageType;
/**
* Can be thumbnail or cover
*/
protected int imageType;
private static final String TAG = "BitmapDecodeWorkerTask";
private ImageView target;
protected CachedBitmap cBitmap;
private static final String TAG = "BitmapDecodeWorkerTask";
private ImageView target;
protected CachedBitmap cBitmap;
protected ImageLoader.ImageWorkerTaskResource imageResource;
protected ImageLoader.ImageWorkerTaskResource imageResource;
private Handler handler;
private Handler handler;
private final int defaultCoverResource;
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();
}
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() == null
|| target.getTag().equals(imageResource.getImageLoaderCacheKey());
}
/**
* 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) {
target.setImageBitmap(cBitmap.getBitmap());
} else {
if (AppConfig.DEBUG)
Log.d(TAG, "Not displaying image");
}
}
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");
@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();
}
endBackgroundTask();
}
protected final void endBackgroundTask() {
handler.post(new Runnable() {
protected final void endBackgroundTask() {
handler.post(new Runnable() {
@Override
public void run() {
onPostExecute();
}
@Override
public void run() {
onPostExecute();
}
});
}
});
}
protected void onInvalidStream() {
cBitmap = new CachedBitmap(BitmapFactory.decodeResource(
target.getResources(), defaultCoverResource), PREFERRED_LENGTH);
}
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 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);
}
}
}

View File

@ -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;
}
};
}
}
}

View File

@ -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;
}
/**

View File

@ -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);
}

View File

@ -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();
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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>();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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());
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -75,6 +75,27 @@ public final class DBReader {
return feeds;
}
/**
* 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.
*

View File

@ -23,6 +23,7 @@ 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;
@ -40,6 +41,39 @@ public final class DBTasks {
private 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}.
@ -111,6 +145,8 @@ public final class DBTasks {
refreshFeeds(context, DBReader.getFeedList(context));
}
isRefreshing.set(false);
GpodnetSyncService.sendSyncIntent(context);
}
}.start();
} else {

View File

@ -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;
@ -173,6 +176,8 @@ public class DBWriter {
}
adapter.removeFeed(feed);
adapter.close();
GpodnetPreferences.addRemovedFeed(feed.getDownload_url());
EventDistributor.getInstance().sendFeedUpdateBroadcast();
}
}
@ -616,6 +621,7 @@ public class DBWriter {
adapter.setCompleteFeed(feed);
adapter.close();
GpodnetPreferences.addAddedFeed(feed.getDownload_url());
EventDistributor.getInstance().sendFeedUpdateBroadcast();
}
});
@ -718,6 +724,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) {

View File

@ -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,

View File

@ -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();
}
}

View File

@ -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);
}
}