summaryrefslogtreecommitdiff
path: root/src/de/danoeh
diff options
context:
space:
mode:
authordaniel oeh <daniel.oeh@gmail.com>2012-07-13 12:23:47 +0200
committerdaniel oeh <daniel.oeh@gmail.com>2012-07-13 12:23:47 +0200
commitba2d2afbbc6cbb79fc75493703425b5d6d040530 (patch)
treee731a1209160e8224679cb238c0a964c3e757590 /src/de/danoeh
parent1ae00a0f2531fdb05a44877dda88ee2300e3ffec (diff)
downloadAntennaPod-ba2d2afbbc6cbb79fc75493703425b5d6d040530.zip
Renamed package and application
Diffstat (limited to 'src/de/danoeh')
-rw-r--r--src/de/danoeh/antennapod/PodcastApp.java77
-rw-r--r--src/de/danoeh/antennapod/activity/AddFeedActivity.java238
-rw-r--r--src/de/danoeh/antennapod/activity/DownloadActivity.java190
-rw-r--r--src/de/danoeh/antennapod/activity/DownloadLogActivity.java45
-rw-r--r--src/de/danoeh/antennapod/activity/FeedInfoActivity.java78
-rw-r--r--src/de/danoeh/antennapod/activity/FeedItemlistActivity.java102
-rw-r--r--src/de/danoeh/antennapod/activity/FlattrAuthActivity.java100
-rw-r--r--src/de/danoeh/antennapod/activity/ItemviewActivity.java123
-rw-r--r--src/de/danoeh/antennapod/activity/MainActivity.java179
-rw-r--r--src/de/danoeh/antennapod/activity/MediaplayerActivity.java739
-rw-r--r--src/de/danoeh/antennapod/activity/PreferenceActivity.java80
-rw-r--r--src/de/danoeh/antennapod/activity/StorageErrorActivity.java66
-rw-r--r--src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java86
-rw-r--r--src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java101
-rw-r--r--src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java166
-rw-r--r--src/de/danoeh/antennapod/adapter/FeedlistAdapter.java116
-rw-r--r--src/de/danoeh/antennapod/asynctask/DownloadObserver.java211
-rw-r--r--src/de/danoeh/antennapod/asynctask/DownloadStatus.java95
-rw-r--r--src/de/danoeh/antennapod/asynctask/FeedImageLoader.java178
-rw-r--r--src/de/danoeh/antennapod/asynctask/FeedRemover.java62
-rw-r--r--src/de/danoeh/antennapod/feed/Feed.java129
-rw-r--r--src/de/danoeh/antennapod/feed/FeedCategory.java20
-rw-r--r--src/de/danoeh/antennapod/feed/FeedComponent.java26
-rw-r--r--src/de/danoeh/antennapod/feed/FeedFile.java55
-rw-r--r--src/de/danoeh/antennapod/feed/FeedImage.java32
-rw-r--r--src/de/danoeh/antennapod/feed/FeedItem.java117
-rw-r--r--src/de/danoeh/antennapod/feed/FeedManager.java744
-rw-r--r--src/de/danoeh/antennapod/feed/FeedMedia.java73
-rw-r--r--src/de/danoeh/antennapod/feed/SimpleChapter.java22
-rw-r--r--src/de/danoeh/antennapod/fragment/CoverFragment.java90
-rw-r--r--src/de/danoeh/antennapod/fragment/FeedlistFragment.java182
-rw-r--r--src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java153
-rw-r--r--src/de/danoeh/antennapod/fragment/ItemlistFragment.java215
-rw-r--r--src/de/danoeh/antennapod/fragment/QueueFragment.java104
-rw-r--r--src/de/danoeh/antennapod/fragment/UnreadItemlistFragment.java79
-rw-r--r--src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java42
-rw-r--r--src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java34
-rw-r--r--src/de/danoeh/antennapod/receiver/PlayerWidget.java42
-rw-r--r--src/de/danoeh/antennapod/service/DownloadService.java492
-rw-r--r--src/de/danoeh/antennapod/service/PlaybackService.java678
-rw-r--r--src/de/danoeh/antennapod/service/PlayerStatus.java5
-rw-r--r--src/de/danoeh/antennapod/service/PlayerWidgetService.java135
-rw-r--r--src/de/danoeh/antennapod/storage/DownloadRequester.java247
-rw-r--r--src/de/danoeh/antennapod/storage/PodDBAdapter.java591
-rw-r--r--src/de/danoeh/antennapod/syndication/handler/FeedHandler.java29
-rw-r--r--src/de/danoeh/antennapod/syndication/handler/HandlerState.java69
-rw-r--r--src/de/danoeh/antennapod/syndication/handler/SyndHandler.java112
-rw-r--r--src/de/danoeh/antennapod/syndication/handler/TypeGetter.java76
-rw-r--r--src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java29
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/Namespace.java23
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/SyndElement.java22
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java47
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java146
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/content/NSContent.java31
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/itunes/NSITunes.java42
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/rss20/NSRSS20.java115
-rw-r--r--src/de/danoeh/antennapod/syndication/namespace/simplechapters/NSSimpleChapters.java43
-rw-r--r--src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java102
-rw-r--r--src/de/danoeh/antennapod/util/ConnectionTester.java66
-rw-r--r--src/de/danoeh/antennapod/util/Converter.java81
-rw-r--r--src/de/danoeh/antennapod/util/DownloadError.java42
-rw-r--r--src/de/danoeh/antennapod/util/FeedItemMenuHandler.java120
-rw-r--r--src/de/danoeh/antennapod/util/FeedItemPubdateComparator.java19
-rw-r--r--src/de/danoeh/antennapod/util/FeedMenuHandler.java84
-rw-r--r--src/de/danoeh/antennapod/util/FeedtitleComparator.java15
-rw-r--r--src/de/danoeh/antennapod/util/FlattrUtils.java215
-rw-r--r--src/de/danoeh/antennapod/util/MediaPlayerError.java23
-rw-r--r--src/de/danoeh/antennapod/util/NumberGenerator.java25
-rw-r--r--src/de/danoeh/antennapod/util/ShareUtils.java34
-rw-r--r--src/de/danoeh/antennapod/util/StorageUtils.java28
-rw-r--r--src/de/danoeh/antennapod/util/URLChecker.java39
71 files changed, 9016 insertions, 0 deletions
diff --git a/src/de/danoeh/antennapod/PodcastApp.java b/src/de/danoeh/antennapod/PodcastApp.java
new file mode 100644
index 000000000..a82356663
--- /dev/null
+++ b/src/de/danoeh/antennapod/PodcastApp.java
@@ -0,0 +1,77 @@
+package de.danoeh.antennapod;
+
+import java.util.concurrent.TimeUnit;
+
+import android.app.AlarmManager;
+import android.app.Application;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import de.danoeh.antennapod.asynctask.FeedImageLoader;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.receiver.FeedUpdateReceiver;
+import de.danoeh.antennapod.util.StorageUtils;
+
+public class PodcastApp extends Application implements
+ SharedPreferences.OnSharedPreferenceChangeListener {
+
+ private static final String TAG = "PodcastApp";
+ public static final String PREF_NAME = "AntennapodPrefs";
+
+ public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect";
+ public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue";
+ public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly";
+ public static final String PREF_UPDATE_INTERVALL = "prefAutoUpdateIntervall";
+ public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate";
+
+ private static PodcastApp singleton;
+
+ public static PodcastApp getInstance() {
+ return singleton;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ singleton = this;
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(this);
+ prefs.registerOnSharedPreferenceChangeListener(this);
+ if (StorageUtils.storageAvailable()) {
+ FeedManager manager = FeedManager.getInstance();
+ manager.loadDBData(getApplicationContext());
+ }
+ }
+
+ @Override
+ public void onLowMemory() {
+ super.onLowMemory();
+ Log.w(TAG, "Received onLowOnMemory warning. Cleaning image cache...");
+ FeedImageLoader.getInstance().wipeImageCache();
+ }
+
+ @Override
+ public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
+ String key) {
+ Log.d(TAG, "Registered change of application preferences");
+ if (key.equals(PREF_UPDATE_INTERVALL)) {
+ AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+ int hours = Integer.parseInt(sharedPreferences.getString(
+ PREF_UPDATE_INTERVALL, "0"));
+ PendingIntent updateIntent = PendingIntent.getBroadcast(this, 0,
+ new Intent(FeedUpdateReceiver.ACTION_REFRESH_FEEDS), 0);
+ alarmManager.cancel(updateIntent);
+ if (hours != 0) {
+ long newIntervall = TimeUnit.HOURS.toMillis(hours);
+ alarmManager.setRepeating(AlarmManager.RTC_WAKEUP,
+ newIntervall, newIntervall, updateIntent);
+ Log.d(TAG, "Changed alarm to new intervall");
+ } else {
+ Log.d(TAG, "Automatic update was deactivated");
+ }
+ }
+ }
+}
diff --git a/src/de/danoeh/antennapod/activity/AddFeedActivity.java b/src/de/danoeh/antennapod/activity/AddFeedActivity.java
new file mode 100644
index 000000000..433f8c2c3
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/AddFeedActivity.java
@@ -0,0 +1,238 @@
+package de.danoeh.antennapod.activity;
+
+import android.os.Bundle;
+import android.widget.Button;
+import android.widget.EditText;
+import android.view.View;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.util.Log;
+import de.danoeh.antennapod.R;
+import de.danoeh.antennapod.asynctask.DownloadObserver;
+import de.danoeh.antennapod.asynctask.DownloadStatus;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.service.DownloadService;
+import de.danoeh.antennapod.storage.DownloadRequester;
+import de.danoeh.antennapod.util.ConnectionTester;
+import de.danoeh.antennapod.util.DownloadError;
+import de.danoeh.antennapod.util.StorageUtils;
+import de.danoeh.antennapod.util.URLChecker;
+import com.actionbarsherlock.app.SherlockActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+
+import java.util.Date;
+import java.util.concurrent.Callable;
+
+/** Activity for adding/editing a Feed */
+public class AddFeedActivity extends SherlockActivity {
+ private static final String TAG = "AddFeedActivity";
+
+ private DownloadRequester requester;
+ private FeedManager manager;
+
+ private EditText etxtFeedurl;
+ private Button butConfirm;
+ private Button butCancel;
+ private long downloadId;
+
+ private boolean hasImage;
+ private boolean isWaitingForImage = false;
+ private long imageDownloadId;
+
+ private ProgressDialog progDialog;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ StorageUtils.checkStorageAvailability(this);
+ setContentView(R.layout.addfeed);
+
+ requester = DownloadRequester.getInstance();
+ manager = FeedManager.getInstance();
+
+ progDialog = new ProgressDialog(this) {
+ @Override
+ public void onBackPressed() {
+ if (isWaitingForImage) {
+ requester.cancelDownload(getContext(), imageDownloadId);
+ } else {
+ requester.cancelDownload(getContext(), downloadId);
+ }
+
+ try {
+ unregisterReceiver(downloadCompleted);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ dismiss();
+ }
+
+ };
+
+ etxtFeedurl = (EditText) findViewById(R.id.etxtFeedurl);
+ butConfirm = (Button) findViewById(R.id.butConfirm);
+ butCancel = (Button) findViewById(R.id.butCancel);
+
+ butConfirm.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ addNewFeed();
+ }
+ });
+
+ butCancel.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ setResult(RESULT_CANCELED);
+ finish();
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ StorageUtils.checkStorageAvailability(this);
+
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.d(TAG, "Stopping Activity");
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ try {
+ unregisterReceiver(downloadCompleted);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+
+ }
+
+ private void addNewFeed() {
+ String url = etxtFeedurl.getText().toString();
+ url = URLChecker.prepareURL(url);
+
+ if (url != null) {
+ final Feed feed = new Feed(url, new Date());
+ final ConnectionTester conTester = new ConnectionTester(url, this,
+ new ConnectionTester.Callback() {
+
+ @Override
+ public void onConnectionSuccessful() {
+ downloadId = requester.downloadFeed(
+ AddFeedActivity.this, feed);
+
+ }
+
+ @Override
+ public void onConnectionFailure() {
+ int reason = DownloadError.ERROR_CONNECTION_ERROR;
+ long statusId = manager.addDownloadStatus(
+ AddFeedActivity.this, new DownloadStatus(
+ feed, reason, false));
+ Intent intent = new Intent(
+ DownloadService.ACTION_DOWNLOAD_HANDLED);
+ intent.putExtra(DownloadService.EXTRA_DOWNLOAD_ID,
+ downloadId);
+ intent.putExtra(DownloadService.EXTRA_STATUS_ID,
+ statusId);
+ AddFeedActivity.this.sendBroadcast(intent);
+ }
+ });
+ observeDownload(feed);
+ new Thread(conTester).start();
+
+ }
+ }
+
+ private void observeDownload(Feed feed) {
+ progDialog.show();
+ progDialog.setMessage("Downloading Feed");
+ registerReceiver(downloadCompleted, new IntentFilter(
+ DownloadService.ACTION_DOWNLOAD_HANDLED));
+ }
+
+ private void updateProgDialog(final String msg) {
+ if (progDialog.isShowing()) {
+ runOnUiThread(new Runnable() {
+
+ @Override
+ public void run() {
+ progDialog.setMessage(msg);
+
+ }
+
+ });
+ }
+ }
+
+ private void handleDownloadError(DownloadStatus status) {
+ final AlertDialog errorDialog = new AlertDialog.Builder(this).create();
+ errorDialog.setTitle(R.string.error_label);
+ errorDialog.setMessage(getString(R.string.error_msg_prefix) + " "
+ + DownloadError.getErrorString(this, status.getReason()));
+ errorDialog.setButton("OK", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ errorDialog.dismiss();
+ }
+ });
+ if (progDialog.isShowing()) {
+ progDialog.dismiss();
+ }
+ errorDialog.show();
+ }
+
+ private BroadcastReceiver downloadCompleted = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ long receivedDownloadId = intent.getLongExtra(
+ DownloadService.EXTRA_DOWNLOAD_ID, -1);
+ if (receivedDownloadId == downloadId
+ || (isWaitingForImage && receivedDownloadId == imageDownloadId)) {
+ long statusId = intent.getLongExtra(
+ DownloadService.EXTRA_STATUS_ID, 0);
+ DownloadStatus status = manager.getDownloadStatus(statusId);
+ if (status.isSuccessful()) {
+ if (!isWaitingForImage) {
+ hasImage = intent.getBooleanExtra(
+ DownloadService.EXTRA_FEED_HAS_IMAGE, false);
+ if (!hasImage) {
+ progDialog.dismiss();
+ finish();
+ } else {
+ imageDownloadId = intent
+ .getLongExtra(
+ DownloadService.EXTRA_IMAGE_DOWNLOAD_ID,
+ -1);
+ isWaitingForImage = true;
+ updateProgDialog("Downloading Image");
+ }
+ } else {
+ progDialog.dismiss();
+ finish();
+ }
+ } else {
+ handleDownloadError(status);
+ }
+ }
+
+ }
+
+ };
+
+}
diff --git a/src/de/danoeh/antennapod/activity/DownloadActivity.java b/src/de/danoeh/antennapod/activity/DownloadActivity.java
new file mode 100644
index 000000000..180a1b643
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/DownloadActivity.java
@@ -0,0 +1,190 @@
+package de.danoeh.antennapod.activity;
+
+import de.danoeh.antennapod.adapter.DownloadlistAdapter;
+import de.danoeh.antennapod.asynctask.DownloadObserver;
+import de.danoeh.antennapod.asynctask.DownloadStatus;
+import de.danoeh.antennapod.feed.FeedFile;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.service.DownloadService;
+import de.danoeh.antennapod.storage.DownloadRequester;
+import de.danoeh.antennapod.R;
+import com.actionbarsherlock.app.SherlockListActivity;
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnLongClickListener;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemLongClickListener;
+
+/** Shows all running downloads in a list */
+public class DownloadActivity extends SherlockListActivity implements
+ ActionMode.Callback, DownloadObserver.Callback {
+
+ private static final String TAG = "DownloadActivity";
+ private static final int MENU_SHOW_LOG = 0;
+ private static final int MENU_CANCEL_ALL_DOWNLOADS = 1;
+ private DownloadlistAdapter dla;
+ private DownloadRequester requester;
+
+ private ActionMode mActionMode;
+ private DownloadStatus selectedDownload;
+ private DownloadObserver downloadObserver;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.d(TAG, "Creating Activity");
+ requester = DownloadRequester.getInstance();
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ unbindService(mConnection);
+ if (downloadObserver != null) {
+ downloadObserver.unregisterCallback(DownloadActivity.this);
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "Trying to bind service");
+ bindService(new Intent(this, DownloadService.class), mConnection, 0);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.d(TAG, "Stopping Activity");
+ }
+
+ @Override
+ protected void onPostCreate(Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ getListView().setOnItemLongClickListener(new OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> arg0, View view,
+ int position, long id) {
+ DownloadStatus selection = dla.getItem(position);
+ if (selection != null && mActionMode != null) {
+ mActionMode.finish();
+ }
+ dla.setSelectedItemIndex(position);
+ selectedDownload = selection;
+ mActionMode = startActionMode(DownloadActivity.this);
+ return true;
+ }
+
+ });
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ menu.add(Menu.NONE, MENU_SHOW_LOG, Menu.NONE,
+ R.string.show_download_log).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ menu.add(Menu.NONE, MENU_CANCEL_ALL_DOWNLOADS, Menu.NONE,
+ R.string.cancel_all_downloads_label).setShowAsAction(
+ MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ case MENU_SHOW_LOG:
+ startActivity(new Intent(this, DownloadLogActivity.class));
+ break;
+ case MENU_CANCEL_ALL_DOWNLOADS:
+ requester.cancelAllDownloads(this);
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ if (!selectedDownload.isDone()) {
+ menu.add(Menu.NONE, R.id.cancel_download_item, Menu.NONE,
+ R.string.cancel_download_label).setIcon(
+ R.drawable.navigation_cancel);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ boolean handled = false;
+ switch (item.getItemId()) {
+ case R.id.cancel_download_item:
+ requester.cancelDownload(this, selectedDownload.getFeedFile()
+ .getDownloadId());
+ handled = true;
+ break;
+ }
+ mActionMode.finish();
+ return handled;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mActionMode = null;
+ selectedDownload = null;
+ dla.setSelectedItemIndex(DownloadlistAdapter.SELECTION_NONE);
+ }
+
+ private DownloadService downloadService = null;
+ boolean mIsBound;
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ downloadService = ((DownloadService.LocalBinder) service)
+ .getService();
+ Log.d(TAG, "Connection to service established");
+ dla = new DownloadlistAdapter(DownloadActivity.this, 0,
+ downloadService.getDownloadObserver().getStatusList());
+ setListAdapter(dla);
+ downloadObserver = downloadService.getDownloadObserver();
+ downloadObserver.registerCallback(DownloadActivity.this);
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ downloadService = null;
+ mIsBound = false;
+ Log.i(TAG, "Closed connection with DownloadService.");
+ }
+ };
+
+ @Override
+ public void onProgressUpdate() {
+ dla.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onFinish() {
+ Log.d(TAG, "Observer has finished, clearing adapter");
+ dla.clear();
+ dla.notifyDataSetInvalidated();
+ }
+}
diff --git a/src/de/danoeh/antennapod/activity/DownloadLogActivity.java b/src/de/danoeh/antennapod/activity/DownloadLogActivity.java
new file mode 100644
index 000000000..66240a3a7
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/DownloadLogActivity.java
@@ -0,0 +1,45 @@
+package de.danoeh.antennapod.activity;
+
+import android.os.Bundle;
+
+import com.actionbarsherlock.app.SherlockListActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import de.danoeh.antennapod.adapter.DownloadLogAdapter;
+import de.danoeh.antennapod.feed.FeedManager;
+
+public class DownloadLogActivity extends SherlockListActivity {
+ private static final String TAG = "DownloadLogActivity";
+
+ DownloadLogAdapter dla;
+ FeedManager manager;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ manager = FeedManager.getInstance();
+
+ dla = new DownloadLogAdapter(this, 0, manager.getDownloadLog());
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ setListAdapter(dla);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/activity/FeedInfoActivity.java b/src/de/danoeh/antennapod/activity/FeedInfoActivity.java
new file mode 100644
index 000000000..2d0528e22
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/FeedInfoActivity.java
@@ -0,0 +1,78 @@
+package de.danoeh.antennapod.activity;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import de.danoeh.antennapod.asynctask.FeedImageLoader;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.R;
+
+/** Displays information about a feed. */
+public class FeedInfoActivity extends SherlockActivity {
+ private static final String TAG = "FeedInfoActivity";
+
+ public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId";
+
+ private ImageView imgvCover;
+ private TextView txtvTitle;
+ private TextView txtvDescription;
+ private TextView txtvLanguage;
+ private TextView txtvAuthor;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.feedinfo);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ long feedId = getIntent().getLongExtra(EXTRA_FEED_ID, -1);
+ FeedManager manager = FeedManager.getInstance();
+ Feed feed = manager.getFeed(feedId);
+ if (feed != null) {
+ Log.d(TAG, "Language is " + feed.getLanguage());
+ Log.d(TAG, "Author is " + feed.getAuthor());
+ imgvCover = (ImageView) findViewById(R.id.imgvCover);
+ txtvTitle = (TextView) findViewById(R.id.txtvTitle);
+ txtvDescription = (TextView) findViewById(R.id.txtvDescription);
+ txtvLanguage = (TextView) findViewById(R.id.txtvLanguage);
+ txtvAuthor = (TextView) findViewById(R.id.txtvAuthor);
+ FeedImageLoader.getInstance().loadBitmap(feed.getImage(), imgvCover);
+
+ txtvTitle.setText(feed.getTitle());
+ txtvDescription.setText(feed.getDescription());
+ if (feed.getAuthor() != null) {
+ txtvAuthor.setText(feed.getAuthor());
+ }
+ if (feed.getLanguage() != null) {
+ txtvLanguage.setText(feed.getLanguage());
+ }
+ } else {
+ Log.e(TAG, "Activity was started with invalid arguments");
+ }
+
+ }
+
+
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ return true;
+ default:
+ return false;
+ }
+ }
+}
diff --git a/src/de/danoeh/antennapod/activity/FeedItemlistActivity.java b/src/de/danoeh/antennapod/activity/FeedItemlistActivity.java
new file mode 100644
index 000000000..5e0971473
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/FeedItemlistActivity.java
@@ -0,0 +1,102 @@
+package de.danoeh.antennapod.activity;
+
+import android.graphics.drawable.BitmapDrawable;
+import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.View;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.Window;
+
+import de.danoeh.antennapod.asynctask.FeedRemover;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.fragment.FeedlistFragment;
+import de.danoeh.antennapod.fragment.ItemlistFragment;
+import de.danoeh.antennapod.util.FeedMenuHandler;
+import de.danoeh.antennapod.util.StorageUtils;
+import de.danoeh.antennapod.R;
+
+/** Displays a List of FeedItems */
+public class FeedItemlistActivity extends SherlockFragmentActivity {
+ private static final String TAG = "FeedItemlistActivity";
+
+ private FeedManager manager;
+
+ /** The feed which the activity displays */
+ private Feed feed;
+ private ItemlistFragment filf;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ StorageUtils.checkStorageAvailability(this);
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ setContentView(R.layout.feeditemlist_activity);
+
+ manager = FeedManager.getInstance();
+ long feedId = getIntent().getLongExtra(
+ FeedlistFragment.EXTRA_SELECTED_FEED, -1);
+ if (feedId == -1)
+ Log.e(TAG, "Received invalid feed selection.");
+
+ feed = manager.getFeed(feedId);
+ setTitle(feed.getTitle());
+
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ FragmentTransaction fT = fragmentManager.beginTransaction();
+
+ filf = ItemlistFragment.newInstance(feed.getId());
+ fT.add(R.id.feeditemlistFragment, filf);
+ fT.commit();
+
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ StorageUtils.checkStorageAvailability(this);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return FeedMenuHandler
+ .onCreateOptionsMenu(new MenuInflater(this), menu);
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ return FeedMenuHandler.onPrepareOptionsMenu(menu, feed);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (FeedMenuHandler.onOptionsItemClicked(this, item, feed)) {
+ filf.getListAdapter().notifyDataSetChanged();
+ } else {
+ switch (item.getItemId()) {
+ case R.id.remove_item:
+ FeedRemover remover = new FeedRemover(this) {
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ finish();
+ }
+ };
+ remover.execute(feed);
+ break;
+ case android.R.id.home:
+ finish();
+ }
+ }
+ return true;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/activity/FlattrAuthActivity.java b/src/de/danoeh/antennapod/activity/FlattrAuthActivity.java
new file mode 100644
index 000000000..26773ae60
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/FlattrAuthActivity.java
@@ -0,0 +1,100 @@
+package de.danoeh.antennapod.activity;
+
+import org.shredzone.flattr4j.exception.FlattrException;
+
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import de.danoeh.antennapod.util.FlattrUtils;
+import de.danoeh.antennapod.R;
+
+/** Guides the user through the authentication process */
+public class FlattrAuthActivity extends SherlockActivity {
+ private static final String TAG = "FlattrAuthActivity";
+
+ private TextView txtvExplanation;
+ private Button butAuthenticate;
+ private Button butReturn;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.d(TAG, "Activity created");
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ setContentView(R.layout.flattr_auth);
+ txtvExplanation = (TextView) findViewById(R.id.txtvExplanation);
+ butAuthenticate = (Button) findViewById(R.id.but_authenticate);
+ butReturn = (Button) findViewById(R.id.but_return_home);
+
+ butReturn.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startActivity(new Intent(FlattrAuthActivity.this,
+ MainActivity.class));
+ }
+ });
+
+ butAuthenticate.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ try {
+ FlattrUtils.startAuthProcess(FlattrAuthActivity.this);
+ } catch (FlattrException e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "Activity resumed");
+ Uri uri = getIntent().getData();
+ if (uri != null) {
+ Log.d(TAG, "Received uri");
+ try {
+ if (FlattrUtils.handleCallback(uri) != null) {
+ handleAuthenticationSuccess();
+ Log.d(TAG, "Authentication seemed to be successful");
+ }
+ } catch (FlattrException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ private void handleAuthenticationSuccess() {
+ txtvExplanation.setText(R.string.flattr_auth_success);
+ butAuthenticate.setEnabled(false);
+ butReturn.setVisibility(View.VISIBLE);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/activity/ItemviewActivity.java b/src/de/danoeh/antennapod/activity/ItemviewActivity.java
new file mode 100644
index 000000000..2e071134c
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/ItemviewActivity.java
@@ -0,0 +1,123 @@
+package de.danoeh.antennapod.activity;
+
+import java.text.DateFormat;
+
+import android.graphics.drawable.BitmapDrawable;
+import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.Window;
+
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.fragment.FeedlistFragment;
+import de.danoeh.antennapod.fragment.ItemDescriptionFragment;
+import de.danoeh.antennapod.fragment.ItemlistFragment;
+import de.danoeh.antennapod.util.FeedItemMenuHandler;
+import de.danoeh.antennapod.util.StorageUtils;
+import de.danoeh.antennapod.R;
+
+/** Displays a single FeedItem and provides various actions */
+public class ItemviewActivity extends SherlockFragmentActivity {
+ private static final String TAG = "ItemviewActivity";
+
+ private FeedManager manager;
+ private FeedItem item;
+
+ // Widgets
+ private TextView txtvTitle;
+ private TextView txtvPublished;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ StorageUtils.checkStorageAvailability(this);
+ manager = FeedManager.getInstance();
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ getSupportActionBar().setDisplayShowTitleEnabled(false);
+ extractFeeditem();
+ populateUI();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ StorageUtils.checkStorageAvailability(this);
+
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ Log.d(TAG, "Stopping Activity");
+ }
+
+ /** Extracts FeedItem object the activity is supposed to display */
+ private void extractFeeditem() {
+ long itemId = getIntent().getLongExtra(
+ ItemlistFragment.EXTRA_SELECTED_FEEDITEM, -1);
+ long feedId = getIntent().getLongExtra(
+ FeedlistFragment.EXTRA_SELECTED_FEED, -1);
+ if (itemId == -1 || feedId == -1) {
+ Log.e(TAG, "Received invalid selection of either feeditem or feed.");
+ }
+ Feed feed = manager.getFeed(feedId);
+ item = manager.getFeedItem(itemId, feed);
+ Log.d(TAG, "Title of item is " + item.getTitle());
+ Log.d(TAG, "Title of feed is " + item.getFeed().getTitle());
+ }
+
+ private void populateUI() {
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ setContentView(R.layout.feeditemview);
+ txtvTitle = (TextView) findViewById(R.id.txtvItemname);
+ txtvPublished = (TextView) findViewById(R.id.txtvPublished);
+ setTitle(item.getFeed().getTitle());
+
+ txtvPublished.setText(DateUtils.formatSameDayTime(item.getPubDate()
+ .getTime(), System.currentTimeMillis(), DateFormat.MEDIUM,
+ DateFormat.SHORT));
+ txtvTitle.setText(item.getTitle());
+
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ FragmentTransaction fragmentTransaction = fragmentManager
+ .beginTransaction();
+ ItemDescriptionFragment fragment = ItemDescriptionFragment.newInstance(
+ item, false);
+ fragmentTransaction.add(R.id.description_fragment, fragment);
+ fragmentTransaction.commit();
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return FeedItemMenuHandler.onCreateMenu(new MenuInflater(this), menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem menuItem) {
+ if (!FeedItemMenuHandler.onMenuItemClicked(this, menuItem, item)) {
+ switch (menuItem.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ }
+ }
+ invalidateOptionsMenu();
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ return FeedItemMenuHandler.onPrepareMenu(menu, item);
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/activity/MainActivity.java b/src/de/danoeh/antennapod/activity/MainActivity.java
new file mode 100644
index 000000000..8ef7a1a62
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/MainActivity.java
@@ -0,0 +1,179 @@
+package de.danoeh.antennapod.activity;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+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.view.ViewPager;
+import android.util.Log;
+
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.Window;
+import com.viewpagerindicator.TabPageIndicator;
+
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.fragment.FeedlistFragment;
+import de.danoeh.antennapod.fragment.QueueFragment;
+import de.danoeh.antennapod.fragment.UnreadItemlistFragment;
+import de.danoeh.antennapod.service.DownloadService;
+import de.danoeh.antennapod.storage.DownloadRequester;
+import de.danoeh.antennapod.util.StorageUtils;
+import de.danoeh.antennapod.R;
+
+public class MainActivity extends SherlockFragmentActivity {
+ private static final String TAG = "MainActivity";
+
+ private FeedManager manager;
+ private ViewPager viewpager;
+ private MainPagerAdapter pagerAdapter;
+ private TabPageIndicator tabs;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ StorageUtils.checkStorageAvailability(this);
+ manager = FeedManager.getInstance();
+ requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
+ setContentView(R.layout.main);
+ pagerAdapter = new MainPagerAdapter(getSupportFragmentManager(), this);
+
+ viewpager = (ViewPager) findViewById(R.id.viewpager);
+ tabs = (TabPageIndicator) findViewById(R.id.tabs);
+
+ viewpager.setAdapter(pagerAdapter);
+ tabs.setViewPager(viewpager);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ unregisterReceiver(contentUpdate);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ StorageUtils.checkStorageAvailability(this);
+ updateProgressBarVisibility();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(DownloadService.ACTION_DOWNLOAD_HANDLED);
+ filter.addAction(DownloadRequester.ACTION_DOWNLOAD_QUEUED);
+ registerReceiver(contentUpdate, filter);
+ }
+
+ private BroadcastReceiver contentUpdate = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "Received contentUpdate Intent.");
+ updateProgressBarVisibility();
+ }
+ };
+
+ private void updateProgressBarVisibility() {
+ if (DownloadService.isRunning
+ && DownloadRequester.getInstance().isDownloadingFeeds()) {
+ setSupportProgressBarIndeterminateVisibility(true);
+ } else {
+ setSupportProgressBarIndeterminateVisibility(false);
+ }
+ invalidateOptionsMenu();
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.add_feed:
+ startActivity(new Intent(this, AddFeedActivity.class));
+ return true;
+ case R.id.all_feed_refresh:
+ manager.refreshAllFeeds(this);
+ return true;
+ case R.id.show_downloads:
+ startActivity(new Intent(this, DownloadActivity.class));
+ return true;
+ case R.id.show_preferences:
+ startActivity(new Intent(this, PreferenceActivity.class));
+ return true;
+ case R.id.show_player:
+ startActivity(new Intent(this, MediaplayerActivity.class));
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ MenuItem refreshAll = menu.findItem(R.id.all_feed_refresh);
+ if (DownloadService.isRunning
+ && DownloadRequester.getInstance().isDownloadingFeeds()) {
+ refreshAll.setVisible(false);
+ } else {
+ refreshAll.setVisible(true);
+ }
+ return true;
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ MenuInflater inflater = new MenuInflater(this);
+ inflater.inflate(R.menu.podfetcher, menu);
+ return true;
+ }
+
+ public static class MainPagerAdapter extends FragmentStatePagerAdapter {
+ private static final int NUM_ITEMS = 3;
+
+ private static final int POS_FEEDLIST = 0;
+ private static final int POS_NEW_ITEMS = 1;
+ private static final int POS_QUEUE = 2;
+
+ private Context context;
+
+ public MainPagerAdapter(FragmentManager fm, Context context) {
+ super(fm);
+ this.context = context;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ switch (position) {
+ case POS_FEEDLIST:
+ return new FeedlistFragment();
+ case POS_NEW_ITEMS:
+ return new UnreadItemlistFragment();
+ case POS_QUEUE:
+ return new QueueFragment();
+ default:
+ return null;
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return NUM_ITEMS;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case POS_FEEDLIST:
+ return context.getString(R.string.feeds_label);
+ case POS_NEW_ITEMS:
+ return context.getString(R.string.new_label);
+ case POS_QUEUE:
+ return context.getString(R.string.queue_label);
+ default:
+ return null;
+ }
+ }
+
+ }
+}
diff --git a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
new file mode 100644
index 000000000..6b1cd60e2
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java
@@ -0,0 +1,739 @@
+package de.danoeh.antennapod.activity;
+
+import android.app.AlertDialog;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.media.MediaPlayer;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentPagerAdapter;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.support.v4.view.ViewPager;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.WindowManager;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+import android.widget.VideoView;
+import android.widget.ViewSwitcher;
+
+import com.actionbarsherlock.app.SherlockActivity;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+import com.actionbarsherlock.view.Window;
+import com.viewpagerindicator.TabPageIndicator;
+
+import de.danoeh.antennapod.PodcastApp;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.fragment.CoverFragment;
+import de.danoeh.antennapod.fragment.ItemDescriptionFragment;
+import de.danoeh.antennapod.service.PlaybackService;
+import de.danoeh.antennapod.service.PlayerStatus;
+import de.danoeh.antennapod.util.Converter;
+import de.danoeh.antennapod.util.DownloadError;
+import de.danoeh.antennapod.util.MediaPlayerError;
+import de.danoeh.antennapod.util.StorageUtils;
+import de.danoeh.antennapod.R;
+
+public class MediaplayerActivity extends SherlockFragmentActivity implements
+ SurfaceHolder.Callback {
+
+ private final String TAG = "MediaplayerActivity";
+
+ private static final int DEFAULT_SEEK_DELTA = 30000; // Seek-Delta to use
+ // when using FF or
+ // Rev Buttons
+ /** Current screen orientation. */
+ private int orientation;
+
+ /** True if video controls are currently visible. */
+ private boolean videoControlsShowing = true;
+ /** True if media information was loaded. */
+ private boolean mediaInfoLoaded = false;
+
+ private PlaybackService playbackService;
+ private MediaPositionObserver positionObserver;
+ private VideoControlsHider videoControlsToggler;
+
+ private FeedMedia media;
+ private PlayerStatus status;
+ private FeedManager manager;
+
+ // Widgets
+ private CoverFragment coverFragment;
+ private ItemDescriptionFragment descriptionFragment;
+ private ViewPager viewpager;
+ private TabPageIndicator tabs;
+ private MediaPlayerPagerAdapter pagerAdapter;
+ private VideoView videoview;
+ private TextView txtvStatus;
+ private TextView txtvPosition;
+ private TextView txtvLength;
+ private SeekBar sbPosition;
+ private ImageButton butPlay;
+ private ImageButton butRev;
+ private ImageButton butFF;
+ private LinearLayout videoOverlay;
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ Log.d(TAG, "Activity stopped");
+ try {
+ unregisterReceiver(statusUpdate);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+
+ try {
+ unregisterReceiver(notificationReceiver);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+
+ try {
+ unbindService(mConnection);
+ } catch (IllegalArgumentException e) {
+ // ignore
+ }
+ if (positionObserver != null) {
+ positionObserver.cancel(true);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "Resuming Activity");
+ StorageUtils.checkStorageAvailability(this);
+ bindToService();
+
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ Log.d(TAG, "Configuration changed");
+ orientation = newConfig.orientation;
+ if (positionObserver != null) {
+ positionObserver.cancel(true);
+ }
+ setupGUI();
+ handleStatus();
+
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (playbackService.isRunning && playbackService != null
+ && playbackService.isPlayingVideo()) {
+ playbackService.stop();
+ }
+ if (videoControlsToggler != null) {
+ videoControlsToggler.cancel(true);
+ }
+ finish();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.d(TAG, "Creating Activity");
+ StorageUtils.checkStorageAvailability(this);
+
+ orientation = getResources().getConfiguration().orientation;
+ manager = FeedManager.getInstance();
+ getWindow().setFormat(PixelFormat.TRANSPARENT);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ bindToService();
+ }
+
+ private void bindToService() {
+ Intent serviceIntent = new Intent(this, PlaybackService.class);
+ boolean bound = false;
+ if (!PlaybackService.isRunning) {
+ Log.d(TAG, "Trying to restore last played media");
+ SharedPreferences prefs = getApplicationContext()
+ .getSharedPreferences(PodcastApp.PREF_NAME, 0);
+ long mediaId = prefs.getLong(PlaybackService.PREF_LAST_PLAYED_ID,
+ -1);
+ long feedId = prefs.getLong(
+ PlaybackService.PREF_LAST_PLAYED_FEED_ID, -1);
+ if (mediaId != -1 && feedId != -1) {
+ serviceIntent.putExtra(PlaybackService.EXTRA_FEED_ID, feedId);
+ serviceIntent.putExtra(PlaybackService.EXTRA_MEDIA_ID, mediaId);
+ serviceIntent.putExtra(
+ PlaybackService.EXTRA_START_WHEN_PREPARED, false);
+ serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM,
+ prefs.getBoolean(PlaybackService.PREF_LAST_IS_STREAM,
+ true));
+ startService(serviceIntent);
+ bound = bindService(serviceIntent, mConnection,
+ Context.BIND_AUTO_CREATE);
+ } else {
+ Log.d(TAG, "No last played media found");
+ status = PlayerStatus.STOPPED;
+ handleStatus();
+ }
+ } else {
+ bound = bindService(serviceIntent, mConnection, 0);
+ }
+ Log.d(TAG, "Result for service binding: " + bound);
+ }
+
+ private void handleStatus() {
+ switch (status) {
+
+ case ERROR:
+ setStatusMsg(R.string.player_error_msg, View.VISIBLE);
+ break;
+ case PAUSED:
+ setStatusMsg(R.string.player_paused_msg, View.VISIBLE);
+ loadMediaInfo();
+ if (positionObserver != null) {
+ positionObserver.cancel(true);
+ positionObserver = null;
+ }
+ butPlay.setImageResource(android.R.drawable.ic_media_play);
+ break;
+ case PLAYING:
+ setStatusMsg(R.string.player_playing_msg, View.INVISIBLE);
+ loadMediaInfo();
+ setupPositionObserver();
+ butPlay.setImageResource(android.R.drawable.ic_media_pause);
+ break;
+ case PREPARING:
+ setStatusMsg(R.string.player_preparing_msg, View.VISIBLE);
+ loadMediaInfo();
+ break;
+ case STOPPED:
+ setStatusMsg(R.string.player_stopped_msg, View.VISIBLE);
+ break;
+ case PREPARED:
+ loadMediaInfo();
+ setStatusMsg(R.string.player_ready_msg, View.VISIBLE);
+ butPlay.setImageResource(android.R.drawable.ic_media_play);
+ break;
+ case SEEKING:
+ setStatusMsg(R.string.player_seeking_msg, View.VISIBLE);
+ break;
+ case AWAITING_VIDEO_SURFACE:
+ Log.d(TAG, "Preparing video playback");
+ this.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ }
+ }
+
+ private void setStatusMsg(int resId, int visibility) {
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ if (visibility == View.VISIBLE) {
+ txtvStatus.setText(resId);
+ }
+ txtvStatus.setVisibility(visibility);
+ }
+ }
+
+ private void setupPositionObserver() {
+ if (positionObserver == null || positionObserver.isCancelled()) {
+ positionObserver = new MediaPositionObserver() {
+
+ @Override
+ protected void onProgressUpdate(Void... v) {
+ super.onProgressUpdate();
+ txtvPosition.setText(Converter
+ .getDurationStringLong(playbackService.getPlayer()
+ .getCurrentPosition()));
+ txtvLength.setText(Converter
+ .getDurationStringLong(playbackService.getPlayer()
+ .getDuration()));
+ updateProgressbarPosition();
+ }
+
+ };
+ positionObserver.execute(playbackService.getPlayer());
+ }
+ }
+
+ private void updateProgressbarPosition() {
+ Log.d(TAG, "Updating progressbar info");
+ MediaPlayer player = playbackService.getPlayer();
+ float progress = ((float) player.getCurrentPosition())
+ / player.getDuration();
+ sbPosition.setProgress((int) (progress * sbPosition.getMax()));
+ }
+
+ private void loadMediaInfo() {
+ if (!mediaInfoLoaded) {
+ Log.d(TAG, "Loading media info");
+ if (media != null) {
+
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ getSupportActionBar().setSubtitle(
+ media.getItem().getTitle());
+ getSupportActionBar().setTitle(
+ media.getItem().getFeed().getTitle());
+ pagerAdapter.notifyDataSetChanged();
+
+ }
+
+ txtvPosition.setText(Converter.getDurationStringLong((media
+ .getPosition())));
+
+ if (!playbackService.isShouldStream()) {
+ txtvLength.setText(Converter.getDurationStringLong(media
+ .getDuration()));
+ float progress = ((float) media.getPosition())
+ / media.getDuration();
+ sbPosition.setProgress((int) (progress * sbPosition
+ .getMax()));
+ }
+ }
+ mediaInfoLoaded = true;
+ }
+ }
+
+ private void setupGUI() {
+ setContentView(R.layout.mediaplayer_activity);
+ sbPosition = (SeekBar) findViewById(R.id.sbPosition);
+ txtvPosition = (TextView) findViewById(R.id.txtvPosition);
+ txtvLength = (TextView) findViewById(R.id.txtvLength);
+ butPlay = (ImageButton) findViewById(R.id.butPlay);
+ butRev = (ImageButton) findViewById(R.id.butRev);
+ butFF = (ImageButton) findViewById(R.id.butFF);
+
+ // SEEKBAR SETUP
+
+ sbPosition.setOnSeekBarChangeListener(new OnSeekBarChangeListener() {
+ int duration;
+ float prog;
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress,
+ boolean fromUser) {
+ if (fromUser) {
+ prog = progress / ((float) seekBar.getMax());
+ duration = playbackService.getPlayer().getDuration();
+ txtvPosition.setText(Converter
+ .getDurationStringLong((int) (prog * duration)));
+ }
+
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ // interrupt position Observer, restart later
+ if (positionObserver != null) {
+ positionObserver.cancel(true);
+ positionObserver = null;
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ playbackService.seek((int) (prog * duration));
+ setupPositionObserver();
+ }
+ });
+
+ // BUTTON SETUP
+
+ butPlay.setOnClickListener(playbuttonListener);
+
+ butFF.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (status == PlayerStatus.PLAYING) {
+ playbackService.seekDelta(DEFAULT_SEEK_DELTA);
+ }
+ }
+ });
+
+ butRev.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (status == PlayerStatus.PLAYING) {
+ playbackService.seekDelta(-DEFAULT_SEEK_DELTA);
+ }
+ }
+ });
+
+ // PORTRAIT ORIENTATION SETUP
+
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ txtvStatus = (TextView) findViewById(R.id.txtvStatus);
+ viewpager = (ViewPager) findViewById(R.id.viewpager);
+ tabs = (TabPageIndicator) findViewById(R.id.tabs);
+ pagerAdapter = new MediaPlayerPagerAdapter(
+ getSupportFragmentManager(), 2, this);
+ viewpager.setAdapter(pagerAdapter);
+ tabs.setViewPager(viewpager);
+ } else {
+ videoOverlay = (LinearLayout) findViewById(R.id.overlay);
+ videoview = (VideoView) findViewById(R.id.videoview);
+ videoview.getHolder().addCallback(this);
+ videoview.setOnClickListener(playbuttonListener);
+ videoview.setOnTouchListener(onVideoviewTouched);
+ setupVideoControlsToggler();
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
+ WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+ }
+
+ private OnClickListener playbuttonListener = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (status == PlayerStatus.PLAYING) {
+ playbackService.pause(true);
+ } else if (status == PlayerStatus.PAUSED
+ || status == PlayerStatus.PREPARED) {
+ playbackService.play();
+ }
+ }
+ };
+
+ private View.OnTouchListener onVideoviewTouched = new View.OnTouchListener() {
+
+ @Override
+ public boolean onTouch(View v, MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_DOWN) {
+ if (videoControlsToggler != null) {
+ videoControlsToggler.cancel(true);
+ }
+ toggleVideoControlsVisibility();
+ if (videoControlsShowing) {
+ setupVideoControlsToggler();
+ }
+
+ return true;
+ } else {
+ return false;
+ }
+ }
+ };
+
+ private void setupVideoControlsToggler() {
+ if (videoControlsToggler != null) {
+ videoControlsToggler.cancel(true);
+ }
+ videoControlsToggler = new VideoControlsHider();
+ videoControlsToggler.execute();
+ }
+
+ private void toggleVideoControlsVisibility() {
+ if (videoControlsShowing) {
+ getSupportActionBar().hide();
+ videoOverlay.setVisibility(View.GONE);
+ } else {
+ getSupportActionBar().show();
+ videoOverlay.setVisibility(View.VISIBLE);
+ }
+ videoControlsShowing = !videoControlsShowing;
+ }
+
+ private void handleError(int errorCode) {
+ final AlertDialog errorDialog = new AlertDialog.Builder(this).create();
+ errorDialog.setTitle(R.string.error_label);
+ errorDialog
+ .setMessage(MediaPlayerError.getErrorString(this, errorCode));
+ errorDialog.setButton("OK", new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ errorDialog.dismiss();
+ finish();
+ }
+ });
+ errorDialog.show();
+ }
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ playbackService = ((PlaybackService.LocalBinder) service)
+ .getService();
+ int requestedOrientation;
+ status = playbackService.getStatus();
+ media = playbackService.getMedia();
+
+ registerReceiver(statusUpdate, new IntentFilter(
+ PlaybackService.ACTION_PLAYER_STATUS_CHANGED));
+
+ registerReceiver(notificationReceiver, new IntentFilter(
+ PlaybackService.ACTION_PLAYER_NOTIFICATION));
+
+ if (playbackService.isPlayingVideo()) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+ } else {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
+ requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+ }
+ // check if orientation is correct
+ if ((requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE && orientation == Configuration.ORIENTATION_LANDSCAPE)
+ || (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT && orientation == Configuration.ORIENTATION_PORTRAIT)) {
+ Log.d(TAG, "Orientation correct");
+ setupGUI();
+ handleStatus();
+ } else {
+ Log.d(TAG,
+ "Orientation incorrect, waiting for orientation change");
+ }
+
+ Log.d(TAG, "Connection to Service established");
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ playbackService = null;
+ Log.d(TAG, "Disconnected from Service");
+
+ }
+ };
+
+ private BroadcastReceiver statusUpdate = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "Received statusUpdate Intent.");
+ status = playbackService.getStatus();
+ handleStatus();
+ }
+ };
+
+ private BroadcastReceiver notificationReceiver = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int type = intent.getIntExtra(
+ PlaybackService.EXTRA_NOTIFICATION_TYPE, -1);
+ int code = intent.getIntExtra(
+ PlaybackService.EXTRA_NOTIFICATION_CODE, -1);
+ if (code != -1 && type != -1) {
+ switch (type) {
+ case PlaybackService.NOTIFICATION_TYPE_ERROR:
+ handleError(code);
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_BUFFER_UPDATE:
+ if (sbPosition != null) {
+ float progress = ((float) code) / 100;
+ sbPosition.setSecondaryProgress((int) progress
+ * sbPosition.getMax());
+ }
+ break;
+ case PlaybackService.NOTIFICATION_TYPE_RELOAD:
+ try {
+ unbindService(mConnection);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ }
+ if (positionObserver != null) {
+ positionObserver.cancel(true);
+ positionObserver = null;
+ }
+ mediaInfoLoaded = false;
+ bindToService();
+ break;
+ }
+
+ } else {
+ Log.d(TAG, "Bad arguments. Won't handle intent");
+ }
+
+ }
+
+ };
+
+ /** Refreshes the current position of the media file that is playing. */
+ public class MediaPositionObserver extends
+ AsyncTask<MediaPlayer, Void, Void> {
+
+ private static final String TAG = "MediaPositionObserver";
+ private static final int WAITING_INTERVALL = 1000;
+ private MediaPlayer player;
+
+ @Override
+ protected void onCancelled() {
+ Log.d(TAG, "Task was cancelled");
+ }
+
+ @Override
+ protected Void doInBackground(MediaPlayer... p) {
+ Log.d(TAG, "Background Task started");
+ player = p[0];
+ try {
+ while (player.isPlaying() && !isCancelled()) {
+ try {
+ Thread.sleep(WAITING_INTERVALL);
+ } catch (InterruptedException e) {
+ Log.d(TAG,
+ "Thread was interrupted while waiting. Finishing now");
+ return null;
+ }
+ publishProgress();
+
+ }
+ } catch (IllegalStateException e) {
+ Log.d(TAG, "player is in illegal state, exiting now");
+ }
+ Log.d(TAG, "Background Task finished");
+ return null;
+ }
+ }
+
+ /** Hides the videocontrols after a certain period of time. */
+ public class VideoControlsHider extends AsyncTask<Void, Void, Void> {
+ @Override
+ protected void onCancelled() {
+ videoControlsToggler = null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ videoControlsToggler = null;
+ }
+
+ private static final int WAITING_INTERVALL = 5000;
+ private static final String TAG = "VideoControlsToggler";
+
+ @Override
+ protected void onProgressUpdate(Void... values) {
+ if (videoControlsShowing) {
+ Log.d(TAG, "Hiding video controls");
+ getSupportActionBar().hide();
+ videoOverlay.setVisibility(View.GONE);
+ videoControlsShowing = false;
+ }
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ try {
+ Thread.sleep(WAITING_INTERVALL);
+ } catch (InterruptedException e) {
+ return null;
+ }
+ publishProgress();
+ return null;
+ }
+
+ }
+
+ private boolean holderCreated;
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width,
+ int height) {
+ holder.setFixedSize(width, height);
+ }
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ holderCreated = true;
+ Log.d(TAG, "Videoview holder created");
+ if (status == PlayerStatus.AWAITING_VIDEO_SURFACE) {
+ playbackService.setVideoSurface(holder);
+ }
+
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ holderCreated = false;
+ }
+
+ public static class MediaPlayerPagerAdapter extends FragmentPagerAdapter {
+ private int numItems;
+ private MediaplayerActivity activity;
+
+ private static final int POS_COVER = 0;
+ private static final int POS_DESCR = 1;
+ private static final int POS_CHAPTERS = 2;
+
+ public MediaPlayerPagerAdapter(FragmentManager fm, int numItems,
+ MediaplayerActivity activity) {
+ super(fm);
+ this.numItems = numItems;
+ this.activity = activity;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ if (activity.media != null) {
+ switch (position) {
+ case POS_COVER:
+ activity.coverFragment = CoverFragment
+ .newInstance(activity.media.getItem());
+ return activity.coverFragment;
+ case POS_DESCR:
+ activity.descriptionFragment = ItemDescriptionFragment
+ .newInstance(activity.media.getItem(), true);
+ return activity.descriptionFragment;
+ default:
+ return CoverFragment.newInstance(null);
+ }
+ } else {
+ return CoverFragment.newInstance(null);
+ }
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ switch (position) {
+ case POS_COVER:
+ return activity.getString(R.string.cover_label);
+ case POS_DESCR:
+ return activity.getString(R.string.description_label);
+ default:
+ return super.getPageTitle(position);
+ }
+ }
+
+ @Override
+ public int getCount() {
+ return numItems;
+ }
+
+ @Override
+ public int getItemPosition(Object object) {
+ return POSITION_UNCHANGED;
+ }
+
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/activity/PreferenceActivity.java b/src/de/danoeh/antennapod/activity/PreferenceActivity.java
new file mode 100644
index 000000000..374443103
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/PreferenceActivity.java
@@ -0,0 +1,80 @@
+package de.danoeh.antennapod.activity;
+
+import android.os.Bundle;
+import android.preference.Preference;
+import android.preference.Preference.OnPreferenceClickListener;
+import android.util.Log;
+
+import com.actionbarsherlock.app.SherlockPreferenceActivity;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuItem;
+
+import de.danoeh.antennapod.util.FlattrUtils;
+import de.danoeh.antennapod.R;
+
+public class PreferenceActivity extends SherlockPreferenceActivity {
+ private static final String TAG = "PreferenceActivity";
+
+ private static final String PREF_FLATTR_THIS_APP = "prefFlattrThisApp";
+ private static final String PREF_FLATTR_AUTH = "pref_flattr_authenticate";
+ private static final String PREF_FLATTR_REVOKE = "prefRevokeAccess";
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ addPreferencesFromResource(R.xml.preferences);
+ findPreference(PREF_FLATTR_THIS_APP).setOnPreferenceClickListener(
+ new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Log.d(TAG, "Flattring this app"); // TODO implement
+ return true;
+ }
+ });
+ findPreference(PREF_FLATTR_REVOKE).setOnPreferenceClickListener(new OnPreferenceClickListener() {
+
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ FlattrUtils.revokeAccessToken(PreferenceActivity.this);
+ checkItemVisibility();
+ return true;
+ }
+
+ });
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ checkItemVisibility();
+ }
+
+ @SuppressWarnings("deprecation")
+ private void checkItemVisibility() {
+ boolean hasFlattrToken = FlattrUtils.hasToken();
+ findPreference(PREF_FLATTR_AUTH).setEnabled(!hasFlattrToken);
+ findPreference(PREF_FLATTR_REVOKE).setEnabled(hasFlattrToken);
+
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ finish();
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/activity/StorageErrorActivity.java b/src/de/danoeh/antennapod/activity/StorageErrorActivity.java
new file mode 100644
index 000000000..6e2d1b0cf
--- /dev/null
+++ b/src/de/danoeh/antennapod/activity/StorageErrorActivity.java
@@ -0,0 +1,66 @@
+package de.danoeh.antennapod.activity;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.actionbarsherlock.app.SherlockActivity;
+
+import de.danoeh.antennapod.util.StorageUtils;
+import de.danoeh.antennapod.R;
+
+public class StorageErrorActivity extends SherlockActivity {
+ private static final String TAG = "StorageErrorActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.storage_error);
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ try {
+ unregisterReceiver(mediaUpdate);
+ } catch (IllegalArgumentException e) {
+
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (StorageUtils.storageAvailable()) {
+ leaveErrorState();
+ } else {
+ registerReceiver(mediaUpdate, new IntentFilter(
+ Intent.ACTION_MEDIA_MOUNTED));
+ }
+ }
+
+ private void leaveErrorState() {
+ finish();
+ startActivity(new Intent(this, MainActivity.class));
+ }
+
+ private BroadcastReceiver mediaUpdate = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_MEDIA_MOUNTED)) {
+ if (intent.getBooleanExtra("read-only", true)) {
+ Log.d(TAG, "Media was mounted; Finishing activity");
+ leaveErrorState();
+ } else {
+ Log.d(TAG, "Media seemed to have been mounted read only");
+ }
+ }
+ }
+
+ };
+
+}
diff --git a/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java
new file mode 100644
index 000000000..98948617c
--- /dev/null
+++ b/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java
@@ -0,0 +1,86 @@
+package de.danoeh.antennapod.adapter;
+
+import java.text.DateFormat;
+import java.util.List;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+import de.danoeh.antennapod.asynctask.DownloadStatus;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedFile;
+import de.danoeh.antennapod.feed.FeedImage;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.util.DownloadError;
+import de.danoeh.antennapod.R;
+
+/** Displays a list of DownloadStatus entries. */
+public class DownloadLogAdapter extends ArrayAdapter<DownloadStatus> {
+
+ public DownloadLogAdapter(Context context,
+ int textViewResourceId, List<DownloadStatus> objects) {
+ super(context, textViewResourceId, objects);
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+ DownloadStatus status = getItem(position);
+ FeedFile feedfile = status.getFeedFile();
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.downloadlog_item, null);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.type = (TextView) convertView.findViewById(R.id.txtvType);
+ holder.date = (TextView) convertView.findViewById(R.id.txtvDate);
+ holder.successful = (TextView) convertView
+ .findViewById(R.id.txtvStatus);
+ holder.reason = (TextView) convertView
+ .findViewById(R.id.txtvReason);
+ if (feedfile.getClass() == Feed.class) {
+ holder.title.setText(((Feed) feedfile).getTitle());
+ holder.type.setText("Feed");
+ } else if (feedfile.getClass() == FeedMedia.class) {
+ holder.title.setText(((FeedMedia) feedfile).getItem()
+ .getTitle());
+ holder.type.setText(((FeedMedia) feedfile).getMime_type());
+ } else if (feedfile.getClass() == FeedImage.class) {
+ holder.title.setText(((FeedImage) feedfile).getTitle());
+ holder.type.setText("Image");
+ }
+ holder.date.setText("On "
+ + DateUtils.formatSameDayTime(status.getCompletionDate()
+ .getTime(), System.currentTimeMillis(),
+ DateFormat.SHORT, DateFormat.SHORT));
+ if (status.isSuccessful()) {
+ holder.successful.setTextColor(Color.parseColor("green"));
+ holder.successful.setText("Download succeeded");
+ holder.reason.setVisibility(View.GONE);
+ } else {
+ holder.successful.setTextColor(Color.parseColor("red"));
+ holder.successful.setText("Download failed");
+ holder.reason.setText(DownloadError.getErrorString(getContext(), status.getReason()));
+ }
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ return convertView;
+ }
+
+ static class Holder {
+ TextView title;
+ TextView type;
+ TextView date;
+ TextView successful;
+ TextView reason;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java
new file mode 100644
index 000000000..380ba32be
--- /dev/null
+++ b/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java
@@ -0,0 +1,101 @@
+package de.danoeh.antennapod.adapter;
+
+import java.util.List;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+import de.danoeh.antennapod.asynctask.DownloadStatus;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedFile;
+import de.danoeh.antennapod.feed.FeedImage;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.util.Converter;
+import de.danoeh.antennapod.R;
+
+public class DownloadlistAdapter extends ArrayAdapter<DownloadStatus> {
+ private int selectedItemIndex;
+
+ public static final int SELECTION_NONE = -1;
+
+ public DownloadlistAdapter(Context context, int textViewResourceId,
+ List<DownloadStatus> objects) {
+ super(context, textViewResourceId, objects);
+ this.selectedItemIndex = SELECTION_NONE;
+ }
+
+
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+ DownloadStatus status = getItem(position);
+ FeedFile feedFile = status.getFeedFile();
+ // Inflate layout
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.downloadlist_item, null);
+ holder.title = (TextView) convertView.findViewById(R.id.txtvTitle);
+ holder.message = (TextView) convertView
+ .findViewById(R.id.txtvMessage);
+ holder.downloaded = (TextView) convertView
+ .findViewById(R.id.txtvDownloaded);
+ holder.percent = (TextView) convertView
+ .findViewById(R.id.txtvPercent);
+ holder.progbar = (ProgressBar) convertView
+ .findViewById(R.id.progProgress);
+
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ if (position == selectedItemIndex) {
+ convertView.setBackgroundColor(convertView.getResources().getColor(
+ R.color.selection_background));
+ } else {
+ convertView.setBackgroundResource(0);
+ }
+
+ String titleText = null;
+ if (feedFile.getClass() == FeedMedia.class) {
+ titleText = ((FeedMedia) feedFile).getItem().getTitle();
+ } else if (feedFile.getClass() == Feed.class) {
+ titleText = ((Feed) feedFile).getTitle();
+ } else if (feedFile.getClass() == FeedImage.class) {
+ titleText = "[Image] " + ((FeedImage) feedFile).getTitle();
+ }
+ holder.title.setText(titleText);
+ holder.message.setText(status.getStatusMsg());
+ holder.downloaded.setText(Converter.byteToString(status.getSoFar())
+ + " / " + Converter.byteToString(status.getSize()));
+ holder.percent.setText(status.getProgressPercent() + "%");
+ holder.progbar.setProgress(status.getProgressPercent());
+
+ return convertView;
+ }
+
+ static class Holder {
+ TextView title;
+ TextView message;
+ TextView downloaded;
+ TextView percent;
+ ProgressBar progbar;
+ }
+
+ public int getSelectedItemIndex() {
+ return selectedItemIndex;
+ }
+
+ public void setSelectedItemIndex(int selectedItemIndex) {
+ this.selectedItemIndex = selectedItemIndex;
+ notifyDataSetChanged();
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java b/src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java
new file mode 100644
index 000000000..5f068f590
--- /dev/null
+++ b/src/de/danoeh/antennapod/adapter/FeedItemlistAdapter.java
@@ -0,0 +1,166 @@
+package de.danoeh.antennapod.adapter;
+
+import java.text.DateFormat;
+import java.util.List;
+
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.util.Converter;
+import de.danoeh.antennapod.R;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.text.format.DateUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.Typeface;
+
+public class FeedItemlistAdapter extends ArrayAdapter<FeedItem> {
+ private OnClickListener onButActionClicked;
+ private boolean showFeedtitle;
+ private int selectedItemIndex;
+
+ public static final int SELECTION_NONE = -1;
+
+ public FeedItemlistAdapter(Context context, int textViewResourceId,
+ List<FeedItem> objects, OnClickListener onButActionClicked,
+ boolean showFeedtitle) {
+ super(context, textViewResourceId, objects);
+ this.onButActionClicked = onButActionClicked;
+ this.showFeedtitle = showFeedtitle;
+ this.selectedItemIndex = SELECTION_NONE;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+ FeedItem item = getItem(position);
+
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ convertView = inflater.inflate(R.layout.feeditemlist_item, null);
+ holder.title = (TextView) convertView
+ .findViewById(R.id.txtvItemname);
+ holder.lenSize = (TextView) convertView
+ .findViewById(R.id.txtvLenSize);
+ holder.butAction = (ImageButton) convertView
+ .findViewById(R.id.butAction);
+ holder.published = (TextView) convertView
+ .findViewById(R.id.txtvPublished);
+ holder.inPlaylist = (ImageView) convertView
+ .findViewById(R.id.imgvInPlaylist);
+ holder.downloaded = (ImageView) convertView
+ .findViewById(R.id.imgvDownloaded);
+ holder.type = (ImageView) convertView.findViewById(R.id.imgvType);
+ holder.downloading = (ImageView) convertView
+ .findViewById(R.id.imgvDownloading);
+ holder.encInfo = (RelativeLayout) convertView
+ .findViewById(R.id.enc_info);
+ if (showFeedtitle) {
+ holder.feedtitle = (TextView) convertView
+ .findViewById(R.id.txtvFeedname);
+ }
+
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+ }
+
+ if (position == selectedItemIndex) {
+ convertView.setBackgroundColor(convertView.getResources().getColor(
+ R.color.selection_background));
+ } else {
+ convertView.setBackgroundResource(0);
+ }
+
+ holder.title.setText(item.getTitle());
+ if (showFeedtitle) {
+ holder.feedtitle.setVisibility(View.VISIBLE);
+ holder.feedtitle.setText(item.getFeed().getTitle());
+ }
+ if (!item.isRead()) {
+ holder.title.setTypeface(Typeface.DEFAULT_BOLD);
+ } else {
+ holder.title.setTypeface(Typeface.DEFAULT);
+ }
+
+ holder.published.setText("Published: "
+ + DateUtils.formatSameDayTime(item.getPubDate().getTime(),
+ System.currentTimeMillis(), DateFormat.SHORT,
+ DateFormat.SHORT));
+
+ if (item.getMedia() == null) {
+ holder.encInfo.setVisibility(View.GONE);
+ } else {
+ holder.encInfo.setVisibility(View.VISIBLE);
+ if (FeedManager.getInstance().isInQueue(item)) {
+ holder.inPlaylist.setVisibility(View.VISIBLE);
+ } else {
+ holder.inPlaylist.setVisibility(View.GONE);
+ }
+ if (item.getMedia().isDownloaded()) {
+ holder.lenSize.setText(Converter.getDurationStringShort(item
+ .getMedia().getDuration()));
+ holder.downloaded.setVisibility(View.VISIBLE);
+ } else {
+ holder.lenSize.setText(Converter.byteToString(item.getMedia()
+ .getSize()));
+ holder.downloaded.setVisibility(View.GONE);
+ }
+
+ if (item.getMedia().isDownloading()) {
+ holder.downloading.setVisibility(View.VISIBLE);
+ } else {
+ holder.downloading.setVisibility(View.GONE);
+ }
+
+ String type = item.getMedia().getMime_type();
+
+ if (type.startsWith("audio")) {
+ holder.type.setImageResource(R.drawable.type_audio);
+ } else if (type.startsWith("video")) {
+ holder.type.setImageResource(R.drawable.type_video);
+ } else {
+ holder.type.setImageBitmap(null);
+ }
+ }
+
+ holder.butAction.setFocusable(false);
+ holder.butAction.setOnClickListener(onButActionClicked);
+
+ return convertView;
+
+ }
+
+ static class Holder {
+ TextView title;
+ TextView feedtitle;
+ TextView published;
+ TextView lenSize;
+ ImageView inPlaylist;
+ ImageView downloaded;
+ ImageView type;
+ ImageView downloading;
+ ImageButton butAction;
+ RelativeLayout encInfo;
+ }
+
+ public int getSelectedItemIndex() {
+ return selectedItemIndex;
+ }
+
+ public void setSelectedItemIndex(int selectedItemIndex) {
+ this.selectedItemIndex = selectedItemIndex;
+ notifyDataSetChanged();
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/adapter/FeedlistAdapter.java b/src/de/danoeh/antennapod/adapter/FeedlistAdapter.java
new file mode 100644
index 000000000..aeb0fede7
--- /dev/null
+++ b/src/de/danoeh/antennapod/adapter/FeedlistAdapter.java
@@ -0,0 +1,116 @@
+package de.danoeh.antennapod.adapter;
+
+import java.io.File;
+import java.text.DateFormat;
+import java.util.List;
+
+import de.danoeh.antennapod.asynctask.FeedImageLoader;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.storage.DownloadRequester;
+import de.danoeh.antennapod.R;
+import android.content.Context;
+import android.net.Uri;
+import android.text.format.DateUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+import android.graphics.BitmapFactory;
+import android.graphics.Color;
+
+public class FeedlistAdapter extends ArrayAdapter<Feed> {
+ private static final String TAG = "FeedlistAdapter";
+
+ private int selectedItemIndex;
+ private FeedImageLoader imageLoader;
+ public static final int SELECTION_NONE = -1;
+
+ public FeedlistAdapter(Context context, int textViewResourceId,
+ List<Feed> objects) {
+ super(context, textViewResourceId, objects);
+ selectedItemIndex = SELECTION_NONE;
+ imageLoader = FeedImageLoader.getInstance();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ Holder holder;
+ Feed feed = getItem(position);
+
+ // Inflate Layout
+ if (convertView == null) {
+ holder = new Holder();
+ LayoutInflater inflater = (LayoutInflater) getContext()
+ .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ convertView = inflater.inflate(R.layout.feedlist_item, null);
+ holder.title = (TextView) convertView
+ .findViewById(R.id.txtvFeedname);
+
+ holder.newEpisodes = (TextView) convertView
+ .findViewById(R.id.txtvNewEps);
+ holder.image = (ImageView) convertView
+ .findViewById(R.id.imgvFeedimage);
+ holder.lastUpdate = (TextView) convertView
+ .findViewById(R.id.txtvLastUpdate);
+ holder.numberOfEpisodes = (TextView) convertView
+ .findViewById(R.id.txtvNumEpisodes);
+ convertView.setTag(holder);
+ } else {
+ holder = (Holder) convertView.getTag();
+
+ }
+
+ if (position == selectedItemIndex) {
+ convertView.setBackgroundColor(convertView.getResources().getColor(
+ R.color.selection_background));
+ } else {
+ convertView.setBackgroundResource(0);
+ }
+
+ holder.title.setText(feed.getTitle());
+ if (DownloadRequester.getInstance().isDownloadingFile(feed)) {
+ holder.lastUpdate.setText(R.string.refreshing_label);
+ } else {
+ holder.lastUpdate.setText("Last Update: "
+ + DateUtils.formatSameDayTime(feed.getLastUpdate()
+ .getTime(), System.currentTimeMillis(),
+ DateFormat.SHORT, DateFormat.SHORT));
+ }
+ holder.numberOfEpisodes.setText(feed.getItems().size() + " Episodes");
+ int newItems = feed.getNumOfNewItems();
+ if (newItems > 0) {
+ holder.newEpisodes.setText(Integer.toString(newItems));
+ holder.newEpisodes.setVisibility(View.VISIBLE);
+ } else {
+ holder.newEpisodes.setVisibility(View.INVISIBLE);
+ }
+
+ imageLoader.loadBitmap(feed.getImage(), holder.image);
+
+ // TODO find new Episodes txtvNewEpisodes.setText(feed)
+ return convertView;
+ }
+
+ static class Holder {
+ TextView title;
+ TextView lastUpdate;
+ TextView numberOfEpisodes;
+ TextView newEpisodes;
+ ImageView image;
+ }
+
+ public int getSelectedItemIndex() {
+ return selectedItemIndex;
+ }
+
+ public void setSelectedItemIndex(int selectedItemIndex) {
+ this.selectedItemIndex = selectedItemIndex;
+ notifyDataSetChanged();
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/asynctask/DownloadObserver.java b/src/de/danoeh/antennapod/asynctask/DownloadObserver.java
new file mode 100644
index 000000000..1aca934ed
--- /dev/null
+++ b/src/de/danoeh/antennapod/asynctask/DownloadObserver.java
@@ -0,0 +1,211 @@
+package de.danoeh.antennapod.asynctask;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import android.app.DownloadManager;
+import android.content.Context;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.util.Log;
+import de.danoeh.antennapod.feed.FeedFile;
+import de.danoeh.antennapod.storage.DownloadRequester;
+import de.danoeh.antennapod.R;
+
+/** Observes the status of a specific Download */
+public class DownloadObserver extends AsyncTask<Void, Void, Void> {
+ private static final String TAG = "DownloadObserver";
+
+ /** Types of downloads to observe. */
+ public static final int TYPE_FEED = 0;
+ public static final int TYPE_IMAGE = 1;
+ public static final int TYPE_MEDIA = 2;
+
+ /** Error codes */
+ public static final int ALREADY_DOWNLOADED = 1;
+ public static final int NO_DOWNLOAD_FOUND = 2;
+
+ private final long DEFAULT_WAITING_INTERVALL = 1000L;
+
+ private DownloadRequester requester;
+ private Context context;
+ private ArrayList<DownloadStatus> statusList;
+ private List<DownloadObserver.Callback> observer;
+
+ public DownloadObserver(Context context) {
+ super();
+ this.context = context;
+ requester = DownloadRequester.getInstance();
+ statusList = new ArrayList<DownloadStatus>();
+ observer = Collections
+ .synchronizedList(new ArrayList<DownloadObserver.Callback>());
+ }
+
+ @Override
+ protected void onCancelled() {
+ Log.d(TAG, "Task was cancelled.");
+ statusList.clear();
+ for (DownloadObserver.Callback callback : observer) {
+ callback.onFinish();
+ }
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ Log.d(TAG, "Background task has finished");
+ statusList.clear();
+ for (DownloadObserver.Callback callback : observer) {
+ callback.onFinish();
+ }
+ }
+
+ protected Void doInBackground(Void... params) {
+ Log.d(TAG, "Background Task started.");
+ while (downloadsLeft() && !isCancelled()) {
+ refreshStatuslist();
+ publishProgress();
+ try {
+ Thread.sleep(DEFAULT_WAITING_INTERVALL);
+ } catch (InterruptedException e) {
+ Log.w(TAG, "Thread was interrupted while waiting.");
+ }
+ }
+ Log.d(TAG, "Background Task finished.");
+ return null;
+ }
+
+ @Override
+ protected void onProgressUpdate(Void... values) {
+ for (DownloadObserver.Callback callback : observer) {
+ callback.onProgressUpdate();
+ }
+ }
+
+ private void refreshStatuslist() {
+ ArrayList<DownloadStatus> unhandledItems = new ArrayList<DownloadStatus>(
+ statusList);
+
+ Cursor cursor = getDownloadCursor();
+ if (cursor.moveToFirst()) {
+ do {
+ long downloadId = getDownloadStatus(cursor,
+ DownloadManager.COLUMN_ID);
+ FeedFile feedFile = requester.getFeedFile(downloadId);
+ if (feedFile != null) {
+ DownloadStatus status = findDownloadStatus(feedFile);
+
+ if (status == null) {
+ status = new DownloadStatus(feedFile);
+ statusList.add(status);
+ } else {
+ unhandledItems.remove(status);
+ }
+
+ // refresh status
+ int statusId = getDownloadStatus(cursor,
+ DownloadManager.COLUMN_STATUS);
+ getDownloadProgress(cursor, status);
+ switch (statusId) {
+ case DownloadManager.STATUS_SUCCESSFUL:
+ status.statusMsg = R.string.download_successful;
+ status.successful = true;
+ status.done = true;
+ case DownloadManager.STATUS_RUNNING:
+ status.statusMsg = R.string.download_running;
+ break;
+ case DownloadManager.STATUS_FAILED:
+ status.statusMsg = R.string.download_failed;
+ requester.notifyDownloadService(context);
+ status.successful = false;
+ status.done = true;
+ status.reason = getDownloadStatus(cursor,
+ DownloadManager.COLUMN_REASON);
+ case DownloadManager.STATUS_PENDING:
+ status.statusMsg = R.string.download_pending;
+ break;
+ default:
+ status.done = true;
+ status.successful = false;
+ status.statusMsg = R.string.download_cancelled_msg;
+ }
+ }
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+
+ // remove unhandled items from statuslist
+ for (DownloadStatus status : unhandledItems) {
+ statusList.remove(status);
+ }
+ }
+
+ /** Request a cursor with all running Feedfile downloads */
+ private Cursor getDownloadCursor() {
+ // Collect download ids
+ int numDownloads = requester.getNumberOfDownloads();
+ long ids[] = new long[numDownloads];
+ for (int i = 0; i < numDownloads; i++) {
+ ids[i] = requester.downloads.get(i).getDownloadId();
+ }
+ DownloadManager.Query query = new DownloadManager.Query();
+ query.setFilterById(ids);
+ DownloadManager manager = (DownloadManager) context
+ .getSystemService(Context.DOWNLOAD_SERVICE);
+
+ Cursor result = manager.query(query);
+ return result;
+ }
+
+ /** Return value of a specific column */
+ private int getDownloadStatus(Cursor c, String column) {
+ int status = c.getInt(c.getColumnIndex(column));
+ return status;
+ }
+
+ private void getDownloadProgress(Cursor c, DownloadStatus status) {
+ status.size = c.getLong(c
+ .getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES));
+ status.soFar = c
+ .getLong(c
+ .getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR));
+ status.progressPercent = (int) (((double) status.soFar / (double) status.size) * 100);
+ }
+
+ public Context getContext() {
+ return context;
+ }
+
+ /** Find a DownloadStatus entry by its FeedFile */
+ public DownloadStatus findDownloadStatus(FeedFile f) {
+ for (DownloadStatus status : statusList) {
+ if (status.feedfile == f) {
+ return status;
+ }
+ }
+ return null;
+ }
+
+ public ArrayList<DownloadStatus> getStatusList() {
+ return statusList;
+ }
+
+ private boolean downloadsLeft() {
+ return !requester.downloads.isEmpty();
+ }
+
+ public void registerCallback(DownloadObserver.Callback callback) {
+ observer.add(callback);
+ }
+
+ public void unregisterCallback(DownloadObserver.Callback callback) {
+ observer.remove(callback);
+ }
+
+ public interface Callback {
+ public void onProgressUpdate();
+
+ public void onFinish();
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/asynctask/DownloadStatus.java b/src/de/danoeh/antennapod/asynctask/DownloadStatus.java
new file mode 100644
index 000000000..67cf4a6d8
--- /dev/null
+++ b/src/de/danoeh/antennapod/asynctask/DownloadStatus.java
@@ -0,0 +1,95 @@
+package de.danoeh.antennapod.asynctask;
+
+import java.util.Date;
+
+import de.danoeh.antennapod.feed.FeedFile;
+
+/** Contains status attributes for one download */
+public class DownloadStatus {
+
+ public Date getCompletionDate() {
+ return completionDate;
+ }
+
+ /** Unique id for storing the object in database. */
+ protected long id;
+
+ protected FeedFile feedfile;
+ protected int progressPercent;
+ protected long soFar;
+ protected long size;
+ protected int statusMsg;
+ protected int reason;
+ protected boolean successful;
+ protected boolean done;
+ protected Date completionDate;
+
+ public DownloadStatus(FeedFile feedfile) {
+ this.feedfile = feedfile;
+ }
+
+ /** Constructor for restoring Download status entries from DB. */
+ public DownloadStatus(long id, FeedFile feedfile, boolean successful, int reason,
+ Date completionDate) {
+ this.id = id;
+ this.feedfile = feedfile;
+ progressPercent = 100;
+ soFar = 0;
+ size = 0;
+ this.reason = reason;
+ this.successful = successful;
+ this.done = true;
+ this.completionDate = completionDate;
+ }
+
+
+ /** Constructor for creating new completed downloads. */
+ public DownloadStatus(FeedFile feedfile, int reason,
+ boolean successful) {
+ this(0, feedfile, successful, reason, new Date());
+ }
+
+ public FeedFile getFeedFile() {
+ return feedfile;
+ }
+
+ public int getProgressPercent() {
+ return progressPercent;
+ }
+
+ public long getSoFar() {
+ return soFar;
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public int getStatusMsg() {
+ return statusMsg;
+ }
+
+ public int getReason() {
+ return reason;
+ }
+
+ public boolean isSuccessful() {
+ return successful;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public boolean isDone() {
+ return done;
+ }
+
+
+
+
+} \ No newline at end of file
diff --git a/src/de/danoeh/antennapod/asynctask/FeedImageLoader.java b/src/de/danoeh/antennapod/asynctask/FeedImageLoader.java
new file mode 100644
index 000000000..7d411a329
--- /dev/null
+++ b/src/de/danoeh/antennapod/asynctask/FeedImageLoader.java
@@ -0,0 +1,178 @@
+package de.danoeh.antennapod.asynctask;
+
+import java.io.File;
+
+import de.danoeh.antennapod.PodcastApp;
+import de.danoeh.antennapod.feed.FeedImage;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.storage.DownloadRequester;
+import de.danoeh.antennapod.R;
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.os.AsyncTask;
+import android.support.v4.util.LruCache;
+import android.util.Log;
+import android.widget.ImageView;
+
+/** Caches and loads FeedImage bitmaps in the background */
+public class FeedImageLoader {
+ private static final String TAG = "FeedImageLoader";
+ private static FeedImageLoader singleton;
+
+ /**
+ * Stores references to loaded bitmaps. Bitmaps can be accessed by the id of
+ * the FeedImage the bitmap belongs to.
+ */
+
+ final int memClass = ((ActivityManager) PodcastApp.getInstance()
+ .getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();
+
+ // Use 1/8th of the available memory for this memory cache.
+ final int cacheSize = 1024 * 1024 * memClass / 8;
+ private LruCache<Long, Bitmap> imageCache;
+
+ private FeedImageLoader() {
+ Log.d(TAG, "Creating cache with size " + cacheSize);
+ imageCache = new LruCache<Long, Bitmap>(cacheSize) {
+
+ @SuppressLint("NewApi")
+ @Override
+ protected int sizeOf(Long key, Bitmap value) {
+ if (Integer.valueOf(android.os.Build.VERSION.SDK_INT) >= 12)
+ return value.getByteCount();
+ else
+ return (value.getRowBytes() * value.getHeight());
+
+ }
+
+ };
+ }
+
+ public static FeedImageLoader getInstance() {
+ if (singleton == null) {
+ singleton = new FeedImageLoader();
+ }
+ return singleton;
+ }
+
+ public void loadBitmap(FeedImage image, ImageView target) {
+ if (image != null) {
+ Bitmap bitmap = getBitmapFromCache(image.getId());
+ if (bitmap != null) {
+ target.setImageBitmap(bitmap);
+ } else {
+ target.setImageResource(R.drawable.default_cover);
+ BitmapWorkerTask worker = new BitmapWorkerTask(target);
+ worker.execute(image);
+ }
+ } else {
+ target.setImageResource(R.drawable.default_cover);
+ }
+ }
+
+ public void addBitmapToCache(long id, Bitmap bitmap) {
+ imageCache.put(id, bitmap);
+ }
+
+ public void wipeImageCache() {
+ imageCache.evictAll();
+ }
+
+ public boolean isInCache(FeedImage image) {
+ return imageCache.get(image.getId()) != null;
+ }
+
+ public Bitmap getBitmapFromCache(long id) {
+ return imageCache.get(id);
+ }
+
+ class BitmapWorkerTask extends AsyncTask<FeedImage, Void, Void> {
+ /** The preferred width and height of a bitmap. */
+ private static final int PREFERRED_LENGTH = 300;
+
+ private static final String TAG = "BitmapWorkerTask";
+ private ImageView target;
+ private Bitmap bitmap;
+ private Bitmap decodedBitmap;
+
+ public BitmapWorkerTask(ImageView target) {
+ super();
+ this.target = target;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ target.setImageBitmap(bitmap);
+ }
+
+ private int calculateSampleSize(int width, int height) {
+ int max = Math.max(width, height);
+ if (max < PREFERRED_LENGTH) {
+ return 1;
+ } else {
+ // find first sample size where max / sampleSize <
+ // PREFERRED_LENGTH
+ for (int sampleSize = 1, power = 0;; power++, sampleSize = (int) Math
+ .pow(2, power)) {
+ int newLength = max / sampleSize;
+ if (newLength <= PREFERRED_LENGTH) {
+ if (newLength > 0) {
+ return sampleSize;
+ } else {
+ return sampleSize - 1;
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ protected Void doInBackground(FeedImage... params) {
+ File f = null;
+ if (params[0].getFile_url() != null) {
+ f = new File(params[0].getFile_url());
+ }
+ if (params[0].getFile_url() != null && f.exists()) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(params[0].getFile_url(), options);
+ int sampleSize = calculateSampleSize(options.outWidth,
+ options.outHeight);
+
+ options.inJustDecodeBounds = false;
+ options.inSampleSize = sampleSize;
+ decodedBitmap = BitmapFactory.decodeFile(
+ params[0].getFile_url(), options);
+ if (decodedBitmap == null) {
+ Log.i(TAG,
+ "Bitmap could not be decoded in custom sample size. Trying default sample size (path was "
+ + params[0].getFile_url() + ")");
+ decodedBitmap = BitmapFactory.decodeFile(params[0]
+ .getFile_url());
+ }
+ bitmap = Bitmap.createScaledBitmap(decodedBitmap,
+ PREFERRED_LENGTH, PREFERRED_LENGTH, false);
+
+ addBitmapToCache(params[0].getId(), bitmap);
+ Log.d(TAG, "Finished loading bitmaps");
+ } else {
+ Log.e(TAG,
+ "FeedImage has no valid file url. Using default image");
+ bitmap = BitmapFactory.decodeResource(target.getResources(),
+ R.drawable.default_cover);
+ if (params[0].getFile_url() != null
+ && !DownloadRequester.getInstance().isDownloadingFile(
+ params[0])) {
+ FeedManager.getInstance().notifyInvalidImageFile(
+ PodcastApp.getInstance(), params[0]);
+ }
+ }
+ return null;
+ }
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/asynctask/FeedRemover.java b/src/de/danoeh/antennapod/asynctask/FeedRemover.java
new file mode 100644
index 000000000..7802b5677
--- /dev/null
+++ b/src/de/danoeh/antennapod/asynctask/FeedRemover.java
@@ -0,0 +1,62 @@
+package de.danoeh.antennapod.asynctask;
+
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedManager;
+import android.app.ProgressDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnCancelListener;
+import android.os.AsyncTask;
+
+/** Removes a feed in the background. */
+public class FeedRemover extends AsyncTask<Feed, Void, Void> {
+ Context context;
+ ProgressDialog dialog;
+
+ public FeedRemover(Context context) {
+ super();
+ this.context = context;
+ }
+
+ @Override
+ protected Void doInBackground(Feed... params) {
+ FeedManager manager = FeedManager.getInstance();
+ for (Feed feed : params) {
+ manager.deleteFeed(context, feed);
+ if (isCancelled()) {
+ break;
+ }
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onCancelled() {
+ dialog.dismiss();
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ dialog.dismiss();
+ }
+
+ @Override
+ protected void onPreExecute() {
+ dialog = new ProgressDialog(context);
+ dialog.setMessage("Removing Feed");
+ dialog.setOnCancelListener(new OnCancelListener() {
+
+ @Override
+ public void onCancel(DialogInterface dialog) {
+ cancel(true);
+
+ }
+
+ });
+ dialog.show();
+ }
+
+
+
+}
diff --git a/src/de/danoeh/antennapod/feed/Feed.java b/src/de/danoeh/antennapod/feed/Feed.java
new file mode 100644
index 000000000..9d732a81f
--- /dev/null
+++ b/src/de/danoeh/antennapod/feed/Feed.java
@@ -0,0 +1,129 @@
+package de.danoeh.antennapod.feed;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+/**
+ * Data Object for a whole feed
+ *
+ * @author daniel
+ *
+ */
+public class Feed extends FeedFile {
+ private String title;
+ /** Link to the website. */
+ private String link;
+ private String description;
+ private String language;
+ /** Name of the author */
+ private String author;
+ private FeedImage image;
+ private FeedCategory category;
+ private ArrayList<FeedItem> items;
+ /** Date of last refresh. */
+ private Date lastUpdate;
+ private String paymentLink;
+
+ public Feed(Date lastUpdate) {
+ super();
+ items = new ArrayList<FeedItem>();
+ this.lastUpdate = lastUpdate;
+ }
+
+ public Feed(String url, Date lastUpdate) {
+ this(lastUpdate);
+ this.download_url = url;
+ }
+
+ /** Returns the number of FeedItems where 'read' is false. */
+ public int getNumOfNewItems() {
+ int count = 0;
+ for (FeedItem item : items) {
+ if (!item.isRead()) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public FeedImage getImage() {
+ return image;
+ }
+
+ public void setImage(FeedImage image) {
+ this.image = image;
+ }
+
+ public FeedCategory getCategory() {
+ return category;
+ }
+
+ public void setCategory(FeedCategory category) {
+ this.category = category;
+ }
+
+ public ArrayList<FeedItem> getItems() {
+ return items;
+ }
+
+ public void setItems(ArrayList<FeedItem> items) {
+ this.items = items;
+ }
+
+ public Date getLastUpdate() {
+ return lastUpdate;
+ }
+
+ public void setLastUpdate(Date lastUpdate) {
+ this.lastUpdate = lastUpdate;
+ }
+
+ public String getPaymentLink() {
+ return paymentLink;
+ }
+
+ public void setPaymentLink(String paymentLink) {
+ this.paymentLink = paymentLink;
+ }
+
+ public String getLanguage() {
+ return language;
+ }
+
+ public void setLanguage(String language) {
+ this.language = language;
+ }
+
+ public String getAuthor() {
+ return author;
+ }
+
+ public void setAuthor(String author) {
+ this.author = author;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/feed/FeedCategory.java b/src/de/danoeh/antennapod/feed/FeedCategory.java
new file mode 100644
index 000000000..fc3d8d79b
--- /dev/null
+++ b/src/de/danoeh/antennapod/feed/FeedCategory.java
@@ -0,0 +1,20 @@
+package de.danoeh.antennapod.feed;
+
+public class FeedCategory extends FeedComponent{
+ protected String name;
+
+ public FeedCategory(String name) {
+ super();
+ this.name = name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+
+
+
+
+
+}
diff --git a/src/de/danoeh/antennapod/feed/FeedComponent.java b/src/de/danoeh/antennapod/feed/FeedComponent.java
new file mode 100644
index 000000000..a192f4bc8
--- /dev/null
+++ b/src/de/danoeh/antennapod/feed/FeedComponent.java
@@ -0,0 +1,26 @@
+package de.danoeh.antennapod.feed;
+
+/**
+ * Represents every possible component of a feed
+ * @author daniel
+ *
+ */
+public class FeedComponent {
+
+ protected long id;
+
+ public FeedComponent() {
+ super();
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+
+
+} \ No newline at end of file
diff --git a/src/de/danoeh/antennapod/feed/FeedFile.java b/src/de/danoeh/antennapod/feed/FeedFile.java
new file mode 100644
index 000000000..c7a9b7bc1
--- /dev/null
+++ b/src/de/danoeh/antennapod/feed/FeedFile.java
@@ -0,0 +1,55 @@
+package de.danoeh.antennapod.feed;
+
+/** Represents a component of a Feed that has to be downloaded*/
+public abstract class FeedFile extends FeedComponent {
+ protected String file_url;
+ protected String download_url;
+ protected long downloadId; // temporary id given by the Android DownloadManager
+ protected boolean downloaded;
+
+ public FeedFile(String file_url, String download_url, boolean downloaded) {
+ super();
+ this.file_url = file_url;
+ this.download_url = download_url;
+ this.downloaded = downloaded;
+ }
+
+ public FeedFile() {
+ this(null, null, false);
+ }
+
+ public String getFile_url() {
+ return file_url;
+ }
+ public void setFile_url(String file_url) {
+ this.file_url = file_url;
+ }
+ public String getDownload_url() {
+ return download_url;
+ }
+ public void setDownload_url(String download_url) {
+ this.download_url = download_url;
+ }
+
+ public long getDownloadId() {
+ return downloadId;
+ }
+
+ public void setDownloadId(long downloadId) {
+ this.downloadId = downloadId;
+ }
+
+ public boolean isDownloaded() {
+ return downloaded;
+ }
+
+ public void setDownloaded(boolean downloaded) {
+ this.downloaded = downloaded;
+ }
+
+ public boolean isDownloading() {
+ return downloadId != 0;
+ }
+
+
+}
diff --git a/src/de/danoeh/antennapod/feed/FeedImage.java b/src/de/danoeh/antennapod/feed/FeedImage.java
new file mode 100644
index 000000000..4b53f3da4
--- /dev/null
+++ b/src/de/danoeh/antennapod/feed/FeedImage.java
@@ -0,0 +1,32 @@
+package de.danoeh.antennapod.feed;
+
+
+public class FeedImage extends FeedFile {
+ protected String title;
+
+ public FeedImage(String download_url, String title) {
+ super(null, download_url, false);
+ this.download_url = download_url;
+ this.title = title;
+ }
+
+ public FeedImage(long id, String title, String file_url,
+ String download_url, boolean downloaded) {
+ super(file_url, download_url, downloaded);
+ this.id = id;
+ this.title = title;
+ }
+
+ public FeedImage() {
+ super();
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/feed/FeedItem.java b/src/de/danoeh/antennapod/feed/FeedItem.java
new file mode 100644
index 000000000..732c61380
--- /dev/null
+++ b/src/de/danoeh/antennapod/feed/FeedItem.java
@@ -0,0 +1,117 @@
+package de.danoeh.antennapod.feed;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+
+/**
+ * Data Object for a XML message
+ * @author daniel
+ *
+ */
+public class FeedItem extends FeedComponent{
+
+ private String title;
+ private String description;
+ private String contentEncoded;
+ private String link;
+ private Date pubDate;
+ private FeedMedia media;
+ private Feed feed;
+ protected boolean read;
+ private String paymentLink;
+ private ArrayList<SimpleChapter> simpleChapters;
+
+ public FeedItem() {
+ this.read = true;
+ }
+
+ public FeedItem(String title, String description, String link,
+ Date pubDate, FeedMedia media, Feed feed) {
+ super();
+ this.title = title;
+ this.description = description;
+ this.link = link;
+ this.pubDate = pubDate;
+ this.media = media;
+ this.feed = feed;
+ this.read = true;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(String title) {
+ this.title = title;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public Date getPubDate() {
+ return pubDate;
+ }
+
+ public void setPubDate(Date pubDate) {
+ this.pubDate = pubDate;
+ }
+
+ public FeedMedia getMedia() {
+ return media;
+ }
+
+ public void setMedia(FeedMedia media) {
+ this.media = media;
+ }
+
+ public Feed getFeed() {
+ return feed;
+ }
+
+ public void setFeed(Feed feed) {
+ this.feed = feed;
+ }
+
+ public boolean isRead() {
+ return read;
+ }
+
+ public String getContentEncoded() {
+ return contentEncoded;
+ }
+
+ public void setContentEncoded(String contentEncoded) {
+ this.contentEncoded = contentEncoded;
+ }
+
+ public String getPaymentLink() {
+ return paymentLink;
+ }
+
+ public void setPaymentLink(String paymentLink) {
+ this.paymentLink = paymentLink;
+ }
+
+ public ArrayList<SimpleChapter> getSimpleChapters() {
+ return simpleChapters;
+ }
+
+ public void setSimpleChapters(ArrayList<SimpleChapter> simpleChapters) {
+ this.simpleChapters = simpleChapters;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/feed/FeedManager.java b/src/de/danoeh/antennapod/feed/FeedManager.java
new file mode 100644
index 000000000..26a3ffa77
--- /dev/null
+++ b/src/de/danoeh/antennapod/feed/FeedManager.java
@@ -0,0 +1,744 @@
+package de.danoeh.antennapod.feed;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+
+import de.danoeh.antennapod.activity.MediaplayerActivity;
+import de.danoeh.antennapod.asynctask.DownloadStatus;
+import de.danoeh.antennapod.service.PlaybackService;
+import de.danoeh.antennapod.storage.*;
+import de.danoeh.antennapod.util.FeedItemPubdateComparator;
+import de.danoeh.antennapod.util.FeedtitleComparator;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.os.Debug;
+import android.util.Log;
+
+/**
+ * Singleton class Manages all feeds, categories and feeditems
+ *
+ *
+ * */
+public class FeedManager {
+ private static final String TAG = "FeedManager";
+
+ public static final String ACTION_UNREAD_ITEMS_UPDATE = "de.danoeh.antennapod.action.feed.unreadItemsUpdate";
+ public static final String ACTION_QUEUE_UPDATE = "de.danoeh.antennapod.action.feed.queueUpdate";
+ public static final String EXTRA_FEED_ITEM_ID = "de.danoeh.antennapod.extra.feed.feedItemId";
+ public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feed.feedId";
+
+ /** Number of completed Download status entries to store. */
+ private static final int DOWNLOAD_LOG_SIZE = 25;
+
+ private static FeedManager singleton;
+
+ private ArrayList<Feed> feeds;
+ private ArrayList<FeedCategory> categories;
+
+ /** Contains all items where 'read' is false */
+ private ArrayList<FeedItem> unreadItems;
+
+ /** Contains completed Download status entries */
+ private ArrayList<DownloadStatus> downloadLog;
+
+ /** Contains the queue of items to be played. */
+ private ArrayList<FeedItem> queue;
+
+ private DownloadRequester requester;
+
+ private FeedManager() {
+ feeds = new ArrayList<Feed>();
+ categories = new ArrayList<FeedCategory>();
+ unreadItems = new ArrayList<FeedItem>();
+ requester = DownloadRequester.getInstance();
+ downloadLog = new ArrayList<DownloadStatus>();
+ queue = new ArrayList<FeedItem>();
+ }
+
+ public static FeedManager getInstance() {
+ if (singleton == null) {
+ singleton = new FeedManager();
+ }
+ return singleton;
+ }
+
+ /**
+ * Play FeedMedia and start the playback service + launch Mediaplayer
+ * Activity.
+ *
+ * @param context
+ * for starting the playbackservice
+ * @param media
+ * that shall be played
+ * @param showPlayer
+ * if Mediaplayer activity shall be started
+ * @param startWhenPrepared
+ * if Mediaplayer shall be started after it has been prepared
+ */
+ public void playMedia(Context context, FeedMedia media, boolean showPlayer,
+ boolean startWhenPrepared, boolean shouldStream) {
+ // Start playback Service
+ Intent launchIntent = new Intent(context, PlaybackService.class);
+ launchIntent.putExtra(PlaybackService.EXTRA_MEDIA_ID, media.getId());
+ launchIntent.putExtra(PlaybackService.EXTRA_FEED_ID, media.getItem()
+ .getFeed().getId());
+ launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED,
+ startWhenPrepared);
+ launchIntent
+ .putExtra(PlaybackService.EXTRA_SHOULD_STREAM, shouldStream);
+ context.startService(launchIntent);
+ if (showPlayer) {
+ // Launch Mediaplayer
+ Intent playerIntent = new Intent(context, MediaplayerActivity.class);
+ context.startActivity(playerIntent);
+ }
+ }
+
+ /** Remove media item that has been downloaded. */
+ public boolean deleteFeedMedia(Context context, FeedMedia media) {
+ boolean result = false;
+ if (media.isDownloaded()) {
+ File mediaFile = new File(media.file_url);
+ if (mediaFile.exists()) {
+ result = mediaFile.delete();
+ }
+ media.setDownloaded(false);
+ media.setFile_url(null);
+ setFeedMedia(context, media);
+ }
+ Log.d(TAG, "Deleting File. Result: " + result);
+ return result;
+ }
+
+ /** Remove a feed with all its items and media files and its image. */
+ public boolean deleteFeed(Context context, Feed feed) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ // delete image file
+ if (feed.getImage() != null) {
+ if (feed.getImage().isDownloaded()
+ && feed.getImage().getFile_url() == null) {
+ File imageFile = new File(feed.getImage().getFile_url());
+ imageFile.delete();
+ }
+ }
+ // delete stored media files and mark them as read
+ for (FeedItem item : feed.getItems()) {
+ if (!item.isRead()) {
+ unreadItems.remove(item);
+ }
+ if (queue.contains(item)) {
+ removeQueueItem(item, adapter);
+ }
+ if (item.getMedia() != null && item.getMedia().isDownloaded()) {
+ File mediaFile = new File(item.getMedia().getFile_url());
+ mediaFile.delete();
+ }
+ }
+
+ adapter.removeFeed(feed);
+ adapter.close();
+ return feeds.remove(feed);
+
+ }
+
+ private void sendUnreadItemsUpdateBroadcast(Context context, FeedItem item) {
+ Intent update = new Intent(ACTION_UNREAD_ITEMS_UPDATE);
+ if (item != null) {
+ update.putExtra(EXTRA_FEED_ID, item.getFeed().getId());
+ update.putExtra(EXTRA_FEED_ITEM_ID, item.getId());
+ }
+ context.sendBroadcast(update);
+ }
+
+ private void sendQueueUpdateBroadcast(Context context, FeedItem item) {
+ Intent update = new Intent(ACTION_QUEUE_UPDATE);
+ if (item != null) {
+ update.putExtra(EXTRA_FEED_ID, item.getFeed().getId());
+ update.putExtra(EXTRA_FEED_ITEM_ID, item.getId());
+ }
+ context.sendBroadcast(update);
+ }
+
+ /**
+ * Sets the 'read'-attribute of a FeedItem. Should be used by all Classes
+ * instead of the setters of FeedItem.
+ */
+ public void markItemRead(Context context, FeedItem item, boolean read) {
+ Log.d(TAG, "Setting item with title " + item.getTitle()
+ + " as read/unread");
+ item.read = read;
+ setFeedItem(context, item);
+ if (read == true) {
+ unreadItems.remove(item);
+ } else {
+ unreadItems.add(item);
+ Collections.sort(unreadItems, new FeedItemPubdateComparator());
+ }
+ sendUnreadItemsUpdateBroadcast(context, item);
+ }
+
+ /**
+ * Sets the 'read' attribute of all FeedItems of a specific feed to true
+ *
+ * @param context
+ */
+ public void markFeedRead(Context context, Feed feed) {
+ for (FeedItem item : feed.getItems()) {
+ if (unreadItems.contains(item)) {
+ markItemRead(context, item, true);
+ }
+ }
+ }
+
+ /** Marks all items in the unread items list as read */
+ public void markAllItemsRead(Context context) {
+ Log.d(TAG, "marking all items as read");
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ for (FeedItem item : unreadItems) {
+ item.read = true;
+ setFeedItem(item, adapter);
+ }
+ adapter.close();
+ unreadItems.clear();
+ sendUnreadItemsUpdateBroadcast(context, null);
+ }
+
+ public void refreshAllFeeds(Context context) {
+ Log.d(TAG, "Refreshing all feeds.");
+ for (Feed feed : feeds) {
+ refreshFeed(context, feed);
+ }
+ }
+
+ /**
+ * Notifies the feed manager that the an image file is invalid. It will try
+ * to redownload it
+ */
+ public void notifyInvalidImageFile(Context context, FeedImage image) {
+ Log.i(TAG,
+ "The feedmanager was notified about an invalid image download. It will now try to redownload the image file");
+ image.setDownloaded(false);
+ image.setFile_url(null);
+ requester.downloadImage(context, image);
+ }
+
+ public void refreshFeed(Context context, Feed feed) {
+ requester.downloadFeed(context, new Feed(feed.getDownload_url(),
+ new Date()));
+ }
+
+ public long addDownloadStatus(Context context, DownloadStatus status) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ downloadLog.add(status);
+ adapter.open();
+ if (downloadLog.size() > DOWNLOAD_LOG_SIZE) {
+ adapter.removeDownloadStatus(downloadLog.remove(0));
+ }
+ long result = adapter.setDownloadStatus(status);
+ adapter.close();
+ return result;
+ }
+
+ public void addQueueItem(Context context, FeedItem item) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ queue.add(item);
+ adapter.open();
+ adapter.setQueue(queue);
+ adapter.close();
+ sendQueueUpdateBroadcast(context, item);
+ }
+
+ /** Removes all items in queue */
+ public void clearQueue(Context context) {
+ Log.d(TAG, "Clearing queue");
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ queue.clear();
+ adapter.setQueue(queue);
+ adapter.close();
+ sendQueueUpdateBroadcast(context, null);
+ }
+
+ /** Uses external adapter. */
+ public void removeQueueItem(FeedItem item, PodDBAdapter adapter) {
+ boolean removed = queue.remove(item);
+ if (removed) {
+ adapter.setQueue(queue);
+ }
+
+ }
+
+ /** Uses its own adapter. */
+ public void removeQueueItem(Context context, FeedItem item) {
+ boolean removed = queue.remove(item);
+ if (removed) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setQueue(queue);
+ adapter.close();
+ }
+ sendQueueUpdateBroadcast(context, item);
+ }
+
+ public void moveQueueItem(Context context, FeedItem item, int delta) {
+ Log.d(TAG, "Moving queue item");
+ int itemIndex = queue.indexOf(item);
+ int newIndex = itemIndex + delta;
+ if (newIndex >= 0 && newIndex < queue.size()) {
+ FeedItem oldItem = queue.set(newIndex, item);
+ queue.set(itemIndex, oldItem);
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setQueue(queue);
+ adapter.close();
+ }
+ sendQueueUpdateBroadcast(context, item);
+ }
+
+ public boolean isInQueue(FeedItem item) {
+ return queue.contains(item);
+ }
+
+ public FeedItem getFirstQueueItem() {
+ if (queue.isEmpty()) {
+ return null;
+ } else {
+ return queue.get(0);
+ }
+ }
+
+ private void addNewFeed(Context context, Feed feed) {
+ feeds.add(feed);
+ Collections.sort(feeds, new FeedtitleComparator());
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ adapter.setCompleteFeed(feed);
+ adapter.close();
+ }
+
+ /**
+ * Updates an existing feed or adds it as a new one if it doesn't exist.
+ *
+ * @return The saved Feed with a database ID
+ */
+ public Feed updateFeed(Context context, final Feed newFeed) {
+ // Look up feed in the feedslist
+ final Feed savedFeed = searchFeedByLink(newFeed.getLink());
+ if (savedFeed == null) {
+ Log.d(TAG,
+ "Found no existing Feed with title " + newFeed.getTitle()
+ + ". Adding as new one.");
+ // Add a new Feed
+ addNewFeed(context, newFeed);
+ markItemRead(context, newFeed.getItems().get(0), false);
+
+ return newFeed;
+ } else {
+ Log.d(TAG, "Feed with title " + newFeed.getTitle()
+ + " already exists. Syncing new with existing one.");
+ // Look for new or updated Items
+ for (int idx = 0; idx < newFeed.getItems().size(); idx++) {
+ FeedItem item = newFeed.getItems().get(idx);
+ FeedItem oldItem = searchFeedItemByTitle(savedFeed,
+ item.getTitle());
+ if (oldItem == null) {
+ // item is new
+ item.setFeed(savedFeed);
+ savedFeed.getItems().add(idx, item);
+ markItemRead(context, item, false);
+ }
+ }
+ savedFeed.setLastUpdate(newFeed.getLastUpdate());
+ setFeed(context, savedFeed);
+ return savedFeed;
+ }
+
+ }
+
+ /** Get a Feed by its link */
+ private Feed searchFeedByLink(String link) {
+ for (Feed feed : feeds) {
+ if (feed.getLink().equals(link)) {
+ return feed;
+ }
+ }
+ return null;
+ }
+
+ /** Get a FeedItem by its link */
+ private FeedItem searchFeedItemByTitle(Feed feed, String title) {
+ for (FeedItem item : feed.getItems()) {
+ if (item.getTitle().equals(title)) {
+ return item;
+ }
+ }
+ return null;
+ }
+
+ /** Updates Information of an existing Feed. Uses external adapter. */
+ public long setFeed(Feed feed, PodDBAdapter adapter) {
+ if (adapter != null) {
+ return adapter.setFeed(feed);
+ } else {
+ Log.w(TAG, "Adapter in setFeed was null");
+ return 0;
+ }
+ }
+
+ /** Updates Information of an existing Feeditem. Uses external adapter. */
+ public long setFeedItem(FeedItem item, PodDBAdapter adapter) {
+ if (adapter != null) {
+ return adapter.setSingleFeedItem(item);
+ } else {
+ Log.w(TAG, "Adapter in setFeedItem was null");
+ return 0;
+ }
+ }
+
+ /** Updates Information of an existing Feedimage. Uses external adapter. */
+ public long setFeedImage(FeedImage image, PodDBAdapter adapter) {
+ if (adapter != null) {
+ return adapter.setImage(image);
+ } else {
+ Log.w(TAG, "Adapter in setFeedImage was null");
+ return 0;
+ }
+ }
+
+ /**
+ * Updates Information of an existing Feedmedia object. Uses external
+ * adapter.
+ */
+ public long setFeedImage(FeedMedia media, PodDBAdapter adapter) {
+ if (adapter != null) {
+ return adapter.setMedia(media);
+ } else {
+ Log.w(TAG, "Adapter in setFeedMedia was null");
+ return 0;
+ }
+ }
+
+ /**
+ * Updates Information of an existing Feed. Creates and opens its own
+ * adapter.
+ */
+ public long setFeed(Context context, Feed feed) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ long result = adapter.setFeed(feed);
+ adapter.close();
+ return result;
+ }
+
+ /**
+ * Updates information of an existing FeedItem. Creates and opens its own
+ * adapter.
+ */
+ public long setFeedItem(Context context, FeedItem item) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ long result = adapter.setSingleFeedItem(item);
+ adapter.close();
+ return result;
+ }
+
+ /**
+ * Updates information of an existing FeedImage. Creates and opens its own
+ * adapter.
+ */
+ public long setFeedImage(Context context, FeedImage image) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ long result = adapter.setImage(image);
+ adapter.close();
+ return result;
+ }
+
+ /**
+ * Updates information of an existing FeedMedia object. Creates and opens
+ * its own adapter.
+ */
+ public long setFeedMedia(Context context, FeedMedia media) {
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ long result = adapter.setMedia(media);
+ adapter.close();
+ return result;
+ }
+
+ /** Get a Feed by its id */
+ public Feed getFeed(long id) {
+ for (Feed f : feeds) {
+ if (f.id == id) {
+ return f;
+ }
+ }
+ Log.e(TAG, "Couldn't find Feed with id " + id);
+ return null;
+ }
+
+ /** Get a Feed Image by its id */
+ public FeedImage getFeedImage(long id) {
+ for (Feed f : feeds) {
+ FeedImage image = f.getImage();
+ if (image != null && image.getId() == id) {
+ return image;
+ }
+ }
+ return null;
+ }
+
+ /** Get a Feed Item by its id and its feed */
+ public FeedItem getFeedItem(long id, Feed feed) {
+ for (FeedItem item : feed.getItems()) {
+ if (item.getId() == id) {
+ return item;
+ }
+ }
+ Log.e(TAG, "Couldn't find FeedItem with id " + id);
+ return null;
+ }
+
+ /** Get a FeedMedia object by the id of the Media object and the feed object */
+ public FeedMedia getFeedMedia(long id, Feed feed) {
+ if (feed != null) {
+ for (FeedItem item : feed.getItems()) {
+ if (item.getMedia().getId() == id) {
+ return item.getMedia();
+ }
+ }
+ }
+ Log.e(TAG, "Couldn't find FeedMedia with id " + id);
+ if (feed == null)
+ Log.e(TAG, "Feed was null");
+ return null;
+ }
+
+ /** Get a FeedMedia object by the id of the Media object. */
+ public FeedMedia getFeedMedia(long id) {
+ for (Feed feed : feeds) {
+ for (FeedItem item : feed.getItems()) {
+ if (item.getMedia() != null && item.getMedia().getId() == id) {
+ return item.getMedia();
+ }
+ }
+ }
+ Log.w(TAG, "Couldn't find FeedMedia with id " + id);
+ return null;
+ }
+
+ public DownloadStatus getDownloadStatus(long statusId) {
+ for (DownloadStatus status : downloadLog) {
+ if (status.getId() == statusId) {
+ return status;
+ }
+ }
+ return null;
+ }
+
+ /** Reads the database */
+ public void loadDBData(Context context) {
+ updateArrays(context);
+ }
+
+ public void updateArrays(Context context) {
+ feeds.clear();
+ categories.clear();
+ PodDBAdapter adapter = new PodDBAdapter(context);
+ adapter.open();
+ extractFeedlistFromCursor(context, adapter);
+ extractDownloadLogFromCursor(context, adapter);
+ extractQueueFromCursor(context, adapter);
+ adapter.close();
+ Collections.sort(feeds, new FeedtitleComparator());
+ Collections.sort(unreadItems, new FeedItemPubdateComparator());
+ }
+
+ private void extractFeedlistFromCursor(Context context, PodDBAdapter adapter) {
+ Log.d(TAG, "Extracting Feedlist");
+ Cursor feedlistCursor = adapter.getAllFeedsCursor();
+ if (feedlistCursor.moveToFirst()) {
+ do {
+ Date lastUpdate = new Date(
+ feedlistCursor.getLong(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_LASTUPDATE)));
+ Feed feed = new Feed(lastUpdate);
+
+ feed.id = feedlistCursor.getLong(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_ID));
+ feed.setTitle(feedlistCursor.getString(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_TITLE)));
+ feed.setLink(feedlistCursor.getString(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_LINK)));
+ feed.setDescription(feedlistCursor.getString(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_DESCRIPTION)));
+ feed.setPaymentLink(feedlistCursor.getString(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK)));
+ feed.setAuthor(feedlistCursor.getString(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_AUTHOR)));
+ feed.setLanguage(feedlistCursor.getString(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_LANGUAGE)));
+ feed.setImage(adapter.getFeedImage(feedlistCursor
+ .getLong(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_IMAGE))));
+ feed.file_url = feedlistCursor.getString(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_FILE_URL));
+ feed.download_url = feedlistCursor.getString(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL));
+ feed.setDownloaded(feedlistCursor.getInt(feedlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_DOWNLOADED)) > 0);
+ // Get FeedItem-Object
+ Cursor itemlistCursor = adapter.getAllItemsOfFeedCursor(feed);
+ feed.setItems(extractFeedItemsFromCursor(context, feed,
+ itemlistCursor, adapter));
+ itemlistCursor.close();
+
+ feeds.add(feed);
+ } while (feedlistCursor.moveToNext());
+ }
+ feedlistCursor.close();
+
+ }
+
+ private ArrayList<FeedItem> extractFeedItemsFromCursor(Context context,
+ Feed feed, Cursor itemlistCursor, PodDBAdapter adapter) {
+ Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle());
+ ArrayList<FeedItem> items = new ArrayList<FeedItem>();
+ if (itemlistCursor.moveToFirst()) {
+ do {
+ FeedItem item = new FeedItem();
+
+ item.id = itemlistCursor.getLong(itemlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_ID));
+ item.setFeed(feed);
+ item.setTitle(itemlistCursor.getString(itemlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_TITLE)));
+ item.setLink(itemlistCursor.getString(itemlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_LINK)));
+ item.setDescription(itemlistCursor.getString(itemlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_DESCRIPTION)));
+ item.setContentEncoded(itemlistCursor.getString(itemlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_CONTENT_ENCODED)));
+ item.setPubDate(new Date(itemlistCursor.getLong(itemlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_PUBDATE))));
+ item.setPaymentLink(itemlistCursor.getString(itemlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK)));
+ long mediaId = itemlistCursor.getLong(itemlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_MEDIA));
+ if (mediaId != 0) {
+ item.setMedia(adapter.getFeedMedia(mediaId, item));
+ }
+ item.read = (itemlistCursor.getInt(itemlistCursor
+ .getColumnIndex(PodDBAdapter.KEY_READ)) > 0) ? true
+ : false;
+ if (!item.read) {
+ unreadItems.add(item);
+ }
+
+ // extract chapters
+ Cursor chapterCursor = adapter
+ .getSimpleChaptersOfFeedItemCursor(item);
+ if (chapterCursor.moveToFirst()) {
+ item.setSimpleChapters(new ArrayList<SimpleChapter>());
+ do {
+ SimpleChapter chapter = new SimpleChapter(
+ chapterCursor
+ .getLong(chapterCursor
+ .getColumnIndex(PodDBAdapter.KEY_START)),
+ chapterCursor.getString(chapterCursor
+ .getColumnIndex(PodDBAdapter.KEY_TITLE)));
+ item.getSimpleChapters().add(chapter);
+ } while (chapterCursor.moveToNext());
+ }
+ chapterCursor.close();
+
+ items.add(item);
+ } while (itemlistCursor.moveToNext());
+ }
+ Collections.sort(items, new FeedItemPubdateComparator());
+ return items;
+ }
+
+ private void extractDownloadLogFromCursor(Context context,
+ PodDBAdapter adapter) {
+ Log.d(TAG, "Extracting DownloadLog");
+ Cursor logCursor = adapter.getDownloadLogCursor();
+ if (logCursor.moveToFirst()) {
+ do {
+ long id = logCursor.getLong(logCursor
+ .getColumnIndex(PodDBAdapter.KEY_ID));
+ long feedfileId = logCursor.getLong(logCursor
+ .getColumnIndex(PodDBAdapter.KEY_FEEDFILE));
+ int feedfileType = logCursor.getInt(logCursor
+ .getColumnIndex(PodDBAdapter.KEY_FEEDFILETYPE));
+ FeedFile feedfile = null;
+ switch (feedfileType) {
+ case PodDBAdapter.FEEDFILETYPE_FEED:
+ feedfile = getFeed(feedfileId);
+ break;
+ case PodDBAdapter.FEEDFILETYPE_FEEDIMAGE:
+ feedfile = getFeedImage(feedfileId);
+ break;
+ case PodDBAdapter.FEEDFILETYPE_FEEDMEDIA:
+ feedfile = getFeedMedia(feedfileId);
+ }
+ if (feedfile != null) { // otherwise ignore status
+ boolean successful = logCursor.getInt(logCursor
+ .getColumnIndex(PodDBAdapter.KEY_SUCCESSFUL)) > 0;
+ int reason = logCursor.getInt(logCursor
+ .getColumnIndex(PodDBAdapter.KEY_REASON));
+ Date completionDate = new Date(logCursor.getLong(logCursor
+ .getColumnIndex(PodDBAdapter.KEY_COMPLETION_DATE)));
+ downloadLog.add(new DownloadStatus(id, feedfile,
+ successful, reason, completionDate));
+ }
+
+ } while (logCursor.moveToNext());
+ }
+ logCursor.close();
+ }
+
+ private void extractQueueFromCursor(Context context, PodDBAdapter adapter) {
+ Log.d(TAG, "Extracting Queue");
+ Cursor cursor = adapter.getQueueCursor();
+ if (cursor.moveToFirst()) {
+ do {
+ int index = cursor.getInt(cursor
+ .getColumnIndex(PodDBAdapter.KEY_ID));
+ Feed feed = getFeed(cursor.getLong(cursor
+ .getColumnIndex(PodDBAdapter.KEY_FEED)));
+ if (feed != null) {
+ FeedItem item = getFeedItem(cursor.getLong(cursor
+ .getColumnIndex(PodDBAdapter.KEY_FEEDITEM)), feed);
+ if (item != null) {
+ queue.add(index, item);
+ }
+ }
+
+ } while (cursor.moveToNext());
+ }
+ cursor.close();
+ }
+
+ public ArrayList<Feed> getFeeds() {
+ return feeds;
+ }
+
+ public ArrayList<FeedItem> getUnreadItems() {
+ return unreadItems;
+ }
+
+ public ArrayList<DownloadStatus> getDownloadLog() {
+ return downloadLog;
+ }
+
+ public ArrayList<FeedItem> getQueue() {
+ return queue;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/feed/FeedMedia.java b/src/de/danoeh/antennapod/feed/FeedMedia.java
new file mode 100644
index 000000000..92059b712
--- /dev/null
+++ b/src/de/danoeh/antennapod/feed/FeedMedia.java
@@ -0,0 +1,73 @@
+package de.danoeh.antennapod.feed;
+
+public class FeedMedia extends FeedFile{
+ private int duration;
+ private int position; // Current position in file
+ private long size; // File size in Byte
+ private String mime_type;
+ private FeedItem item;
+
+ public FeedMedia(FeedItem i, String download_url, long size, String mime_type) {
+ super(null, download_url, false);
+ this.item = i;
+ this.size = size;
+ this.mime_type = mime_type;
+ }
+
+ public FeedMedia(long id, FeedItem item, int duration, int position, long size, String mime_type,
+ String file_url, String download_url, boolean downloaded) {
+ super(file_url, download_url, downloaded);
+ this.id = id;
+ this.item = item;
+ this.duration = duration;
+ this.position = position;
+ this.size = size;
+ this.mime_type = mime_type;
+ }
+
+ public int getDuration() {
+ return duration;
+ }
+
+ public void setDuration(int duration) {
+ this.duration = duration;
+ }
+
+ public int getPosition() {
+ return position;
+ }
+
+ public void setPosition(int position) {
+ this.position = position;
+ }
+
+ public long getSize() {
+ return size;
+ }
+
+ public void setSize(long size) {
+ this.size = size;
+ }
+
+ public String getMime_type() {
+ return mime_type;
+ }
+
+ public void setMime_type(String mime_type) {
+ this.mime_type = mime_type;
+ }
+
+ public FeedItem getItem() {
+ return item;
+ }
+
+ public void setItem(FeedItem item) {
+ this.item = item;
+ }
+
+
+
+
+
+
+}
diff --git a/src/de/danoeh/antennapod/feed/SimpleChapter.java b/src/de/danoeh/antennapod/feed/SimpleChapter.java
new file mode 100644
index 000000000..5e43bfeb6
--- /dev/null
+++ b/src/de/danoeh/antennapod/feed/SimpleChapter.java
@@ -0,0 +1,22 @@
+package de.danoeh.antennapod.feed;
+
+public class SimpleChapter extends FeedComponent {
+ public long getStart() {
+ return start;
+ }
+
+ public SimpleChapter(long start, String title) {
+ super();
+ this.start = start;
+ this.title = title;
+ }
+
+ public String getTitle() {
+ return title;
+ }
+ /** Defines starting point in milliseconds. */
+ private long start;
+ private String title;
+
+
+}
diff --git a/src/de/danoeh/antennapod/fragment/CoverFragment.java b/src/de/danoeh/antennapod/fragment/CoverFragment.java
new file mode 100644
index 000000000..779fe7e3d
--- /dev/null
+++ b/src/de/danoeh/antennapod/fragment/CoverFragment.java
@@ -0,0 +1,90 @@
+package de.danoeh.antennapod.fragment;
+
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+
+import de.danoeh.antennapod.asynctask.FeedImageLoader;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.R;
+
+/** Displays the cover and the title of a FeedItem. */
+public class CoverFragment extends SherlockFragment {
+ private static final String TAG = "CoverFragment";
+ private static final String ARG_FEED_ID = "arg.feedId";
+ private static final String ARG_FEEDITEM_ID = "arg.feedItem";
+
+ private FeedMedia media;
+
+ private TextView txtvTitle;
+ private TextView txtvFeed;
+ private ImageView imgvCover;
+
+ public static CoverFragment newInstance(FeedItem item) {
+ CoverFragment f = new CoverFragment();
+ if (item != null) {
+ Bundle args = new Bundle();
+ args.putLong(ARG_FEED_ID, item.getFeed().getId());
+ args.putLong(ARG_FEEDITEM_ID, item.getId());
+ f.setArguments(args);
+ }
+ return f;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ FeedManager manager = FeedManager.getInstance();
+ FeedItem item = null;
+ Bundle args = getArguments();
+ if (args != null) {
+ long feedId = args.getLong(ARG_FEED_ID, -1);
+ long itemId = args.getLong(ARG_FEEDITEM_ID, -1);
+ if (feedId != -1 && itemId != -1) {
+ Feed feed = manager.getFeed(feedId);
+ item = manager.getFeedItem(itemId, feed);
+ media = item.getMedia();
+ } else {
+ Log.e(TAG, TAG + " was called with invalid arguments");
+ }
+ }
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ View root = inflater.inflate(R.layout.cover_fragment, container, false);
+ txtvTitle = (TextView) root.findViewById(R.id.txtvTitle);
+ txtvFeed = (TextView) root.findViewById(R.id.txtvFeed);
+ imgvCover = (ImageView) root.findViewById(R.id.imgvCover);
+ return root;
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ if (media != null) {
+ loadMediaInfo();
+ } else {
+ Log.w(TAG, "Unable to load media info: media was null");
+ }
+ }
+
+ private void loadMediaInfo() {
+ FeedImageLoader.getInstance().loadBitmap(
+ media.getItem().getFeed().getImage(), imgvCover);
+ txtvTitle.setText(media.getItem().getTitle());
+ txtvFeed.setText(media.getItem().getFeed().getTitle());
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java
new file mode 100644
index 000000000..b09e411ec
--- /dev/null
+++ b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java
@@ -0,0 +1,182 @@
+package de.danoeh.antennapod.fragment;
+
+import de.danoeh.antennapod.activity.*;
+import de.danoeh.antennapod.adapter.FeedlistAdapter;
+import de.danoeh.antennapod.asynctask.FeedRemover;
+import de.danoeh.antennapod.feed.*;
+import de.danoeh.antennapod.service.DownloadService;
+import de.danoeh.antennapod.storage.DownloadRequester;
+import de.danoeh.antennapod.util.FeedMenuHandler;
+import de.danoeh.antennapod.R;
+import android.os.Bundle;
+import android.app.Activity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.LayoutInflater;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemLongClickListener;
+import android.widget.ListView;
+import com.actionbarsherlock.app.SherlockListFragment;
+import com.actionbarsherlock.app.SherlockFragmentActivity;
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+import android.util.Log;
+
+public class FeedlistFragment extends SherlockListFragment implements
+ ActionMode.Callback {
+ private static final String TAG = "FeedlistFragment";
+ public static final String EXTRA_SELECTED_FEED = "extra.de.danoeh.antennapod.activity.selected_feed";
+
+ private FeedManager manager;
+ private FeedlistAdapter fla;
+ private SherlockFragmentActivity pActivity;
+
+ private Feed selectedFeed;
+ private ActionMode mActionMode;
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ pActivity = (SherlockFragmentActivity) activity;
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ pActivity = null;
+ }
+
+
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Log.d(TAG, "Creating");
+ manager = FeedManager.getInstance();
+ fla = new FeedlistAdapter(pActivity, 0, manager.getFeeds());
+ setListAdapter(fla);
+
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ super.onCreateView(inflater, container, savedInstanceState);
+ return inflater.inflate(R.layout.feedlist, container, false);
+
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ getListView().setOnItemLongClickListener(new OnItemLongClickListener() {
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view,
+ int position, long id) {
+ Feed selection = fla.getItem(position);
+ Log.d(TAG, "Selected Feed with title " + selection.getTitle());
+ if (selection != null) {
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+ fla.setSelectedItemIndex(position);
+ selectedFeed = selection;
+ mActionMode = getSherlockActivity().startActionMode(
+ FeedlistFragment.this);
+
+ }
+ return true;
+ }
+
+ });
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(DownloadService.ACTION_DOWNLOAD_HANDLED);
+ filter.addAction(DownloadService.ACTION_FEED_SYNC_COMPLETED);
+ filter.addAction(DownloadRequester.ACTION_DOWNLOAD_QUEUED);
+ filter.addAction(FeedManager.ACTION_UNREAD_ITEMS_UPDATE);
+
+ pActivity.registerReceiver(contentUpdate, filter);
+ fla.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ pActivity.unregisterReceiver(contentUpdate);
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+ }
+
+ private BroadcastReceiver contentUpdate = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "Received contentUpdate Intent.");
+ fla.notifyDataSetChanged();
+ }
+ };
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ Feed selection = fla.getItem(position);
+ Intent showFeed = new Intent(pActivity, FeedItemlistActivity.class);
+ showFeed.putExtra(EXTRA_SELECTED_FEED, selection.getId());
+
+ pActivity.startActivity(showFeed);
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ FeedMenuHandler.onCreateOptionsMenu(mode.getMenuInflater(), menu);
+ mode.setTitle(selectedFeed.getTitle());
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return FeedMenuHandler.onPrepareOptionsMenu(menu, selectedFeed);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ if (FeedMenuHandler.onOptionsItemClicked(getSherlockActivity(), item,
+ selectedFeed)) {
+ fla.notifyDataSetChanged();
+ } else {
+ switch (item.getItemId()) {
+ case R.id.remove_item:
+ FeedRemover remover = new FeedRemover(getSherlockActivity()) {
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ fla.notifyDataSetChanged();
+ }
+ };
+ remover.execute(selectedFeed);
+ break;
+ }
+ }
+ mode.finish();
+ return true;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mActionMode = null;
+ selectedFeed = null;
+ fla.setSelectedItemIndex(FeedlistAdapter.SELECTION_NONE);
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
new file mode 100644
index 000000000..2e13b5ba0
--- /dev/null
+++ b/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java
@@ -0,0 +1,153 @@
+package de.danoeh.antennapod.fragment;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+
+import android.app.Activity;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.webkit.WebView;
+
+import com.actionbarsherlock.app.SherlockFragment;
+
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedManager;
+
+/** Displays the description of a FeedItem in a Webview. */
+public class ItemDescriptionFragment extends SherlockFragment {
+
+ private static final String TAG = "ItemDescriptionFragment";
+ private static final String ARG_FEED_ID = "arg.feedId";
+ private static final String ARG_FEEDITEM_ID = "arg.feedItemId";
+ private static final String ARG_SCROLLBAR_ENABLED = "arg.scrollbarEnabled";
+
+ private static final String WEBVIEW_STYLE = "<head><style type=\"text/css\"> * { font-family: Helvetica; line-height: 1.5em; font-size: 12pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; }</style></head>";
+
+ private boolean scrollbarEnabled;
+ private WebView webvDescription;
+ private FeedItem item;
+
+ private AsyncTask<Void, Void, Void> webViewLoader;
+
+ public static ItemDescriptionFragment newInstance(FeedItem item, boolean scrollbarEnabled) {
+ ItemDescriptionFragment f = new ItemDescriptionFragment();
+ Bundle args = new Bundle();
+ args.putLong(ARG_FEED_ID, item.getFeed().getId());
+ args.putLong(ARG_FEEDITEM_ID, item.getId());
+ args.putBoolean(ARG_SCROLLBAR_ENABLED, scrollbarEnabled);
+ f.setArguments(args);
+ return f;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ webvDescription = new WebView(getActivity());
+ webvDescription.setHorizontalScrollBarEnabled(scrollbarEnabled);
+ return webvDescription;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ if (webViewLoader == null && item != null) {
+ webViewLoader = createLoader();
+ webViewLoader.execute();
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ if (webViewLoader != null) {
+ webViewLoader.cancel(true);
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (webViewLoader != null) {
+ webViewLoader.cancel(true);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ FeedManager manager = FeedManager.getInstance();
+ Bundle args = getArguments();
+ long feedId = args.getLong(ARG_FEED_ID, -1);
+ long itemId = args.getLong(ARG_FEEDITEM_ID, -1);
+ scrollbarEnabled = args.getBoolean(ARG_SCROLLBAR_ENABLED, true);
+ if (feedId != -1 && itemId != -1) {
+ Feed feed = manager.getFeed(feedId);
+ item = manager.getFeedItem(itemId, feed);
+ webViewLoader = createLoader();
+ webViewLoader.execute();
+ } else {
+ Log.e(TAG, TAG + " was called with invalid arguments");
+ }
+
+ }
+
+ private AsyncTask<Void, Void, Void> createLoader() {
+ return new AsyncTask<Void, Void, Void>() {
+ @Override
+ protected void onCancelled() {
+ super.onCancelled();
+ if (getSherlockActivity() != null) {
+ getSherlockActivity()
+ .setSupportProgressBarIndeterminateVisibility(false);
+ }
+ webViewLoader = null;
+ }
+
+ String data;
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ // /webvDescription.loadData(url, "text/html", "utf-8");
+ webvDescription.loadDataWithBaseURL(null, data, "text/html",
+ "utf-8", "about:blank");
+ getSherlockActivity()
+ .setSupportProgressBarIndeterminateVisibility(false);
+ Log.d(TAG, "Webview loaded");
+ webViewLoader = null;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ getSherlockActivity()
+ .setSupportProgressBarIndeterminateVisibility(true);
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ Log.d(TAG, "Loading Webview");
+ data = "";
+ if (item.getContentEncoded() == null
+ && item.getDescription() != null) {
+ data = item.getDescription();
+ } else {
+ data = StringEscapeUtils.unescapeHtml4(item
+ .getContentEncoded());
+ }
+
+ data = WEBVIEW_STYLE + data;
+
+ return null;
+ }
+
+ };
+ }
+}
diff --git a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java
new file mode 100644
index 000000000..fa25eacab
--- /dev/null
+++ b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java
@@ -0,0 +1,215 @@
+package de.danoeh.antennapod.fragment;
+
+import java.util.ArrayList;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.ListView;
+
+import com.actionbarsherlock.app.SherlockListFragment;
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+
+import de.danoeh.antennapod.activity.ItemviewActivity;
+import de.danoeh.antennapod.adapter.FeedItemlistAdapter;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.service.DownloadService;
+import de.danoeh.antennapod.storage.DownloadRequester;
+import de.danoeh.antennapod.util.FeedItemMenuHandler;
+import de.danoeh.antennapod.R;
+
+/** Displays a list of FeedItems. */
+public class ItemlistFragment extends SherlockListFragment implements
+ ActionMode.Callback {
+
+ private static final String TAG = "ItemlistFragment";
+ public static final String EXTRA_SELECTED_FEEDITEM = "extra.de.danoeh.antennapod.activity.selected_feeditem";
+ public static final String ARGUMENT_FEED_ID = "argument.de.danoeh.antennapod.feed_id";
+ protected FeedItemlistAdapter fila;
+ protected FeedManager manager;
+ protected DownloadRequester requester;
+
+ /** The feed which the activity displays */
+ protected ArrayList<FeedItem> items;
+ /**
+ * This is only not null if the fragment displays the items of a specific
+ * feed
+ */
+ protected Feed feed;
+
+ protected FeedItem selectedItem;
+ protected ActionMode mActionMode;
+
+ /** Argument for FeeditemlistAdapter */
+ protected boolean showFeedtitle;
+
+ public ItemlistFragment(ArrayList<FeedItem> items, boolean showFeedtitle) {
+ super();
+ this.items = items;
+ this.showFeedtitle = showFeedtitle;
+ manager = FeedManager.getInstance();
+ requester = DownloadRequester.getInstance();
+ }
+
+ public ItemlistFragment() {
+ }
+
+ /**
+ * Creates new ItemlistFragment which shows the Feeditems of a specific
+ * feed. Sets 'showFeedtitle' to false
+ *
+ * @param feedId
+ * The id of the feed to show
+ * @return the newly created instance of an ItemlistFragment
+ */
+ public static ItemlistFragment newInstance(long feedId) {
+ ItemlistFragment i = new ItemlistFragment();
+ i.showFeedtitle = false;
+ Bundle b = new Bundle();
+ b.putLong(ARGUMENT_FEED_ID, feedId);
+ i.setArguments(b);
+ return i;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (items == null) {
+ long feedId = getArguments().getLong(ARGUMENT_FEED_ID);
+ feed = FeedManager.getInstance().getFeed(feedId);
+ items = feed.getItems();
+ }
+ fila = new FeedItemlistAdapter(getActivity(), 0, items,
+ onButActionClicked, showFeedtitle);
+ setListAdapter(fila);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ getActivity().unregisterReceiver(contentUpdate);
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ fila.notifyDataSetChanged();
+ updateProgressBarVisibility();
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(DownloadService.ACTION_DOWNLOAD_HANDLED);
+ filter.addAction(DownloadRequester.ACTION_DOWNLOAD_QUEUED);
+
+ getActivity().registerReceiver(contentUpdate, filter);
+ }
+
+ @Override
+ public void onListItemClick(ListView l, View v, int position, long id) {
+ FeedItem selection = fila.getItem(position);
+ Intent showItem = new Intent(getActivity(), ItemviewActivity.class);
+ showItem.putExtra(FeedlistFragment.EXTRA_SELECTED_FEED, selection
+ .getFeed().getId());
+ showItem.putExtra(EXTRA_SELECTED_FEEDITEM, selection.getId());
+
+ startActivity(showItem);
+ }
+
+ private BroadcastReceiver contentUpdate = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "Received contentUpdate Intent.");
+ fila.notifyDataSetChanged();
+ updateProgressBarVisibility();
+ }
+ };
+
+ private void updateProgressBarVisibility() {
+ if (feed != null) {
+ if (DownloadService.isRunning
+ && DownloadRequester.getInstance().isDownloadingFile(feed)) {
+ getSherlockActivity()
+ .setSupportProgressBarIndeterminateVisibility(true);
+ } else {
+ getSherlockActivity()
+ .setSupportProgressBarIndeterminateVisibility(false);
+ }
+ getSherlockActivity().invalidateOptionsMenu();
+ }
+ }
+
+ private final OnClickListener onButActionClicked = new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int index = getListView().getPositionForView(v);
+ if (index != ListView.INVALID_POSITION) {
+ FeedItem newSelectedItem = items.get(index);
+ if (newSelectedItem != selectedItem) {
+ if (mActionMode != null) {
+ mActionMode.finish();
+ }
+
+ selectedItem = newSelectedItem;
+ mActionMode = getSherlockActivity().startActionMode(
+ ItemlistFragment.this);
+ fila.setSelectedItemIndex(index);
+ } else {
+ mActionMode.finish();
+ }
+
+ }
+ }
+ };
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ this.getListView().setItemsCanFocus(true);
+ getListView().setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return FeedItemMenuHandler.onPrepareMenu(menu, selectedItem);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ mActionMode = null;
+ selectedItem = null;
+ fila.setSelectedItemIndex(FeedItemlistAdapter.SELECTION_NONE);
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ mode.setTitle(selectedItem.getTitle());
+ return FeedItemMenuHandler.onCreateMenu(mode.getMenuInflater(), menu);
+
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ boolean handled = FeedItemMenuHandler.onMenuItemClicked(
+ getSherlockActivity(), item, selectedItem);
+ if (handled) {
+ fila.notifyDataSetChanged();
+ }
+ mode.finish();
+ return handled;
+ }
+
+ public FeedItemlistAdapter getListAdapter() {
+ return fila;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/fragment/QueueFragment.java b/src/de/danoeh/antennapod/fragment/QueueFragment.java
new file mode 100644
index 000000000..ff2a682ad
--- /dev/null
+++ b/src/de/danoeh/antennapod/fragment/QueueFragment.java
@@ -0,0 +1,104 @@
+package de.danoeh.antennapod.fragment;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.R;
+
+public class QueueFragment extends ItemlistFragment {
+
+ public QueueFragment() {
+ super(FeedManager.getInstance().getQueue(), true);
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ super.onCreateActionMode(mode, menu);
+ menu.add(Menu.NONE, R.id.move_up_item, Menu.NONE,
+ R.string.move_up_label);
+ menu.add(Menu.NONE, R.id.move_down_item, Menu.NONE,
+ R.string.move_down_label);
+ return true;
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ try {
+ getActivity().unregisterReceiver(queueUpdate);
+ } catch (IllegalArgumentException e) {
+
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ getActivity().registerReceiver(queueUpdate,
+ new IntentFilter(FeedManager.ACTION_QUEUE_UPDATE));
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ boolean handled = false;
+ switch (item.getItemId()) {
+ case R.id.move_up_item:
+ manager.moveQueueItem(getActivity(), selectedItem, -1);
+ handled = true;
+ break;
+ case R.id.move_down_item:
+ manager.moveQueueItem(getActivity(), selectedItem, 1);
+ handled = true;
+ break;
+ default:
+ handled = super.onActionItemClicked(mode, item);
+ }
+ fila.notifyDataSetChanged();
+ mode.finish();
+ return handled;
+ }
+
+ private BroadcastReceiver queueUpdate = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ fila.notifyDataSetChanged();
+ }
+
+ };
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ menu.add(Menu.NONE, R.id.clear_queue_item, Menu.NONE, getActivity()
+ .getString(R.string.clear_queue_label));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.clear_queue_item:
+ manager.clearQueue(getActivity());
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/fragment/UnreadItemlistFragment.java b/src/de/danoeh/antennapod/fragment/UnreadItemlistFragment.java
new file mode 100644
index 000000000..8914f781a
--- /dev/null
+++ b/src/de/danoeh/antennapod/fragment/UnreadItemlistFragment.java
@@ -0,0 +1,79 @@
+package de.danoeh.antennapod.fragment;
+
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Bundle;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.R;
+
+/** Contains all unread items. */
+public class UnreadItemlistFragment extends ItemlistFragment {
+
+ public UnreadItemlistFragment() {
+ super(FeedManager.getInstance().getUnreadItems(), true);
+
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ try {
+ getActivity().unregisterReceiver(unreadItemsUpdate);
+ } catch (IllegalArgumentException e) {
+
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ getActivity().registerReceiver(unreadItemsUpdate,
+ new IntentFilter(FeedManager.ACTION_UNREAD_ITEMS_UPDATE));
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
+ }
+
+ private BroadcastReceiver unreadItemsUpdate = new BroadcastReceiver() {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ fila.notifyDataSetChanged();
+ }
+
+ };
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ super.onCreateOptionsMenu(menu, inflater);
+ menu.add(Menu.NONE, R.id.mark_all_read_item, Menu.NONE, getActivity()
+ .getString(R.string.mark_all_read_label));
+ }
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.mark_all_read_item:
+ manager.markAllItemsRead(getActivity());
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java b/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java
new file mode 100644
index 000000000..d4ce74db3
--- /dev/null
+++ b/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java
@@ -0,0 +1,42 @@
+package de.danoeh.antennapod.receiver;
+
+import de.danoeh.antennapod.PodcastApp;
+import de.danoeh.antennapod.feed.FeedManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.preference.PreferenceManager;
+import android.util.Log;
+
+/** Refreshes all feeds when it receives an intent */
+public class FeedUpdateReceiver extends BroadcastReceiver {
+ private static final String TAG = "FeedUpdateReceiver";
+ public static final String ACTION_REFRESH_FEEDS = "de.danoeh.antennapod.feedupdatereceiver.refreshFeeds";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(ACTION_REFRESH_FEEDS)) {
+ Log.d(TAG, "Received intent");
+ boolean mobileUpdate = PreferenceManager
+ .getDefaultSharedPreferences(
+ context.getApplicationContext()).getBoolean(
+ PodcastApp.PREF_MOBILE_UPDATE, false);
+ if (mobileUpdate || connectedToWifi(context)) {
+ FeedManager.getInstance().refreshAllFeeds(context);
+ } else {
+ Log.d(TAG,
+ "Blocking automatic update: no wifi available / no mobile updates allowed");
+ }
+ }
+ }
+
+ private boolean connectedToWifi(Context context) {
+ ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkInfo mWifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
+
+ return mWifi.isConnected();
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java b/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java
new file mode 100644
index 000000000..003f9434b
--- /dev/null
+++ b/src/de/danoeh/antennapod/receiver/MediaButtonReceiver.java
@@ -0,0 +1,34 @@
+package de.danoeh.antennapod.receiver;
+
+import de.danoeh.antennapod.service.PlaybackService;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.KeyEvent;
+
+/** Receives media button events. */
+public class MediaButtonReceiver extends BroadcastReceiver {
+ private static final String TAG = "MediaButtonReceiver";
+ public static final String EXTRA_KEYCODE = "de.danoeh.antennapod.service.extra.MediaButtonReceiver.KEYCODE";
+
+ public static final String NOTIFY_BUTTON_RECEIVER = "de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ Log.d(TAG, "Received intent");
+ KeyEvent event = (KeyEvent) intent.getExtras().get(
+ Intent.EXTRA_KEY_EVENT);
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ Intent serviceIntent = new Intent(context, PlaybackService.class);
+ int keycode = event.getKeyCode();
+ serviceIntent.putExtra(EXTRA_KEYCODE, keycode);
+ context.startService(serviceIntent);
+ }
+
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/receiver/PlayerWidget.java b/src/de/danoeh/antennapod/receiver/PlayerWidget.java
new file mode 100644
index 000000000..328e14c63
--- /dev/null
+++ b/src/de/danoeh/antennapod/receiver/PlayerWidget.java
@@ -0,0 +1,42 @@
+package de.danoeh.antennapod.receiver;
+
+import de.danoeh.antennapod.service.PlayerWidgetService;
+import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProvider;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class PlayerWidget extends AppWidgetProvider {
+ private static final String TAG = "PlayerWidget";
+ public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(FORCE_WIDGET_UPDATE)) {
+ startUpdate(context);
+ }
+
+ }
+
+
+
+ @Override
+ public void onEnabled(Context context) {
+ super.onEnabled(context);
+ Log.d(TAG, "Widget enabled");
+ }
+
+
+
+ @Override
+ public void onUpdate(Context context, AppWidgetManager appWidgetManager,
+ int[] appWidgetIds) {
+ startUpdate(context);
+ }
+
+ private void startUpdate(Context context) {
+ context.startService(new Intent(context, PlayerWidgetService.class));
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/service/DownloadService.java b/src/de/danoeh/antennapod/service/DownloadService.java
new file mode 100644
index 000000000..99e868bff
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/DownloadService.java
@@ -0,0 +1,492 @@
+/**
+ * Registers a DownloadReceiver and waits for all Downloads
+ * to complete, then stops
+ * */
+
+package de.danoeh.antennapod.service;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.xml.sax.SAXException;
+
+import de.danoeh.antennapod.activity.DownloadActivity;
+import de.danoeh.antennapod.activity.MediaplayerActivity;
+import de.danoeh.antennapod.activity.MainActivity;
+import de.danoeh.antennapod.asynctask.DownloadObserver;
+import de.danoeh.antennapod.asynctask.DownloadStatus;
+import de.danoeh.antennapod.feed.*;
+import de.danoeh.antennapod.service.PlaybackService.LocalBinder;
+import de.danoeh.antennapod.storage.DownloadRequester;
+import de.danoeh.antennapod.syndication.handler.FeedHandler;
+import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException;
+import de.danoeh.antennapod.util.DownloadError;
+import android.R;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.app.DownloadManager;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.MediaPlayer;
+import android.os.IBinder;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.os.Binder;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Messenger;
+
+public class DownloadService extends Service {
+ private static final String TAG = "DownloadService";
+
+ public static String ACTION_ALL_FEED_DOWNLOADS_COMPLETED = "action.de.danoeh.antennapod.storage.all_feed_downloads_completed";
+ public static final String ACTION_FEED_SYNC_COMPLETED = "action.de.danoeh.antennapod.service.feed_sync_completed";
+
+ public static final String ACTION_DOWNLOAD_HANDLED = "action.de.danoeh.antennapod.service.download_handled";
+ /** True if handled feed has an image. */
+ public static final String EXTRA_FEED_HAS_IMAGE = "extra.de.danoeh.antennapod.service.feed_has_image";
+ /** ID of DownloadStatus. */
+ public static final String EXTRA_STATUS_ID = "extra.de.danoeh.antennapod.service.feedfile_id";
+ public static final String EXTRA_DOWNLOAD_ID = "extra.de.danoeh.antennapod.service.download_id";
+ public static final String EXTRA_IMAGE_DOWNLOAD_ID = "extra.de.danoeh.antennapod.service.image_download_id";
+
+ private ArrayList<DownloadStatus> completedDownloads;
+
+ private ExecutorService syncExecutor;
+ private DownloadRequester requester;
+ private FeedManager manager;
+ private NotificationCompat.Builder notificationBuilder;
+ private int NOTIFICATION_ID = 2;
+ private int REPORT_ID = 3;
+ /** Needed to determine the duration of a media file */
+ private MediaPlayer mediaplayer;
+ private DownloadManager downloadManager;
+
+ private DownloadObserver downloadObserver;
+
+ private volatile boolean shutdownInitiated = false;
+ /** True if service is running. */
+ public static boolean isRunning = false;
+
+ private final IBinder mBinder = new LocalBinder();
+
+ public class LocalBinder extends Binder {
+ public DownloadService getService() {
+ return DownloadService.this;
+ }
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ queryDownloads();
+ return super.onStartCommand(intent, flags, startId);
+ }
+
+ @Override
+ public void onCreate() {
+ Log.d(TAG, "Service started");
+ isRunning = true;
+ completedDownloads = new ArrayList<DownloadStatus>();
+ registerReceiver(downloadReceiver, createIntentFilter());
+ syncExecutor = Executors.newSingleThreadExecutor();
+ manager = FeedManager.getInstance();
+ requester = DownloadRequester.getInstance();
+ mediaplayer = new MediaPlayer();
+ downloadManager = (DownloadManager) getSystemService(Context.DOWNLOAD_SERVICE);
+ downloadObserver = new DownloadObserver(this);
+ setupNotification();
+ downloadObserver.execute();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.d(TAG, "Service shutting down");
+ isRunning = false;
+ sendBroadcast(new Intent(ACTION_FEED_SYNC_COMPLETED));
+ mediaplayer.release();
+ unregisterReceiver(downloadReceiver);
+ downloadObserver.cancel(true);
+ createReport();
+ }
+
+ private IntentFilter createIntentFilter() {
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
+ return filter;
+ }
+
+ /** Shuts down Executor service and prepares for shutdown */
+ private void initiateShutdown() {
+ Log.d(TAG, "Initiating shutdown");
+ // Wait until PoolExecutor is done
+ Thread waiter = new Thread() {
+ @Override
+ public void run() {
+ syncExecutor.shutdown();
+ try {
+ Log.d(TAG, "Starting to wait for termination");
+ boolean b = syncExecutor.awaitTermination(20L,
+ TimeUnit.SECONDS);
+ Log.d(TAG, "Stopping waiting for termination; Result : "
+ + b);
+
+ stopSelf();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+ };
+ waiter.start();
+ }
+
+ private void setupNotification() {
+ PendingIntent pIntent = PendingIntent.getActivity(this, 0, new Intent(
+ this, DownloadActivity.class),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ Bitmap icon = BitmapFactory.decodeResource(null,
+ R.drawable.stat_notify_sync_noanim);
+ notificationBuilder = new NotificationCompat.Builder(this)
+ .setContentTitle("Downloading Podcast data")
+ .setContentText(
+ requester.getNumberOfDownloads() + " Downloads left")
+ .setOngoing(true).setContentIntent(pIntent).setLargeIcon(icon)
+ .setSmallIcon(R.drawable.stat_notify_sync_noanim);
+
+ startForeground(NOTIFICATION_ID, notificationBuilder.getNotification());
+ Log.d(TAG, "Notification set up");
+ }
+
+ private BroadcastReceiver downloadReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ int status = -1;
+ boolean successful = false;
+ int reason = 0;
+ Log.d(TAG, "Received 'Download Complete' - message.");
+ long downloadId = intent.getLongExtra(
+ DownloadManager.EXTRA_DOWNLOAD_ID, 0);
+ // get status
+ DownloadManager.Query q = new DownloadManager.Query();
+ q.setFilterById(downloadId);
+ Cursor c = downloadManager.query(q);
+ if (c.moveToFirst()) {
+ status = c.getInt(c
+ .getColumnIndex(DownloadManager.COLUMN_STATUS));
+ }
+ FeedFile download = requester.getFeedFile(downloadId);
+ if (download != null) {
+ if (status == DownloadManager.STATUS_SUCCESSFUL) {
+
+ if (download.getClass() == Feed.class) {
+ handleCompletedFeedDownload(context, (Feed) download);
+ } else if (download.getClass() == FeedImage.class) {
+ handleCompletedImageDownload(context,
+ (FeedImage) download);
+ } else if (download.getClass() == FeedMedia.class) {
+ handleCompletedFeedMediaDownload(context,
+ (FeedMedia) download);
+ }
+ successful = true;
+
+ } else if (status == DownloadManager.STATUS_FAILED) {
+ reason = c.getInt(c
+ .getColumnIndex(DownloadManager.COLUMN_REASON));
+ Log.e(TAG, "Download failed");
+ Log.e(TAG, "reason code is " + reason);
+ successful = false;
+ long statusId = saveDownloadStatus(new DownloadStatus(
+ download, reason, successful));
+ requester.removeDownload(download);
+ sendDownloadHandledIntent(download.getDownloadId(),
+ statusId, false, 0);
+ download.setDownloadId(0);
+
+ }
+ queryDownloads();
+ c.close();
+ }
+ }
+
+ };
+
+ /**
+ * Adds a new DownloadStatus object to the list of completed downloads and
+ * saves it in the database
+ *
+ * @param status
+ * the download that is going to be saved
+ */
+ private long saveDownloadStatus(DownloadStatus status) {
+ completedDownloads.add(status);
+ return manager.addDownloadStatus(this, status);
+ }
+
+ private void sendDownloadHandledIntent(long downloadId, long statusId,
+ boolean feedHasImage, long imageDownloadId) {
+ Intent intent = new Intent(ACTION_DOWNLOAD_HANDLED);
+ intent.putExtra(EXTRA_DOWNLOAD_ID, downloadId);
+ intent.putExtra(EXTRA_STATUS_ID, statusId);
+ intent.putExtra(EXTRA_FEED_HAS_IMAGE, feedHasImage);
+ if (feedHasImage) {
+ intent.putExtra(EXTRA_IMAGE_DOWNLOAD_ID, imageDownloadId);
+ }
+ sendBroadcast(intent);
+ }
+
+ /**
+ * Creates a notification at the end of the service lifecycle to notify the
+ * user about the number of completed downloads. A report will only be
+ * created if the number of feeds is > 1 or if at least one media file was
+ * downloaded.
+ */
+ private void createReport() {
+ // check if report should be created
+ boolean createReport = false;
+ int feedCount = 0;
+ for (DownloadStatus status : completedDownloads) {
+ if (status.getFeedFile().getClass() == Feed.class) {
+ feedCount++;
+ if (feedCount > 1) {
+ createReport = true;
+ break;
+ }
+ } else if (status.getFeedFile().getClass() == FeedMedia.class) {
+ createReport = true;
+ break;
+ }
+ }
+ if (createReport) {
+ Log.d(TAG, "Creating report");
+ int successfulDownloads = 0;
+ int failedDownloads = 0;
+ for (DownloadStatus status : completedDownloads) {
+ if (status.isSuccessful()) {
+ successfulDownloads++;
+ } else {
+ failedDownloads++;
+ }
+ }
+ // create notification object
+ Notification notification = new NotificationCompat.Builder(this)
+ .setTicker(
+ getString(de.danoeh.antennapod.R.string.download_report_title))
+ .setContentTitle(
+ getString(de.danoeh.antennapod.R.string.download_report_title))
+ .setContentText(
+ successfulDownloads + " Downloads succeeded, "
+ + failedDownloads + " failed")
+ .setSmallIcon(R.drawable.stat_notify_sync)
+ .setLargeIcon(
+ BitmapFactory.decodeResource(null,
+ R.drawable.stat_notify_sync))
+ .setContentIntent(
+ PendingIntent.getActivity(this, 0, new Intent(this,
+ MainActivity.class), 0))
+ .setAutoCancel(true).getNotification();
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify(REPORT_ID, notification);
+
+ } else {
+ Log.d(TAG, "No report is created");
+ }
+ }
+
+ /** Check if there's something else to download, otherwise stop */
+ public synchronized void queryDownloads() {
+ int numOfDownloads = requester.getNumberOfDownloads();
+ if (!shutdownInitiated && numOfDownloads == 0) {
+ shutdownInitiated = true;
+ initiateShutdown();
+ } else {
+ // update notification
+ notificationBuilder.setContentText(numOfDownloads
+ + " Downloads left");
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify(NOTIFICATION_ID, notificationBuilder.getNotification());
+ }
+ }
+
+ /** Is called whenever a Feed is downloaded */
+ private void handleCompletedFeedDownload(Context context, Feed feed) {
+ Log.d(TAG, "Handling completed Feed Download");
+ syncExecutor.execute(new FeedSyncThread(feed, this));
+
+ }
+
+ /** Is called whenever a Feed-Image is downloaded */
+ private void handleCompletedImageDownload(Context context, FeedImage image) {
+ Log.d(TAG, "Handling completed Image Download");
+ syncExecutor.execute(new ImageHandlerThread(image, this));
+ }
+
+ /** Is called whenever a FeedMedia is downloaded. */
+ private void handleCompletedFeedMediaDownload(Context context,
+ FeedMedia media) {
+ Log.d(TAG, "Handling completed FeedMedia Download");
+ syncExecutor.execute(new MediaHandlerThread(media, this));
+ }
+
+ /**
+ * Takes a single Feed, parses the corresponding file and refreshes
+ * information in the manager
+ */
+ class FeedSyncThread implements Runnable {
+ private static final String TAG = "FeedSyncThread";
+
+ private Feed feed;
+ private DownloadService service;
+
+ public FeedSyncThread(Feed feed, DownloadService service) {
+ this.feed = feed;
+ this.service = service;
+ }
+
+ public void run() {
+ Feed savedFeed = null;
+ long imageId = 0;
+ boolean hasImage = false;
+ long downloadId = feed.getDownloadId();
+ int reason = 0;
+ boolean successful = true;
+ FeedManager manager = FeedManager.getInstance();
+ FeedHandler handler = new FeedHandler();
+ feed.setDownloaded(true);
+
+ try {
+ feed = handler.parseFeed(feed);
+ Log.d(TAG, feed.getTitle() + " parsed");
+
+ feed.setDownloadId(0);
+ // Save information of feed in DB
+ savedFeed = manager.updateFeed(service, feed);
+ // Download Feed Image if provided and not downloaded
+ if (savedFeed.getImage().isDownloaded() == false) {
+ Log.d(TAG, "Feed has image; Downloading....");
+ imageId = requester.downloadImage(service, feed.getImage());
+ hasImage = true;
+ }
+
+ } catch (SAXException e) {
+ successful = false;
+ e.printStackTrace();
+ reason = DownloadError.ERROR_PARSER_EXCEPTION;
+ } catch (IOException e) {
+ successful = false;
+ e.printStackTrace();
+ reason = DownloadError.ERROR_PARSER_EXCEPTION;
+ } catch (ParserConfigurationException e) {
+ successful = false;
+ e.printStackTrace();
+ reason = DownloadError.ERROR_PARSER_EXCEPTION;
+ } catch (UnsupportedFeedtypeException e) {
+ e.printStackTrace();
+ successful = false;
+ reason = DownloadError.ERROR_UNSUPPORTED_TYPE;
+ }
+
+ requester.removeDownload(feed);
+ cleanup();
+ if (savedFeed == null) {
+ savedFeed = feed;
+ }
+ long statusId = saveDownloadStatus(new DownloadStatus(savedFeed,
+ reason, successful));
+ sendDownloadHandledIntent(downloadId, statusId, hasImage, imageId);
+ queryDownloads();
+ }
+
+ /** Delete files that aren't needed anymore */
+ private void cleanup() {
+ if (new File(feed.getFile_url()).delete())
+ Log.d(TAG, "Successfully deleted cache file.");
+ else
+ Log.e(TAG, "Failed to delete cache file.");
+ feed.setFile_url(null);
+ }
+
+ }
+
+ /** Handles a completed image download. */
+ class ImageHandlerThread implements Runnable {
+ private FeedImage image;
+ private DownloadService service;
+
+ public ImageHandlerThread(FeedImage image, DownloadService service) {
+ this.image = image;
+ this.service = service;
+ }
+
+ @Override
+ public void run() {
+ image.setDownloaded(true);
+ requester.removeDownload(image);
+
+ long statusId = saveDownloadStatus(new DownloadStatus(image, 0,
+ true));
+ sendDownloadHandledIntent(image.getDownloadId(), statusId, false, 0);
+ image.setDownloadId(0);
+
+ manager.setFeedImage(service, image);
+ queryDownloads();
+ }
+ }
+
+ /** Handles a completed media download. */
+ class MediaHandlerThread implements Runnable {
+ private FeedMedia media;
+ private DownloadService service;
+
+ public MediaHandlerThread(FeedMedia media, DownloadService service) {
+ super();
+ this.media = media;
+ this.service = service;
+ }
+
+ @Override
+ public void run() {
+ requester.removeDownload(media);
+ media.setDownloaded(true);
+ // Get duration
+ try {
+ mediaplayer.setDataSource(media.getFile_url());
+ mediaplayer.prepare();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ media.setDuration(mediaplayer.getDuration());
+ Log.d(TAG, "Duration of file is " + media.getDuration());
+ mediaplayer.reset();
+ long statusId = saveDownloadStatus(new DownloadStatus(media, 0,
+ true));
+ sendDownloadHandledIntent(media.getDownloadId(), statusId, false, 0);
+ media.setDownloadId(0);
+ manager.setFeedMedia(service, media);
+ queryDownloads();
+ }
+ }
+
+ public DownloadObserver getDownloadObserver() {
+ return downloadObserver;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/service/PlaybackService.java b/src/de/danoeh/antennapod/service/PlaybackService.java
new file mode 100644
index 000000000..07193ce7f
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/PlaybackService.java
@@ -0,0 +1,678 @@
+package de.danoeh.antennapod.service;
+
+import java.io.IOException;
+
+import android.R;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.media.AudioManager;
+import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.media.MediaPlayer;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.IBinder;
+import android.preference.PreferenceManager;
+import android.support.v4.app.NotificationCompat;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.SurfaceHolder;
+import de.danoeh.antennapod.PodcastApp;
+import de.danoeh.antennapod.activity.MediaplayerActivity;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.receiver.MediaButtonReceiver;
+import de.danoeh.antennapod.receiver.PlayerWidget;
+
+/** Controls the MediaPlayer that plays a FeedMedia-file */
+public class PlaybackService extends Service {
+ /** Logging tag */
+ private static final String TAG = "PlaybackService";
+
+ /** Contains the id of the media that was played last. */
+ public static final String PREF_LAST_PLAYED_ID = "de.danoeh.antennapod.preferences.lastPlayedId";
+ /** Contains the feed id of the last played item. */
+ public static final String PREF_LAST_PLAYED_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId";
+ /** True if last played media was streamed. */
+ public static final String PREF_LAST_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream";
+
+ /** Contains the id of the FeedMedia object. */
+ public static final String EXTRA_MEDIA_ID = "extra.de.danoeh.antennapod.service.mediaId";
+ /** Contains the id of the Feed object of the FeedMedia. */
+ public static final String EXTRA_FEED_ID = "extra.de.danoeh.antennapod.service.feedId";
+ /** True if media should be streamed. */
+ public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream";
+ /**
+ * True if playback should be started immediately after media has been
+ * prepared.
+ */
+ public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared";
+
+ public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged";
+
+ public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification";
+ public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode";
+ public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType";
+
+ public static final int NOTIFICATION_TYPE_ERROR = 0;
+ public static final int NOTIFICATION_TYPE_INFO = 1;
+ public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2;
+ public static final int NOTIFICATION_TYPE_RELOAD = 3;
+
+ /** Is true if service is running. */
+ public static boolean isRunning = false;
+
+ private static final int NOTIFICATION_ID = 1;
+ private NotificationCompat.Builder notificationBuilder;
+
+ private AudioManager audioManager;
+ private ComponentName mediaButtonReceiver;
+
+ private MediaPlayer player;
+
+ private FeedMedia media;
+ private Feed feed;
+ /** True if media should be streamed (Extracted from Intent Extra) . */
+ private boolean shouldStream;
+ private boolean startWhenPrepared;
+ private boolean playingVideo;
+ private FeedManager manager;
+ private PlayerStatus status;
+ private PositionSaver positionSaver;
+ private WidgetUpdateWorker widgetUpdater;
+
+ private PlayerStatus statusBeforeSeek;
+
+ /** True if mediaplayer was paused because it lost audio focus temporarily */
+ private boolean pausedBecauseOfTransientAudiofocusLoss;
+
+ private final IBinder mBinder = new LocalBinder();
+
+ public class LocalBinder extends Binder {
+ public PlaybackService getService() {
+ return PlaybackService.this;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ isRunning = true;
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ status = PlayerStatus.STOPPED;
+ Log.d(TAG, "Service created.");
+ audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+ manager = FeedManager.getInstance();
+ player = new MediaPlayer();
+ player.setOnPreparedListener(preparedListener);
+ player.setOnCompletionListener(completionListener);
+ player.setOnSeekCompleteListener(onSeekCompleteListener);
+ player.setOnErrorListener(onErrorListener);
+ player.setOnBufferingUpdateListener(onBufferingUpdateListener);
+ mediaButtonReceiver = new ComponentName(getPackageName(),
+ MediaButtonReceiver.class.getName());
+ audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver);
+ registerReceiver(headsetDisconnected, new IntentFilter(
+ Intent.ACTION_HEADSET_PLUG));
+
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ isRunning = false;
+ unregisterReceiver(headsetDisconnected);
+ Log.d(TAG, "Service is about to be destroyed");
+ audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver);
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ player.release();
+ stopWidgetUpdater();
+ updateWidget();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mBinder;
+ }
+
+ private final OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() {
+
+ @Override
+ public void onAudioFocusChange(int focusChange) {
+ switch (focusChange) {
+ case AudioManager.AUDIOFOCUS_LOSS:
+ Log.d(TAG, "Lost audio focus");
+ pause(true);
+ stopSelf();
+ break;
+ case AudioManager.AUDIOFOCUS_GAIN:
+ Log.d(TAG, "Gained audio focus");
+ if (pausedBecauseOfTransientAudiofocusLoss) {
+ play();
+ }
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+ Log.d(TAG, "Lost audio focus temporarily. Ducking...");
+ audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
+ AudioManager.ADJUST_LOWER, 0);
+ pausedBecauseOfTransientAudiofocusLoss = true;
+ break;
+ case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+ Log.d(TAG, "Lost audio focus temporarily. Pausing...");
+ pause(false);
+ pausedBecauseOfTransientAudiofocusLoss = true;
+ }
+ }
+ };
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1);
+ if (keycode != -1) {
+ Log.d(TAG, "Received media button event");
+ handleKeycode(keycode);
+ } else {
+
+ long mediaId = intent.getLongExtra(EXTRA_MEDIA_ID, -1);
+ long feedId = intent.getLongExtra(EXTRA_FEED_ID, -1);
+ boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM,
+ true);
+ if (mediaId == -1 || feedId == -1) {
+ Log.e(TAG,
+ "Media ID or Feed ID wasn't provided to the Service.");
+ if (media == null || feed == null) {
+ stopSelf();
+ }
+ // Intent values appear to be valid
+ // check if already playing and playbackType is the same
+ } else if (media == null || mediaId != media.getId()
+ || playbackType != shouldStream) {
+ pause(true);
+ player.reset();
+ if (media == null || mediaId != media.getId()) {
+ feed = manager.getFeed(feedId);
+ media = manager.getFeedMedia(mediaId, feed);
+ }
+
+ if (media != null) {
+ shouldStream = playbackType;
+ startWhenPrepared = intent.getBooleanExtra(
+ EXTRA_START_WHEN_PREPARED, false);
+ setupMediaplayer();
+
+ } else {
+ Log.e(TAG, "Media is null");
+ stopSelf();
+ }
+
+ } else if (media != null) {
+ if (status == PlayerStatus.PAUSED) {
+ play();
+ }
+
+ } else {
+ Log.w(TAG, "Something went wrong. Shutting down...");
+ stopSelf();
+ }
+ }
+ return Service.START_NOT_STICKY;
+ }
+
+ /** Handles media button events */
+ private void handleKeycode(int keycode) {
+ switch (keycode) {
+ case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ pause(true);
+ } else if (status == PlayerStatus.PAUSED) {
+ play();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PLAY:
+ if (status == PlayerStatus.PAUSED) {
+ play();
+ }
+ break;
+ case KeyEvent.KEYCODE_MEDIA_PAUSE:
+ if (status == PlayerStatus.PLAYING) {
+ pause(true);
+ }
+ break;
+ }
+ }
+
+ /**
+ * Called by a mediaplayer Activity as soon as it has prepared its
+ * mediaplayer.
+ */
+ public void setVideoSurface(SurfaceHolder sh) {
+ Log.d(TAG, "Setting display");
+ player.setDisplay(null);
+ player.setDisplay(sh);
+ if (status == PlayerStatus.STOPPED
+ || status == PlayerStatus.AWAITING_VIDEO_SURFACE) {
+ try {
+ if (shouldStream) {
+ player.setDataSource(media.getDownload_url());
+ setStatus(PlayerStatus.PREPARING);
+ player.prepareAsync();
+ } else {
+ player.setDataSource(media.getFile_url());
+ setStatus(PlayerStatus.PREPARING);
+ player.prepare();
+ }
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ /** Called when the surface holder of the mediaplayer has to be changed. */
+ public void resetVideoSurface() {
+ positionSaver.cancel(true);
+ player.setDisplay(null);
+ player.reset();
+ player.release();
+ player = new MediaPlayer();
+ player.setOnPreparedListener(preparedListener);
+ player.setOnCompletionListener(completionListener);
+ player.setOnSeekCompleteListener(onSeekCompleteListener);
+ player.setOnErrorListener(onErrorListener);
+ player.setOnBufferingUpdateListener(onBufferingUpdateListener);
+ status = PlayerStatus.STOPPED;
+ setupMediaplayer();
+ }
+
+ /** Called after service has extracted the media it is supposed to play. */
+ private void setupMediaplayer() {
+ try {
+ if (media.getMime_type().startsWith("audio")) {
+ playingVideo = false;
+ if (shouldStream) {
+ player.setDataSource(media.getDownload_url());
+ setStatus(PlayerStatus.PREPARING);
+ player.prepareAsync();
+ } else {
+ player.setDataSource(media.getFile_url());
+ setStatus(PlayerStatus.PREPARING);
+ player.prepare();
+ }
+ } else if (media.getMime_type().startsWith("video")) {
+ playingVideo = true;
+ setStatus(PlayerStatus.AWAITING_VIDEO_SURFACE);
+ player.setScreenOnWhilePlaying(true);
+ }
+
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (SecurityException e) {
+ e.printStackTrace();
+ } catch (IllegalStateException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void setupPositionSaver() {
+ if (positionSaver == null) {
+ positionSaver = new PositionSaver() {
+ @Override
+ protected void onCancelled(Void result) {
+ super.onCancelled(result);
+ positionSaver = null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+ positionSaver = null;
+ }
+ };
+ positionSaver.execute();
+ }
+ }
+
+ private MediaPlayer.OnPreparedListener preparedListener = new MediaPlayer.OnPreparedListener() {
+ @Override
+ public void onPrepared(MediaPlayer mp) {
+ Log.d(TAG, "Resource prepared");
+ mp.seekTo(media.getPosition());
+ setStatus(PlayerStatus.PREPARED);
+ if (startWhenPrepared) {
+ play();
+ }
+ }
+ };
+
+ private MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = new MediaPlayer.OnSeekCompleteListener() {
+
+ @Override
+ public void onSeekComplete(MediaPlayer mp) {
+ if (status == PlayerStatus.SEEKING) {
+ setStatus(statusBeforeSeek);
+ }
+
+ }
+ };
+
+ private MediaPlayer.OnErrorListener onErrorListener = new MediaPlayer.OnErrorListener() {
+ private static final String TAG = "PlaybackService.onErrorListener";
+
+ @Override
+ public boolean onError(MediaPlayer mp, int what, int extra) {
+ Log.w(TAG, "An error has occured: " + what);
+ if (mp.isPlaying()) {
+ pause(true);
+ }
+ sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what);
+ stopSelf();
+ return true;
+ }
+ };
+
+ private MediaPlayer.OnCompletionListener completionListener = new MediaPlayer.OnCompletionListener() {
+
+ @Override
+ public void onCompletion(MediaPlayer mp) {
+ Log.d(TAG, "Playback completed");
+ // Save state
+ positionSaver.cancel(true);
+ media.setPosition(0);
+ manager.markItemRead(PlaybackService.this, media.getItem(), true);
+ boolean isInQueue = manager.isInQueue(media.getItem());
+ if (isInQueue) {
+ manager.removeQueueItem(PlaybackService.this, media.getItem());
+ }
+ manager.setFeedMedia(PlaybackService.this, media);
+
+ // Prepare for playing next item
+ boolean followQueue = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext())
+ .getBoolean(PodcastApp.PREF_FOLLOW_QUEUE, false);
+ FeedItem nextItem = manager.getFirstQueueItem();
+ if (isInQueue && followQueue && nextItem != null) {
+ Log.d(TAG, "Loading next item in queue");
+ media = nextItem.getMedia();
+ feed = nextItem.getFeed();
+ shouldStream = !media.isDownloaded();
+ resetVideoSurface();
+ sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0);
+ } else {
+ Log.d(TAG, "Stopping playback");
+ stopWidgetUpdater();
+ setStatus(PlayerStatus.STOPPED);
+ stopForeground(true);
+ }
+
+ }
+ };
+
+ private MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener() {
+
+ @Override
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent);
+
+ }
+ };
+
+ /**
+ * Saves the current position and pauses playback
+ *
+ * @param abandonFocus
+ * is true if the service should release audio focus
+ */
+ public void pause(boolean abandonFocus) {
+ if (player.isPlaying()) {
+ Log.d(TAG, "Pausing playback.");
+ player.pause();
+ if (abandonFocus) {
+ audioManager.abandonAudioFocus(audioFocusChangeListener);
+ }
+ if (positionSaver != null) {
+ positionSaver.cancel(true);
+ }
+ saveCurrentPosition();
+ stopWidgetUpdater();
+ setStatus(PlayerStatus.PAUSED);
+ stopForeground(true);
+ }
+ }
+
+ /** Pauses playback and destroys service. Recommended for video playback. */
+ public void stop() {
+ pause(true);
+ stopSelf();
+ }
+
+ public void play() {
+ if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED
+ || status == PlayerStatus.STOPPED) {
+ int focusGained = audioManager.requestAudioFocus(
+ audioFocusChangeListener, AudioManager.STREAM_MUSIC,
+ AudioManager.AUDIOFOCUS_GAIN);
+
+ if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
+ Log.d(TAG, "Audiofocus successfully requested");
+ Log.d(TAG, "Resuming/Starting playback");
+ SharedPreferences.Editor editor = getApplicationContext()
+ .getSharedPreferences(PodcastApp.PREF_NAME, 0).edit();
+ editor.putLong(PREF_LAST_PLAYED_ID, media.getId());
+ editor.putLong(PREF_LAST_PLAYED_FEED_ID, feed.getId());
+ editor.putBoolean(PREF_LAST_IS_STREAM, shouldStream);
+ editor.commit();
+
+ player.start();
+ player.seekTo((int) media.getPosition());
+ setStatus(PlayerStatus.PLAYING);
+ setupPositionSaver();
+ setupWidgetUpdater();
+ setupNotification();
+ pausedBecauseOfTransientAudiofocusLoss = false;
+ } else {
+ Log.d(TAG, "Failed to request Audiofocus");
+ }
+ }
+ }
+
+ private void setStatus(PlayerStatus newStatus) {
+ Log.d(TAG, "Setting status to " + newStatus);
+ status = newStatus;
+ sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED));
+ updateWidget();
+ }
+
+ private void sendNotificationBroadcast(int type, int code) {
+ Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION);
+ intent.putExtra(EXTRA_NOTIFICATION_TYPE, type);
+ intent.putExtra(EXTRA_NOTIFICATION_CODE, code);
+ sendBroadcast(intent);
+ }
+
+ /** Prepares notification and starts the service in the foreground. */
+ private void setupNotification() {
+ PendingIntent pIntent = PendingIntent.getActivity(this, 0, new Intent(
+ this, MediaplayerActivity.class),
+ PendingIntent.FLAG_UPDATE_CURRENT);
+
+ Bitmap icon = BitmapFactory.decodeResource(null,
+ R.drawable.stat_notify_sdcard);
+ notificationBuilder = new NotificationCompat.Builder(this)
+ .setContentTitle("Mediaplayer Service")
+ .setContentText("Click here for more info").setOngoing(true)
+ .setContentIntent(pIntent).setLargeIcon(icon)
+ .setSmallIcon(R.drawable.stat_notify_sdcard);
+
+ startForeground(NOTIFICATION_ID, notificationBuilder.getNotification());
+ Log.d(TAG, "Notification set up");
+ }
+
+ /**
+ * Seek a specific position from the current position
+ *
+ * @param delta
+ * offset from current position (positive or negative)
+ * */
+ public void seekDelta(int delta) {
+ seek(player.getCurrentPosition() + delta);
+ }
+
+ public void seek(int i) {
+ Log.d(TAG, "Seeking position " + i);
+ if (shouldStream) {
+ statusBeforeSeek = status;
+ setStatus(PlayerStatus.SEEKING);
+ }
+ player.seekTo(i);
+ saveCurrentPosition();
+ }
+
+ /** Saves the current position of the media file to the DB */
+ private synchronized void saveCurrentPosition() {
+ Log.d(TAG, "Saving current position to " + player.getCurrentPosition());
+ media.setPosition(player.getCurrentPosition());
+ manager.setFeedMedia(this, media);
+ }
+
+ private void stopWidgetUpdater() {
+ if (widgetUpdater != null) {
+ widgetUpdater.cancel(true);
+ }
+ }
+
+ private void setupWidgetUpdater() {
+ if (widgetUpdater == null || widgetUpdater.isCancelled()) {
+ widgetUpdater = new WidgetUpdateWorker();
+ widgetUpdater.execute();
+ }
+ }
+
+ private void updateWidget() {
+ Log.d(TAG, "Sending widget update request");
+ PlaybackService.this.sendBroadcast(new Intent(
+ PlayerWidget.FORCE_WIDGET_UPDATE));
+ }
+
+ /**
+ * Pauses playback when the headset is disconnected and the preference is
+ * set
+ */
+ private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() {
+ private static final String TAG = "headsetDisconnected";
+ private static final int UNPLUGGED = 0;
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(Intent.ACTION_HEADSET_PLUG)) {
+ int state = intent.getIntExtra("state", -1);
+ if (state != -1) {
+ Log.d(TAG, "Headset plug event. State is " + state);
+ boolean pauseOnDisconnect = PreferenceManager
+ .getDefaultSharedPreferences(
+ getApplicationContext())
+ .getBoolean(
+ PodcastApp.PREF_PAUSE_ON_HEADSET_DISCONNECT,
+ false);
+ Log.d(TAG, "pauseOnDisconnect preference is "
+ + pauseOnDisconnect);
+ if (state == UNPLUGGED && pauseOnDisconnect
+ && status == PlayerStatus.PLAYING) {
+ Log.d(TAG,
+ "Pausing playback because headset was disconnected");
+ pause(true);
+ }
+ } else {
+ Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent");
+ }
+ }
+ }
+ };
+
+ /** Periodically saves the position of the media file */
+ class PositionSaver extends AsyncTask<Void, Void, Void> {
+ private static final int WAITING_INTERVALL = 5000;
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ while (!isCancelled() && player.isPlaying()) {
+ try {
+ Thread.sleep(WAITING_INTERVALL);
+ saveCurrentPosition();
+ } catch (InterruptedException e) {
+ Log.d(TAG,
+ "Thread was interrupted while waiting. Finishing now...");
+ return null;
+ } catch (IllegalStateException e) {
+ Log.d(TAG, "Player is in illegal state. Finishing now");
+ return null;
+ }
+
+ }
+ return null;
+ }
+
+ }
+
+ /** Notifies the player widget in the specified intervall */
+ class WidgetUpdateWorker extends AsyncTask<Void, Void, Void> {
+ private static final String TAG = "WidgetUpdateWorker";
+ private static final int NOTIFICATION_INTERVALL = 2000;
+
+ @Override
+ protected void onProgressUpdate(Void... values) {
+ updateWidget();
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ while (PlaybackService.isRunning && !isCancelled()) {
+ publishProgress();
+ try {
+ Thread.sleep(NOTIFICATION_INTERVALL);
+ } catch (InterruptedException e) {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ }
+
+ public boolean isPlayingVideo() {
+ return playingVideo;
+ }
+
+ public boolean isShouldStream() {
+ return shouldStream;
+ }
+
+ public PlayerStatus getStatus() {
+ return status;
+ }
+
+ public FeedMedia getMedia() {
+ return media;
+ }
+
+ public MediaPlayer getPlayer() {
+ return player;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/service/PlayerStatus.java b/src/de/danoeh/antennapod/service/PlayerStatus.java
new file mode 100644
index 000000000..c1c047e2b
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/PlayerStatus.java
@@ -0,0 +1,5 @@
+package de.danoeh.antennapod.service;
+
+public enum PlayerStatus {
+ ERROR, PREPARING, PAUSED, PLAYING, STOPPED, PREPARED, SEEKING, AWAITING_VIDEO_SURFACE
+}
diff --git a/src/de/danoeh/antennapod/service/PlayerWidgetService.java b/src/de/danoeh/antennapod/service/PlayerWidgetService.java
new file mode 100644
index 000000000..e68395062
--- /dev/null
+++ b/src/de/danoeh/antennapod/service/PlayerWidgetService.java
@@ -0,0 +1,135 @@
+package de.danoeh.antennapod.service;
+
+import android.app.PendingIntent;
+import android.app.Service;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.media.MediaPlayer;
+import android.os.IBinder;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.widget.RemoteViews;
+import de.danoeh.antennapod.activity.MediaplayerActivity;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.receiver.MediaButtonReceiver;
+import de.danoeh.antennapod.receiver.PlayerWidget;
+import de.danoeh.antennapod.util.Converter;
+import de.danoeh.antennapod.R;
+
+/** Updates the state of the player widget */
+public class PlayerWidgetService extends Service {
+ private static final String TAG = "PlayerWidgetService";
+
+ private PlaybackService playbackService;
+ /** True while service is updating the widget */
+ private boolean isUpdating;
+
+ public PlayerWidgetService() {
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ Log.d(TAG, "Service created");
+ isUpdating = false;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (!isUpdating) {
+ isUpdating = true;
+ if (playbackService == null && PlaybackService.isRunning) {
+ bindService(new Intent(this, PlaybackService.class),
+ mConnection, 0);
+ } else {
+ updateViews();
+ isUpdating = false;
+ }
+ } else {
+ Log.d(TAG,
+ "Service was called while updating. Ignoring update request");
+ }
+ return Service.START_NOT_STICKY;
+ }
+
+ private void updateViews() {
+ Log.d(TAG, "Updating widget views");
+ ComponentName playerWidget = new ComponentName(this, PlayerWidget.class);
+ AppWidgetManager manager = AppWidgetManager.getInstance(this);
+ RemoteViews views = new RemoteViews(getPackageName(),
+ R.layout.player_widget);
+ PendingIntent startMediaplayer = PendingIntent.getActivity(this, 0,
+ new Intent(this, MediaplayerActivity.class), 0);
+
+ views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer);
+ if (playbackService != null) {
+ FeedMedia media = playbackService.getMedia();
+ MediaPlayer player = playbackService.getPlayer();
+ PlayerStatus status = playbackService.getStatus();
+
+ views.setTextViewText(R.id.txtvTitle, media.getItem().getTitle());
+
+ if (status == PlayerStatus.PLAYING) {
+ views.setTextViewText(R.id.txtvProgress,
+ getProgressString(player));
+ views.setImageViewResource(R.id.butPlay, R.drawable.av_pause);
+ } else {
+ views.setImageViewResource(R.id.butPlay, R.drawable.av_play);
+ }
+ views.setOnClickPendingIntent(R.id.butPlay,
+ createMediaButtonIntent());
+ } else {
+ Log.d(TAG, "No media playing. Displaying defaultt views");
+ views.setViewVisibility(R.id.txtvProgress, View.INVISIBLE);
+ views.setTextViewText(R.id.txtvTitle,
+ this.getString(R.string.no_media_playing_label));
+ views.setImageViewResource(R.id.butPlay, R.drawable.av_play);
+
+ }
+
+ manager.updateAppWidget(playerWidget, views);
+ }
+
+ /** Creates an intent which fakes a mediabutton press */
+ private PendingIntent createMediaButtonIntent() {
+ KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN,
+ KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE);
+ Intent startingIntent = new Intent(
+ MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER);
+ startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event);
+
+ return PendingIntent.getBroadcast(this, 0, startingIntent, 0);
+ }
+
+ private String getProgressString(MediaPlayer player) {
+
+ return Converter.getDurationStringLong(player.getCurrentPosition())
+ + " / " + Converter.getDurationStringLong(player.getDuration());
+ }
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ Log.d(TAG, "Connection to service established");
+ playbackService = ((PlaybackService.LocalBinder) service)
+ .getService();
+ updateViews();
+ isUpdating = false;
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ playbackService = null;
+ Log.d(TAG, "Disconnected from service");
+ }
+
+ };
+
+}
diff --git a/src/de/danoeh/antennapod/storage/DownloadRequester.java b/src/de/danoeh/antennapod/storage/DownloadRequester.java
new file mode 100644
index 000000000..6aaabafaa
--- /dev/null
+++ b/src/de/danoeh/antennapod/storage/DownloadRequester.java
@@ -0,0 +1,247 @@
+package de.danoeh.antennapod.storage;
+
+import java.util.ArrayList;
+import java.io.File;
+import java.util.concurrent.Callable;
+
+import de.danoeh.antennapod.feed.*;
+import de.danoeh.antennapod.service.DownloadService;
+import de.danoeh.antennapod.util.NumberGenerator;
+import de.danoeh.antennapod.R;
+
+import android.util.Log;
+import android.database.Cursor;
+import android.annotation.SuppressLint;
+import android.app.DownloadManager;
+import android.content.Context;
+import android.net.Uri;
+import android.os.Messenger;
+import android.content.ServiceConnection;
+import android.os.IBinder;
+import android.content.ComponentName;
+import android.os.Message;
+import android.os.RemoteException;
+import android.content.Intent;
+import android.webkit.URLUtil;
+
+public class DownloadRequester {
+ private static final String TAG = "DownloadRequester";
+ private static final int currentApi = android.os.Build.VERSION.SDK_INT;
+
+ public static String EXTRA_DOWNLOAD_ID = "extra.de.danoeh.antennapod.storage.download_id";
+ public static String EXTRA_ITEM_ID = "extra.de.danoeh.antennapod.storage.item_id";
+
+ public static String ACTION_DOWNLOAD_QUEUED = "action.de.danoeh.antennapod.storage.downloadQueued";
+
+
+ private static boolean STORE_ON_SD = true;
+ public static String IMAGE_DOWNLOADPATH = "images/";
+ public static String FEED_DOWNLOADPATH = "cache/";
+ public static String MEDIA_DOWNLOADPATH = "media/";
+
+ private static DownloadRequester downloader;
+ private DownloadManager manager;
+
+ public ArrayList<FeedFile> downloads;
+
+ private DownloadRequester() {
+ downloads = new ArrayList<FeedFile>();
+ }
+
+ public static DownloadRequester getInstance() {
+ if (downloader == null) {
+ downloader = new DownloadRequester();
+ }
+ return downloader;
+ }
+
+ @SuppressLint("NewApi")
+ private long download(Context context, FeedFile item, File dest) {
+ if (dest.exists()) {
+ Log.d(TAG, "File already exists. Deleting !");
+ dest.delete();
+ }
+ Log.d(TAG, "Requesting download of url " + item.getDownload_url());
+ downloads.add(item);
+ DownloadManager.Request request = new DownloadManager.Request(
+ Uri.parse(item.getDownload_url())).setDestinationUri(Uri
+ .fromFile(dest));
+ Log.d(TAG, "Version is " + currentApi);
+ if (currentApi >= 11) {
+ request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN);
+ } else {
+ request.setVisibleInDownloadsUi(false);
+ request.setShowRunningNotification(false);
+ }
+
+ // TODO Set Allowed Network Types
+ DownloadManager manager = (DownloadManager) context
+ .getSystemService(Context.DOWNLOAD_SERVICE);
+
+ long downloadId = manager.enqueue(request);
+ item.setDownloadId(downloadId);
+ item.setFile_url(dest.toString());
+ context.startService(new Intent(context, DownloadService.class));
+ context.sendBroadcast(new Intent(ACTION_DOWNLOAD_QUEUED));
+ return downloadId;
+ }
+
+ public long downloadFeed(Context context, Feed feed) {
+ return download(context, feed, new File(getFeedfilePath(context),
+ getFeedfileName(feed)));
+ }
+
+ public long downloadImage(Context context, FeedImage image) {
+ return download(context, image, new File(getImagefilePath(context),
+ getImagefileName(image)));
+ }
+
+ public long downloadMedia(Context context, FeedMedia feedmedia) {
+ return download(context, feedmedia,
+ new File(getMediafilePath(context, feedmedia),
+ getMediafilename(feedmedia)));
+ }
+
+ /**
+ * Cancels a running download.
+ *
+ * @param context
+ * A context needed to get the DownloadManager service
+ * @param id
+ * ID of the download to cancel
+ * */
+ public void cancelDownload(final Context context, final long id) {
+ Log.d(TAG, "Cancelling download with id " + id);
+ DownloadManager dm = (DownloadManager) context
+ .getSystemService(Context.DOWNLOAD_SERVICE);
+ int removed = dm.remove(id);
+ if (removed > 0) {
+ FeedFile f = getFeedFile(id);
+ if (f != null) {
+ downloads.remove(f);
+ f.setFile_url(null);
+ f.setDownloadId(0);
+ }
+ notifyDownloadService(context);
+ }
+ }
+
+ /** Cancels all running downloads */
+ public void cancelAllDownloads(Context context) {
+ Log.d(TAG, "Cancelling all running downloads");
+ DownloadManager dm = (DownloadManager) context
+ .getSystemService(Context.DOWNLOAD_SERVICE);
+ for (FeedFile f : downloads) {
+ dm.remove(f.getDownloadId());
+ f.setFile_url(null);
+ f.setDownloadId(0);
+ }
+ downloads.clear();
+ notifyDownloadService(context);
+ }
+
+ /** Get a feedfile by its download id */
+ public FeedFile getFeedFile(long id) {
+ for (FeedFile f : downloads) {
+ if (f.getDownloadId() == id) {
+ return f;
+ }
+ }
+ return null;
+ }
+
+ /** Returns true if there is at least one Feed in the downloads queue. */
+ public boolean isDownloadingFeeds() {
+ for (FeedFile f : downloads) {
+ if (f.getClass() == Feed.class) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Checks if feedfile is in the downloads list */
+ public boolean isDownloadingFile(FeedFile item) {
+ for (FeedFile f : downloads) {
+ if (f.getDownload_url().equals(item.getDownload_url())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** Remove an object from the downloads-list of the requester. */
+ public void removeDownload(FeedFile f) {
+ downloads.remove(f);
+ }
+
+ public ArrayList<FeedFile> getDownloads() {
+ return downloads;
+ }
+
+ /** Get the number of uncompleted Downloads */
+ public int getNumberOfDownloads() {
+ return downloads.size();
+ }
+
+ public String getFeedfilePath(Context context) {
+ return context.getExternalFilesDir(FEED_DOWNLOADPATH).toString() + "/";
+ }
+
+ public String getFeedfileName(Feed feed) {
+ return "feed-" + NumberGenerator.generateLong(feed.getDownload_url());
+ }
+
+ public String getImagefilePath(Context context) {
+ return context.getExternalFilesDir(IMAGE_DOWNLOADPATH).toString() + "/";
+ }
+
+ public String getImagefileName(FeedImage image) {
+ return "image-" + NumberGenerator.generateLong(image.getDownload_url());
+ }
+
+ public String getMediafilePath(Context context, FeedMedia media) {
+ return context
+ .getExternalFilesDir(
+ MEDIA_DOWNLOADPATH
+ + media.getItem().getFeed().getTitle() + "/")
+ .toString();
+ }
+
+ public String getMediafilename(FeedMedia media) {
+ return URLUtil.guessFileName(media.getDownload_url(), null,
+ media.getMime_type());
+ }
+
+ /*
+ * ------------ Methods for communicating with the DownloadService
+ * -------------
+ */
+ private DownloadService mService = null;
+ private Context mContext = null;
+ boolean mIsBound;
+
+ private ServiceConnection mConnection = new ServiceConnection() {
+ public void onServiceConnected(ComponentName className, IBinder service) {
+ mService = ((DownloadService.LocalBinder) service).getService();
+ Log.d(TAG, "Connection to service established");
+ mService.queryDownloads();
+ mContext.unbindService(mConnection);
+ }
+
+ public void onServiceDisconnected(ComponentName className) {
+ mService = null;
+ mIsBound = false;
+ mContext = null;
+ Log.i(TAG, "Closed connection with DownloadService.");
+ }
+ };
+
+ /** Notifies the DownloadService to check if there are any Downloads left */
+ public void notifyDownloadService(Context context) {
+ context.bindService(new Intent(context, DownloadService.class),
+ mConnection, Context.BIND_AUTO_CREATE);
+ mContext = context;
+ mIsBound = true;
+ }
+}
diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java
new file mode 100644
index 000000000..c9a68717d
--- /dev/null
+++ b/src/de/danoeh/antennapod/storage/PodDBAdapter.java
@@ -0,0 +1,591 @@
+package de.danoeh.antennapod.storage;
+
+import java.util.ArrayList;
+
+import de.danoeh.antennapod.asynctask.DownloadStatus;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedCategory;
+import de.danoeh.antennapod.feed.FeedImage;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.feed.SimpleChapter;
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteDatabase.CursorFactory;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.util.Log;
+
+/**
+ * Implements methods for accessing the database
+ * */
+public class PodDBAdapter {
+ private static final String TAG = "PodDBAdapter";
+ private static final int DATABASE_VERSION = 1;
+ private static final String DATABASE_NAME = "Antennapod.db";
+
+ // Key-constants
+ public static final String KEY_ID = "id";
+ public static final String KEY_TITLE = "title";
+ public static final String KEY_NAME = "name";
+ public static final String KEY_LINK = "link";
+ public static final String KEY_DESCRIPTION = "description";
+ public static final String KEY_FILE_URL = "file_url";
+ public static final String KEY_DOWNLOAD_URL = "download_url";
+ public static final String KEY_PUBDATE = "pubDate";
+ public static final String KEY_READ = "read";
+ public static final String KEY_DURATION = "duration";
+ public static final String KEY_POSITION = "position";
+ public static final String KEY_SIZE = "filesize";
+ public static final String KEY_MIME_TYPE = "mime_type";
+ public static final String KEY_IMAGE = "image";
+ public static final String KEY_CATEGORY = "category";
+ public static final String KEY_FEED = "feed";
+ public static final String KEY_MEDIA = "media";
+ public static final String KEY_DOWNLOADED = "downloaded";
+ public static final String KEY_LASTUPDATE = "last_update";
+ public static final String KEY_FEEDFILE = "feedfile";
+ public static final String KEY_REASON = "reason";
+ public static final String KEY_SUCCESSFUL = "successful";
+ public static final String KEY_FEEDFILETYPE = "feedfile_type";
+ public static final String KEY_COMPLETION_DATE = "completion_date";
+ public static final String KEY_FEEDITEM = "feeditem";
+ public static final String KEY_CONTENT_ENCODED = "content_encoded";
+ public static final String KEY_PAYMENT_LINK = "payment_link";
+ public static final String KEY_START = "start";
+ public static final String KEY_LANGUAGE = "language";
+ public static final String KEY_AUTHOR = "author";
+
+ // Table names
+ public static final String TABLE_NAME_FEEDS = "Feeds";
+ public static final String TABLE_NAME_FEED_ITEMS = "FeedItems";
+ public static final String TABLE_NAME_FEED_CATEGORIES = "FeedCategories";
+ public static final String TABLE_NAME_FEED_IMAGES = "FeedImages";
+ public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia";
+ public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog";
+ public static final String TABLE_NAME_QUEUE = "Queue";
+ public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters";
+
+ // SQL Statements for creating new tables
+ private static final String TABLE_PRIMARY_KEY = KEY_ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT ,";
+ private static final String CREATE_TABLE_FEEDS = "CREATE TABLE "
+ + TABLE_NAME_FEEDS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ + " TEXT," + KEY_LINK + " TEXT," + KEY_DESCRIPTION + " TEXT,"
+ + KEY_IMAGE + " INTEGER," + KEY_CATEGORY + " INTEGER,"
+ + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT,"
+ + KEY_DOWNLOADED + " INTEGER," + KEY_LASTUPDATE + " TEXT,"
+ + KEY_PAYMENT_LINK + " TEXT," + KEY_LANGUAGE + " TEXT,"
+ + KEY_AUTHOR + " TEXT)";
+
+ private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE "
+ + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ + " TEXT," + KEY_LINK + " TEXT," + KEY_DESCRIPTION + " TEXT,"
+ + KEY_CONTENT_ENCODED + " TEXT," + KEY_PUBDATE + " INTEGER,"
+ + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," + KEY_READ
+ + " INTEGER," + KEY_PAYMENT_LINK + " TEXT)";
+
+ private static final String CREATE_TABLE_FEED_CATEGORIES = "CREATE TABLE "
+ + TABLE_NAME_FEED_CATEGORIES + " (" + TABLE_PRIMARY_KEY + KEY_NAME
+ + " TEXT)";
+
+ private static final String CREATE_TABLE_FEED_IMAGES = "CREATE TABLE "
+ + TABLE_NAME_FEED_IMAGES + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT,"
+ + KEY_DOWNLOADED + " INTEGER)";
+
+ private static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE "
+ + TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION
+ + " INTEGER," + KEY_POSITION + " INTEGER," + KEY_SIZE + " INTEGER,"
+ + KEY_MIME_TYPE + " TEXT," + KEY_FILE_URL + " TEXT,"
+ + KEY_DOWNLOAD_URL + " TEXT," + KEY_DOWNLOADED + " INTEGER)";
+
+ private static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE "
+ + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE
+ + " INTEGER," + KEY_FEEDFILETYPE + " INTEGER," + KEY_REASON
+ + " INTEGER," + KEY_SUCCESSFUL + " INTEGER," + KEY_COMPLETION_DATE
+ + " INTEGER)";
+
+ private static final String CREATE_TABLE_QUEUE = "CREATE TABLE "
+ + TABLE_NAME_QUEUE + "(" + KEY_ID + " INTEGER PRIMARY KEY,"
+ + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)";
+
+ private static final String CREATE_TABLE_SIMPLECHAPTERS = "CREATE TABLE "
+ + TABLE_NAME_SIMPLECHAPTERS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
+ + " TEXT," + KEY_START + " INTEGER," + KEY_FEEDITEM + " INTEGER)";
+
+ /**
+ * Used for storing download status entries to determine the type of the
+ * Feedfile.
+ */
+ public static final int FEEDFILETYPE_FEED = 0;
+ public static final int FEEDFILETYPE_FEEDIMAGE = 1;
+ public static final int FEEDFILETYPE_FEEDMEDIA = 2;
+
+ private SQLiteDatabase db;
+ private final Context context;
+ private PodDBHelper helper;
+
+ public PodDBAdapter(Context c) {
+ this.context = c;
+ helper = new PodDBHelper(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ public PodDBAdapter open() {
+ if (db == null || !db.isOpen() || db.isReadOnly()) {
+ Log.d(TAG, "Opening DB");
+ try {
+ db = helper.getWritableDatabase();
+ } catch (SQLException ex) {
+ ex.printStackTrace();
+ db = helper.getReadableDatabase();
+ }
+ }
+ return this;
+ }
+
+ public void close() {
+ Log.d(TAG, "Closing DB");
+ db.close();
+ }
+
+ /**
+ * Inserts or updates a feed entry
+ *
+ * @return the id of the entry
+ * */
+ public long setFeed(Feed feed) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_TITLE, feed.getTitle());
+ values.put(KEY_LINK, feed.getLink());
+ values.put(KEY_DESCRIPTION, feed.getDescription());
+ values.put(KEY_PAYMENT_LINK, feed.getPaymentLink());
+ values.put(KEY_AUTHOR, feed.getAuthor());
+ values.put(KEY_LANGUAGE, feed.getLanguage());
+ if (feed.getImage() != null) {
+ if (feed.getImage().getId() == 0) {
+ setImage(feed.getImage());
+ }
+ values.put(KEY_IMAGE, feed.getImage().getId());
+ }
+ if (feed.getCategory() != null) {
+ if (feed.getCategory().getId() == 0) {
+ setCategory(feed.getCategory());
+ }
+ values.put(KEY_CATEGORY, feed.getCategory().getId());
+ }
+ values.put(KEY_FILE_URL, feed.getFile_url());
+ values.put(KEY_DOWNLOAD_URL, feed.getDownload_url());
+ values.put(KEY_DOWNLOADED, feed.isDownloaded());
+ values.put(KEY_LASTUPDATE, feed.getLastUpdate().getTime());
+ if (feed.getId() == 0) {
+ // Create new entry
+ Log.d(this.toString(), "Inserting new Feed into db");
+ feed.setId(db.insert(TABLE_NAME_FEEDS, null, values));
+ } else {
+ Log.d(this.toString(), "Updating existing Feed in db");
+ db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?",
+ new String[] { Long.toString(feed.getId()) });
+ }
+ return feed.getId();
+ }
+
+ /**
+ * Inserts or updates a category entry
+ *
+ * @return the id of the entry
+ * */
+ public long setCategory(FeedCategory category) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_NAME, category.getName());
+ if (category.getId() == 0) {
+ category.setId(db.insert(TABLE_NAME_FEED_CATEGORIES, null, values));
+ } else {
+ db.update(TABLE_NAME_FEED_CATEGORIES, values, KEY_ID + "=?",
+ new String[] { String.valueOf(category.getId()) });
+
+ }
+ return category.getId();
+ }
+
+ /**
+ * Inserts or updates an image entry
+ *
+ * @return the id of the entry
+ * */
+ public long setImage(FeedImage image) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_TITLE, image.getTitle());
+ values.put(KEY_DOWNLOAD_URL, image.getDownload_url());
+ values.put(KEY_DOWNLOADED, image.isDownloaded());
+ values.put(KEY_FILE_URL, image.getFile_url());
+ if (image.getId() == 0) {
+ image.setId(db.insert(TABLE_NAME_FEED_IMAGES, null, values));
+ } else {
+ db.update(TABLE_NAME_FEED_IMAGES, values, KEY_ID + "=?",
+ new String[] { String.valueOf(image.getId()) });
+ }
+ return image.getId();
+ }
+
+ /**
+ * Inserts or updates an image entry
+ *
+ * @return the id of the entry
+ */
+ public long setMedia(FeedMedia media) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_DURATION, media.getDuration());
+ values.put(KEY_POSITION, media.getPosition());
+ values.put(KEY_SIZE, media.getSize());
+ values.put(KEY_MIME_TYPE, media.getMime_type());
+ values.put(KEY_DOWNLOAD_URL, media.getDownload_url());
+ values.put(KEY_DOWNLOADED, media.isDownloaded());
+ values.put(KEY_FILE_URL, media.getFile_url());
+ if (media.getId() == 0) {
+ media.setId(db.insert(TABLE_NAME_FEED_MEDIA, null, values));
+ } else {
+ db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?",
+ new String[] { String.valueOf(media.getId()) });
+ }
+ return media.getId();
+ }
+
+ /** Insert all FeedItems of a feed and the feed object itself in a single transaction */
+ public void setCompleteFeed(Feed feed) {
+ db.beginTransaction();
+ setFeed(feed);
+ for (FeedItem item : feed.getItems()) {
+ setFeedItem(item);
+ }
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ }
+
+ public long setSingleFeedItem(FeedItem item) {
+ db.beginTransaction();
+ long result = setFeedItem(item);
+ db.setTransactionSuccessful();
+ db.endTransaction();
+ return result;
+ }
+
+ /**
+ * Inserts or updates a feeditem entry
+ *
+ * @return the id of the entry
+ */
+ private long setFeedItem(FeedItem item) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_TITLE, item.getTitle());
+ values.put(KEY_LINK, item.getLink());
+ values.put(KEY_DESCRIPTION, item.getDescription());
+ values.put(KEY_CONTENT_ENCODED, item.getContentEncoded());
+ values.put(KEY_PUBDATE, item.getPubDate().getTime());
+ values.put(KEY_PAYMENT_LINK, item.getPaymentLink());
+ if (item.getMedia() != null) {
+ if (item.getMedia().getId() == 0) {
+ setMedia(item.getMedia());
+ }
+ values.put(KEY_MEDIA, item.getMedia().getId());
+ }
+ if (item.getFeed().getId() == 0) {
+ setFeed(item.getFeed());
+ }
+ values.put(KEY_FEED, item.getFeed().getId());
+ values.put(KEY_READ, item.isRead());
+
+ if (item.getId() == 0) {
+ Log.d(TAG, "inserting new feeditem into db");
+ item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values));
+ } else {
+ Log.d(TAG, "updating existing feeditem in db");
+ db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?",
+ new String[] { String.valueOf(item.getId()) });
+ }
+ if (item.getSimpleChapters() != null) {
+ setSimpleChapters(item);
+ }
+ return item.getId();
+ }
+
+ public void setSimpleChapters(FeedItem item) {
+ ContentValues values = new ContentValues();
+ for (SimpleChapter chapter : item.getSimpleChapters()) {
+ values.put(KEY_TITLE, chapter.getTitle());
+ values.put(KEY_START, chapter.getStart());
+ values.put(KEY_FEEDITEM, item.getId());
+ if (chapter.getId() == 0) {
+ chapter.setId(db
+ .insert(TABLE_NAME_SIMPLECHAPTERS, null, values));
+ } else {
+ db.update(TABLE_NAME_SIMPLECHAPTERS, values, KEY_ID + "=?",
+ new String[] { String.valueOf(chapter.getId()) });
+ }
+ }
+ }
+
+ /**
+ * Inserts or updates a download status.
+ * */
+ public long setDownloadStatus(DownloadStatus status) {
+ // Don't save failed downloads
+ if (status.getFeedFile() != null) {
+ ContentValues values = new ContentValues();
+ values.put(KEY_FEEDFILE, status.getFeedFile().getId());
+ if (status.getFeedFile().getClass() == Feed.class) {
+ values.put(KEY_FEEDFILETYPE, FEEDFILETYPE_FEED);
+ } else if (status.getFeedFile().getClass() == FeedImage.class) {
+ values.put(KEY_FEEDFILETYPE, FEEDFILETYPE_FEEDIMAGE);
+ } else if (status.getFeedFile().getClass() == FeedMedia.class) {
+ values.put(KEY_FEEDFILETYPE, FEEDFILETYPE_FEEDMEDIA);
+ }
+
+ values.put(KEY_REASON, status.getReason());
+ values.put(KEY_SUCCESSFUL, status.isSuccessful());
+ values.put(KEY_COMPLETION_DATE, status.getCompletionDate()
+ .getTime());
+ if (status.getId() == 0) {
+ status.setId(db.insert(TABLE_NAME_DOWNLOAD_LOG, null, values));
+ } else {
+ db.update(TABLE_NAME_DOWNLOAD_LOG, values, KEY_ID + "=?",
+ new String[] { String.valueOf(status.getId()) });
+ }
+ }
+ return status.getId();
+ }
+
+ public void setQueue(ArrayList<FeedItem> queue) {
+ ContentValues values = new ContentValues();
+ db.delete(TABLE_NAME_QUEUE, null, null);
+ for (int i = 0; i < queue.size(); i++) {
+ FeedItem item = queue.get(i);
+ values.put(KEY_ID, i);
+ values.put(KEY_FEEDITEM, item.getId());
+ values.put(KEY_FEED, item.getFeed().getId());
+ db.insertWithOnConflict(TABLE_NAME_QUEUE, null, values,
+ SQLiteDatabase.CONFLICT_REPLACE);
+ }
+ }
+
+ public void removeFeedMedia(FeedMedia media) {
+ db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?",
+ new String[] { String.valueOf(media.getId()) });
+ }
+
+ public void removeFeedImage(FeedImage image) {
+ db.delete(TABLE_NAME_FEED_IMAGES, KEY_ID + "=?",
+ new String[] { String.valueOf(image.getId()) });
+ }
+
+ /** Remove a FeedItem and its FeedMedia entry. */
+ public void removeFeedItem(FeedItem item) {
+ if (item.getMedia() != null) {
+ removeFeedMedia(item.getMedia());
+ }
+ db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + "=?",
+ new String[] { String.valueOf(item.getId()) });
+ }
+
+ /** Remove a feed with all its FeedItems and Media entries. */
+ public void removeFeed(Feed feed) {
+ if (feed.getImage() != null) {
+ removeFeedImage(feed.getImage());
+ }
+ for (FeedItem item : feed.getItems()) {
+ removeFeedItem(item);
+ }
+ db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?",
+ new String[] { String.valueOf(feed.getId()) });
+ }
+
+ public void removeDownloadStatus(DownloadStatus remove) {
+ db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_ID + "=?",
+ new String[] { String.valueOf(remove.getId()) });
+ }
+
+ /**
+ * Get all Categories from the Categories Table.
+ *
+ * @return The cursor of the query
+ * */
+ public final Cursor getAllCategoriesCursor() {
+ open();
+ Cursor c = db.query(TABLE_NAME_FEED_CATEGORIES, null, null, null, null,
+ null, null);
+ return c;
+ }
+
+ /**
+ * Get all Feeds from the Feed Table.
+ *
+ * @return The cursor of the query
+ * */
+ public final Cursor getAllFeedsCursor() {
+ open();
+ Cursor c = db.query(TABLE_NAME_FEEDS, null, null, null, null, null,
+ null);
+ return c;
+ }
+
+ /**
+ * Returns a cursor with all FeedItems of a Feed.
+ *
+ * @param feed
+ * The feed you want to get the FeedItems from.
+ * @return The cursor of the query
+ * */
+ public final Cursor getAllItemsOfFeedCursor(final Feed feed) {
+ open();
+ Cursor c = db
+ .query(TABLE_NAME_FEED_ITEMS, null, KEY_FEED + "=?",
+ new String[] { String.valueOf(feed.getId()) }, null,
+ null, null);
+ return c;
+ }
+
+ /**
+ * Returns a cursor for a DB query in the FeedMedia table for a given ID.
+ *
+ * @param item
+ * The item you want to get the FeedMedia from
+ * @return The cursor of the query
+ * */
+ public final Cursor getFeedMediaOfItemCursor(final FeedItem item) {
+ open();
+ Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?",
+ new String[] { String.valueOf(item.getMedia().getId()) }, null,
+ null, null);
+ return c;
+ }
+
+ /**
+ * Returns a cursor for a DB query in the FeedImages table for a given ID.
+ *
+ * @param id
+ * ID of the FeedImage
+ * @return The cursor of the query
+ * */
+ public final Cursor getImageOfFeedCursor(final long id) {
+ open();
+ Cursor c = db.query(TABLE_NAME_FEED_IMAGES, null, KEY_ID + "=?",
+ new String[] { String.valueOf(id) }, null, null, null);
+ return c;
+ }
+
+ public final Cursor getSimpleChaptersOfFeedItemCursor(final FeedItem item) {
+ open();
+ Cursor c = db
+ .query(TABLE_NAME_SIMPLECHAPTERS, null, KEY_ID + "=?",
+ new String[] { String.valueOf(item.getId()) }, null,
+ null, null);
+ return c;
+ }
+
+ public final Cursor getDownloadLogCursor() {
+ open();
+ Cursor c = db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null,
+ null, null);
+ return c;
+ }
+
+ public final Cursor getQueueCursor() {
+ open();
+ Cursor c = db.query(TABLE_NAME_QUEUE, null, null, null, null, null,
+ null);
+ return c;
+ }
+
+ /**
+ * Get a FeedMedia object from the Database.
+ *
+ * @param rowIndex
+ * DB Index of Media object
+ * @param owner
+ * FeedItem the Media object belongs to
+ * @return A newly created FeedMedia object
+ * */
+ public final FeedMedia getFeedMedia(final long rowIndex,
+ final FeedItem owner) throws SQLException {
+ Cursor cursor = db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?",
+ new String[] { String.valueOf(rowIndex) }, null, null, null);
+ if ((cursor.getCount() == 0) || !cursor.moveToFirst()) {
+ throw new SQLException("No FeedMedia found at index: " + rowIndex);
+ }
+ FeedMedia media = new FeedMedia(rowIndex, owner, cursor.getInt(cursor
+ .getColumnIndex(KEY_DURATION)), cursor.getInt(cursor
+ .getColumnIndex(KEY_POSITION)), cursor.getLong(cursor
+ .getColumnIndex(KEY_SIZE)), cursor.getString(cursor
+ .getColumnIndex(KEY_MIME_TYPE)), cursor.getString(cursor
+ .getColumnIndex(KEY_FILE_URL)), cursor.getString(cursor
+ .getColumnIndex(KEY_DOWNLOAD_URL)), cursor.getInt(cursor
+ .getColumnIndex(KEY_DOWNLOADED)) > 0);
+ cursor.close();
+ return media;
+ }
+
+ /**
+ * Searches the DB for a FeedImage of the given id.
+ *
+ * @param id
+ * The id of the object
+ * @return The found object
+ * */
+ public final FeedImage getFeedImage(final long id) throws SQLException {
+ Cursor cursor = this.getImageOfFeedCursor(id);
+ if ((cursor.getCount() == 0) || !cursor.moveToFirst()) {
+ throw new SQLException("No FeedImage found at index: " + id);
+ }
+ FeedImage image = new FeedImage(id, cursor.getString(cursor
+ .getColumnIndex(KEY_TITLE)), cursor.getString(cursor
+ .getColumnIndex(KEY_FILE_URL)), cursor.getString(cursor
+ .getColumnIndex(KEY_DOWNLOAD_URL)), cursor.getInt(cursor
+ .getColumnIndex(KEY_DOWNLOADED)) > 0);
+ cursor.close();
+ return image;
+ }
+
+ /** Helper class for opening the Antennapod database. */
+ private static class PodDBHelper extends SQLiteOpenHelper {
+
+ /**
+ * Constructor.
+ *
+ * @param context
+ * Context to use
+ * @param name
+ * Name of the database
+ * @param factory
+ * to use for creating cursor objects
+ * @param version
+ * number of the database
+ * */
+ public PodDBHelper(final Context context, final String name,
+ final CursorFactory factory, final int version) {
+ super(context, name, factory, version);
+ }
+
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ db.execSQL(CREATE_TABLE_FEEDS);
+ db.execSQL(CREATE_TABLE_FEED_ITEMS);
+ db.execSQL(CREATE_TABLE_FEED_CATEGORIES);
+ db.execSQL(CREATE_TABLE_FEED_IMAGES);
+ db.execSQL(CREATE_TABLE_FEED_MEDIA);
+ db.execSQL(CREATE_TABLE_DOWNLOAD_LOG);
+ db.execSQL(CREATE_TABLE_QUEUE);
+ db.execSQL(CREATE_TABLE_SIMPLECHAPTERS);
+ }
+
+ @Override
+ public void onUpgrade(final SQLiteDatabase db, final int oldVersion,
+ final int newVersion) {
+ Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to "
+ + newVersion + ".");
+ // TODO delete Database
+ }
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/handler/FeedHandler.java b/src/de/danoeh/antennapod/syndication/handler/FeedHandler.java
new file mode 100644
index 000000000..dfcfcf98d
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/handler/FeedHandler.java
@@ -0,0 +1,29 @@
+package de.danoeh.antennapod.syndication.handler;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+import org.xml.sax.SAXException;
+
+import de.danoeh.antennapod.feed.Feed;
+
+public class FeedHandler {
+
+ public Feed parseFeed(Feed feed) throws SAXException, IOException,
+ ParserConfigurationException, UnsupportedFeedtypeException {
+ TypeGetter tg = new TypeGetter();
+ TypeGetter.Type type = tg.getType(feed);
+ SyndHandler handler = new SyndHandler(feed, type);
+
+ SAXParserFactory factory = SAXParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ SAXParser saxParser = factory.newSAXParser();
+ saxParser.parse(new File(feed.getFile_url()), handler);
+
+ return handler.state.feed;
+ }
+}
diff --git a/src/de/danoeh/antennapod/syndication/handler/HandlerState.java b/src/de/danoeh/antennapod/syndication/handler/HandlerState.java
new file mode 100644
index 000000000..1d81d0fb1
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/handler/HandlerState.java
@@ -0,0 +1,69 @@
+package de.danoeh.antennapod.syndication.handler;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Stack;
+
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.syndication.namespace.Namespace;
+import de.danoeh.antennapod.syndication.namespace.SyndElement;
+
+/** Contains all relevant information to describe the current state of a SyndHandler.*/
+public class HandlerState {
+
+ /** Feed that the Handler is currently processing. */
+ protected Feed feed;
+ protected FeedItem currentItem;
+ protected Stack<SyndElement> tagstack;
+ /** Namespaces that have been defined so far. */
+ protected HashMap<String, Namespace> namespaces;
+ protected Stack<Namespace> defaultNamespaces;
+ /** Buffer for saving characters. */
+ protected StringBuffer contentBuf;
+
+ public HandlerState(Feed feed) {
+ this.feed = feed;
+ tagstack = new Stack<SyndElement>();
+ namespaces = new HashMap<String, Namespace>();
+ defaultNamespaces = new Stack<Namespace>();
+ }
+
+
+ public Feed getFeed() {
+ return feed;
+ }
+ public FeedItem getCurrentItem() {
+ return currentItem;
+ }
+ public Stack<SyndElement> getTagstack() {
+ return tagstack;
+ }
+
+
+ public void setFeed(Feed feed) {
+ this.feed = feed;
+ }
+
+
+ public void setCurrentItem(FeedItem currentItem) {
+ this.currentItem = currentItem;
+ }
+
+ /** Returns the SyndElement that comes after the top element of the tagstack. */
+ public SyndElement getSecondTag() {
+ SyndElement top = tagstack.pop();
+ SyndElement second = tagstack.peek();
+ tagstack.push(top);
+ return second;
+ }
+
+ public StringBuffer getContentBuf() {
+ return contentBuf;
+ }
+
+
+
+
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/handler/SyndHandler.java b/src/de/danoeh/antennapod/syndication/handler/SyndHandler.java
new file mode 100644
index 000000000..396f170c5
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/handler/SyndHandler.java
@@ -0,0 +1,112 @@
+package de.danoeh.antennapod.syndication.handler;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import android.util.Log;
+
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.syndication.namespace.Namespace;
+import de.danoeh.antennapod.syndication.namespace.SyndElement;
+import de.danoeh.antennapod.syndication.namespace.atom.NSAtom;
+import de.danoeh.antennapod.syndication.namespace.content.NSContent;
+import de.danoeh.antennapod.syndication.namespace.itunes.NSITunes;
+import de.danoeh.antennapod.syndication.namespace.rss20.NSRSS20;
+import de.danoeh.antennapod.syndication.namespace.simplechapters.NSSimpleChapters;
+
+/** Superclass for all SAX Handlers which process Syndication formats */
+public class SyndHandler extends DefaultHandler {
+ private static final String TAG = "SyndHandler";
+ private static final String DEFAULT_PREFIX = "";
+ protected HandlerState state;
+
+
+ public SyndHandler(Feed feed, TypeGetter.Type type) {
+ state = new HandlerState(feed);
+ if (type == TypeGetter.Type.RSS20) {
+ state.defaultNamespaces.push(new NSRSS20());
+ }
+ }
+
+ @Override
+ public void startElement(String uri, String localName, String qName,
+ Attributes attributes) throws SAXException {
+ state.contentBuf = new StringBuffer();
+ Namespace handler = getHandlingNamespace(uri);
+ if (handler != null) {
+ SyndElement element = handler.handleElementStart(localName, state,
+ attributes);
+ state.tagstack.push(element);
+
+ }
+ }
+
+ @Override
+ public void characters(char[] ch, int start, int length)
+ throws SAXException {
+ if (!state.tagstack.empty()) {
+ if (state.getTagstack().size() >= 2) {
+ if (state.contentBuf != null) {
+ String content = new String(ch, start, length);
+ state.contentBuf.append(content);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void endElement(String uri, String localName, String qName)
+ throws SAXException {
+ Namespace handler = getHandlingNamespace(uri);
+ if (handler != null) {
+ handler.handleElementEnd(localName, state);
+ state.tagstack.pop();
+
+ }
+ state.contentBuf = null;
+
+ }
+
+ @Override
+ public void endPrefixMapping(String prefix) throws SAXException {
+ // TODO remove Namespace
+ }
+
+ @Override
+ public void startPrefixMapping(String prefix, String uri)
+ throws SAXException {
+ // Find the right namespace
+ if (uri.equals(NSAtom.NSURI)) {
+ if (prefix.equals(DEFAULT_PREFIX)) {
+ state.defaultNamespaces.push(new NSAtom());
+ } else if (prefix.equals(NSAtom.NSTAG)) {
+ state.namespaces.put(uri, new NSAtom());
+ Log.d(TAG, "Recognized Atom namespace");
+ }
+ } else if (uri.equals(NSContent.NSURI) && prefix.equals(NSContent.NSTAG)) {
+ state.namespaces.put(uri, new NSContent());
+ Log.d(TAG, "Recognized Content namespace");
+ } else if (uri.equals(NSITunes.NSURI) && prefix.equals(NSITunes.NSTAG)) {
+ state.namespaces.put(uri, new NSITunes());
+ Log.d(TAG, "Recognized ITunes namespace");
+ } else if (uri.equals(NSSimpleChapters.NSURI) && prefix.equals(NSSimpleChapters.NSTAG)) {
+ state.namespaces.put(uri, new NSSimpleChapters());
+ Log.d(TAG, "Recognized SimpleChapters namespace");
+ }
+ }
+
+ private Namespace getHandlingNamespace(String uri) {
+ Namespace handler = state.namespaces.get(uri);
+ if (handler == null && uri.equals(DEFAULT_PREFIX)
+ && !state.defaultNamespaces.empty()) {
+ handler = state.defaultNamespaces.peek();
+ }
+ return handler;
+ }
+
+ public HandlerState getState() {
+ return state;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java b/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java
new file mode 100644
index 000000000..7e346ca5c
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/handler/TypeGetter.java
@@ -0,0 +1,76 @@
+package de.danoeh.antennapod.syndication.handler;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileReader;
+import java.io.IOException;
+import java.io.Reader;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import android.util.Log;
+
+import de.danoeh.antennapod.feed.Feed;
+
+/** Gets the type of a specific feed by reading the root element. */
+public class TypeGetter {
+ private static final String TAG = "TypeGetter";
+
+ enum Type {
+ RSS20, ATOM, INVALID
+ }
+
+ private static final String ATOM_ROOT = "feed";
+ private static final String RSS_ROOT = "rss";
+
+ public Type getType(Feed feed) throws UnsupportedFeedtypeException {
+ XmlPullParserFactory factory;
+ try {
+ factory = XmlPullParserFactory.newInstance();
+ factory.setNamespaceAware(true);
+ XmlPullParser xpp = factory.newPullParser();
+ xpp.setInput(createReader(feed));
+ int eventType = xpp.getEventType();
+
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG) {
+ String tag = xpp.getName();
+ if (tag.equals(ATOM_ROOT)) {
+ Log.d(TAG, "Recognized type Atom");
+ return Type.ATOM;
+ } else if (tag.equals(RSS_ROOT)
+ && (xpp.getAttributeValue(null, "version")
+ .equals("2.0"))) {
+ Log.d(TAG, "Recognized type RSS 2.0");
+ return Type.RSS20;
+ } else {
+ Log.d(TAG, "Type is invalid");
+ throw new UnsupportedFeedtypeException(Type.INVALID);
+ }
+ } else {
+ eventType = xpp.next();
+ }
+ }
+
+ } catch (XmlPullParserException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ Log.d(TAG, "Type is invalid");
+ throw new UnsupportedFeedtypeException(Type.INVALID);
+ }
+
+ private Reader createReader(Feed feed) {
+ FileReader reader;
+ try {
+ reader = new FileReader(new File(feed.getFile_url()));
+ } catch (FileNotFoundException e) {
+ e.printStackTrace();
+ return null;
+ }
+ return reader;
+ }
+}
diff --git a/src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java b/src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java
new file mode 100644
index 000000000..67fbc9cc9
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/handler/UnsupportedFeedtypeException.java
@@ -0,0 +1,29 @@
+package de.danoeh.antennapod.syndication.handler;
+
+import de.danoeh.antennapod.syndication.handler.TypeGetter.Type;
+
+public class UnsupportedFeedtypeException extends Exception {
+ private static final long serialVersionUID = 9105878964928170669L;
+ private TypeGetter.Type type;
+
+ public UnsupportedFeedtypeException(Type type) {
+ super();
+ this.type = type;
+
+ }
+
+ public TypeGetter.Type getType() {
+ return type;
+ }
+
+ @Override
+ public String getMessage() {
+ if (type == TypeGetter.Type.INVALID) {
+ return "Invalid type";
+ } else {
+ return "Type " + type + " not supported";
+ }
+ }
+
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/Namespace.java b/src/de/danoeh/antennapod/syndication/namespace/Namespace.java
new file mode 100644
index 000000000..496d314a9
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/namespace/Namespace.java
@@ -0,0 +1,23 @@
+package de.danoeh.antennapod.syndication.namespace;
+
+import org.xml.sax.Attributes;
+
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.syndication.handler.HandlerState;
+
+
+public abstract class Namespace {
+ public static final String NSTAG = null;
+ public static final String NSURI = null;
+
+ /** Called by a Feedhandler when in startElement and it detects a namespace element
+ * @return The SyndElement to push onto the stack
+ * */
+ public abstract SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes);
+
+ /** Called by a Feedhandler when in endElement and it detects a namespace element
+ * @return true if namespace handled the element, false if it ignored it
+ * */
+ public abstract void handleElementEnd(String localName, HandlerState state);
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/SyndElement.java b/src/de/danoeh/antennapod/syndication/namespace/SyndElement.java
new file mode 100644
index 000000000..187312c9e
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/namespace/SyndElement.java
@@ -0,0 +1,22 @@
+package de.danoeh.antennapod.syndication.namespace;
+
+/** Defines a XML Element that is pushed on the tagstack */
+public class SyndElement {
+ protected String name;
+ protected Namespace namespace;
+
+ public SyndElement(String name, Namespace namespace) {
+ this.name = name;
+ this.namespace = namespace;
+ }
+
+ public Namespace getNamespace() {
+ return namespace;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java b/src/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java
new file mode 100644
index 000000000..16beb277b
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/namespace/atom/AtomText.java
@@ -0,0 +1,47 @@
+package de.danoeh.antennapod.syndication.namespace.atom;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+
+import de.danoeh.antennapod.syndication.namespace.Namespace;
+import de.danoeh.antennapod.syndication.namespace.SyndElement;
+
+/** Represents Atom Element which contains text (content, title, summary). */
+public class AtomText extends SyndElement {
+ public static final String TYPE_TEXT = "text";
+ public static final String TYPE_HTML = "html";
+ public static final String TYPE_XHTML = "xhtml";
+
+ private String type;
+ private String content;
+
+ public AtomText(String name, Namespace namespace, String type) {
+ super(name, namespace);
+ this.type = type;
+ }
+
+ /** Processes the content according to the type and returns it. */
+ public String getProcessedContent() {
+ if (type.equals(TYPE_HTML)) {
+ return StringEscapeUtils.unescapeHtml4(content);
+ } else if (type.equals(TYPE_XHTML)) {
+ return content;
+ } else { // Handle as text by default
+ return content;
+ }
+ }
+
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(String content) {
+ this.content = content;
+ }
+
+ public String getType() {
+ return type;
+ }
+
+
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java b/src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java
new file mode 100644
index 000000000..dbe1334b6
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/namespace/atom/NSAtom.java
@@ -0,0 +1,146 @@
+package de.danoeh.antennapod.syndication.namespace.atom;
+
+import org.xml.sax.Attributes;
+
+import android.util.Log;
+
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedImage;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.syndication.handler.HandlerState;
+import de.danoeh.antennapod.syndication.namespace.Namespace;
+import de.danoeh.antennapod.syndication.namespace.SyndElement;
+import de.danoeh.antennapod.syndication.namespace.rss20.NSRSS20;
+import de.danoeh.antennapod.syndication.util.SyndDateUtils;
+
+public class NSAtom extends Namespace {
+ private static final String TAG = "NSAtom";
+ public static final String NSTAG = "atom";
+ public static final String NSURI = "http://www.w3.org/2005/Atom";
+
+ private static final String FEED = "feed";
+ private static final String TITLE = "title";
+ private static final String ENTRY = "entry";
+ private static final String LINK = "link";
+ private static final String UPDATED = "updated";
+ private static final String AUTHOR = "author";
+ private static final String CONTENT = "content";
+ private static final String IMAGE = "logo";
+ private static final String SUBTITLE = "subtitle";
+ private static final String PUBLISHED = "published";
+
+ private static final String TEXT_TYPE = "type";
+ // Link
+ private static final String LINK_HREF = "href";
+ private static final String LINK_REL = "rel";
+ private static final String LINK_TYPE = "type";
+ private static final String LINK_TITLE = "title";
+ private static final String LINK_LENGTH = "length";
+ // rel-values
+ private static final String LINK_REL_ALTERNATE = "alternate";
+ private static final String LINK_REL_ENCLOSURE = "enclosure";
+ private static final String LINK_REL_PAYMENT = "payment";
+ private static final String LINK_REL_RELATED = "related";
+ private static final String LINK_REL_SELF = "self";
+
+ /** Regexp to test whether an Element is a Text Element. */
+ private static final String isText = TITLE + "|" + CONTENT + "|" + "|"
+ + SUBTITLE;
+
+ public static final String isFeed = FEED + "|" + NSRSS20.CHANNEL;
+ public static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM;
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state,
+ Attributes attributes) {
+ if (localName.equals(ENTRY)) {
+ state.setCurrentItem(new FeedItem());
+ state.getFeed().getItems().add(state.getCurrentItem());
+ state.getCurrentItem().setFeed(state.getFeed());
+ } else if (localName.matches(isText)) {
+ String type = attributes.getValue(TEXT_TYPE);
+ return new AtomText(localName, this, type);
+ } else if (localName.equals(LINK)) {
+ String href = attributes.getValue(LINK_HREF);
+ String rel = attributes.getValue(LINK_REL);
+ SyndElement parent = state.getTagstack().peek();
+ if (parent.getName().matches(isFeedItem)) {
+ if (rel == null || rel.equals(LINK_REL_ALTERNATE)) {
+ state.getCurrentItem().setLink(href);
+ } else if (rel.equals(LINK_REL_ENCLOSURE)) {
+ String strSize = attributes.getValue(LINK_LENGTH);
+ long size = 0;
+ if (strSize != null)
+ size = Long.parseLong(strSize);
+ String type = attributes.getValue(LINK_TYPE);
+ String download_url = attributes
+ .getValue(LINK_REL_ENCLOSURE);
+ state.getCurrentItem().setMedia(
+ new FeedMedia(state.getCurrentItem(), download_url,
+ size, type));
+ } else if (rel.equals(LINK_REL_PAYMENT)) {
+ state.getCurrentItem().setPaymentLink(href);
+ }
+ } else if (parent.getName().matches(isFeed)) {
+ if (rel == null || rel.equals(LINK_REL_ALTERNATE)) {
+ state.getFeed().setLink(href);
+ } else if (rel.equals(LINK_REL_PAYMENT)) {
+ state.getFeed().setPaymentLink(href);
+ }
+ }
+ }
+ return new SyndElement(localName, this);
+ }
+
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+ if (localName.equals(ENTRY)) {
+ state.setCurrentItem(null);
+ }
+
+ if (state.getTagstack().size() >= 2) {
+ AtomText textElement = null;
+ String content = state.getContentBuf().toString();
+ SyndElement topElement = state.getTagstack().peek();
+ String top = topElement.getName();
+ SyndElement secondElement = state.getSecondTag();
+ String second = secondElement.getName();
+
+ if (top.matches(isText)) {
+ textElement = (AtomText) topElement;
+ textElement.setContent(content);
+ }
+
+ if (top.equals(TITLE)) {
+
+ if (second.equals(FEED)) {
+ state.getFeed().setTitle(textElement.getProcessedContent());
+ } else if (second.equals(ENTRY)) {
+ state.getCurrentItem().setTitle(
+ textElement.getProcessedContent());
+ }
+ } else if (top.equals(SUBTITLE)) {
+ if (second.equals(FEED)) {
+ state.getFeed().setDescription(
+ textElement.getProcessedContent());
+ }
+ } else if (top.equals(CONTENT)) {
+ if (second.equals(ENTRY)) {
+ state.getCurrentItem().setDescription(
+ textElement.getProcessedContent());
+ }
+ } else if (top.equals(PUBLISHED)) {
+ if (second.equals(ENTRY)) {
+ state.getCurrentItem().setPubDate(
+ SyndDateUtils.parseRFC3339Date(content));
+ }
+ } else if (top.equals(IMAGE)) {
+ state.getFeed().setImage(new FeedImage(content, null));
+ }
+
+ }
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/content/NSContent.java b/src/de/danoeh/antennapod/syndication/namespace/content/NSContent.java
new file mode 100644
index 000000000..7713eb9c3
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/namespace/content/NSContent.java
@@ -0,0 +1,31 @@
+package de.danoeh.antennapod.syndication.namespace.content;
+
+import org.xml.sax.Attributes;
+
+import de.danoeh.antennapod.syndication.handler.HandlerState;
+import de.danoeh.antennapod.syndication.namespace.Namespace;
+import de.danoeh.antennapod.syndication.namespace.SyndElement;
+import de.danoeh.antennapod.syndication.namespace.rss20.NSRSS20;
+
+import org.apache.commons.lang3.StringEscapeUtils;
+
+public class NSContent extends Namespace {
+ public static final String NSTAG = "content";
+ public static final String NSURI = "http://purl.org/rss/1.0/modules/content/";
+
+ private static final String ENCODED = "encoded";
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state,
+ Attributes attributes) {
+ return new SyndElement(localName, this);
+ }
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+ if (localName.equals(ENCODED)) {
+ state.getCurrentItem().setContentEncoded(state.getContentBuf().toString());
+ }
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/itunes/NSITunes.java b/src/de/danoeh/antennapod/syndication/namespace/itunes/NSITunes.java
new file mode 100644
index 000000000..92f25f15c
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/namespace/itunes/NSITunes.java
@@ -0,0 +1,42 @@
+package de.danoeh.antennapod.syndication.namespace.itunes;
+
+import org.xml.sax.Attributes;
+
+import de.danoeh.antennapod.feed.FeedImage;
+import de.danoeh.antennapod.syndication.handler.HandlerState;
+import de.danoeh.antennapod.syndication.namespace.Namespace;
+import de.danoeh.antennapod.syndication.namespace.SyndElement;
+
+public class NSITunes extends Namespace{
+ public static final String NSTAG = "itunes";
+ public static final String NSURI = "http://www.itunes.com/dtds/podcast-1.0.dtd";
+
+ private static final String IMAGE = "image";
+ private static final String IMAGE_TITLE = "image";
+ private static final String IMAGE_HREF = "href";
+
+ private static final String AUTHOR = "author";
+
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state,
+ Attributes attributes) {
+ if (localName.equals(IMAGE) && state.getFeed().getImage() == null) {
+ FeedImage image = new FeedImage();
+ image.setTitle(IMAGE_TITLE);
+ image.setDownload_url(attributes.getValue(IMAGE_HREF));
+ state.getFeed().setImage(image);
+ }
+
+ return new SyndElement(localName, this);
+ }
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+ if (localName.equals(AUTHOR)) {
+ state.getFeed().setAuthor(state.getContentBuf().toString());
+ }
+
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/rss20/NSRSS20.java b/src/de/danoeh/antennapod/syndication/namespace/rss20/NSRSS20.java
new file mode 100644
index 000000000..6dcd8daa0
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/namespace/rss20/NSRSS20.java
@@ -0,0 +1,115 @@
+package de.danoeh.antennapod.syndication.namespace.rss20;
+
+import java.util.ArrayList;
+import java.util.Date;
+
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedImage;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedMedia;
+import de.danoeh.antennapod.syndication.handler.HandlerState;
+import de.danoeh.antennapod.syndication.handler.SyndHandler;
+import de.danoeh.antennapod.syndication.namespace.Namespace;
+import de.danoeh.antennapod.syndication.namespace.SyndElement;
+import de.danoeh.antennapod.syndication.util.SyndDateUtils;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+/**
+ * SAX-Parser for reading RSS-Feeds
+ *
+ * @author daniel
+ *
+ */
+public class NSRSS20 extends Namespace {
+ public static final String NSTAG = "rss";
+ public static final String NSURI = "";
+
+ public final static String CHANNEL = "channel";
+ public final static String ITEM = "item";
+ public final static String TITLE = "title";
+ public final static String LINK = "link";
+ public final static String DESCR = "description";
+ public final static String PUBDATE = "pubDate";
+ public final static String ENCLOSURE = "enclosure";
+ public final static String IMAGE = "image";
+ public final static String URL = "url";
+ public final static String LANGUAGE = "language";
+
+ public final static String ENC_URL = "url";
+ public final static String ENC_LEN = "length";
+ public final static String ENC_TYPE = "type";
+
+ public final static String VALID_MIMETYPE = "audio/.*" + "|" + "video/.*";
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state,
+ Attributes attributes) {
+ if (localName.equals(ITEM)) {
+ state.setCurrentItem(new FeedItem());
+ state.getFeed().getItems().add(state.getCurrentItem());
+ state.getCurrentItem().setFeed(state.getFeed());
+
+ } else if (localName.equals(ENCLOSURE)) {
+ String type = attributes.getValue(ENC_TYPE);
+ if (state.getCurrentItem().getMedia() == null
+ && (type.matches(VALID_MIMETYPE))) {
+ state.getCurrentItem().setMedia(
+ new FeedMedia(state.getCurrentItem(), attributes
+ .getValue(ENC_URL), Long.parseLong(attributes
+ .getValue(ENC_LEN)), attributes
+ .getValue(ENC_TYPE)));
+ }
+
+ } else if (localName.equals(IMAGE)) {
+ state.getFeed().setImage(new FeedImage());
+ }
+ return new SyndElement(localName, this);
+ }
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+ if (localName.equals(ITEM)) {
+ state.setCurrentItem(null);
+ } else if (state.getTagstack().size() >= 2
+ && state.getContentBuf() != null) {
+ String content = state.getContentBuf().toString();
+ SyndElement topElement = state.getTagstack().peek();
+ String top = topElement.getName();
+ SyndElement secondElement = state.getSecondTag();
+ String second = secondElement.getName();
+ if (top.equals(TITLE)) {
+ if (second.equals(ITEM)) {
+ state.getCurrentItem().setTitle(content);
+ } else if (second.equals(CHANNEL)) {
+ state.getFeed().setTitle(content);
+ } else if (second.equals(IMAGE)) {
+ state.getFeed().getImage().setTitle(IMAGE);
+ }
+ } else if (top.equals(LINK)) {
+ if (second.equals(CHANNEL)) {
+ state.getFeed().setLink(content);
+ } else if (second.equals(ITEM)) {
+ state.getCurrentItem().setLink(content);
+ }
+ } else if (top.equals(PUBDATE) && second.equals(ITEM)) {
+ state.getCurrentItem().setPubDate(
+ SyndDateUtils.parseRFC822Date(content));
+ } else if (top.equals(URL) && second.equals(IMAGE)) {
+ state.getFeed().getImage().setDownload_url(content);
+ } else if (localName.equals(DESCR)) {
+ if (second.equals(CHANNEL)) {
+ state.getFeed().setDescription(content);
+ } else {
+ state.getCurrentItem().setDescription(content);
+ }
+
+ } else if (localName.equals(LANGUAGE)) {
+ state.getFeed().setLanguage(content.toLowerCase());
+ }
+ }
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/namespace/simplechapters/NSSimpleChapters.java b/src/de/danoeh/antennapod/syndication/namespace/simplechapters/NSSimpleChapters.java
new file mode 100644
index 000000000..3c7853304
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/namespace/simplechapters/NSSimpleChapters.java
@@ -0,0 +1,43 @@
+package de.danoeh.antennapod.syndication.namespace.simplechapters;
+
+import java.util.ArrayList;
+
+import org.xml.sax.Attributes;
+
+import de.danoeh.antennapod.feed.SimpleChapter;
+import de.danoeh.antennapod.syndication.handler.HandlerState;
+import de.danoeh.antennapod.syndication.namespace.Namespace;
+import de.danoeh.antennapod.syndication.namespace.SyndElement;
+import de.danoeh.antennapod.syndication.util.SyndDateUtils;
+
+public class NSSimpleChapters extends Namespace {
+ public static final String NSTAG = "sc";
+ public static final String NSURI = "http://podlove.org/simple-chapters";
+
+ public static final String CHAPTERS = "chapters";
+ public static final String CHAPTER = "chapter";
+ public static final String START = "start";
+ public static final String TITLE = "title";
+
+ @Override
+ public SyndElement handleElementStart(String localName, HandlerState state,
+ Attributes attributes) {
+ if (localName.equals(CHAPTERS)) {
+ state.getCurrentItem().setSimpleChapters(
+ new ArrayList<SimpleChapter>());
+ } else if (localName.equals(CHAPTER)) {
+ state.getCurrentItem()
+ .getSimpleChapters()
+ .add(new SimpleChapter(SyndDateUtils
+ .parseTimeString(attributes.getValue(START)),
+ attributes.getValue(TITLE)));
+ }
+
+ return new SyndElement(localName, this);
+ }
+
+ @Override
+ public void handleElementEnd(String localName, HandlerState state) {
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java b/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java
new file mode 100644
index 000000000..226a79721
--- /dev/null
+++ b/src/de/danoeh/antennapod/syndication/util/SyndDateUtils.java
@@ -0,0 +1,102 @@
+package de.danoeh.antennapod.syndication.util;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import android.util.Log;
+
+/** Parses several date formats. */
+public class SyndDateUtils {
+ private static final String TAG = "DateUtils";
+ public static final String RFC822 = "dd MMM yyyy HH:mm:ss Z";
+ /** RFC 822 date format with day of the week. */
+ public static final String RFC822DAY = "EEE, " + RFC822;
+
+ /** RFC 3339 date format for UTC dates. */
+ public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'";
+
+ /** RFC 3339 date format for localtime dates with offset. */
+ public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ";
+
+ private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() {
+ @Override
+ protected SimpleDateFormat initialValue() {
+ return new SimpleDateFormat(RFC822DAY, Locale.US);
+ }
+
+ };
+
+ private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() {
+ @Override
+ protected SimpleDateFormat initialValue() {
+ return new SimpleDateFormat(RFC3339UTC, Locale.US);
+ }
+
+ };
+
+ public static Date parseRFC822Date(final String date) {
+ Date result = null;
+ SimpleDateFormat format = RFC822Formatter.get();
+ try {
+ result = format.parse(date);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ format.applyPattern(RFC822);
+ try {
+ result = format.parse(date);
+ } catch (ParseException e1) {
+ e1.printStackTrace();
+ }
+ }
+
+ return result;
+ }
+
+ public static Date parseRFC3339Date(final String date) {
+ Date result = null;
+ SimpleDateFormat format = RFC3339Formatter.get();
+ if (date.endsWith("Z")) {
+ try {
+ result = format.parse(date);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+ } else {
+ format.applyPattern(RFC3339LOCAL);
+ // remove last colon
+ StringBuffer buf = new StringBuffer(date.length() - 1);
+ int colonIdx = date.lastIndexOf(':');
+ for (int x = 0; x < date.length(); x++) {
+ if (x != colonIdx)
+ buf.append(date.charAt(x));
+ }
+ String bufStr = buf.toString();
+ try {
+ result = format.parse(bufStr);
+ } catch (ParseException e) {
+ e.printStackTrace();
+ }
+
+ }
+
+ return result;
+
+ }
+ /** Takes a string of the form [HH:]MM:SS[.mmm] and converts it to milliseconds. */
+ public static long parseTimeString(final String time) {
+ String[] parts = time.split(":");
+ long result = 0;
+ int idx = 0;
+ if (parts.length == 3) {
+ // string has hours
+ result += Integer.valueOf(parts[idx]) * 3600000;
+ idx++;
+ }
+ result += Integer.valueOf(parts[idx]) * 60000;
+ idx++;
+ result += ( Float.valueOf(parts[idx])) * 1000;
+ return result;
+ }
+}
diff --git a/src/de/danoeh/antennapod/util/ConnectionTester.java b/src/de/danoeh/antennapod/util/ConnectionTester.java
new file mode 100644
index 000000000..d50e63f00
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/ConnectionTester.java
@@ -0,0 +1,66 @@
+package de.danoeh.antennapod.util;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLConnection;
+
+import android.content.Context;
+import android.util.Log;
+
+/** Tests a connection before downloading something. */
+public class ConnectionTester implements Runnable {
+ private static final String TAG = "ConnectionTester";
+ private String strUrl;
+ private Context context;
+ private int connectTimeout;
+ private int readTimeout;
+ private Callback callback;
+ private int reason;
+
+ public ConnectionTester(String url, Context context, Callback callback) {
+ super();
+ this.strUrl = url;
+ this.context = context;
+ this.callback = callback;
+ connectTimeout = 500;
+ readTimeout = connectTimeout;
+ }
+
+
+
+ @Override
+ public void run() {
+ Log.d(TAG, "Testing connection");
+ try {
+ URL url = new URL(strUrl);
+ HttpURLConnection con = (HttpURLConnection) url.openConnection();
+ con.connect();
+ callback.onConnectionSuccessful();
+ Log.d(TAG, "Connection seems to work");
+ } catch (MalformedURLException e) {
+ e.printStackTrace();
+ reason = DownloadError.ERROR_CONNECTION_ERROR;
+ Log.d(TAG, "Connection failed");
+ callback.onConnectionFailure();
+ } catch (IOException e) {
+ e.printStackTrace();
+ reason = DownloadError.ERROR_CONNECTION_ERROR;
+ Log.d(TAG, "Connection failed");
+ callback.onConnectionFailure();
+ }
+ }
+
+
+ public static abstract class Callback {
+ public abstract void onConnectionSuccessful();
+ public abstract void onConnectionFailure();
+ }
+
+ public int getReason() {
+ return reason;
+ }
+
+
+}
diff --git a/src/de/danoeh/antennapod/util/Converter.java b/src/de/danoeh/antennapod/util/Converter.java
new file mode 100644
index 000000000..f02e8ea69
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/Converter.java
@@ -0,0 +1,81 @@
+package de.danoeh.antennapod.util;
+
+import android.util.Log;
+
+/** Provides methods for converting various units. */
+public final class Converter {
+ /** Class shall not be instantiated. */
+ private Converter() {
+ }
+
+ /** Logging tag. */
+ private static final String TAG = "Converter";
+
+
+ /** Indicates that the value is in the Byte range.*/
+ private static final int B_RANGE = 0;
+ /** Indicates that the value is in the Kilobyte range.*/
+ private static final int KB_RANGE = 1;
+ /** Indicates that the value is in the Megabyte range.*/
+ private static final int MB_RANGE = 2;
+ /** Indicates that the value is in the Gigabyte range.*/
+ private static final int GB_RANGE = 3;
+ /** Determines the length of the number for best readability.*/
+ private static final int NUM_LENGTH = 1000;
+
+
+ private static final int HOURS_MIL = 3600000;
+ private static final int MINUTES_MIL = 60000;
+ private static final int SECONDS_MIL = 1000;
+
+ /** Takes a byte-value and converts it into a more readable
+ * String.
+ * @param input The value to convert
+ * @return The converted String with a unit
+ * */
+ public static String byteToString(final long input) {
+ int i = 0;
+ int result = 0;
+
+ for (i = 0; i < GB_RANGE + 1; i++) {
+ result = (int) (input / Math.pow(1024, i));
+ if (result < NUM_LENGTH) {
+ break;
+ }
+ }
+
+ switch (i) {
+ case B_RANGE:
+ return result + " B";
+ case KB_RANGE:
+ return result + " KB";
+ case MB_RANGE:
+ return result + " MB";
+ case GB_RANGE:
+ return result + " GB";
+ default:
+ Log.e(TAG, "Error happened in byteToString");
+ return "ERROR";
+ }
+ }
+
+ /** Converts milliseconds to a string containing hours, minutes and seconds */
+ public static String getDurationStringLong(int duration) {
+ int h = duration / HOURS_MIL;
+ int rest = duration - h * HOURS_MIL;
+ int m = rest / MINUTES_MIL;
+ rest -= m * MINUTES_MIL;
+ int s = rest / SECONDS_MIL;
+
+ return String.format("%02d:%02d:%02d", h, m, s);
+ }
+
+ /** Converts milliseconds to a string containing hours and minutes */
+ public static String getDurationStringShort(int duration) {
+ int h = duration / HOURS_MIL;
+ int rest = duration - h * HOURS_MIL;
+ int m = rest / MINUTES_MIL;
+
+ return String.format("%02d:%02d", h, m);
+ }
+}
diff --git a/src/de/danoeh/antennapod/util/DownloadError.java b/src/de/danoeh/antennapod/util/DownloadError.java
new file mode 100644
index 000000000..b2f43a8dd
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/DownloadError.java
@@ -0,0 +1,42 @@
+package de.danoeh.antennapod.util;
+
+import de.danoeh.antennapod.R;
+import android.app.DownloadManager;
+import android.content.Context;
+
+/** Utility class for Download Errors. */
+public class DownloadError {
+ public static final int ERROR_PARSER_EXCEPTION = 1;
+ public static final int ERROR_UNSUPPORTED_TYPE = 2;
+ public static final int ERROR_CONNECTION_ERROR = 3;
+
+
+ /** Get a human-readable string for a specific error code. */
+ public static String getErrorString(Context context, int code) {
+ int resId;
+ switch(code) {
+ case DownloadManager.ERROR_DEVICE_NOT_FOUND:
+ resId = R.string.download_error_insufficient_space;
+ break;
+ case DownloadManager.ERROR_FILE_ERROR:
+ resId = R.string.download_error_file_error;
+ break;
+ case DownloadManager.ERROR_HTTP_DATA_ERROR:
+ resId = R.string.download_error_http_data_error;
+ break;
+ case ERROR_PARSER_EXCEPTION:
+ resId = R.string.download_error_parser_exception;
+ break;
+ case ERROR_UNSUPPORTED_TYPE:
+ resId = R.string.download_error_unsupported_type;
+ break;
+ case ERROR_CONNECTION_ERROR:
+ resId = R.string.download_error_connection_error;
+ break;
+ default:
+ resId = R.string.download_error_error_unknown;
+ }
+ return context.getString(resId);
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/util/FeedItemMenuHandler.java b/src/de/danoeh/antennapod/util/FeedItemMenuHandler.java
new file mode 100644
index 000000000..b97bf35b3
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/FeedItemMenuHandler.java
@@ -0,0 +1,120 @@
+package de.danoeh.antennapod.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.storage.DownloadRequester;
+import de.danoeh.antennapod.R;
+
+/** Handles interactions with the FeedItemMenu. */
+public class FeedItemMenuHandler {
+ private FeedItemMenuHandler() {
+
+ }
+
+ public static boolean onPrepareMenu(Menu menu, FeedItem selectedItem) {
+ FeedManager manager = FeedManager.getInstance();
+
+ if (selectedItem.getMedia() != null) {
+ if (selectedItem.getMedia().isDownloaded()) {
+ menu.findItem(R.id.play_item).setVisible(true);
+ menu.findItem(R.id.remove_item).setVisible(true);
+ } else if (selectedItem.getMedia().getFile_url() == null) {
+ menu.findItem(R.id.download_item).setVisible(true);
+ menu.findItem(R.id.stream_item).setVisible(true);
+ } else {
+ menu.findItem(R.id.cancel_download_item).setVisible(true);
+ }
+
+ if (manager.isInQueue(selectedItem)) {
+ menu.findItem(R.id.remove_from_queue_item).setVisible(true);
+ } else {
+ menu.findItem(R.id.add_to_queue_item).setVisible(true);
+ }
+
+ menu.findItem(R.id.share_link_item).setVisible(selectedItem.getLink() != null);
+ }
+
+ if (selectedItem.isRead()) {
+ menu.findItem(R.id.mark_unread_item).setVisible(true);
+ } else {
+ menu.findItem(R.id.mark_read_item).setVisible(true);
+ }
+
+ if (selectedItem.getLink() != null) {
+ menu.findItem(R.id.visit_website_item).setVisible(true);
+ }
+
+ if (selectedItem.getPaymentLink() != null) {
+ menu.findItem(R.id.support_item).setVisible(true);
+ }
+
+ return true;
+ }
+
+ public static boolean onMenuItemClicked(Context context, MenuItem item,
+ FeedItem selectedItem) {
+ DownloadRequester requester = DownloadRequester.getInstance();
+ FeedManager manager = FeedManager.getInstance();
+ switch (item.getItemId()) {
+ case R.id.download_item:
+ requester.downloadMedia(context, selectedItem.getMedia());
+ break;
+ case R.id.play_item:
+ manager.playMedia(context, selectedItem.getMedia(), true, true,
+ false);
+ break;
+ case R.id.remove_item:
+ manager.deleteFeedMedia(context, selectedItem.getMedia());
+ break;
+ case R.id.cancel_download_item:
+ requester.cancelDownload(context, selectedItem.getMedia()
+ .getDownloadId());
+ break;
+ case R.id.mark_read_item:
+ manager.markItemRead(context, selectedItem, true);
+ break;
+ case R.id.mark_unread_item:
+ manager.markItemRead(context, selectedItem, false);
+ break;
+ case R.id.add_to_queue_item:
+ manager.addQueueItem(context, selectedItem);
+ break;
+ case R.id.remove_from_queue_item:
+ manager.removeQueueItem(context, selectedItem);
+ break;
+ case R.id.stream_item:
+ manager.playMedia(context, selectedItem.getMedia(), true, true,
+ true);
+ break;
+ case R.id.visit_website_item:
+ Uri uri = Uri.parse(selectedItem.getLink());
+ context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ break;
+ case R.id.support_item:
+ FlattrUtils.clickUrl(context, selectedItem.getPaymentLink());
+ break;
+ case R.id.share_link_item:
+ ShareUtils.shareFeedItemLink(context, selectedItem);
+ break;
+ default:
+ return false;
+ }
+ // Refresh menu state
+
+ return true;
+ }
+
+ public static boolean onCreateMenu(MenuInflater inflater, Menu menu) {
+ inflater.inflate(R.menu.feeditem, menu);
+ return true;
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/util/FeedItemPubdateComparator.java b/src/de/danoeh/antennapod/util/FeedItemPubdateComparator.java
new file mode 100644
index 000000000..bfeaf59e8
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/FeedItemPubdateComparator.java
@@ -0,0 +1,19 @@
+package de.danoeh.antennapod.util;
+
+import java.util.Comparator;
+
+import de.danoeh.antennapod.feed.FeedItem;
+
+/** Compares the pubDate of two FeedItems for sorting*/
+public class FeedItemPubdateComparator implements Comparator<FeedItem> {
+
+ /** Returns a new instance of this comparator in reverse order.
+ public static FeedItemPubdateComparator newInstance() {
+ FeedItemPubdateComparator
+ }*/
+ @Override
+ public int compare(FeedItem lhs, FeedItem rhs) {
+ return -lhs.getPubDate().compareTo(rhs.getPubDate());
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/util/FeedMenuHandler.java b/src/de/danoeh/antennapod/util/FeedMenuHandler.java
new file mode 100644
index 000000000..10afc687c
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/FeedMenuHandler.java
@@ -0,0 +1,84 @@
+package de.danoeh.antennapod.util;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+
+import com.actionbarsherlock.view.ActionMode;
+import com.actionbarsherlock.view.Menu;
+import com.actionbarsherlock.view.MenuInflater;
+import com.actionbarsherlock.view.MenuItem;
+
+import de.danoeh.antennapod.activity.FeedInfoActivity;
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedItem;
+import de.danoeh.antennapod.feed.FeedManager;
+import de.danoeh.antennapod.service.DownloadService;
+import de.danoeh.antennapod.storage.DownloadRequester;
+import de.danoeh.antennapod.R;
+
+/** Handles interactions with the FeedItemMenu. */
+public class FeedMenuHandler {
+ private static final String TAG = "FeedMenuHandler";
+
+ public static boolean onCreateOptionsMenu(MenuInflater inflater, Menu menu) {
+ inflater.inflate(R.menu.feedlist, menu);
+ return true;
+ }
+
+ public static boolean onPrepareOptionsMenu(Menu menu, Feed selectedFeed) {
+ Log.d(TAG, "Preparing options menu");
+ if (selectedFeed.getPaymentLink() != null) {
+ menu.findItem(R.id.support_item).setVisible(true);
+ }
+ MenuItem refresh = menu.findItem(R.id.refresh_item);
+ if (DownloadService.isRunning
+ && DownloadRequester.getInstance().isDownloadingFile(
+ selectedFeed)) {
+ refresh.setVisible(false);
+ } else {
+ refresh.setVisible(true);
+ }
+
+ menu.findItem(R.id.share_link_item).setVisible(selectedFeed.getLink() != null);
+
+ return true;
+ }
+
+ /** NOTE: This method does not handle clicks on the 'remove feed' - item. */
+ public static boolean onOptionsItemClicked(Context context, MenuItem item,
+ Feed selectedFeed) {
+ FeedManager manager = FeedManager.getInstance();
+ switch (item.getItemId()) {
+ case R.id.show_info_item:
+ Intent startIntent = new Intent(context, FeedInfoActivity.class);
+ startIntent.putExtra(FeedInfoActivity.EXTRA_FEED_ID,
+ selectedFeed.getId());
+ context.startActivity(startIntent);
+ break;
+ case R.id.refresh_item:
+ manager.refreshFeed(context, selectedFeed);
+ break;
+ case R.id.mark_all_read_item:
+ manager.markFeedRead(context, selectedFeed);
+ break;
+ case R.id.visit_website_item:
+ Uri uri = Uri.parse(selectedFeed.getLink());
+ context.startActivity(new Intent(Intent.ACTION_VIEW, uri));
+ break;
+ case R.id.support_item:
+ FlattrUtils.clickUrl(context, selectedFeed.getPaymentLink());
+ break;
+ case R.id.share_link_item:
+ ShareUtils.shareFeedlink(context, selectedFeed);
+ break;
+ case R.id.share_source_item:
+ ShareUtils.shareFeedDownloadLink(context, selectedFeed);
+ break;
+ default:
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/src/de/danoeh/antennapod/util/FeedtitleComparator.java b/src/de/danoeh/antennapod/util/FeedtitleComparator.java
new file mode 100644
index 000000000..39a258b62
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/FeedtitleComparator.java
@@ -0,0 +1,15 @@
+package de.danoeh.antennapod.util;
+
+import java.util.Comparator;
+
+import de.danoeh.antennapod.feed.Feed;
+
+/** Compares the title of two feeds for sorting. */
+public class FeedtitleComparator implements Comparator<Feed> {
+
+ @Override
+ public int compare(Feed lhs, Feed rhs) {
+ return lhs.getTitle().compareTo(rhs.getTitle());
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/util/FlattrUtils.java b/src/de/danoeh/antennapod/util/FlattrUtils.java
new file mode 100644
index 000000000..b8f1d02bc
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/FlattrUtils.java
@@ -0,0 +1,215 @@
+package de.danoeh.antennapod.util;
+
+import java.util.EnumSet;
+
+import org.shredzone.flattr4j.FlattrFactory;
+import org.shredzone.flattr4j.FlattrService;
+import org.shredzone.flattr4j.exception.FlattrException;
+import org.shredzone.flattr4j.oauth.AccessToken;
+import org.shredzone.flattr4j.oauth.AndroidAuthenticator;
+import org.shredzone.flattr4j.oauth.Scope;
+
+import android.app.AlertDialog;
+import android.app.AlertDialog.Builder;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.preference.PreferenceManager;
+import android.util.Log;
+import android.widget.Toast;
+import de.danoeh.antennapod.PodcastApp;
+import de.danoeh.antennapod.activity.FlattrAuthActivity;
+import de.danoeh.antennapod.R;
+
+/** Utility methods for doing something with flattr. */
+public class FlattrUtils {
+ private static final String TAG = "FlattrUtils";
+
+ private static final String HOST_NAME = "de.danoeh.antennapod";
+ private static final String APP_KEY = "qBoVa9rhUOSPCrBwYPjzyNytHRbkPul5VzRWz93jNMZf4rCS7LhwpGPWnR73biZW";
+ private static final String APP_SECRET = "IGJ8FDiif7n9pPSmr0JHwotK5rD7AsU8Yt7uWfC2cQ2svKFNAekXtExpV1mlk7k8";
+
+ private static final String PREF_ACCESS_TOKEN = "de.danoeh.antennapod.preference.flattrAccessToken";
+
+ private static AndroidAuthenticator createAuthenticator() {
+ return new AndroidAuthenticator(HOST_NAME, APP_KEY, APP_SECRET);
+ }
+
+ public static void startAuthProcess(Context context) throws FlattrException {
+ AndroidAuthenticator auth = createAuthenticator();
+ auth.setScope(EnumSet.of(Scope.FLATTR));
+ Intent intent = auth.createAuthenticateIntent();
+ context.startActivity(intent);
+ }
+
+ /**
+ * Returns the access token from the preferences or null if no access token
+ * was saved before.
+ */
+ public static AccessToken retrieveToken() {
+ Log.d(TAG, "Retrieving access token");
+ String token = PreferenceManager.getDefaultSharedPreferences(
+ PodcastApp.getInstance()).getString(PREF_ACCESS_TOKEN, null);
+ if (token != null) {
+ Log.d(TAG, "Found access token");
+ return new AccessToken(token);
+ } else {
+ Log.d(TAG, "No access token found");
+ return null;
+ }
+ }
+
+ /** Returns true if the application has saved an access token */
+ public static boolean hasToken() {
+ return retrieveToken() != null;
+ }
+
+ /** Stores the token as a preference */
+ private static void storeToken(AccessToken token) {
+ Log.d(TAG, "Storing token");
+ SharedPreferences.Editor editor = PreferenceManager
+ .getDefaultSharedPreferences(PodcastApp.getInstance()).edit();
+ if (token != null) {
+ editor.putString(PREF_ACCESS_TOKEN, token.getToken());
+ } else {
+ editor.putString(PREF_ACCESS_TOKEN, null);
+ }
+ editor.commit();
+ }
+
+ public static void deleteToken() {
+ Log.d(TAG, "Deleting flattr token");
+ storeToken(null);
+ }
+
+ public static void clickUrl(Context context, String url) {
+ FlattrFactory factory = FlattrFactory.getInstance();
+ AccessToken token = retrieveToken();
+ if (token != null) {
+ FlattrService fs = factory.createFlattrService(retrieveToken());
+ try {
+ fs.click(url);
+ Toast toast = Toast.makeText(context.getApplicationContext(),
+ R.string.flattr_click_success, Toast.LENGTH_LONG);
+ toast.show();
+ } catch (FlattrException e) {
+ e.printStackTrace();
+ showErrorDialog(context, e.getMessage());
+ }
+ } else {
+ showNoTokenDialog(context, url);
+ }
+ }
+
+ public static AccessToken handleCallback(Uri uri) throws FlattrException {
+ AndroidAuthenticator auth = createAuthenticator();
+ AccessToken token = auth.fetchAccessToken(uri);
+ if (token != null) {
+ Log.d(TAG, "Successfully got token");
+ storeToken(token);
+ return token;
+ } else {
+ Log.w(TAG, "Flattr token was null");
+ return null;
+ }
+ }
+
+ public static void revokeAccessToken(Context context) {
+ Log.d(TAG, "Revoking access token");
+ deleteToken();
+ showRevokeDialog(context);
+ }
+
+
+ //------------------------------------------------ DIALOGS
+
+ private static void showRevokeDialog(final Context context) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.access_revoked_title);
+ builder.setMessage(R.string.access_revoked_info);
+ builder.setNeutralButton(android.R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+ builder.create().show();
+ }
+
+ private static void showNoTokenDialog(final Context context,
+ final String url) {
+ Log.d(TAG, "Creating showNoTokenDialog");
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.no_flattr_token_title);
+ builder.setMessage(R.string.no_flattr_token_msg);
+ builder.setPositiveButton(R.string.authenticate_now_label,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ context.startActivity(new Intent(context,
+ FlattrAuthActivity.class));
+ }
+
+ });
+ builder.setNegativeButton(R.string.visit_website_label,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Uri uri = Uri.parse(url);
+ context.startActivity(new Intent(Intent.ACTION_VIEW,
+ uri));
+ }
+
+ });
+ builder.create().show();
+ }
+
+ private static void showForbiddenDialog(final Context context,
+ final String url) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.action_forbidden_title);
+ builder.setMessage(R.string.action_forbidden_msg);
+ builder.setPositiveButton(R.string.authenticate_now_label,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ context.startActivity(new Intent(context,
+ FlattrAuthActivity.class));
+ }
+
+ });
+ builder.setNegativeButton(R.string.visit_website_label,
+ new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ Uri uri = Uri.parse(url);
+ context.startActivity(new Intent(Intent.ACTION_VIEW,
+ uri));
+ }
+
+ });
+ builder.create().show();
+ }
+
+ private static void showErrorDialog(final Context context, final String msg) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(context);
+ builder.setTitle(R.string.error_label);
+ builder.setMessage(msg);
+ builder.setNeutralButton(android.R.string.ok, new OnClickListener() {
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ });
+ builder.create().show();
+ }
+}
diff --git a/src/de/danoeh/antennapod/util/MediaPlayerError.java b/src/de/danoeh/antennapod/util/MediaPlayerError.java
new file mode 100644
index 000000000..0ce95fe65
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/MediaPlayerError.java
@@ -0,0 +1,23 @@
+package de.danoeh.antennapod.util;
+
+import de.danoeh.antennapod.R;
+import android.content.Context;
+import android.media.MediaPlayer;
+
+/** Utility class for MediaPlayer errors. */
+public class MediaPlayerError {
+
+ /** Get a human-readable string for a specific error code. */
+ public static String getErrorString(Context context, int code) {
+ int resId;
+ switch(code) {
+ case MediaPlayer.MEDIA_ERROR_SERVER_DIED:
+ resId = R.string.playback_error_server_died;
+ break;
+ default:
+ resId = R.string.playback_error_unknown;
+ break;
+ }
+ return context.getString(resId);
+ }
+}
diff --git a/src/de/danoeh/antennapod/util/NumberGenerator.java b/src/de/danoeh/antennapod/util/NumberGenerator.java
new file mode 100644
index 000000000..6f9ac2e78
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/NumberGenerator.java
@@ -0,0 +1,25 @@
+package de.danoeh.antennapod.util;
+
+import java.util.Random;
+import android.util.Log;
+
+/**Utility class for creating large random numbers.*/
+public final class NumberGenerator {
+ /** Class shall not be instantiated.*/
+ private NumberGenerator() {
+ }
+
+ /**Logging tag.*/
+ private static final String TAG = "NumberGenerator";
+
+ /** Takes a string and generates a random value out of
+ * the hash-value of that string.
+ * @param strSeed The string to take for the return value
+ * @return The generated random value
+ * */
+ public static long generateLong(final String strSeed) {
+ long seed = (long) strSeed.hashCode();
+ Log.d(TAG, "Taking " + seed + " as seed.");
+ return new Random(seed).nextLong();
+ }
+}
diff --git a/src/de/danoeh/antennapod/util/ShareUtils.java b/src/de/danoeh/antennapod/util/ShareUtils.java
new file mode 100644
index 000000000..2d6dee138
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/ShareUtils.java
@@ -0,0 +1,34 @@
+package de.danoeh.antennapod.util;
+
+import de.danoeh.antennapod.feed.Feed;
+import de.danoeh.antennapod.feed.FeedItem;
+import android.content.Context;
+import android.content.Intent;
+
+/** Utility methods for sharing data */
+public class ShareUtils {
+ private static final String TAG = "ShareUtils";
+
+ private ShareUtils() {}
+
+ private static void shareLink(Context context, String link) {
+ Intent i = new Intent(Intent.ACTION_SEND);
+ i.setType("text/plain");
+ i.putExtra(Intent.EXTRA_SUBJECT, "Sharing URL");
+ i.putExtra(Intent.EXTRA_TEXT, link);
+ context.startActivity(Intent.createChooser(i, "Share URL"));
+ }
+
+ public static void shareFeedItemLink(Context context, FeedItem item) {
+ shareLink(context, item.getLink());
+ }
+
+ public static void shareFeedDownloadLink(Context context, Feed feed) {
+ shareLink(context, feed.getDownload_url());
+ }
+
+ public static void shareFeedlink(Context context, Feed feed) {
+ shareLink(context, feed.getLink());
+ }
+
+}
diff --git a/src/de/danoeh/antennapod/util/StorageUtils.java b/src/de/danoeh/antennapod/util/StorageUtils.java
new file mode 100644
index 000000000..942c333fb
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/StorageUtils.java
@@ -0,0 +1,28 @@
+package de.danoeh.antennapod.util;
+
+import de.danoeh.antennapod.activity.StorageErrorActivity;
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Environment;
+
+/** Utility functions for handling storage errors */
+public class StorageUtils {
+ public static boolean storageAvailable() {
+ String state = Environment.getExternalStorageState();
+ return state.equals(Environment.MEDIA_MOUNTED);
+ }
+
+ /**Checks if external storage is available. If external storage isn't
+ * available, the current activity is finsished an an error activity is launched.
+ * @param activity the activity which would be finished if no storage is available
+ * @return true if external storage is available
+ */
+ public static boolean checkStorageAvailability(Activity activity) {
+ boolean storageAvailable = storageAvailable();
+ if (!storageAvailable) {
+ activity.finish();
+ activity.startActivity(new Intent(activity, StorageErrorActivity.class));
+ }
+ return storageAvailable;
+ }
+}
diff --git a/src/de/danoeh/antennapod/util/URLChecker.java b/src/de/danoeh/antennapod/util/URLChecker.java
new file mode 100644
index 000000000..f5e202946
--- /dev/null
+++ b/src/de/danoeh/antennapod/util/URLChecker.java
@@ -0,0 +1,39 @@
+package de.danoeh.antennapod.util;
+
+import android.util.Log;
+
+/** Provides methods for checking and editing a URL.*/
+public final class URLChecker {
+
+ /**Class shall not be instantiated.*/
+ private URLChecker() {
+ }
+
+ /**Logging tag.*/
+ private static final String TAG = "URLChecker";
+ /**Indicator for URLs made by Feedburner.*/
+ private static final String FEEDBURNER_URL = "feeds.feedburner.com";
+ /**Prefix that is appended to URLs by Feedburner.*/
+ private static final String FEEDBURNER_PREFIX = "?format=xml";
+
+ /** Checks if URL is valid and modifies it if necessary.
+ * @param url The url which is going to be prepared
+ * @return The prepared url
+ * */
+ public static String prepareURL(final String url) {
+ StringBuilder builder = new StringBuilder();
+
+ if (!url.startsWith("http")) {
+ builder.append("http://");
+ Log.d(TAG, "Missing http; appending");
+ }
+ builder.append(url);
+
+ if (url.contains(FEEDBURNER_URL)) {
+ Log.d(TAG,
+ "URL seems to be Feedburner URL; appending prefix");
+ builder.append(FEEDBURNER_PREFIX);
+ }
+ return builder.toString();
+ }
+}