diff options
Diffstat (limited to 'src')
85 files changed, 11741 insertions, 9377 deletions
diff --git a/src/de/danoeh/antennapod/PodcastApp.java b/src/de/danoeh/antennapod/PodcastApp.java index e9f46256b..2141f71e8 100644 --- a/src/de/danoeh/antennapod/PodcastApp.java +++ b/src/de/danoeh/antennapod/PodcastApp.java @@ -5,7 +5,6 @@ import android.content.res.Configuration; import android.util.Log; import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.feed.EventDistributor; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.preferences.PlaybackPreferences; import de.danoeh.antennapod.preferences.UserPreferences; @@ -32,8 +31,6 @@ public class PodcastApp extends Application { UserPreferences.createInstance(this); PlaybackPreferences.createInstance(this); EventDistributor.getInstance(); - FeedManager manager = FeedManager.getInstance(); - manager.loadDBData(getApplicationContext()); } @Override diff --git a/src/de/danoeh/antennapod/activity/AboutActivity.java b/src/de/danoeh/antennapod/activity/AboutActivity.java index e3265d1eb..27fdbe241 100644 --- a/src/de/danoeh/antennapod/activity/AboutActivity.java +++ b/src/de/danoeh/antennapod/activity/AboutActivity.java @@ -1,15 +1,15 @@ package de.danoeh.antennapod.activity; import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; import android.webkit.WebView; import android.webkit.WebViewClient; -import com.actionbarsherlock.app.SherlockActivity; import de.danoeh.antennapod.R; /** Displays the 'about' screen */ -public class AboutActivity extends SherlockActivity { +public class AboutActivity extends ActionBarActivity { private WebView webview; diff --git a/src/de/danoeh/antennapod/activity/AddFeedActivity.java b/src/de/danoeh/antennapod/activity/AddFeedActivity.java index 7e702f28b..4085fc8d2 100644 --- a/src/de/danoeh/antennapod/activity/AddFeedActivity.java +++ b/src/de/danoeh/antennapod/activity/AddFeedActivity.java @@ -2,6 +2,9 @@ package de.danoeh.antennapod.activity; import java.util.Date; +import android.support.v7.app.ActionBarActivity; +import android.view.Menu; +import android.view.MenuItem; import org.apache.commons.lang3.StringUtils; import android.app.AlertDialog; @@ -15,10 +18,6 @@ import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; -import com.actionbarsherlock.app.SherlockActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.Feed; @@ -31,7 +30,7 @@ import de.danoeh.antennapod.util.StorageUtils; import de.danoeh.antennapod.util.URLChecker; /** Activity for adding a Feed */ -public class AddFeedActivity extends SherlockActivity { +public class AddFeedActivity extends ActionBarActivity { private static final String TAG = "AddFeedActivity"; private DownloadRequester requester; @@ -161,7 +160,7 @@ public class AddFeedActivity extends SherlockActivity { } @Override - public void onConnectionFailure(int reason) { + public void onConnectionFailure(DownloadError reason) { handleDownloadError(reason); } }); @@ -177,11 +176,11 @@ public class AddFeedActivity extends SherlockActivity { progDialog.setMessage(getString(R.string.loading_label)); } - private void handleDownloadError(int reason) { + private void handleDownloadError(DownloadError reason) { 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, reason)); + + reason.getErrorString(this)); errorDialog.setButton(getString(android.R.string.ok), new DialogInterface.OnClickListener() { @Override @@ -200,6 +199,8 @@ public class AddFeedActivity extends SherlockActivity { return true; } + + @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { diff --git a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java index b73a17125..0a4c8ae14 100644 --- a/src/de/danoeh/antennapod/activity/AudioplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/AudioplayerActivity.java @@ -7,18 +7,17 @@ import android.content.res.TypedArray; import android.os.Bundle; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentTransaction; +import android.support.v4.app.ListFragment; import android.util.Log; import android.view.View; import android.view.View.OnClickListener; +import android.view.Window; import android.widget.ArrayAdapter; import android.widget.ImageButton; import android.widget.ImageView.ScaleType; import android.widget.ListView; import android.widget.TextView; -import com.actionbarsherlock.app.SherlockListFragment; -import com.actionbarsherlock.view.Window; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.ChapterListAdapter; @@ -48,7 +47,7 @@ public class AudioplayerActivity extends MediaplayerActivity { private CoverFragment coverFragment; private ItemDescriptionFragment descriptionFragment; - private SherlockListFragment chapterFragment; + private ListFragment chapterFragment; private Fragment currentlyShownFragment; private int currentlyShownPosition = -1; @@ -248,8 +247,7 @@ public class AudioplayerActivity extends MediaplayerActivity { /** * Changes the currently displayed fragment. * - * @param Must - * be POS_COVER, POS_DESCR, or POS_CHAPTERS + * @param pos Must be POS_COVER, POS_DESCR, or POS_CHAPTERS * */ private void switchToFragment(int pos) { if (AppConfig.DEBUG) @@ -280,7 +278,7 @@ public class AudioplayerActivity extends MediaplayerActivity { break; case POS_CHAPTERS: if (chapterFragment == null) { - chapterFragment = new SherlockListFragment() { + chapterFragment = new ListFragment() { @Override public void onListItemClick(ListView l, View v, diff --git a/src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java b/src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java index 54c4f0589..984491174 100644 --- a/src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java +++ b/src/de/danoeh/antennapod/activity/DirectoryChooserActivity.java @@ -12,7 +12,11 @@ import android.content.Intent; import android.os.Bundle; import android.os.Environment; import android.os.FileObserver; +import android.support.v7.app.ActionBarActivity; import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.AdapterView; @@ -24,11 +28,6 @@ import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; -import com.actionbarsherlock.app.SherlockActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.preferences.UserPreferences; @@ -37,7 +36,7 @@ import de.danoeh.antennapod.preferences.UserPreferences; * Let's the user choose a directory on the storage device. The selected folder * will be sent back to the starting activity as an activity result. */ -public class DirectoryChooserActivity extends SherlockActivity { +public class DirectoryChooserActivity extends ActionBarActivity { private static final String TAG = "DirectoryChooserActivity"; private static final String CREATE_DIRECTORY_NAME = "AntennaPod"; diff --git a/src/de/danoeh/antennapod/activity/DownloadActivity.java b/src/de/danoeh/antennapod/activity/DownloadActivity.java index 10ebb1285..40c75d336 100644 --- a/src/de/danoeh/antennapod/activity/DownloadActivity.java +++ b/src/de/danoeh/antennapod/activity/DownloadActivity.java @@ -11,21 +11,23 @@ import android.content.res.TypedArray; import android.os.AsyncTask; import android.os.Bundle; import android.os.IBinder; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.view.ActionMode; import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; import android.widget.AdapterView.OnItemLongClickListener; -import com.actionbarsherlock.app.SherlockListActivity; -import com.actionbarsherlock.view.ActionMode; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; +import android.widget.ListView; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.DownloadlistAdapter; -import de.danoeh.antennapod.asynctask.DownloadStatus; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.service.download.DownloadRequest; import de.danoeh.antennapod.service.download.DownloadService; import de.danoeh.antennapod.storage.DownloadRequester; @@ -33,221 +35,228 @@ import de.danoeh.antennapod.storage.DownloadRequester; * Shows all running downloads in a list. The list objects are DownloadStatus * objects created by a DownloadObserver. */ -public class DownloadActivity extends SherlockListActivity implements - ActionMode.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 DownloadService downloadService = null; - boolean mIsBound; - - private AsyncTask<Void, Void, Void> contentRefresher; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - if (AppConfig.DEBUG) - Log.d(TAG, "Creating Activity"); - requester = DownloadRequester.getInstance(); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - } - - @Override - protected void onPause() { - super.onPause(); - unbindService(mConnection); - unregisterReceiver(contentChanged); - } - - @Override - protected void onResume() { - super.onResume(); - registerReceiver(contentChanged, new IntentFilter( - DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED)); - bindService(new Intent(this, DownloadService.class), mConnection, 0); - startContentRefresher(); - if (dla != null) { - dla.notifyDataSetChanged(); - } - } - - @Override - protected void onStop() { - super.onStop(); - if (AppConfig.DEBUG) - Log.d(TAG, "Stopping Activity"); - stopContentRefresher(); - } - - private ServiceConnection mConnection = new ServiceConnection() { - public void onServiceDisconnected(ComponentName className) { - downloadService = null; - mIsBound = false; - Log.i(TAG, "Closed connection with DownloadService."); - } - - public void onServiceConnected(ComponentName name, IBinder service) { - downloadService = ((DownloadService.LocalBinder) service) - .getService(); - mIsBound = true; - if (AppConfig.DEBUG) - Log.d(TAG, "Connection to service established"); - dla = new DownloadlistAdapter(DownloadActivity.this, 0, - downloadService.getDownloads()); - setListAdapter(dla); - dla.notifyDataSetChanged(); - } - }; - - @SuppressLint("NewApi") - private void startContentRefresher() { - if (contentRefresher != null) { - contentRefresher.cancel(true); - } - contentRefresher = new AsyncTask<Void, Void, Void>() { - private final int WAITING_INTERVALL = 1000; - - @Override - protected void onProgressUpdate(Void... values) { - super.onProgressUpdate(values); - if (dla != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Refreshing content automatically"); - dla.notifyDataSetChanged(); - } - } - - @Override - protected Void doInBackground(Void... params) { - while (!isCancelled()) { - try { - Thread.sleep(WAITING_INTERVALL); - publishProgress(); - } catch (InterruptedException e) { - return null; - } - } - return null; - } - }; - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - contentRefresher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - contentRefresher.execute(); - } - } - - private void stopContentRefresher() { - if (contentRefresher != null) { - contentRefresher.cancel(true); - } - } - - @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).getStatus(); - 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_COLLAPSE_ACTION_VIEW); - menu.add(Menu.NONE, MENU_CANCEL_ALL_DOWNLOADS, Menu.NONE, - R.string.cancel_all_downloads_label).setShowAsAction( - MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - Intent intent = new Intent(this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP - | Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(intent); - 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()) { - TypedArray drawables = obtainStyledAttributes(new int[] { R.attr.navigation_cancel }); - menu.add(Menu.NONE, R.id.cancel_download_item, Menu.NONE, - R.string.cancel_download_label).setIcon( - drawables.getDrawable(0)); - } - 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()); - handled = true; - break; - } - mActionMode.finish(); - return handled; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - mActionMode = null; - selectedDownload = null; - dla.setSelectedItemIndex(DownloadlistAdapter.SELECTION_NONE); - } - - private BroadcastReceiver contentChanged = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (dla != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Refreshing content"); - dla.notifyDataSetChanged(); - } - } - }; +public class DownloadActivity extends ActionBarActivity implements + ActionMode.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 DownloadRequest selectedDownload; + + private DownloadService downloadService = null; + boolean mIsBound; + + private AsyncTask<Void, Void, Void> contentRefresher; + + private ListView listview; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + setContentView(R.layout.listview_activity); + + listview = (ListView) findViewById(R.id.listview); + + if (AppConfig.DEBUG) + Log.d(TAG, "Creating Activity"); + requester = DownloadRequester.getInstance(); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + } + + @Override + protected void onPause() { + super.onPause(); + unbindService(mConnection); + unregisterReceiver(contentChanged); + } + + @Override + protected void onResume() { + super.onResume(); + registerReceiver(contentChanged, new IntentFilter( + DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED)); + bindService(new Intent(this, DownloadService.class), mConnection, 0); + startContentRefresher(); + if (dla != null) { + dla.notifyDataSetChanged(); + } + } + + @Override + protected void onStop() { + super.onStop(); + if (AppConfig.DEBUG) + Log.d(TAG, "Stopping Activity"); + stopContentRefresher(); + } + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceDisconnected(ComponentName className) { + downloadService = null; + mIsBound = false; + Log.i(TAG, "Closed connection with DownloadService."); + } + + public void onServiceConnected(ComponentName name, IBinder service) { + downloadService = ((DownloadService.LocalBinder) service) + .getService(); + mIsBound = true; + if (AppConfig.DEBUG) + Log.d(TAG, "Connection to service established"); + dla = new DownloadlistAdapter(DownloadActivity.this, 0, + downloadService.getDownloads()); + listview.setAdapter(dla); + dla.notifyDataSetChanged(); + } + }; + + @SuppressLint("NewApi") + private void startContentRefresher() { + if (contentRefresher != null) { + contentRefresher.cancel(true); + } + contentRefresher = new AsyncTask<Void, Void, Void>() { + private final int WAITING_INTERVALL = 1000; + + @Override + protected void onProgressUpdate(Void... values) { + super.onProgressUpdate(values); + if (dla != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Refreshing content automatically"); + dla.notifyDataSetChanged(); + } + } + + @Override + protected Void doInBackground(Void... params) { + while (!isCancelled()) { + try { + Thread.sleep(WAITING_INTERVALL); + publishProgress(); + } catch (InterruptedException e) { + return null; + } + } + return null; + } + }; + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + contentRefresher.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + contentRefresher.execute(); + } + } + + private void stopContentRefresher() { + if (contentRefresher != null) { + contentRefresher.cancel(true); + } + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + listview.setOnItemLongClickListener(new OnItemLongClickListener() { + + @Override + public boolean onItemLongClick(AdapterView<?> arg0, View view, + int position, long id) { + DownloadRequest selection = dla.getItem(position) + .getDownloadRequest(); + if (selection != null && mActionMode != null) { + mActionMode.finish(); + } + dla.setSelectedItemIndex(position); + selectedDownload = selection; + mActionMode = startSupportActionMode(DownloadActivity.this); + return true; + } + + }); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, MENU_SHOW_LOG, Menu.NONE, + R.string.show_download_log), + MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, MENU_CANCEL_ALL_DOWNLOADS, Menu.NONE, + R.string.cancel_all_downloads_label), + MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP + | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + 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 != null) { + TypedArray drawables = obtainStyledAttributes(new int[]{R.attr.navigation_cancel}); + menu.add(Menu.NONE, R.id.cancel_download_item, Menu.NONE, + R.string.cancel_download_label).setIcon( + drawables.getDrawable(0)); + } + 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.getSource()); + handled = true; + break; + } + mActionMode.finish(); + return handled; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mActionMode = null; + selectedDownload = null; + dla.setSelectedItemIndex(DownloadlistAdapter.SELECTION_NONE); + } + + private BroadcastReceiver contentChanged = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (dla != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Refreshing content"); + dla.notifyDataSetChanged(); + } + } + }; } diff --git a/src/de/danoeh/antennapod/activity/DownloadLogActivity.java b/src/de/danoeh/antennapod/activity/DownloadLogActivity.java index 232a7ba1d..949834596 100644 --- a/src/de/danoeh/antennapod/activity/DownloadLogActivity.java +++ b/src/de/danoeh/antennapod/activity/DownloadLogActivity.java @@ -1,35 +1,47 @@ package de.danoeh.antennapod.activity; +import android.os.AsyncTask; import android.os.Bundle; -import com.actionbarsherlock.app.SherlockListActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; +import android.support.v7.app.ActionBarActivity; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.widget.ListView; +import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.DownloadLogAdapter; import de.danoeh.antennapod.feed.EventDistributor; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.service.download.DownloadStatus; +import de.danoeh.antennapod.storage.DBReader; + +import java.util.List; /** - * Displays completed and failed downloads in a list. The data comes from the - * FeedManager. + * Displays completed and failed downloads in a list. */ -public class DownloadLogActivity extends SherlockListActivity { +public class DownloadLogActivity extends ActionBarActivity { private static final String TAG = "DownloadLogActivity"; - DownloadLogAdapter dla; - FeedManager manager; + private List<DownloadStatus> downloadLog; + private DownloadLogAdapter dla; + + private ListView listview; @Override protected void onCreate(Bundle savedInstanceState) { setTheme(UserPreferences.getTheme()); super.onCreate(savedInstanceState); - manager = FeedManager.getInstance(); + setContentView(R.layout.listview_activity); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + - dla = new DownloadLogAdapter(this); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setListAdapter(dla); + listview = (ListView) findViewById(R.layout.listview_activity); + + dla = new DownloadLogAdapter(this, itemAccess); + listview.setAdapter(dla); + loadData(); } @Override @@ -62,12 +74,48 @@ public class DownloadLogActivity extends SherlockListActivity { return true; } + private void loadData() { + AsyncTask<Void, Void, List<DownloadStatus>> loadTask = new AsyncTask<Void, Void, List<DownloadStatus>>() { + @Override + protected List<DownloadStatus> doInBackground(Void... voids) { + return DBReader.getDownloadLog(DownloadLogActivity.this); + } + + @Override + protected void onPostExecute(List<DownloadStatus> downloadStatuses) { + super.onPostExecute(downloadStatuses); + if (downloadStatuses != null) { + downloadLog = downloadStatuses; + if (dla != null) { + dla.notifyDataSetChanged(); + } + } else { + Log.e(TAG, "Could not load download log"); + } + } + }; + loadTask.execute(); + } + + private DownloadLogAdapter.ItemAccess itemAccess = new DownloadLogAdapter.ItemAccess() { + + @Override + public int getCount() { + return (downloadLog != null) ? downloadLog.size() : 0; + } + + @Override + public DownloadStatus getItem(int position) { + return (downloadLog != null) ? downloadLog.get(position) : null; + } + }; + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { @Override public void update(EventDistributor eventDistributor, Integer arg) { if ((arg & EventDistributor.DOWNLOADLOG_UPDATE) != 0) { - dla.notifyDataSetChanged(); + loadData(); } } }; diff --git a/src/de/danoeh/antennapod/activity/FeedInfoActivity.java b/src/de/danoeh/antennapod/activity/FeedInfoActivity.java index c57a5794b..4a8a2f1f8 100644 --- a/src/de/danoeh/antennapod/activity/FeedInfoActivity.java +++ b/src/de/danoeh/antennapod/activity/FeedInfoActivity.java @@ -1,28 +1,28 @@ package de.danoeh.antennapod.activity; +import android.os.AsyncTask; import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.widget.ImageView; import android.widget.TextView; -import com.actionbarsherlock.app.SherlockActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.util.LangUtils; import de.danoeh.antennapod.util.menuhandler.FeedMenuHandler; /** Displays information about a feed. */ -public class FeedInfoActivity extends SherlockActivity { +public class FeedInfoActivity extends ActionBarActivity { private static final String TAG = "FeedInfoActivity"; public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.extra.feedId"; @@ -42,47 +42,66 @@ public class FeedInfoActivity extends SherlockActivity { setContentView(R.layout.feedinfo); getSupportActionBar().setDisplayHomeAsUpEnabled(true); long feedId = getIntent().getLongExtra(EXTRA_FEED_ID, -1); - FeedManager manager = FeedManager.getInstance(); - feed = manager.getFeed(feedId); - if (feed != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Language is " + feed.getLanguage()); - if (AppConfig.DEBUG) - 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); - imgvCover.post(new Runnable() { - - @Override - public void run() { - ImageLoader.getInstance().loadThumbnailBitmap( - feed.getImage(), imgvCover); - } - }); + + AsyncTask<Long, Void, Feed> loadTask = new AsyncTask<Long, Void, Feed>() { - txtvTitle.setText(feed.getTitle()); - txtvDescription.setText(feed.getDescription()); - if (feed.getAuthor() != null) { - txtvAuthor.setText(feed.getAuthor()); + @Override + protected Feed doInBackground(Long... params) { + return DBReader.getFeed(FeedInfoActivity.this, params[0]); } - if (feed.getLanguage() != null) { - txtvLanguage.setText(LangUtils.getLanguageString(feed - .getLanguage())); - } - } else { - Log.e(TAG, "Activity was started with invalid arguments"); - } + @Override + protected void onPostExecute(Feed result) { + super.onPostExecute(result); + if (result != null) { + feed = result; + if (feed != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Language is " + feed.getLanguage()); + if (AppConfig.DEBUG) + 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); + imgvCover.post(new Runnable() { + + @Override + public void run() { + ImageLoader.getInstance().loadThumbnailBitmap( + 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(LangUtils + .getLanguageString(feed.getLanguage())); + } + supportInvalidateOptionsMenu(); + } + } else { + Log.e(TAG, "Activity was started with invalid arguments"); + } + } + }; + loadTask.execute(feedId); } @Override public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = new MenuInflater(this); - inflater.inflate(R.menu.feedinfo, menu); - return true; + if (feed != null) { + MenuInflater inflater = new MenuInflater(this); + inflater.inflate(R.menu.feedinfo, menu); + return true; + } else { + return false; + } } @Override @@ -105,7 +124,8 @@ public class FeedInfoActivity extends SherlockActivity { return FeedMenuHandler.onOptionsItemClicked(this, item, feed); } catch (DownloadRequestException e) { e.printStackTrace(); - DownloadRequestErrorDialogCreator.newRequestErrorDialog(this, e.getMessage()); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(this, + e.getMessage()); } return super.onOptionsItemSelected(item); } diff --git a/src/de/danoeh/antennapod/activity/FeedItemlistActivity.java b/src/de/danoeh/antennapod/activity/FeedItemlistActivity.java index fdca48e8a..64e4f2d30 100644 --- a/src/de/danoeh/antennapod/activity/FeedItemlistActivity.java +++ b/src/de/danoeh/antennapod/activity/FeedItemlistActivity.java @@ -4,148 +4,185 @@ import android.annotation.SuppressLint; import android.content.DialogInterface; import android.content.Intent; import android.content.res.TypedArray; +import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; 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 android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.Window; +import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.FeedRemover; import de.danoeh.antennapod.dialog.ConfirmationDialog; import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.fragment.ExternalPlayerFragment; import de.danoeh.antennapod.fragment.FeedlistFragment; import de.danoeh.antennapod.fragment.ItemlistFragment; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.util.StorageUtils; import de.danoeh.antennapod.util.menuhandler.FeedMenuHandler; -/** 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; - private ExternalPlayerFragment externalPlayerFragment; - - @Override - public void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - 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.replace(R.id.feeditemlistFragment, filf); - - externalPlayerFragment = new ExternalPlayerFragment(); - fT.replace(R.id.playerFragment, externalPlayerFragment); - fT.commit(); - - } - - @Override - protected void onResume() { - super.onResume(); - StorageUtils.checkStorageAvailability(this); - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - TypedArray drawables = obtainStyledAttributes(new int[] { R.attr.action_search }); - menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label) - .setIcon(drawables.getDrawable(0)) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); - return FeedMenuHandler - .onCreateOptionsMenu(new MenuInflater(this), menu); - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - return FeedMenuHandler.onPrepareOptionsMenu(menu, feed); - } - - @SuppressLint("NewApi") - @Override - public boolean onOptionsItemSelected(MenuItem item) { - try { - if (FeedMenuHandler.onOptionsItemClicked(this, item, feed)) { - filf.getListAdapter().notifyDataSetChanged(); - } else { - switch (item.getItemId()) { - case R.id.remove_item: - final FeedRemover remover = new FeedRemover( - FeedItemlistActivity.this, feed) { - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - finish(); - } - }; - ConfirmationDialog conDialog = new ConfirmationDialog(this, - R.string.remove_feed_label, - R.string.feed_delete_confirmation_msg) { - - @Override - public void onConfirmButtonPressed( - DialogInterface dialog) { - dialog.dismiss(); - remover.executeAsync(); - } - }; - conDialog.createNewDialog().show(); - break; - case R.id.search_item: - onSearchRequested(); - break; - case android.R.id.home: - Intent intent = new Intent(this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - break; - } - } - } catch (DownloadRequestException e) { - e.printStackTrace(); - DownloadRequestErrorDialogCreator.newRequestErrorDialog(this, - e.getMessage()); - } - return true; - } - - @Override - public boolean onSearchRequested() { - Bundle bundle = new Bundle(); - bundle.putLong(SearchActivity.EXTRA_FEED_ID, feed.getId()); - startSearch(null, false, bundle, false); - return true; - } +/** + * Displays a List of FeedItems + */ +public class FeedItemlistActivity extends ActionBarActivity { + private static final String TAG = "FeedItemlistActivity"; + + /** + * The feed which the activity displays + */ + private Feed feed; + private ItemlistFragment filf; + private ExternalPlayerFragment externalPlayerFragment; + + @Override + public void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + StorageUtils.checkStorageAvailability(this); + requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.feeditemlist_activity); + + long feedId = getIntent().getLongExtra( + FeedlistFragment.EXTRA_SELECTED_FEED, -1); + if (feedId == -1) { + Log.e(TAG, "Received invalid feed selection."); + } else { + loadData(feedId); + } + + } + + private void loadData(long id) { + AsyncTask<Long, Void, Feed> loadTask = new AsyncTask<Long, Void, Feed>() { + + @Override + protected Feed doInBackground(Long... longs) { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading feed data in background"); + return DBReader.getFeed(FeedItemlistActivity.this, longs[0]); + } + + @Override + protected void onPostExecute(Feed result) { + super.onPostExecute(result); + if (result != null) { + if (AppConfig.DEBUG) Log.d(TAG, "Finished loading feed data"); + feed = result; + setTitle(feed.getTitle()); + + FragmentManager fragmentManager = getSupportFragmentManager(); + FragmentTransaction fT = fragmentManager.beginTransaction(); + + filf = ItemlistFragment.newInstance(feed.getId()); + fT.replace(R.id.feeditemlistFragment, filf); + + externalPlayerFragment = new ExternalPlayerFragment(); + fT.replace(R.id.playerFragment, externalPlayerFragment); + fT.commit(); + supportInvalidateOptionsMenu(); + } else { + Log.e(TAG, "Error: Feed was null"); + } + } + }; + loadTask.execute(id); + } + + @Override + protected void onResume() { + super.onResume(); + StorageUtils.checkStorageAvailability(this); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + if (feed != null) { + TypedArray drawables = obtainStyledAttributes(new int[]{R.attr.action_search}); + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label) + .setIcon(drawables.getDrawable(0)), + MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW); + return FeedMenuHandler + .onCreateOptionsMenu(new MenuInflater(this), menu); + } else { + return false; + } + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + return FeedMenuHandler.onPrepareOptionsMenu(menu, feed); + } + + @SuppressLint("NewApi") + @Override + public boolean onOptionsItemSelected(MenuItem item) { + try { + if (FeedMenuHandler.onOptionsItemClicked(this, item, feed)) { + filf.getListAdapter().notifyDataSetChanged(); + } else { + switch (item.getItemId()) { + case R.id.remove_item: + final FeedRemover remover = new FeedRemover( + FeedItemlistActivity.this, feed) { + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + finish(); + } + }; + ConfirmationDialog conDialog = new ConfirmationDialog(this, + R.string.remove_feed_label, + R.string.feed_delete_confirmation_msg) { + + @Override + public void onConfirmButtonPressed( + DialogInterface dialog) { + dialog.dismiss(); + remover.executeAsync(); + } + }; + conDialog.createNewDialog().show(); + break; + case R.id.search_item: + onSearchRequested(); + break; + case android.R.id.home: + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + break; + } + } + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(this, + e.getMessage()); + } + return true; + } + + @Override + public boolean onSearchRequested() { + if (feed != null) { + Bundle bundle = new Bundle(); + bundle.putLong(SearchActivity.EXTRA_FEED_ID, feed.getId()); + startSearch(null, false, bundle, false); + return true; + } else { + return false; + } + } } diff --git a/src/de/danoeh/antennapod/activity/FlattrAuthActivity.java b/src/de/danoeh/antennapod/activity/FlattrAuthActivity.java index 75e513816..2ab95e287 100644 --- a/src/de/danoeh/antennapod/activity/FlattrAuthActivity.java +++ b/src/de/danoeh/antennapod/activity/FlattrAuthActivity.java @@ -1,6 +1,9 @@ package de.danoeh.antennapod.activity; +import android.support.v7.app.ActionBarActivity; +import android.view.Menu; +import android.view.MenuItem; import org.shredzone.flattr4j.exception.FlattrException; import android.content.Intent; @@ -12,10 +15,6 @@ 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.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.preferences.UserPreferences; @@ -23,7 +22,7 @@ import de.danoeh.antennapod.util.flattr.FlattrUtils; /** Guides the user through the authentication process */ -public class FlattrAuthActivity extends SherlockActivity { +public class FlattrAuthActivity extends ActionBarActivity { private static final String TAG = "FlattrAuthActivity"; private TextView txtvExplanation; diff --git a/src/de/danoeh/antennapod/activity/ItemviewActivity.java b/src/de/danoeh/antennapod/activity/ItemviewActivity.java index 5ead667dc..43eea93e4 100644 --- a/src/de/danoeh/antennapod/activity/ItemviewActivity.java +++ b/src/de/danoeh/antennapod/activity/ItemviewActivity.java @@ -1,55 +1,58 @@ package de.danoeh.antennapod.activity; -import java.text.DateFormat; - +import android.os.AsyncTask; import android.os.Bundle; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; +import android.support.v7.app.ActionBarActivity; import android.text.format.DateUtils; import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.Window; 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.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; -import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.feed.EventDistributor; 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.preferences.UserPreferences; +import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DownloadRequestException; +import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.StorageUtils; import de.danoeh.antennapod.util.menuhandler.FeedItemMenuHandler; +import java.text.DateFormat; + /** Displays a single FeedItem and provides various actions */ -public class ItemviewActivity extends SherlockFragmentActivity { +public class ItemviewActivity extends ActionBarActivity { private static final String TAG = "ItemviewActivity"; - private FeedManager manager; - private FeedItem item; + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED; - // Widgets - private TextView txtvTitle; - private TextView txtvPublished; + private FeedItem item; @Override public void onCreate(Bundle savedInstanceState) { setTheme(UserPreferences.getTheme()); super.onCreate(savedInstanceState); StorageUtils.checkStorageAvailability(this); - manager = FeedManager.getInstance(); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); getSupportActionBar().setDisplayShowTitleEnabled(false); - extractFeeditem(); - populateUI(); + EventDistributor.getInstance().register(contentUpdate); + + long itemId = getIntent().getLongExtra( + ItemlistFragment.EXTRA_SELECTED_FEEDITEM, -1); + if (itemId == -1) { + Log.e(TAG, "Received invalid selection of either feeditem or feed."); + } else { + loadData(itemId); + } } @Override @@ -66,28 +69,38 @@ public class ItemviewActivity extends SherlockFragmentActivity { 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); - if (AppConfig.DEBUG) - Log.d(TAG, "Title of item is " + item.getTitle()); - if (AppConfig.DEBUG) - Log.d(TAG, "Title of feed is " + item.getFeed().getTitle()); - } + private void loadData(long itemId) { + AsyncTask<Long, Void, FeedItem> loadTask = new AsyncTask<Long, Void, FeedItem>() { + + @Override + protected FeedItem doInBackground(Long... longs) { + return DBReader.getFeedItem(ItemviewActivity.this, longs[0]); + } + + @Override + protected void onPostExecute(FeedItem feedItem) { + super.onPostExecute(feedItem); + if (feedItem != null && feedItem.getFeed() != null) { + item = feedItem; + populateUI(); + supportInvalidateOptionsMenu(); + } else { + if (feedItem == null) { + Log.e(TAG, "Error: FeedItem was null"); + } else if (feedItem.getFeed() == null) { + Log.e(TAG, "Error: Feed was null"); + } + } + } + }; + loadTask.execute(itemId); + } private void populateUI() { getSupportActionBar().setDisplayHomeAsUpEnabled(true); setContentView(R.layout.feeditemview); - txtvTitle = (TextView) findViewById(R.id.txtvItemname); - txtvPublished = (TextView) findViewById(R.id.txtvPublished); + TextView txtvTitle = (TextView) findViewById(R.id.txtvItemname); + TextView txtvPublished = (TextView) findViewById(R.id.txtvPublished); setTitle(item.getFeed().getTitle()); txtvPublished.setText(DateUtils.formatSameDayTime(item.getPubDate() @@ -106,9 +119,13 @@ public class ItemviewActivity extends SherlockFragmentActivity { @Override public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = getSupportMenuInflater(); - inflater.inflate(R.menu.feeditem, menu); - return true; + if (item != null) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.feeditem, menu); + return true; + } else { + return false; + } } @Override @@ -134,13 +151,28 @@ public class ItemviewActivity extends SherlockFragmentActivity { @Override public boolean onPrepareOptionsMenu(final Menu menu) { return FeedItemMenuHandler.onPrepareMenu( - new FeedItemMenuHandler.MenuInterface() { + new FeedItemMenuHandler.MenuInterface() { - @Override - public void setItemVisibility(int id, boolean visible) { - menu.findItem(id).setVisible(visible); - } - }, item, true); + @Override + public void setItemVisibility(int id, boolean visible) { + menu.findItem(id).setVisible(visible); + } + }, item, true, QueueAccess.NotInQueueAccess()); } + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((EVENTS & arg) != 0) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received contentUpdate Intent."); + if (item != null) { + loadData(item.getId()); + } + } + } + }; + + } diff --git a/src/de/danoeh/antennapod/activity/MainActivity.java b/src/de/danoeh/antennapod/activity/MainActivity.java index 410617b23..92b56461c 100644 --- a/src/de/danoeh/antennapod/activity/MainActivity.java +++ b/src/de/danoeh/antennapod/activity/MainActivity.java @@ -9,37 +9,35 @@ import android.support.v4.app.Fragment; import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentTransaction; import android.support.v4.view.ViewPager; +import android.support.v7.app.ActionBar; +import android.support.v7.app.ActionBarActivity; import android.util.Log; -import com.actionbarsherlock.app.ActionBar; -import com.actionbarsherlock.app.ActionBar.Tab; -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 android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.Window; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.EventDistributor; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.fragment.EpisodesFragment; import de.danoeh.antennapod.fragment.ExternalPlayerFragment; import de.danoeh.antennapod.fragment.FeedlistFragment; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.service.PlaybackService; import de.danoeh.antennapod.service.download.DownloadService; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.StorageUtils; /** The activity that is shown when the user launches the app. */ -public class MainActivity extends SherlockFragmentActivity { +public class MainActivity extends ActionBarActivity { private static final String TAG = "MainActivity"; private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED | EventDistributor.DOWNLOAD_QUEUED; - private FeedManager manager; private ViewPager viewpager; private TabsAdapter pagerAdapter; private ExternalPlayerFragment externalPlayerFragment; @@ -51,7 +49,6 @@ public class MainActivity extends SherlockFragmentActivity { setTheme(UserPreferences.getTheme()); super.onCreate(savedInstanceState); StorageUtils.checkStorageAvailability(this); - manager = FeedManager.getInstance(); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setContentView(R.layout.main); @@ -62,9 +59,9 @@ public class MainActivity extends SherlockFragmentActivity { viewpager.setAdapter(pagerAdapter); - Tab feedsTab = getSupportActionBar().newTab(); + ActionBar.Tab feedsTab = getSupportActionBar().newTab(); feedsTab.setText(R.string.podcasts_label); - Tab episodesTab = getSupportActionBar().newTab(); + ActionBar.Tab episodesTab = getSupportActionBar().newTab(); episodesTab.setText(R.string.episodes_label); pagerAdapter.addTab(feedsTab, FeedlistFragment.class, null); @@ -80,7 +77,7 @@ public class MainActivity extends SherlockFragmentActivity { if (!appLaunched && getIntent().getAction() != null && getIntent().getAction().equals(Intent.ACTION_MAIN)) { appLaunched = true; - if (manager.getUnreadItemsSize(true) > 0) { + if (DBReader.getNumberOfUnreadItems(this) > 0) { // select 'episodes' tab getSupportActionBar().setSelectedNavigationItem(1); } @@ -142,7 +139,7 @@ public class MainActivity extends SherlockFragmentActivity { startActivity(new Intent(this, AddFeedActivity.class)); return true; case R.id.all_feed_refresh: - manager.refreshAllFeeds(this); + DBTasks.refreshAllFeeds(this, null); return true; case R.id.show_downloads: startActivity(new Intent(this, DownloadActivity.class)); @@ -173,9 +170,6 @@ public class MainActivity extends SherlockFragmentActivity { } else { refreshAll.setVisible(true); } - - boolean hasFeeds = manager.getFeedsSize() > 0; - menu.findItem(R.id.all_feed_refresh).setVisible(hasFeeds); return true; } @@ -248,7 +242,7 @@ public class MainActivity extends SherlockFragmentActivity { } @Override - public void onTabSelected(Tab tab, FragmentTransaction ft) { + public void onTabSelected(ActionBar.Tab tab, FragmentTransaction ft) { Object tag = tab.getTag(); for (int i = 0; i < mTabs.size(); i++) { if (mTabs.get(i) == tag) { @@ -258,12 +252,12 @@ public class MainActivity extends SherlockFragmentActivity { } @Override - public void onTabUnselected(Tab tab, FragmentTransaction ft) { + public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction ft) { } @Override - public void onTabReselected(Tab tab, FragmentTransaction ft) { + public void onTabReselected(ActionBar.Tab tab, FragmentTransaction ft) { } } diff --git a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java index 16b03809a..b5b694995 100644 --- a/src/de/danoeh/antennapod/activity/MediaplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/MediaplayerActivity.java @@ -6,22 +6,20 @@ import android.content.Intent; import android.graphics.PixelFormat; import android.net.Uri; import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.SeekBar.OnSeekBarChangeListener; 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 de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.FlattrClickWorker; import de.danoeh.antennapod.dialog.TimeDialog; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.service.PlaybackService; import de.danoeh.antennapod.util.Converter; @@ -35,12 +33,10 @@ import de.danoeh.antennapod.util.playback.PlaybackController; * Provides general features which are both needed for playing audio and video * files. */ -public abstract class MediaplayerActivity extends SherlockFragmentActivity +public abstract class MediaplayerActivity extends ActionBarActivity implements OnSeekBarChangeListener { private static final String TAG = "MediaplayerActivity"; - protected FeedManager manager; - protected PlaybackController controller; protected TextView txtvPosition; @@ -149,7 +145,6 @@ public abstract class MediaplayerActivity extends SherlockFragmentActivity StorageUtils.checkStorageAvailability(this); orientation = getResources().getConfiguration().orientation; - manager = FeedManager.getInstance(); getWindow().setFormat(PixelFormat.TRANSPARENT); getSupportActionBar().setDisplayHomeAsUpEnabled(true); } diff --git a/src/de/danoeh/antennapod/activity/MiroGuideCategoryActivity.java b/src/de/danoeh/antennapod/activity/MiroGuideCategoryActivity.java index bb50944cc..c039e96f8 100644 --- a/src/de/danoeh/antennapod/activity/MiroGuideCategoryActivity.java +++ b/src/de/danoeh/antennapod/activity/MiroGuideCategoryActivity.java @@ -5,13 +5,11 @@ 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.support.v7.app.ActionBarActivity; import android.util.Log; -import com.actionbarsherlock.app.SherlockFragmentActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; -import com.viewpagerindicator.TabPageIndicator; - +import android.view.Menu; +import android.view.MenuItem; import de.danoeh.antennapod.R; import de.danoeh.antennapod.fragment.MiroGuideChannellistFragment; import de.danoeh.antennapod.preferences.UserPreferences; @@ -21,14 +19,13 @@ import de.danoeh.antennapod.preferences.UserPreferences; * activity uses MiroGuideChannelListFragments for these lists. If the user * selects a channel, the MiroGuideChannelViewActivity is started. */ -public class MiroGuideCategoryActivity extends SherlockFragmentActivity { +public class MiroGuideCategoryActivity extends ActionBarActivity { private static final String TAG = "MiroGuideCategoryActivity"; public static String EXTRA_CATEGORY = "category"; private ViewPager viewpager; private CategoryPagerAdapter pagerAdapter; - private TabPageIndicator tabs; private String category; @@ -40,14 +37,12 @@ public class MiroGuideCategoryActivity extends SherlockFragmentActivity { setContentView(R.layout.miroguide_category); viewpager = (ViewPager) findViewById(R.id.viewpager); - tabs = (TabPageIndicator) findViewById(R.id.tabs); category = getIntent().getStringExtra(EXTRA_CATEGORY); if (category != null) { getSupportActionBar().setTitle(category); pagerAdapter = new CategoryPagerAdapter(getSupportFragmentManager()); viewpager.setAdapter(pagerAdapter); - tabs.setViewPager(viewpager); } else { Log.e(TAG, "Activity was started with invalid arguments"); } diff --git a/src/de/danoeh/antennapod/activity/MiroGuideChannelViewActivity.java b/src/de/danoeh/antennapod/activity/MiroGuideChannelViewActivity.java index f9fe912cd..d4b4597f2 100644 --- a/src/de/danoeh/antennapod/activity/MiroGuideChannelViewActivity.java +++ b/src/de/danoeh/antennapod/activity/MiroGuideChannelViewActivity.java @@ -1,13 +1,18 @@ package de.danoeh.antennapod.activity; import java.util.Date; +import java.util.List; import android.annotation.SuppressLint; import android.content.Intent; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; import android.util.Log; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.widget.ListView; import android.widget.ProgressBar; @@ -15,21 +20,16 @@ import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; -import com.actionbarsherlock.app.SherlockActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.MiroGuideItemlistAdapter; import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.miroguide.conn.MiroGuideException; import de.danoeh.antennapod.miroguide.conn.MiroGuideService; import de.danoeh.antennapod.miroguide.model.MiroGuideChannel; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.storage.DownloadRequester; @@ -37,147 +37,164 @@ import de.danoeh.antennapod.storage.DownloadRequester; * Displays information about one channel and lets the user add this channel to * his library. */ -public class MiroGuideChannelViewActivity extends SherlockActivity { +public class MiroGuideChannelViewActivity extends ActionBarActivity { private static final String TAG = "MiroGuideChannelViewActivity"; - public static final String EXTRA_CHANNEL_ID = "id"; - public static final String EXTRA_CHANNEL_URL = "url"; - - private RelativeLayout layoutContent; - private ProgressBar progLoading; - private TextView txtvTitle; - private TextView txtVDescription; - private ListView listEntries; - - private long channelId; - private String channelUrl; - private MiroGuideChannel channel; - - @Override - protected void onPause() { - super.onPause(); - channelLoader.cancel(true); - } - - @SuppressLint("NewApi") - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setContentView(R.layout.miroguide_channelview); - - layoutContent = (RelativeLayout) findViewById(R.id.layout_content); - progLoading = (ProgressBar) findViewById(R.id.progLoading); - txtvTitle = (TextView) findViewById(R.id.txtvTitle); - txtVDescription = (TextView) findViewById(R.id.txtvDescription); - listEntries = (ListView) findViewById(R.id.itemlist); - - channelId = getIntent().getLongExtra(EXTRA_CHANNEL_ID, -1); - channelUrl = getIntent().getStringExtra(EXTRA_CHANNEL_URL); - - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - channelLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - channelLoader.execute(); - } - - } - - /** Is used to load channel information asynchronously. */ - private AsyncTask<Void, Void, Void> channelLoader = new AsyncTask<Void, Void, Void>() { - private static final String TAG = "ChannelLoader"; - private Exception exception; - - @Override - protected Void doInBackground(Void... params) { - if (AppConfig.DEBUG) - Log.d(TAG, "Starting background task"); - MiroGuideService service = new MiroGuideService(); - try { - channel = service.getChannel(channelId); - } catch (MiroGuideException e) { - e.printStackTrace(); - exception = e; - } - return null; - } - - @SuppressLint("NewApi") - @Override - protected void onPostExecute(Void result) { - if (AppConfig.DEBUG) - Log.d(TAG, "Loading finished"); - if (exception == null) { - txtvTitle.setText(channel.getName()); - txtVDescription.setText(channel.getDescription()); - - MiroGuideItemlistAdapter listAdapter = new MiroGuideItemlistAdapter( - MiroGuideChannelViewActivity.this, 0, - channel.getItems()); - listEntries.setAdapter(listAdapter); - progLoading.setVisibility(View.GONE); - layoutContent.setVisibility(View.VISIBLE); - supportInvalidateOptionsMenu(); - } else { - finish(); - } - } - - }; - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - MenuInflater inflater = new MenuInflater(this); - inflater.inflate(R.menu.channelview, menu); - return true; - } - - @Override - public boolean onPrepareOptionsMenu(Menu menu) { - boolean channelLoaded = channel != null; - boolean beingDownloaded = channelLoaded - && DownloadRequester.getInstance().isDownloadingFile( - channel.getDownloadUrl()); - boolean notAdded = channelLoaded - && !((FeedManager.getInstance().feedExists( - channel.getDownloadUrl()) || beingDownloaded)); - menu.findItem(R.id.add_feed).setVisible(notAdded); - menu.findItem(R.id.visit_website_item).setVisible( - channelLoaded && channel.getWebsiteUrl() != null); - return true; - } - - @SuppressLint("NewApi") - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - return true; - case R.id.visit_website_item: - Uri uri = Uri.parse(channel.getWebsiteUrl()); - startActivity(new Intent(Intent.ACTION_VIEW, uri)); - return true; - case R.id.add_feed: - try { - DownloadRequester.getInstance().downloadFeed( - this, - new Feed(channel.getDownloadUrl(), new Date(), channel - .getName())); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DownloadRequestErrorDialogCreator.newRequestErrorDialog(this, - e.getMessage()); - } - Toast toast = Toast.makeText(this, R.string.miro_feed_added, - Toast.LENGTH_LONG); - toast.show(); - supportInvalidateOptionsMenu(); - return true; - default: - return false; - } - } + public static final String EXTRA_CHANNEL_ID = "id"; + public static final String EXTRA_CHANNEL_URL = "url"; + + private RelativeLayout layoutContent; + private ProgressBar progLoading; + private TextView txtvTitle; + private TextView txtVDescription; + private ListView listEntries; + + private long channelId; + private String channelUrl; + private MiroGuideChannel channel; + private volatile List<Feed> feeds; + + @Override + protected void onPause() { + super.onPause(); + channelLoader.cancel(true); + } + + @SuppressLint("NewApi") + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.miroguide_channelview); + + layoutContent = (RelativeLayout) findViewById(R.id.layout_content); + progLoading = (ProgressBar) findViewById(R.id.progLoading); + txtvTitle = (TextView) findViewById(R.id.txtvTitle); + txtVDescription = (TextView) findViewById(R.id.txtvDescription); + listEntries = (ListView) findViewById(R.id.itemlist); + + channelId = getIntent().getLongExtra(EXTRA_CHANNEL_ID, -1); + channelUrl = getIntent().getStringExtra(EXTRA_CHANNEL_URL); + + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + channelLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + channelLoader.execute(); + } + + } + + /** + * Is used to load channel information asynchronously. + */ + private AsyncTask<Void, Void, Void> channelLoader = new AsyncTask<Void, Void, Void>() { + private static final String TAG = "ChannelLoader"; + private Exception exception; + + @Override + protected Void doInBackground(Void... params) { + if (AppConfig.DEBUG) + Log.d(TAG, "Starting background task"); + feeds = DBReader.getFeedList(MiroGuideChannelViewActivity.this); + MiroGuideService service = new MiroGuideService(); + try { + channel = service.getChannel(channelId); + } catch (MiroGuideException e) { + e.printStackTrace(); + exception = e; + } + return null; + } + + @SuppressLint("NewApi") + @Override + protected void onPostExecute(Void result) { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading finished"); + if (exception == null) { + txtvTitle.setText(channel.getName()); + txtVDescription.setText(channel.getDescription()); + + MiroGuideItemlistAdapter listAdapter = new MiroGuideItemlistAdapter( + MiroGuideChannelViewActivity.this, 0, + channel.getItems()); + listEntries.setAdapter(listAdapter); + progLoading.setVisibility(View.GONE); + layoutContent.setVisibility(View.VISIBLE); + supportInvalidateOptionsMenu(); + } else { + finish(); + } + } + + }; + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = new MenuInflater(this); + inflater.inflate(R.menu.channelview, menu); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + boolean channelLoaded = channel != null; + boolean beingDownloaded = channelLoaded + && DownloadRequester.getInstance().isDownloadingFile( + channel.getDownloadUrl()); + boolean notAdded = channelLoaded + && !((feedExists( + channel.getDownloadUrl()) || beingDownloaded)); + menu.findItem(R.id.add_feed).setVisible(notAdded); + menu.findItem(R.id.visit_website_item).setVisible( + channelLoaded && channel.getWebsiteUrl() != null); + return true; + } + + @SuppressLint("NewApi") + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + case R.id.visit_website_item: + Uri uri = Uri.parse(channel.getWebsiteUrl()); + startActivity(new Intent(Intent.ACTION_VIEW, uri)); + return true; + case R.id.add_feed: + try { + DownloadRequester.getInstance().downloadFeed( + this, + new Feed(channel.getDownloadUrl(), new Date(), channel + .getName())); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog(this, + e.getMessage()); + } + Toast toast = Toast.makeText(this, R.string.miro_feed_added, + Toast.LENGTH_LONG); + toast.show(); + supportInvalidateOptionsMenu(); + return true; + default: + return false; + } + } + + private boolean feedExists(String downloadUrl) { + if (feeds == null) { + return false; + } + + for (Feed feed : feeds) { + if (feed.getDownload_url().equals(downloadUrl)) { + return true; + } + } + return false; + } } diff --git a/src/de/danoeh/antennapod/activity/MiroGuideMainActivity.java b/src/de/danoeh/antennapod/activity/MiroGuideMainActivity.java index 8b33ef1da..99da9944f 100644 --- a/src/de/danoeh/antennapod/activity/MiroGuideMainActivity.java +++ b/src/de/danoeh/antennapod/activity/MiroGuideMainActivity.java @@ -4,16 +4,17 @@ import android.annotation.SuppressLint; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; +import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.TextView; -import com.actionbarsherlock.app.SherlockListActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.miroguide.conn.MiroGuideException; @@ -24,132 +25,133 @@ import de.danoeh.antennapod.preferences.UserPreferences; * Shows a list of available categories and offers a search button. If the user * selects a category, the MiroGuideCategoryActivity is started. */ -public class MiroGuideMainActivity extends SherlockListActivity { - private static final String TAG = "MiroGuideMainActivity"; - - private static String[] categories; - private ArrayAdapter<String> listAdapter; - - private TextView txtvStatus; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setContentView(R.layout.miroguide_categorylist); - - txtvStatus = (TextView) findViewById(android.R.id.empty); - } - - @Override - protected void onPause() { - super.onPause(); - } - - @Override - protected void onResume() { - super.onResume(); - if (categories != null) { - createAdapter(); - } else { - loadCategories(); - } - } - - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - super.onListItemClick(l, v, position, id); - String selection = listAdapter.getItem(position); - Intent launchIntent = new Intent(this, MiroGuideCategoryActivity.class); - launchIntent.putExtra(MiroGuideCategoryActivity.EXTRA_CATEGORY, - selection); - startActivity(launchIntent); - } - - private void createAdapter() { - if (categories != null) { - listAdapter = new ArrayAdapter<String>(this, - android.R.layout.simple_list_item_1, categories); - txtvStatus.setText(R.string.no_items_label); - setListAdapter(listAdapter); - } - } - - /** - * Launches an AsyncTask to load the available categories in the background. - */ - @SuppressLint("NewApi") - private void loadCategories() { - AsyncTask<Void, Void, Void> listLoader = new AsyncTask<Void, Void, Void>() { - - private String[] c; - private MiroGuideException exception; - - @Override - protected void onPostExecute(Void result) { - if (exception == null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Successfully loaded categories"); - categories = c; - createAdapter(); - } else { - Log.e(TAG, "Error happened while trying to load categories"); - txtvStatus.setText(exception.getMessage()); - } - } - - @Override - protected void onPreExecute() { - txtvStatus.setText(R.string.loading_categories_label); - } - - @Override - protected Void doInBackground(Void... params) { - MiroGuideService service = new MiroGuideService(); - try { - c = service.getCategories(); - } catch (MiroGuideException e) { - e.printStackTrace(); - exception = e; - } finally { - service.close(); - } - return null; - } - - }; - - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - listLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - listLoader.execute(); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label) - .setIcon( - obtainStyledAttributes( - new int[] { R.attr.action_search }) - .getDrawable(0)) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - return true; - case R.id.search_item: - onSearchRequested(); - return true; - default: - return false; - } - } - +public class MiroGuideMainActivity extends ActionBarActivity implements AdapterView.OnItemClickListener { + private static final String TAG = "MiroGuideMainActivity"; + + private static String[] categories; + private ArrayAdapter<String> listAdapter; + + private TextView txtvStatus; + private ListView listView; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.miroguide_categorylist); + + txtvStatus = (TextView) findViewById(android.R.id.empty); + listView = (ListView) findViewById(android.R.id.list); + listView.setOnItemClickListener(this); + } + + @Override + protected void onPause() { + super.onPause(); + } + + @Override + protected void onResume() { + super.onResume(); + if (categories != null) { + createAdapter(); + } else { + loadCategories(); + } + } + + private void createAdapter() { + if (categories != null) { + listAdapter = new ArrayAdapter<String>(this, + android.R.layout.simple_list_item_1, categories); + txtvStatus.setText(R.string.no_items_label); + listView.setAdapter(listAdapter); + } + } + + /** + * Launches an AsyncTask to load the available categories in the background. + */ + @SuppressLint("NewApi") + private void loadCategories() { + AsyncTask<Void, Void, Void> listLoader = new AsyncTask<Void, Void, Void>() { + + private String[] c; + private MiroGuideException exception; + + @Override + protected void onPostExecute(Void result) { + if (exception == null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Successfully loaded categories"); + categories = c; + createAdapter(); + } else { + Log.e(TAG, "Error happened while trying to load categories"); + txtvStatus.setText(exception.getMessage()); + } + } + + @Override + protected void onPreExecute() { + txtvStatus.setText(R.string.loading_categories_label); + } + + @Override + protected Void doInBackground(Void... params) { + MiroGuideService service = new MiroGuideService(); + try { + c = service.getCategories(); + } catch (MiroGuideException e) { + e.printStackTrace(); + exception = e; + } finally { + service.close(); + } + return null; + } + + }; + + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + listLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + listLoader.execute(); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label) + .setIcon( + obtainStyledAttributes( + new int[]{R.attr.action_search}) + .getDrawable(0)), + MenuItem.SHOW_AS_ACTION_IF_ROOM); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + case R.id.search_item: + onSearchRequested(); + return true; + default: + return false; + } + } + + @Override + public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) { + String selection = listAdapter.getItem(position); + Intent launchIntent = new Intent(this, MiroGuideCategoryActivity.class); + launchIntent.putExtra(MiroGuideCategoryActivity.EXTRA_CATEGORY, + selection); + startActivity(launchIntent); + } } diff --git a/src/de/danoeh/antennapod/activity/MiroGuideSearchActivity.java b/src/de/danoeh/antennapod/activity/MiroGuideSearchActivity.java index a30777fb1..4ea0b1699 100644 --- a/src/de/danoeh/antennapod/activity/MiroGuideSearchActivity.java +++ b/src/de/danoeh/antennapod/activity/MiroGuideSearchActivity.java @@ -4,12 +4,12 @@ import android.app.SearchManager; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; import android.util.Log; -import com.actionbarsherlock.app.SherlockFragmentActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; - +import android.view.Menu; +import android.view.MenuItem; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.fragment.MiroGuideChannellistFragment; @@ -19,72 +19,72 @@ import de.danoeh.antennapod.preferences.UserPreferences; * Displays results when a search for miroguide channels has been performed. It * uses a MiroGuideChannelListFragment to display the results. */ -public class MiroGuideSearchActivity extends SherlockFragmentActivity { - private static final String TAG = "MiroGuideSearchActivity"; +public class MiroGuideSearchActivity extends ActionBarActivity { + private static final String TAG = "MiroGuideSearchActivity"; - private MiroGuideChannellistFragment listFragment; + private MiroGuideChannellistFragment listFragment; - @Override - protected void onCreate(Bundle arg0) { - setTheme(UserPreferences.getTheme()); - super.onCreate(arg0); - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setContentView(R.layout.miroguidesearch); - } + @Override + protected void onCreate(Bundle arg0) { + setTheme(UserPreferences.getTheme()); + super.onCreate(arg0); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.miroguidesearch); + } - @Override - protected void onResume() { - super.onResume(); - Intent intent = getIntent(); - if (Intent.ACTION_SEARCH.equals(intent.getAction())) { - String query = intent.getStringExtra(SearchManager.QUERY); - getSupportActionBar() - .setSubtitle( - getString(R.string.search_term_label) + "\"" - + query + "\""); - handleSearchRequest(query); - } - } + @Override + protected void onResume() { + super.onResume(); + Intent intent = getIntent(); + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + String query = intent.getStringExtra(SearchManager.QUERY); + getSupportActionBar() + .setSubtitle( + getString(R.string.search_term_label) + "\"" + + query + "\""); + handleSearchRequest(query); + } + } - private void handleSearchRequest(String query) { - if (AppConfig.DEBUG) - Log.d(TAG, "Performing search"); - FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); - listFragment = MiroGuideChannellistFragment.newInstance("name", query, - "name"); - ft.replace(R.id.channellistFragment, listFragment); - ft.commit(); - } + private void handleSearchRequest(String query) { + if (AppConfig.DEBUG) + Log.d(TAG, "Performing search"); + FragmentTransaction ft = getSupportFragmentManager().beginTransaction(); + listFragment = MiroGuideChannellistFragment.newInstance("name", query, + "name"); + ft.replace(R.id.channellistFragment, listFragment); + ft.commit(); + } - @Override - protected void onNewIntent(Intent intent) { - setIntent(intent); - } + @Override + protected void onNewIntent(Intent intent) { + setIntent(intent); + } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label) - .setIcon( - obtainStyledAttributes( - new int[] { R.attr.action_search }) - .getDrawable(0)) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label) + .setIcon( + obtainStyledAttributes( + new int[]{R.attr.action_search}) + .getDrawable(0)), + MenuItem.SHOW_AS_ACTION_IF_ROOM); - return true; - } + return true; + } - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - finish(); - return true; - case R.id.search_item: - onSearchRequested(); - return true; - default: - return false; - } - } + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + finish(); + return true; + case R.id.search_item: + onSearchRequested(); + return true; + default: + return false; + } + } } diff --git a/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java b/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java index d6ff537a3..cb1c66cab 100644 --- a/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java +++ b/src/de/danoeh/antennapod/activity/OnlineFeedViewActivity.java @@ -6,6 +6,7 @@ import java.util.Date; import javax.xml.parsers.ParserConfigurationException; +import android.support.v7.app.ActionBarActivity; import org.xml.sax.SAXException; import android.app.AlertDialog; @@ -17,13 +18,12 @@ import android.view.Gravity; import android.widget.LinearLayout; import android.widget.ProgressBar; -import com.actionbarsherlock.app.SherlockFragmentActivity; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.DownloadStatus; import de.danoeh.antennapod.feed.Feed; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.service.download.DownloadRequest; +import de.danoeh.antennapod.service.download.DownloadStatus; import de.danoeh.antennapod.service.download.Downloader; import de.danoeh.antennapod.service.download.DownloaderCallback; import de.danoeh.antennapod.service.download.HttpDownloader; @@ -42,10 +42,10 @@ import de.danoeh.antennapod.util.URLChecker; * If the feed cannot be downloaded or parsed, an error dialog will be displayed * and the activity will finish as soon as the error dialog is closed. */ -public abstract class OnlineFeedViewActivity extends SherlockFragmentActivity { +public abstract class OnlineFeedViewActivity extends ActionBarActivity { private static final String TAG = "OnlineFeedViewActivity"; private static final String ARG_FEEDURL = "arg.feedurl"; - + public static final int RESULT_ERROR = 2; private Feed feed; @@ -70,7 +70,7 @@ public abstract class OnlineFeedViewActivity extends SherlockFragmentActivity { @Override protected void onStop() { super.onStop(); - if (downloader != null && downloader.getStatus().isDone() == false) { + if (downloader != null && !downloader.isFinished()) { downloader.cancel(); } } @@ -82,15 +82,14 @@ public abstract class OnlineFeedViewActivity extends SherlockFragmentActivity { @Override public void run() { - DownloadStatus status = downloader.getStatus(); + DownloadStatus status = downloader.getResult(); if (status != null) { if (!status.isCancelled()) { if (status.isSuccessful()) { parseFeed(); } else { - String errorMsg = DownloadError.getErrorString( - OnlineFeedViewActivity.this, - status.getReason()); + String errorMsg = status.getReason().getErrorString( + OnlineFeedViewActivity.this); if (errorMsg != null && status.getReasonDetailed() != null) { errorMsg += " (" @@ -119,10 +118,13 @@ public abstract class OnlineFeedViewActivity extends SherlockFragmentActivity { FileNameGenerator.generateFileName(feed.getDownload_url())) .toString(); feed.setFile_url(fileUrl); - DownloadStatus status = new DownloadStatus(feed, "OnlineFeed"); + DownloadRequest request = new DownloadRequest(feed.getFile_url(), + feed.getDownload_url(), "OnlineFeed", 0, Feed.FEEDFILETYPE_FEED); + /* TODO update HttpDownloader httpDownloader = new HttpDownloader(downloaderCallback, - status); + request); httpDownloader.start(); + */ } /** Displays a progress indicator. */ @@ -187,9 +189,9 @@ public abstract class OnlineFeedViewActivity extends SherlockFragmentActivity { } }); } else { - final String errorMsg = DownloadError.getErrorString( - OnlineFeedViewActivity.this, - DownloadError.ERROR_PARSER_EXCEPTION) + final String errorMsg = + DownloadError.ERROR_PARSER_EXCEPTION.getErrorString( + OnlineFeedViewActivity.this) + " (" + reasonDetailed + ")"; runOnUiThread(new Runnable() { @@ -217,13 +219,14 @@ public abstract class OnlineFeedViewActivity extends SherlockFragmentActivity { } else { builder.setMessage(R.string.error_msg_prefix); } - builder.setNeutralButton(android.R.string.ok, new DialogInterface.OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - dialog.cancel(); - } - }); + builder.setNeutralButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); builder.setOnCancelListener(new OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { diff --git a/src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java b/src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java index 9ba355baf..2ec42e9ef 100644 --- a/src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java +++ b/src/de/danoeh/antennapod/activity/OpmlFeedChooserActivity.java @@ -5,17 +5,17 @@ import java.util.List; import android.content.Intent; import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; import android.util.SparseBooleanArray; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ListView; -import com.actionbarsherlock.app.SherlockActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; - import de.danoeh.antennapod.R; import de.danoeh.antennapod.opml.OpmlElement; import de.danoeh.antennapod.preferences.UserPreferences; @@ -24,110 +24,111 @@ import de.danoeh.antennapod.preferences.UserPreferences; * Displays the feeds that the OPML-Importer has read and lets the user choose * which feeds he wants to import. */ -public class OpmlFeedChooserActivity extends SherlockActivity { - private static final String TAG = "OpmlFeedChooserActivity"; - - public static final String EXTRA_SELECTED_ITEMS = "de.danoeh.antennapod.selectedItems"; - - private Button butConfirm; - private Button butCancel; - private ListView feedlist; - private ArrayAdapter<String> listAdapter; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - - setContentView(R.layout.opml_selection); - butConfirm = (Button) findViewById(R.id.butConfirm); - butCancel = (Button) findViewById(R.id.butCancel); - feedlist = (ListView) findViewById(R.id.feedlist); - - feedlist.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); - listAdapter = new ArrayAdapter<String>(this, - android.R.layout.simple_list_item_multiple_choice, - getTitleList()); - - feedlist.setAdapter(listAdapter); - - butCancel.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - setResult(RESULT_CANCELED); - finish(); - } - }); - - butConfirm.setOnClickListener(new OnClickListener() { - - @Override - public void onClick(View v) { - Intent intent = new Intent(); - SparseBooleanArray checked = feedlist.getCheckedItemPositions(); - - int checkedCount = 0; - // Get number of checked items - for (int i = 0; i < checked.size(); i++) { - if (checked.valueAt(i)) { - checkedCount++; - } - } - int[] selection = new int[checkedCount]; - for (int i = 0, collected = 0; collected < checkedCount; i++) { - if (checked.valueAt(i)) { - selection[collected] = checked.keyAt(i); - collected++; - } - } - intent.putExtra(EXTRA_SELECTED_ITEMS, selection); - setResult(RESULT_OK, intent); - finish(); - } - }); - - } - - private List<String> getTitleList() { - List<String> result = new ArrayList<String>(); - if (OpmlImportHolder.getReadElements() != null) { - for (OpmlElement element : OpmlImportHolder.getReadElements()) { - result.add(element.getText()); - } - - } - return result; - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - menu.add(Menu.NONE, R.id.select_all_item, Menu.NONE, - R.string.select_all_label).setShowAsAction( - MenuItem.SHOW_AS_ACTION_IF_ROOM); - menu.add(Menu.NONE, R.id.deselect_all_item, Menu.NONE, - R.string.deselect_all_label).setShowAsAction( - MenuItem.SHOW_AS_ACTION_IF_ROOM); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.select_all_item: - selectAllItems(true); - return true; - case R.id.deselect_all_item: - selectAllItems(false); - return true; - default: - return false; - } - } - - private void selectAllItems(boolean b) { - for (int i = 0; i < feedlist.getCount(); i++) { - feedlist.setItemChecked(i, b); - } - } +public class OpmlFeedChooserActivity extends ActionBarActivity { + private static final String TAG = "OpmlFeedChooserActivity"; + + public static final String EXTRA_SELECTED_ITEMS = "de.danoeh.antennapod.selectedItems"; + + private Button butConfirm; + private Button butCancel; + private ListView feedlist; + private ArrayAdapter<String> listAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + + setContentView(R.layout.opml_selection); + butConfirm = (Button) findViewById(R.id.butConfirm); + butCancel = (Button) findViewById(R.id.butCancel); + feedlist = (ListView) findViewById(R.id.feedlist); + + feedlist.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + listAdapter = new ArrayAdapter<String>(this, + android.R.layout.simple_list_item_multiple_choice, + getTitleList()); + + feedlist.setAdapter(listAdapter); + + butCancel.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + setResult(RESULT_CANCELED); + finish(); + } + }); + + butConfirm.setOnClickListener(new OnClickListener() { + + @Override + public void onClick(View v) { + Intent intent = new Intent(); + SparseBooleanArray checked = feedlist.getCheckedItemPositions(); + + int checkedCount = 0; + // Get number of checked items + for (int i = 0; i < checked.size(); i++) { + if (checked.valueAt(i)) { + checkedCount++; + } + } + int[] selection = new int[checkedCount]; + for (int i = 0, collected = 0; collected < checkedCount; i++) { + if (checked.valueAt(i)) { + selection[collected] = checked.keyAt(i); + collected++; + } + } + intent.putExtra(EXTRA_SELECTED_ITEMS, selection); + setResult(RESULT_OK, intent); + finish(); + } + }); + + } + + private List<String> getTitleList() { + List<String> result = new ArrayList<String>(); + if (OpmlImportHolder.getReadElements() != null) { + for (OpmlElement element : OpmlImportHolder.getReadElements()) { + result.add(element.getText()); + } + + } + return result; + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.select_all_item, Menu.NONE, + R.string.select_all_label), + MenuItem.SHOW_AS_ACTION_IF_ROOM); + + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.deselect_all_item, Menu.NONE, + R.string.deselect_all_label), + MenuItem.SHOW_AS_ACTION_IF_ROOM); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.select_all_item: + selectAllItems(true); + return true; + case R.id.deselect_all_item: + selectAllItems(false); + return true; + default: + return false; + } + } + + private void selectAllItems(boolean b) { + for (int i = 0; i < feedlist.getCount(); i++) { + feedlist.setItemChecked(i, b); + } + } } diff --git a/src/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java b/src/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java index f887fdd94..905183aa2 100644 --- a/src/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java +++ b/src/de/danoeh/antennapod/activity/OpmlImportBaseActivity.java @@ -4,10 +4,9 @@ import java.io.Reader; import java.util.ArrayList; import android.content.Intent; +import android.support.v7.app.ActionBarActivity; import android.util.Log; -import com.actionbarsherlock.app.SherlockActivity; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.asynctask.OpmlFeedQueuer; import de.danoeh.antennapod.asynctask.OpmlImportWorker; @@ -16,7 +15,7 @@ import de.danoeh.antennapod.opml.OpmlElement; /** * Base activity for Opml Import - e.g. with code what to do afterwards * */ -public class OpmlImportBaseActivity extends SherlockActivity { +public class OpmlImportBaseActivity extends ActionBarActivity { private static final String TAG = "OpmlImportBaseActivity"; private OpmlImportWorker importWorker; diff --git a/src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java b/src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java index b38e0c443..259689abf 100644 --- a/src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java +++ b/src/de/danoeh/antennapod/activity/OpmlImportFromPathActivity.java @@ -9,15 +9,14 @@ import android.app.AlertDialog; import android.content.DialogInterface; import android.os.Bundle; import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.TextView; import android.widget.Toast; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.preferences.UserPreferences; diff --git a/src/de/danoeh/antennapod/activity/OrganizeQueueActivity.java b/src/de/danoeh/antennapod/activity/OrganizeQueueActivity.java index 7269f7549..77dae303e 100644 --- a/src/de/danoeh/antennapod/activity/OrganizeQueueActivity.java +++ b/src/de/danoeh/antennapod/activity/OrganizeQueueActivity.java @@ -1,10 +1,13 @@ package de.danoeh.antennapod.activity; import android.content.Context; -import android.content.res.TypedArray; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.support.v7.app.ActionBarActivity; +import android.view.*; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -12,28 +15,32 @@ import android.widget.BaseAdapter; import android.widget.ImageView; import android.widget.TextView; -import com.actionbarsherlock.app.SherlockListActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; import com.mobeta.android.dslv.DragSortListView; - import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.DBTasks; +import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.util.UndoBarController; -public class OrganizeQueueActivity extends SherlockListActivity implements +import java.util.List; + +public class OrganizeQueueActivity extends ActionBarActivity implements UndoBarController.UndoListener { private static final String TAG = "OrganizeQueueActivity"; private static final int MENU_ID_ACCEPT = 2; + private List<FeedItem> queue; + private OrganizeAdapter adapter; private UndoBarController undoBarController; + private DragSortListView listView; + @Override protected void onCreate(Bundle savedInstanceState) { setTheme(UserPreferences.getTheme()); @@ -41,17 +48,41 @@ public class OrganizeQueueActivity extends SherlockListActivity implements setContentView(R.layout.organize_queue); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - DragSortListView listView = (DragSortListView) getListView(); + listView = (DragSortListView) findViewById(android.R.id.list); listView.setDropListener(dropListener); listView.setRemoveListener(removeListener); - adapter = new OrganizeAdapter(this); - setListAdapter(adapter); - + loadData(); undoBarController = new UndoBarController(findViewById(R.id.undobar), this); } + private void loadData() { + AsyncTask<Void, Void, List<FeedItem>> loadTask = new AsyncTask<Void, Void, List<FeedItem>>() { + + @Override + protected List<FeedItem> doInBackground(Void... voids) { + return DBReader.getQueue(OrganizeQueueActivity.this); + } + + @Override + protected void onPostExecute(List<FeedItem> feedItems) { + super.onPostExecute(feedItems); + if (feedItems != null) { + queue = feedItems; + if (adapter == null) { + adapter = new OrganizeAdapter(OrganizeQueueActivity.this); + listView.setAdapter(adapter); + } + adapter.notifyDataSetChanged(); + } else { + Log.e(TAG, "Queue was null"); + } + } + }; + loadTask.execute(); + } + @Override protected void onPause() { super.onPause(); @@ -61,8 +92,7 @@ public class OrganizeQueueActivity extends SherlockListActivity implements @Override protected void onStop() { super.onStop(); - FeedManager.getInstance().autodownloadUndownloadedItems( - getApplicationContext()); + DBTasks.autodownloadUndownloadedItems(getApplicationContext()); } @Override @@ -76,9 +106,7 @@ public class OrganizeQueueActivity extends SherlockListActivity implements @Override public void update(EventDistributor eventDistributor, Integer arg) { if (((EventDistributor.QUEUE_UPDATE | EventDistributor.FEED_LIST_UPDATE) & arg) != 0) { - if (adapter != null) { - adapter.notifyDataSetChanged(); - } + loadData(); } } }; @@ -87,9 +115,10 @@ public class OrganizeQueueActivity extends SherlockListActivity implements @Override public void drop(int from, int to) { - FeedManager manager = FeedManager.getInstance(); - manager.moveQueueItem(OrganizeQueueActivity.this, from, to, false); - adapter.notifyDataSetChanged(); + final FeedItem item = queue.remove(from); + queue.add(to, item); + adapter.notifyDataSetChanged(); + DBWriter.moveQueueItem(OrganizeQueueActivity.this, from, to, true); } }; @@ -97,9 +126,8 @@ public class OrganizeQueueActivity extends SherlockListActivity implements @Override public void remove(int which) { - FeedManager manager = FeedManager.getInstance(); - FeedItem item = (FeedItem) getListAdapter().getItem(which); - manager.removeQueueItem(OrganizeQueueActivity.this, item, false); + FeedItem item = (FeedItem) listView.getAdapter().getItem(which); + DBWriter.removeQueueItem(OrganizeQueueActivity.this, item.getId(), true); undoBarController.showUndoBar(false, getString(R.string.removed_from_queue), new UndoToken(item, which)); @@ -127,22 +155,18 @@ public class OrganizeQueueActivity extends SherlockListActivity implements public void onUndo(Parcelable token) { // Perform the undo UndoToken undoToken = (UndoToken) token; - FeedItem feedItem = undoToken.getFeedItem(); + long itemId = undoToken.getFeedItemId(); int position = undoToken.getPosition(); - - FeedManager manager = FeedManager.getInstance(); - manager.addQueueItemAt(OrganizeQueueActivity.this, feedItem, position, - false); + DBWriter.addQueueItemAt(OrganizeQueueActivity.this, itemId, position, false); } private static class OrganizeAdapter extends BaseAdapter { - private Context context; - private FeedManager manager = FeedManager.getInstance(); + private OrganizeQueueActivity organizeQueueActivity; - public OrganizeAdapter(Context context) { + public OrganizeAdapter(OrganizeQueueActivity organizeQueueActivity) { super(); - this.context = context; + this.organizeQueueActivity = organizeQueueActivity; } @Override @@ -152,7 +176,7 @@ public class OrganizeQueueActivity extends SherlockListActivity implements if (convertView == null) { holder = new Holder(); - LayoutInflater inflater = (LayoutInflater) context + LayoutInflater inflater = (LayoutInflater) organizeQueueActivity .getSystemService(Context.LAYOUT_INFLATER_SERVICE); convertView = inflater.inflate( R.layout.organize_queue_listitem, null); @@ -189,13 +213,20 @@ public class OrganizeQueueActivity extends SherlockListActivity implements @Override public int getCount() { - int queueSize = manager.getQueueSize(true); - return queueSize; + if (organizeQueueActivity.queue != null) { + return organizeQueueActivity.queue.size(); + } else { + return 0; + } } @Override public FeedItem getItem(int position) { - return manager.getQueueItemAtIndex(position, true); + if (organizeQueueActivity.queue != null) { + return organizeQueueActivity.queue.get(position); + } else { + return null; + } } @Override @@ -211,7 +242,6 @@ public class OrganizeQueueActivity extends SherlockListActivity implements private int position; public UndoToken(FeedItem item, int position) { - FeedManager manager = FeedManager.getInstance(); this.itemId = item.getId(); this.feedId = item.getFeed().getId(); this.position = position; @@ -243,9 +273,8 @@ public class OrganizeQueueActivity extends SherlockListActivity implements out.writeInt(position); } - public FeedItem getFeedItem() { - FeedManager manager = FeedManager.getInstance(); - return manager.getFeedItem(itemId, feedId); + public long getFeedItemId() { + return itemId; } public int getPosition() { diff --git a/src/de/danoeh/antennapod/activity/PlaybackHistoryActivity.java b/src/de/danoeh/antennapod/activity/PlaybackHistoryActivity.java index 1a5a2cac9..f329581e5 100644 --- a/src/de/danoeh/antennapod/activity/PlaybackHistoryActivity.java +++ b/src/de/danoeh/antennapod/activity/PlaybackHistoryActivity.java @@ -3,25 +3,25 @@ package de.danoeh.antennapod.activity; import android.content.Intent; import android.os.Bundle; import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; import android.util.Log; -import com.actionbarsherlock.app.SherlockFragmentActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; - +import android.view.Menu; +import android.view.MenuItem; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.fragment.PlaybackHistoryFragment; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.storage.DBWriter; -public class PlaybackHistoryActivity extends SherlockFragmentActivity { +public class PlaybackHistoryActivity extends ActionBarActivity { private static final String TAG = "PlaybackHistoryActivity"; @Override public boolean onCreateOptionsMenu(Menu menu) { - menu.add(Menu.NONE, R.id.clear_history_item, Menu.NONE, - R.string.clear_history_label).setShowAsAction( + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.clear_history_item, Menu.NONE, + R.string.clear_history_label), MenuItem.SHOW_AS_ACTION_IF_ROOM); return true; } @@ -36,7 +36,7 @@ public class PlaybackHistoryActivity extends SherlockFragmentActivity { startActivity(intent); return true; case R.id.clear_history_item: - FeedManager.getInstance().clearPlaybackHistory(this); + DBWriter.clearPlaybackHistory(this); return true; } return super.onOptionsItemSelected(item); diff --git a/src/de/danoeh/antennapod/activity/PreferenceActivity.java b/src/de/danoeh/antennapod/activity/PreferenceActivity.java index 9fcf57ac2..880724c28 100644 --- a/src/de/danoeh/antennapod/activity/PreferenceActivity.java +++ b/src/de/danoeh/antennapod/activity/PreferenceActivity.java @@ -19,369 +19,358 @@ import android.preference.Preference.OnPreferenceClickListener; import android.preference.PreferenceScreen; import android.util.Log; -import com.actionbarsherlock.app.SherlockPreferenceActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; - +import android.view.Menu; +import android.view.MenuItem; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.FlattrClickWorker; import de.danoeh.antennapod.asynctask.OpmlExportWorker; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.util.flattr.FlattrUtils; -/** The main preference activity */ -public class PreferenceActivity extends SherlockPreferenceActivity { - private static final String TAG = "PreferenceActivity"; +/** + * The main preference activity + */ +public class PreferenceActivity extends android.preference.PreferenceActivity { + 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"; + private static final String PREF_OPML_EXPORT = "prefOpmlExport"; + private static final String PREF_ABOUT = "prefAbout"; + private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; + private static final String AUTO_DL_PREF_SCREEN = "prefAutoDownloadSettings"; - 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"; - private static final String PREF_OPML_EXPORT = "prefOpmlExport"; - private static final String PREF_ABOUT = "prefAbout"; - private static final String PREF_CHOOSE_DATA_DIR = "prefChooseDataDir"; - private static final String AUTO_DL_PREF_SCREEN = "prefAutoDownloadSettings"; + private CheckBoxPreference[] selectedNetworks; - private CheckBoxPreference[] selectedNetworks; + @SuppressWarnings("deprecation") + @Override + public void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); - @SuppressWarnings("deprecation") - @Override - public void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); + if (android.os.Build.VERSION.SDK_INT >= 11) { + getActionBar().setDisplayHomeAsUpEnabled(true); + } - getSupportActionBar().setDisplayHomeAsUpEnabled(true); addPreferencesFromResource(R.xml.preferences); findPreference(PREF_FLATTR_THIS_APP).setOnPreferenceClickListener( new OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - new FlattrClickWorker(PreferenceActivity.this, - FlattrUtils.APP_URL).executeAsync(); - - return true; - } - }); - - findPreference(PREF_FLATTR_REVOKE).setOnPreferenceClickListener( - new OnPreferenceClickListener() { - - @Override - public boolean onPreferenceClick(Preference preference) { - FlattrUtils.revokeAccessToken(PreferenceActivity.this); - checkItemVisibility(); - return true; - } - - }); - - findPreference(PREF_ABOUT).setOnPreferenceClickListener( - new OnPreferenceClickListener() { - - @Override - public boolean onPreferenceClick(Preference preference) { - PreferenceActivity.this.startActivity(new Intent( - PreferenceActivity.this, AboutActivity.class)); - return true; - } - - }); - - findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener( - new OnPreferenceClickListener() { - - @Override - public boolean onPreferenceClick(Preference preference) { - if (FeedManager.getInstance().getFeedsSize() > 0) { - new OpmlExportWorker(PreferenceActivity.this) - .executeAsync(); - } - return true; - } - }); - - findPreference(PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener( - new OnPreferenceClickListener() { - - @Override - public boolean onPreferenceClick(Preference preference) { - startActivityForResult( - new Intent(PreferenceActivity.this, - DirectoryChooserActivity.class), - DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED); - return true; - } - }); - findPreference(UserPreferences.PREF_THEME) - .setOnPreferenceChangeListener( - new OnPreferenceChangeListener() { - - @Override - public boolean onPreferenceChange( - Preference preference, Object newValue) { - Intent i = getIntent(); - i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK - | Intent.FLAG_ACTIVITY_NEW_TASK); - finish(); - startActivity(i); - return true; - } - }); - findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) - .setOnPreferenceChangeListener( - new OnPreferenceChangeListener() { - - @Override - public boolean onPreferenceChange( - Preference preference, Object newValue) { - if (newValue instanceof Boolean) { - setSelectedNetworksEnabled((Boolean) newValue); - return true; - } else { - return false; - } - } - }); - findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE) - .setOnPreferenceChangeListener( - new OnPreferenceChangeListener() { - - @Override - public boolean onPreferenceChange( - Preference preference, Object newValue) { - if (newValue instanceof String) { - setEpisodeCacheSizeText(Integer - .valueOf((String) newValue)); - } - return true; - } - }); - findPreference(UserPreferences.PREF_ENABLE_AUTODL) - .setOnPreferenceClickListener(new OnPreferenceClickListener() { - - @Override - public boolean onPreferenceClick(Preference preference) { - checkItemVisibility(); - return true; - } - }); - buildUpdateIntervalPreference(); - buildAutodownloadSelectedNetworsPreference(); - setSelectedNetworksEnabled(UserPreferences - .isEnableAutodownloadWifiFilter()); - - } - - private void buildUpdateIntervalPreference() { - ListPreference pref = (ListPreference) findPreference(UserPreferences.PREF_UPDATE_INTERVAL); - String[] values = getResources().getStringArray( - R.array.update_intervall_values); - String[] entries = new String[values.length]; - for (int x = 0; x < values.length; x++) { - Integer v = Integer.parseInt(values[x]); - switch (v) { - case 0: - entries[x] = getString(R.string.pref_update_interval_hours_manual); - break; - case 1: - entries[x] = v - + " " - + getString(R.string.pref_update_interval_hours_singular); - break; - default: - entries[x] = v + " " - + getString(R.string.pref_update_interval_hours_plural); - break; - - } - } - pref.setEntries(entries); - - } - - private void setSelectedNetworksEnabled(boolean b) { - if (selectedNetworks != null) { - for (Preference p : selectedNetworks) { - p.setEnabled(b); - } - } - } - - @Override - protected void onResume() { - super.onResume(); - checkItemVisibility(); - setEpisodeCacheSizeText(UserPreferences.getEpisodeCacheSize()); - setDataFolderText(); - } - - @SuppressWarnings("deprecation") - private void checkItemVisibility() { - - boolean hasFlattrToken = FlattrUtils.hasToken(); - - findPreference(PREF_FLATTR_AUTH).setEnabled(!hasFlattrToken); - findPreference(PREF_FLATTR_REVOKE).setEnabled(hasFlattrToken); - - findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) - .setEnabled(UserPreferences.isEnableAutodownload()); - setSelectedNetworksEnabled(UserPreferences.isEnableAutodownload() - && UserPreferences.isEnableAutodownloadWifiFilter()); - - } - - private void setEpisodeCacheSizeText(int cacheSize) { - String s; - if (cacheSize == getResources().getInteger( - R.integer.episode_cache_size_unlimited)) { - s = getString(R.string.pref_episode_cache_unlimited); - } else { - s = Integer.toString(cacheSize) - + getString(R.string.episodes_suffix); - } - findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setSummary(s); - } - - private void setDataFolderText() { - File f = UserPreferences.getDataFolder(this, null); - if (f != null) { - findPreference(PREF_CHOOSE_DATA_DIR) - .setSummary(f.getAbsolutePath()); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - Intent intent = new Intent(this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - break; - default: - return false; - } - return true; - } - - @Override - protected void onApplyThemeResource(Theme theme, int resid, boolean first) { - theme.applyStyle(UserPreferences.getTheme(), true); - } - - @Override - protected void onActivityResult(int requestCode, int resultCode, Intent data) { - super.onActivityResult(requestCode, resultCode, data); - if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) { - String dir = data - .getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR); - if (AppConfig.DEBUG) - Log.d(TAG, "Setting data folder"); - UserPreferences.setDataFolder(dir); - } - } - - private void buildAutodownloadSelectedNetworsPreference() { - if (selectedNetworks != null) { - clearAutodownloadSelectedNetworsPreference(); - } - // get configured networks - WifiManager wifiservice = (WifiManager) getSystemService(Context.WIFI_SERVICE); - List<WifiConfiguration> networks = wifiservice.getConfiguredNetworks(); - - if (networks != null) { - selectedNetworks = new CheckBoxPreference[networks.size()]; - List<String> prefValues = Arrays.asList(UserPreferences - .getAutodownloadSelectedNetworks()); - PreferenceScreen prefScreen = (PreferenceScreen) findPreference(AUTO_DL_PREF_SCREEN); - OnPreferenceClickListener clickListener = new OnPreferenceClickListener() { - - @Override - public boolean onPreferenceClick(Preference preference) { - if (preference instanceof CheckBoxPreference) { - String key = preference.getKey(); - ArrayList<String> prefValuesList = new ArrayList<String>( - Arrays.asList(UserPreferences - .getAutodownloadSelectedNetworks())); - boolean newValue = ((CheckBoxPreference) preference) - .isChecked(); - if (AppConfig.DEBUG) - Log.d(TAG, "Selected network " + key - + ". New state: " + newValue); - - int index = prefValuesList.indexOf(key); - if (index >= 0 && newValue == false) { - // remove network - prefValuesList.remove(index); - } else if (index < 0 && newValue == true) { - prefValuesList.add(key); - } - - UserPreferences.setAutodownloadSelectedNetworks( - PreferenceActivity.this, prefValuesList - .toArray(new String[prefValuesList - .size()])); - return true; - } else { - return false; - } - } - }; - // create preference for each known network. attach listener and set - // value - for (int i = 0; i < networks.size(); i++) { - WifiConfiguration config = networks.get(i); - - CheckBoxPreference pref = new CheckBoxPreference(this); - String key = Integer.toString(config.networkId); - pref.setTitle(config.SSID); - pref.setKey(key); - pref.setOnPreferenceClickListener(clickListener); - pref.setPersistent(false); - pref.setChecked(prefValues.contains(key)); - selectedNetworks[i] = pref; - prefScreen.addPreference(pref); - } - } else { - Log.e(TAG, "Couldn't get list of configure Wi-Fi networks"); - } - } - - private void clearAutodownloadSelectedNetworsPreference() { - if (selectedNetworks != null) { - PreferenceScreen prefScreen = (PreferenceScreen) findPreference(AUTO_DL_PREF_SCREEN); - - for (int i = 0; i < selectedNetworks.length; i++) { - if (selectedNetworks[i] != null) { - prefScreen.removePreference(selectedNetworks[i]); - } - } - } - } - - @SuppressWarnings("deprecation") - @Override - public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, - Preference preference) { - super.onPreferenceTreeClick(preferenceScreen, preference); - if (preference != null) - if (preference instanceof PreferenceScreen) - if (((PreferenceScreen) preference).getDialog() != null) - ((PreferenceScreen) preference) - .getDialog() - .getWindow() - .getDecorView() - .setBackgroundDrawable( - this.getWindow().getDecorView() - .getBackground().getConstantState() - .newDrawable()); - return false; - } + @Override + public boolean onPreferenceClick(Preference preference) { + new FlattrClickWorker(PreferenceActivity.this, + FlattrUtils.APP_URL).executeAsync(); + + return true; + } + }); + + findPreference(PREF_FLATTR_REVOKE).setOnPreferenceClickListener( + new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + FlattrUtils.revokeAccessToken(PreferenceActivity.this); + checkItemVisibility(); + return true; + } + + }); + + findPreference(PREF_ABOUT).setOnPreferenceClickListener( + new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + PreferenceActivity.this.startActivity(new Intent( + PreferenceActivity.this, AboutActivity.class)); + return true; + } + + }); + + findPreference(PREF_OPML_EXPORT).setOnPreferenceClickListener( + new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + new OpmlExportWorker(PreferenceActivity.this) + .executeAsync(); + + return true; + } + }); + + findPreference(PREF_CHOOSE_DATA_DIR).setOnPreferenceClickListener( + new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + startActivityForResult( + new Intent(PreferenceActivity.this, + DirectoryChooserActivity.class), + DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED); + return true; + } + }); + findPreference(UserPreferences.PREF_THEME) + .setOnPreferenceChangeListener( + new OnPreferenceChangeListener() { + + @Override + public boolean onPreferenceChange( + Preference preference, Object newValue) { + Intent i = getIntent(); + i.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK + | Intent.FLAG_ACTIVITY_NEW_TASK); + finish(); + startActivity(i); + return true; + } + }); + findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) + .setOnPreferenceChangeListener( + new OnPreferenceChangeListener() { + + @Override + public boolean onPreferenceChange( + Preference preference, Object newValue) { + if (newValue instanceof Boolean) { + setSelectedNetworksEnabled((Boolean) newValue); + return true; + } else { + return false; + } + } + }); + findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE) + .setOnPreferenceChangeListener( + new OnPreferenceChangeListener() { + + + @Override + public boolean onPreferenceChange(Preference preference, Object o) { + checkItemVisibility(); + return true; + } + }); + buildUpdateIntervalPreference(); + buildAutodownloadSelectedNetworsPreference(); + setSelectedNetworksEnabled(UserPreferences + .isEnableAutodownloadWifiFilter()); + + } + + private void buildUpdateIntervalPreference() { + ListPreference pref = (ListPreference) findPreference(UserPreferences.PREF_UPDATE_INTERVAL); + String[] values = getResources().getStringArray( + R.array.update_intervall_values); + String[] entries = new String[values.length]; + for (int x = 0; x < values.length; x++) { + Integer v = Integer.parseInt(values[x]); + switch (v) { + case 0: + entries[x] = getString(R.string.pref_update_interval_hours_manual); + break; + case 1: + entries[x] = v + + " " + + getString(R.string.pref_update_interval_hours_singular); + break; + default: + entries[x] = v + " " + + getString(R.string.pref_update_interval_hours_plural); + break; + + } + } + pref.setEntries(entries); + + } + + private void setSelectedNetworksEnabled(boolean b) { + if (selectedNetworks != null) { + for (Preference p : selectedNetworks) { + p.setEnabled(b); + } + } + } + + @Override + protected void onResume() { + super.onResume(); + checkItemVisibility(); + setEpisodeCacheSizeText(UserPreferences.getEpisodeCacheSize()); + setDataFolderText(); + } + + @SuppressWarnings("deprecation") + private void checkItemVisibility() { + + boolean hasFlattrToken = FlattrUtils.hasToken(); + + findPreference(PREF_FLATTR_AUTH).setEnabled(!hasFlattrToken); + findPreference(PREF_FLATTR_REVOKE).setEnabled(hasFlattrToken); + + findPreference(UserPreferences.PREF_ENABLE_AUTODL_WIFI_FILTER) + .setEnabled(UserPreferences.isEnableAutodownload()); + setSelectedNetworksEnabled(UserPreferences.isEnableAutodownload() + && UserPreferences.isEnableAutodownloadWifiFilter()); + + } + + private void setEpisodeCacheSizeText(int cacheSize) { + String s; + if (cacheSize == getResources().getInteger( + R.integer.episode_cache_size_unlimited)) { + s = getString(R.string.pref_episode_cache_unlimited); + } else { + s = Integer.toString(cacheSize) + + getString(R.string.episodes_suffix); + } + findPreference(UserPreferences.PREF_EPISODE_CACHE_SIZE).setSummary(s); + } + + private void setDataFolderText() { + File f = UserPreferences.getDataFolder(this, null); + if (f != null) { + findPreference(PREF_CHOOSE_DATA_DIR) + .setSummary(f.getAbsolutePath()); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + break; + default: + return false; + } + return true; + } + + @Override + protected void onApplyThemeResource(Theme theme, int resid, boolean first) { + theme.applyStyle(UserPreferences.getTheme(), true); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == DirectoryChooserActivity.RESULT_CODE_DIR_SELECTED) { + String dir = data + .getStringExtra(DirectoryChooserActivity.RESULT_SELECTED_DIR); + if (AppConfig.DEBUG) + Log.d(TAG, "Setting data folder"); + UserPreferences.setDataFolder(dir); + } + } + + private void buildAutodownloadSelectedNetworsPreference() { + if (selectedNetworks != null) { + clearAutodownloadSelectedNetworsPreference(); + } + // get configured networks + WifiManager wifiservice = (WifiManager) getSystemService(Context.WIFI_SERVICE); + List<WifiConfiguration> networks = wifiservice.getConfiguredNetworks(); + + if (networks != null) { + selectedNetworks = new CheckBoxPreference[networks.size()]; + List<String> prefValues = Arrays.asList(UserPreferences + .getAutodownloadSelectedNetworks()); + PreferenceScreen prefScreen = (PreferenceScreen) findPreference(AUTO_DL_PREF_SCREEN); + OnPreferenceClickListener clickListener = new OnPreferenceClickListener() { + + @Override + public boolean onPreferenceClick(Preference preference) { + if (preference instanceof CheckBoxPreference) { + String key = preference.getKey(); + ArrayList<String> prefValuesList = new ArrayList<String>( + Arrays.asList(UserPreferences + .getAutodownloadSelectedNetworks())); + boolean newValue = ((CheckBoxPreference) preference) + .isChecked(); + if (AppConfig.DEBUG) + Log.d(TAG, "Selected network " + key + + ". New state: " + newValue); + + int index = prefValuesList.indexOf(key); + if (index >= 0 && newValue == false) { + // remove network + prefValuesList.remove(index); + } else if (index < 0 && newValue == true) { + prefValuesList.add(key); + } + + UserPreferences.setAutodownloadSelectedNetworks( + PreferenceActivity.this, prefValuesList + .toArray(new String[prefValuesList + .size()])); + return true; + } else { + return false; + } + } + }; + // create preference for each known network. attach listener and set + // value + for (int i = 0; i < networks.size(); i++) { + WifiConfiguration config = networks.get(i); + + CheckBoxPreference pref = new CheckBoxPreference(this); + String key = Integer.toString(config.networkId); + pref.setTitle(config.SSID); + pref.setKey(key); + pref.setOnPreferenceClickListener(clickListener); + pref.setPersistent(false); + pref.setChecked(prefValues.contains(key)); + selectedNetworks[i] = pref; + prefScreen.addPreference(pref); + } + } else { + Log.e(TAG, "Couldn't get list of configure Wi-Fi networks"); + } + } + + private void clearAutodownloadSelectedNetworsPreference() { + if (selectedNetworks != null) { + PreferenceScreen prefScreen = (PreferenceScreen) findPreference(AUTO_DL_PREF_SCREEN); + + for (int i = 0; i < selectedNetworks.length; i++) { + if (selectedNetworks[i] != null) { + prefScreen.removePreference(selectedNetworks[i]); + } + } + } + } + + @SuppressWarnings("deprecation") + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, + Preference preference) { + super.onPreferenceTreeClick(preferenceScreen, preference); + if (preference != null) + if (preference instanceof PreferenceScreen) + if (((PreferenceScreen) preference).getDialog() != null) + ((PreferenceScreen) preference) + .getDialog() + .getWindow() + .getDecorView() + .setBackgroundDrawable( + this.getWindow().getDecorView() + .getBackground().getConstantState() + .newDrawable()); + return false; + } } diff --git a/src/de/danoeh/antennapod/activity/SearchActivity.java b/src/de/danoeh/antennapod/activity/SearchActivity.java index 152710112..b6bdab83c 100644 --- a/src/de/danoeh/antennapod/activity/SearchActivity.java +++ b/src/de/danoeh/antennapod/activity/SearchActivity.java @@ -1,185 +1,191 @@ package de.danoeh.antennapod.activity; -import java.util.ArrayList; +import java.util.List; import android.annotation.SuppressLint; import android.app.SearchManager; import android.content.Intent; import android.os.Bundle; +import android.support.v4.view.MenuItemCompat; +import android.support.v7.app.ActionBarActivity; import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; import android.view.View; +import android.widget.AdapterView; import android.widget.ListView; import android.widget.TextView; -import com.actionbarsherlock.app.SherlockListActivity; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.adapter.SearchlistAdapter; import de.danoeh.antennapod.feed.Feed; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; -import de.danoeh.antennapod.feed.FeedSearcher; +import de.danoeh.antennapod.storage.FeedSearcher; import de.danoeh.antennapod.feed.SearchResult; import de.danoeh.antennapod.fragment.FeedlistFragment; import de.danoeh.antennapod.fragment.ItemlistFragment; import de.danoeh.antennapod.preferences.UserPreferences; -/** Displays the results when the user searches for FeedItems or Feeds. */ -public class SearchActivity extends SherlockListActivity { - private static final String TAG = "SearchActivity"; - - public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.searchactivity.extra.feedId"; - - private SearchlistAdapter searchAdapter; - private ArrayList<SearchResult> content; - - /** Feed that is being searched or null if the search is global. */ - private Feed selectedFeed; - - private TextView txtvStatus; - - @Override - protected void onCreate(Bundle savedInstanceState) { - setTheme(UserPreferences.getTheme()); - super.onCreate(savedInstanceState); - - getSupportActionBar().setDisplayHomeAsUpEnabled(true); - setContentView(R.layout.searchlist); - txtvStatus = (TextView) findViewById(android.R.id.empty); - } - - @Override - protected void onNewIntent(Intent intent) { - setIntent(intent); - } - - @Override - protected void onResume() { - super.onResume(); - Intent intent = getIntent(); - if (Intent.ACTION_SEARCH.equals(intent.getAction())) { - Bundle extra = intent.getBundleExtra(SearchManager.APP_DATA); - if (extra != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Found bundle extra"); - long feedId = extra.getLong(EXTRA_FEED_ID); - selectedFeed = FeedManager.getInstance().getFeed(feedId); - } - if (AppConfig.DEBUG) - Log.d(TAG, "Starting search"); - String query = intent.getStringExtra(SearchManager.QUERY); - getSupportActionBar() - .setSubtitle( - getString(R.string.search_term_label) + "\"" - + query + "\""); - handleSearchRequest(query); - } - } - - @Override - public boolean onCreateOptionsMenu(Menu menu) { - menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label) - .setIcon( - obtainStyledAttributes( - new int[] { R.attr.action_search }) - .getDrawable(0)) - .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); - return true; - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case android.R.id.home: - Intent intent = new Intent(this, MainActivity.class); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - startActivity(intent); - return true; - case R.id.search_item: - onSearchRequested(); - return true; - default: - return false; - } - } - - @Override - public boolean onSearchRequested() { - Bundle extra = null; - if (selectedFeed != null) { - extra = new Bundle(); - extra.putLong(EXTRA_FEED_ID, selectedFeed.getId()); - } - startSearch(null, false, extra, false); - return true; - } - - @Override - protected void onListItemClick(ListView l, View v, int position, long id) { - super.onListItemClick(l, v, position, id); - SearchResult selection = searchAdapter.getItem(position); - if (selection.getComponent().getClass() == Feed.class) { - Feed feed = (Feed) selection.getComponent(); - Intent launchIntent = new Intent(this, FeedItemlistActivity.class); - launchIntent.putExtra(FeedlistFragment.EXTRA_SELECTED_FEED, - feed.getId()); - startActivity(launchIntent); - - } else if (selection.getComponent().getClass() == FeedItem.class) { - FeedItem item = (FeedItem) selection.getComponent(); - Intent launchIntent = new Intent(this, ItemviewActivity.class); - launchIntent.putExtra(FeedlistFragment.EXTRA_SELECTED_FEED, item - .getFeed().getId()); - launchIntent.putExtra(ItemlistFragment.EXTRA_SELECTED_FEEDITEM, - item.getId()); - startActivity(launchIntent); - } - } - - @SuppressLint({ "NewApi", "NewApi" }) - private void handleSearchRequest(final String query) { - if (searchAdapter != null) { - searchAdapter.clear(); - searchAdapter.notifyDataSetChanged(); - } - txtvStatus.setText(R.string.search_status_searching); - - Thread thread = new Thread() { - - @Override - public void run() { - Log.d(TAG, "Starting background work"); - final ArrayList<SearchResult> result = FeedSearcher - .performSearch(SearchActivity.this, query, selectedFeed); - if (SearchActivity.this != null) { - SearchActivity.this.runOnUiThread(new Runnable() { - - @Override - public void run() { - if (AppConfig.DEBUG) - Log.d(TAG, "Background work finished"); - if (AppConfig.DEBUG) - Log.d(TAG, "Found " + result.size() - + " results"); - content = result; - - searchAdapter = new SearchlistAdapter( - SearchActivity.this, 0, content); - getListView().setAdapter(searchAdapter); - searchAdapter.notifyDataSetChanged(); - if (content.isEmpty()) { - txtvStatus - .setText(R.string.search_status_no_results); - } - } - }); - } - } - }; - thread.start(); - - } +/** + * Displays the results when the user searches for FeedItems or Feeds. + */ +public class SearchActivity extends ActionBarActivity implements AdapterView.OnItemClickListener { + private static final String TAG = "SearchActivity"; + + public static final String EXTRA_FEED_ID = "de.danoeh.antennapod.searchactivity.extra.feedId"; + + private SearchlistAdapter searchAdapter; + private List<SearchResult> content; + + /** + * ID of the feed that is being searched or null if the search is global. + */ + private long feedID; + + private ListView listView; + private TextView txtvStatus; + + @Override + protected void onCreate(Bundle savedInstanceState) { + setTheme(UserPreferences.getTheme()); + super.onCreate(savedInstanceState); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + setContentView(R.layout.searchlist); + listView = (ListView) findViewById(android.R.id.list); + txtvStatus = (TextView) findViewById(android.R.id.empty); + + listView.setOnItemClickListener(this); + } + + @Override + protected void onNewIntent(Intent intent) { + setIntent(intent); + } + + @Override + protected void onResume() { + super.onResume(); + Intent intent = getIntent(); + if (Intent.ACTION_SEARCH.equals(intent.getAction())) { + Bundle extra = intent.getBundleExtra(SearchManager.APP_DATA); + if (extra != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Found bundle extra"); + feedID = extra.getLong(EXTRA_FEED_ID); + } + if (AppConfig.DEBUG) + Log.d(TAG, "Starting search"); + String query = intent.getStringExtra(SearchManager.QUERY); + getSupportActionBar() + .setSubtitle( + getString(R.string.search_term_label) + "\"" + + query + "\""); + handleSearchRequest(query); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuItemCompat.setShowAsAction(menu.add(Menu.NONE, R.id.search_item, Menu.NONE, R.string.search_label) + .setIcon( + obtainStyledAttributes( + new int[]{R.attr.action_search}) + .getDrawable(0)), + (MenuItem.SHOW_AS_ACTION_IF_ROOM)); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case android.R.id.home: + Intent intent = new Intent(this, MainActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + return true; + case R.id.search_item: + onSearchRequested(); + return true; + default: + return false; + } + } + + @Override + public boolean onSearchRequested() { + Bundle extra = null; + if (feedID != 0) { + extra = new Bundle(); + extra.putLong(EXTRA_FEED_ID, feedID); + } + startSearch(null, false, extra, false); + return true; + } + + @SuppressLint({"NewApi", "NewApi"}) + private void handleSearchRequest(final String query) { + if (searchAdapter != null) { + searchAdapter.clear(); + searchAdapter.notifyDataSetChanged(); + } + txtvStatus.setText(R.string.search_status_searching); + + Thread thread = new Thread() { + + @Override + public void run() { + Log.d(TAG, "Starting background work"); + final List<SearchResult> result = FeedSearcher + .performSearch(SearchActivity.this, query, feedID); + if (SearchActivity.this != null) { + SearchActivity.this.runOnUiThread(new Runnable() { + + @Override + public void run() { + if (AppConfig.DEBUG) + Log.d(TAG, "Background work finished"); + if (AppConfig.DEBUG) + Log.d(TAG, "Found " + result.size() + + " results"); + content = result; + + searchAdapter = new SearchlistAdapter( + SearchActivity.this, 0, content); + listView.setAdapter(searchAdapter); + searchAdapter.notifyDataSetChanged(); + if (content.isEmpty()) { + txtvStatus + .setText(R.string.search_status_no_results); + } + } + }); + } + } + }; + thread.start(); + + } + + @Override + public void onItemClick(AdapterView<?> adapterView, View view, int position, long l) { + SearchResult selection = searchAdapter.getItem(position); + if (selection.getComponent().getClass() == Feed.class) { + Feed feed = (Feed) selection.getComponent(); + Intent launchIntent = new Intent(this, FeedItemlistActivity.class); + launchIntent.putExtra(FeedlistFragment.EXTRA_SELECTED_FEED, + feed.getId()); + startActivity(launchIntent); + + } else if (selection.getComponent().getClass() == FeedItem.class) { + FeedItem item = (FeedItem) selection.getComponent(); + Intent launchIntent = new Intent(this, ItemviewActivity.class); + launchIntent.putExtra(FeedlistFragment.EXTRA_SELECTED_FEED, item + .getFeed().getId()); + launchIntent.putExtra(ItemlistFragment.EXTRA_SELECTED_FEEDITEM, + item.getId()); + startActivity(launchIntent); + } + } } diff --git a/src/de/danoeh/antennapod/activity/StorageErrorActivity.java b/src/de/danoeh/antennapod/activity/StorageErrorActivity.java index 4d9184dcf..33277ebc9 100644 --- a/src/de/danoeh/antennapod/activity/StorageErrorActivity.java +++ b/src/de/danoeh/antennapod/activity/StorageErrorActivity.java @@ -5,17 +5,16 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; +import android.support.v7.app.ActionBarActivity; import android.util.Log; -import com.actionbarsherlock.app.SherlockActivity; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.util.StorageUtils; /** Is show if there is now external storage available. */ -public class StorageErrorActivity extends SherlockActivity { +public class StorageErrorActivity extends ActionBarActivity { private static final String TAG = "StorageErrorActivity"; @Override diff --git a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java index b3567e417..01841f099 100644 --- a/src/de/danoeh/antennapod/activity/VideoplayerActivity.java +++ b/src/de/danoeh/antennapod/activity/VideoplayerActivity.java @@ -5,18 +5,13 @@ import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; -import android.view.MotionEvent; -import android.view.SurfaceHolder; -import android.view.View; -import android.view.WindowManager; +import android.view.*; import android.view.animation.AnimationUtils; import android.widget.LinearLayout; import android.widget.ProgressBar; import android.widget.SeekBar; import android.widget.VideoView; -import com.actionbarsherlock.view.Window; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.MediaType; diff --git a/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java b/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java index f97210cf3..c067ac5d2 100644 --- a/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java +++ b/src/de/danoeh/antennapod/adapter/DownloadLogAdapter.java @@ -8,21 +8,22 @@ import android.view.ViewGroup; import android.widget.BaseAdapter; import android.widget.TextView; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.DownloadStatus; import de.danoeh.antennapod.feed.Feed; import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.feed.FeedMedia; +import de.danoeh.antennapod.service.download.DownloadStatus; import de.danoeh.antennapod.util.DownloadError; /** Displays a list of DownloadStatus entries. */ public class DownloadLogAdapter extends BaseAdapter { private Context context; - private FeedManager manager = FeedManager.getInstance(); - public DownloadLogAdapter(Context context) { + private ItemAccess itemAccess; + + public DownloadLogAdapter(Context context, ItemAccess itemAccess) { super(); + this.itemAccess = itemAccess; this.context = context; } @@ -70,8 +71,7 @@ public class DownloadLogAdapter extends BaseAdapter { holder.successful.setTextColor(convertView.getResources().getColor( R.color.download_failed_red)); holder.successful.setText(R.string.download_failed); - String reasonText = DownloadError.getErrorString(context, - status.getReason()); + String reasonText = status.getReason().getErrorString(context); if (status.getReasonDetailed() != null) { reasonText += ": " + status.getReasonDetailed(); } @@ -92,12 +92,12 @@ public class DownloadLogAdapter extends BaseAdapter { @Override public int getCount() { - return manager.getDownloadLogSize(); + return itemAccess.getCount(); } @Override public DownloadStatus getItem(int position) { - return manager.getDownloadStatusFromLogAtIndex(position); + return itemAccess.getItem(position); } @Override @@ -105,4 +105,9 @@ public class DownloadLogAdapter extends BaseAdapter { return position; } + public static interface ItemAccess { + public int getCount(); + public DownloadStatus getItem(int position); + } + } diff --git a/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java b/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java index 685906d6f..75e837969 100644 --- a/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java +++ b/src/de/danoeh/antennapod/adapter/DownloadlistAdapter.java @@ -10,11 +10,12 @@ import android.widget.ArrayAdapter; import android.widget.ProgressBar; import android.widget.TextView; import de.danoeh.antennapod.R; -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.service.download.DownloadRequest; +import de.danoeh.antennapod.service.download.DownloadStatus; import de.danoeh.antennapod.service.download.Downloader; import de.danoeh.antennapod.util.Converter; import de.danoeh.antennapod.util.ThemeUtils; @@ -33,8 +34,7 @@ public class DownloadlistAdapter extends ArrayAdapter<Downloader> { @Override public View getView(int position, View convertView, ViewGroup parent) { Holder holder; - DownloadStatus status = getItem(position).getStatus(); - FeedFile feedFile = status.getFeedFile(); + DownloadRequest request = getItem(position).getDownloadRequest(); // Inflate layout if (convertView == null) { holder = new Holder(); @@ -62,31 +62,16 @@ public class DownloadlistAdapter extends ArrayAdapter<Downloader> { } 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) { - FeedImage image = (FeedImage) feedFile; - if (image.getFeed() != null) { - titleText = convertView.getResources().getString( - R.string.image_of_prefix) - + image.getFeed().getTitle(); - } else { - titleText = ((FeedImage) feedFile).getTitle(); - } - } - holder.title.setText(titleText); - if (status.getStatusMsg() != 0) { - holder.message.setText(status.getStatusMsg()); + + holder.title.setText(request.getTitle()); + if (request.getStatusMsg() != 0) { + holder.message.setText(request.getStatusMsg()); } - String strDownloaded = Converter.byteToString(status.getSoFar()); - if (status.getSize() != DownloadStatus.SIZE_UNKNOWN) { - strDownloaded += " / " + Converter.byteToString(status.getSize()); - holder.percent.setText(status.getProgressPercent() + "%"); - holder.progbar.setProgress(status.getProgressPercent()); + String strDownloaded = Converter.byteToString(request.getSoFar()); + if (request.getSize() != DownloadStatus.SIZE_UNKNOWN) { + strDownloaded += " / " + Converter.byteToString(request.getSize()); + holder.percent.setText(request.getProgressPercent() + "%"); + holder.progbar.setProgress(request.getProgressPercent()); holder.percent.setVisibility(View.VISIBLE); } else { holder.progbar.setProgress(0); diff --git a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java index 916e13469..b3156f765 100644 --- a/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java +++ b/src/de/danoeh/antennapod/adapter/ExternalEpisodesListAdapter.java @@ -14,7 +14,6 @@ import android.widget.TextView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.Converter; @@ -30,17 +29,18 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { public static final int GROUP_POS_UNREAD = 1; private Context context; - private FeedManager manager = FeedManager.getInstance(); + private ItemAccess itemAccess; private ActionButtonCallback feedItemActionCallback; private OnGroupActionClicked groupActionCallback; public ExternalEpisodesListAdapter(Context context, ActionButtonCallback callback, - OnGroupActionClicked groupActionCallback) { + OnGroupActionClicked groupActionCallback, + ItemAccess itemAccess) { super(); this.context = context; - + this.itemAccess = itemAccess; this.feedItemActionCallback = callback; this.groupActionCallback = groupActionCallback; } @@ -53,10 +53,10 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { @Override public FeedItem getChild(int groupPosition, int childPosition) { if (groupPosition == GROUP_POS_QUEUE) { - return manager.getQueueItemAtIndex(childPosition, true); + return itemAccess.getQueueItemAt(childPosition); } else if (groupPosition == GROUP_POS_UNREAD) { - return manager.getUnreadItemAtIndex(childPosition, true); - } + return itemAccess.getUnreadItemAt(childPosition); + } return null; } @@ -200,9 +200,9 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { @Override public int getChildrenCount(int groupPosition) { if (groupPosition == GROUP_POS_QUEUE) { - return manager.getQueueSize(true); + return itemAccess.getQueueSize(); } else if (groupPosition == GROUP_POS_UNREAD) { - return manager.getUnreadItemsSize(true); + return itemAccess.getUnreadItemsSize(); } return 0; } @@ -210,7 +210,7 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { @Override public int getGroupCount() { // Hide 'unread items' group if empty - if (manager.getUnreadItemsSize(true) > 0) { + if (itemAccess.getUnreadItemsSize() > 0) { return 2; } else { return 1; @@ -264,8 +264,8 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { @Override public boolean isEmpty() { - return manager.getUnreadItemsSize(true) == 0 - && manager.getQueueSize(true) == 0; + return itemAccess.getUnreadItemsSize() == 0 + && itemAccess.getQueueSize() == 0; } @Override @@ -287,4 +287,11 @@ public class ExternalEpisodesListAdapter extends BaseExpandableListAdapter { public void onClick(long groupId); } + public static interface ItemAccess { + public int getQueueSize(); + public int getUnreadItemsSize(); + public FeedItem getQueueItemAt(int position); + public FeedItem getUnreadItemAt(int position); + } + } diff --git a/src/de/danoeh/antennapod/adapter/FeedlistAdapter.java b/src/de/danoeh/antennapod/adapter/FeedlistAdapter.java index 03b46cce5..89427a47e 100644 --- a/src/de/danoeh/antennapod/adapter/FeedlistAdapter.java +++ b/src/de/danoeh/antennapod/adapter/FeedlistAdapter.java @@ -11,23 +11,31 @@ import android.widget.TextView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.storage.DownloadRequester; +import de.danoeh.antennapod.storage.FeedItemStatistics; import de.danoeh.antennapod.util.ThemeUtils; public class FeedlistAdapter extends BaseAdapter { private static final String TAG = "FeedlistAdapter"; private Context context; - private FeedManager manager = FeedManager.getInstance(); + protected ItemAccess itemAccess; private int selectedItemIndex; private ImageLoader imageLoader; public static final int SELECTION_NONE = -1; - public FeedlistAdapter(Context context) { + public FeedlistAdapter(Context context, ItemAccess itemAccess) { super(); + if (context == null) { + throw new IllegalArgumentException("context must not be null"); + } + if (itemAccess == null) { + throw new IllegalArgumentException("itemAccess must not be null"); + } + this.context = context; + this.itemAccess = itemAccess; selectedItemIndex = SELECTION_NONE; imageLoader = ImageLoader.getInstance(); } @@ -36,6 +44,7 @@ public class FeedlistAdapter extends BaseAdapter { public View getView(int position, View convertView, ViewGroup parent) { final Holder holder; final Feed feed = getItem(position); + final FeedItemStatistics feedItemStatistics = itemAccess.getFeedItemStatistics(position); // Inflate Layout if (convertView == null) { @@ -75,42 +84,40 @@ public class FeedlistAdapter extends BaseAdapter { } holder.title.setText(feed.getTitle()); - int numOfItems = feed.getNumOfItems(true); - if (DownloadRequester.getInstance().isDownloadingFile(feed)) { - holder.lastUpdate.setText(R.string.refreshing_label); - } else { - if (numOfItems > 0) { - holder.lastUpdate.setText(convertView.getResources().getString( - R.string.most_recent_prefix) - + DateUtils.getRelativeTimeSpanString( - feed.getItemAtIndex(true, 0).getPubDate().getTime(), - System.currentTimeMillis(), 0, 0)); - } else { - holder.lastUpdate.setText(""); - } - } - holder.numberOfEpisodes.setText(numOfItems - + convertView.getResources() - .getString(R.string.episodes_suffix)); - - int newItems = feed.getNumOfNewItems(); - int inProgressItems = feed.getNumOfStartedItems(); - - if (newItems > 0) { - holder.newEpisodes.setText(Integer.toString(newItems)); - holder.newEpisodesLabel.setVisibility(View.VISIBLE); - } else { - holder.newEpisodesLabel.setVisibility(View.INVISIBLE); - } - - if (inProgressItems > 0) { - holder.inProgressEpisodes - .setText(Integer.toString(inProgressItems)); - holder.inProgressEpisodesLabel.setVisibility(View.VISIBLE); - } else { - holder.inProgressEpisodesLabel.setVisibility(View.INVISIBLE); - } + if (feedItemStatistics != null) { + if (DownloadRequester.getInstance().isDownloadingFile(feed)) { + holder.lastUpdate.setText(R.string.refreshing_label); + } else { + if (feedItemStatistics.getNumberOfItems() > 0) { + holder.lastUpdate.setText(convertView.getResources().getString( + R.string.most_recent_prefix) + + DateUtils.getRelativeTimeSpanString( + feedItemStatistics.getLastUpdate().getTime(), + System.currentTimeMillis(), 0, 0)); + } else { + holder.lastUpdate.setText(""); + } + } + holder.numberOfEpisodes.setText(feedItemStatistics.getNumberOfItems() + + convertView.getResources() + .getString(R.string.episodes_suffix)); + + if (feedItemStatistics.getNumberOfNewItems() > 0) { + holder.newEpisodes.setText(Integer.toString(feedItemStatistics.getNumberOfNewItems())); + holder.newEpisodesLabel.setVisibility(View.VISIBLE); + } else { + holder.newEpisodesLabel.setVisibility(View.INVISIBLE); + } + + if (feedItemStatistics.getNumberOfInProgressItems() > 0) { + holder.inProgressEpisodes + .setText(Integer.toString(feedItemStatistics.getNumberOfInProgressItems())); + holder.inProgressEpisodesLabel.setVisibility(View.VISIBLE); + } else { + holder.inProgressEpisodesLabel.setVisibility(View.INVISIBLE); + } + } final String imageUrl = (feed.getImage() != null) ? feed.getImage() .getFile_url() : null; holder.image.setTag(imageUrl); @@ -145,12 +152,12 @@ public class FeedlistAdapter extends BaseAdapter { @Override public int getCount() { - return manager.getFeedsSize(); + return itemAccess.getCount(); } @Override public Feed getItem(int position) { - return manager.getFeedAtIndex(position); + return itemAccess.getItem(position); } @Override @@ -158,4 +165,11 @@ public class FeedlistAdapter extends BaseAdapter { return position; } + public interface ItemAccess { + int getCount(); + + Feed getItem(int position); + + FeedItemStatistics getFeedItemStatistics(int position); + } } diff --git a/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java b/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java index e5c12f018..b8bec44c8 100644 --- a/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java +++ b/src/de/danoeh/antennapod/adapter/InternalFeedItemlistAdapter.java @@ -14,13 +14,14 @@ import android.widget.ProgressBar; import android.widget.TextView; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.feed.MediaType; import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.Converter; import de.danoeh.antennapod.util.ThemeUtils; +import java.util.Iterator; + /** List adapter for items of feeds that the user has already subscribed to. */ public class InternalFeedItemlistAdapter extends DefaultFeedItemlistAdapter { @@ -31,7 +32,7 @@ public class InternalFeedItemlistAdapter extends DefaultFeedItemlistAdapter { public static final int SELECTION_NONE = -1; public InternalFeedItemlistAdapter(Context context, - DefaultFeedItemlistAdapter.ItemAccess itemAccess, + ItemAccess itemAccess, ActionButtonCallback callback, boolean showFeedtitle) { super(context, itemAccess); this.callback = callback; @@ -155,7 +156,7 @@ public class InternalFeedItemlistAdapter extends DefaultFeedItemlistAdapter { } holder.lenSize.setVisibility(View.VISIBLE); - if (FeedManager.getInstance().isInQueue(item)) { + if (((ItemAccess) itemAccess).isInQueue(item)) { holder.inPlaylist.setVisibility(View.VISIBLE); } else { holder.inPlaylist.setVisibility(View.GONE); @@ -224,4 +225,8 @@ public class InternalFeedItemlistAdapter extends DefaultFeedItemlistAdapter { notifyDataSetChanged(); } + public static interface ItemAccess extends DefaultFeedItemlistAdapter.ItemAccess { + public boolean isInQueue(FeedItem item); + } + } diff --git a/src/de/danoeh/antennapod/asynctask/DownloadStatus.java b/src/de/danoeh/antennapod/asynctask/DownloadStatus.java deleted file mode 100644 index e9225d33b..000000000 --- a/src/de/danoeh/antennapod/asynctask/DownloadStatus.java +++ /dev/null @@ -1,198 +0,0 @@ -package de.danoeh.antennapod.asynctask; - -import java.util.Date; - -import de.danoeh.antennapod.feed.FeedFile; - -/** Contains status attributes for one download */ -public class DownloadStatus { - /** - * Downloaders should use this constant for the size attribute if necessary - * so that the listadapters etc. can react properly. - */ - public static final int SIZE_UNKNOWN = -1; - - public Date getCompletionDate() { - return completionDate; - } - - // ----------------------------------- ATTRIBUTES STORED IN DB - /** Unique id for storing the object in database. */ - protected long id; - /** - * A human-readable string which is shown to the user so that he can - * identify the download. Should be the title of the item/feed/media or the - * URL if the download has no other title. - */ - protected String title; - protected int reason; - /** - * A message which can be presented to the user to give more information. - * Should be null if Download was successful. - */ - protected String reasonDetailed; - protected boolean successful; - protected Date completionDate; - protected FeedFile feedfile; - /** - * Is used to determine the type of the feedfile even if the feedfile does - * not exist anymore. The value should be FEEDFILETYPE_FEED, - * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA - */ - protected int feedfileType; - - // ------------------------------------ NOT STORED IN DB - protected int progressPercent; - protected long soFar; - protected long size; - protected int statusMsg; - protected boolean done; - protected boolean cancelled; - - public DownloadStatus(FeedFile feedfile, String title) { - this.feedfile = feedfile; - if (feedfile != null) { - feedfileType = feedfile.getTypeAsInt(); - } - this.title = title; - } - - /** Constructor for restoring Download status entries from DB. */ - public DownloadStatus(long id, String title, FeedFile feedfile, - int feedfileType, boolean successful, int reason, - Date completionDate, String reasonDetailed) { - progressPercent = 100; - soFar = 0; - size = 0; - - this.id = id; - this.title = title; - this.done = true; - this.feedfile = feedfile; - this.reason = reason; - this.successful = successful; - this.completionDate = completionDate; - this.reasonDetailed = reasonDetailed; - this.feedfileType = feedfileType; - } - - /** Constructor for creating new completed downloads. */ - public DownloadStatus(FeedFile feedfile, String title, int reason, - boolean successful, String reasonDetailed) { - this(0, title, feedfile, feedfile.getTypeAsInt(), successful, reason, - new Date(), reasonDetailed); - } - - @Override - public String toString() { - return "DownloadStatus [id=" + id + ", title=" + title + ", reason=" - + reason + ", reasonDetailed=" + reasonDetailed - + ", successful=" + successful + ", completionDate=" - + completionDate + ", feedfile=" + feedfile + ", feedfileType=" - + feedfileType + ", progressPercent=" + progressPercent - + ", soFar=" + soFar + ", size=" + size + ", statusMsg=" - + statusMsg + ", done=" + done + ", cancelled=" + cancelled - + "]"; - } - - 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; - } - - public void setProgressPercent(int progressPercent) { - this.progressPercent = progressPercent; - } - - public void setSoFar(long soFar) { - this.soFar = soFar; - } - - public void setSize(long size) { - this.size = size; - } - - public void setStatusMsg(int statusMsg) { - this.statusMsg = statusMsg; - } - - public void setReason(int reason) { - this.reason = reason; - } - - public void setSuccessful(boolean successful) { - this.successful = successful; - } - - public void setDone(boolean done) { - this.done = done; - } - - public void setCompletionDate(Date completionDate) { - this.completionDate = completionDate; - } - - public String getReasonDetailed() { - return reasonDetailed; - } - - public void setReasonDetailed(String reasonDetailed) { - this.reasonDetailed = reasonDetailed; - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public int getFeedfileType() { - return feedfileType; - } - - public boolean isCancelled() { - return cancelled; - } - - public void setCancelled(boolean cancelled) { - this.cancelled = cancelled; - } - -}
\ No newline at end of file diff --git a/src/de/danoeh/antennapod/asynctask/FeedRemover.java b/src/de/danoeh/antennapod/asynctask/FeedRemover.java index 829a14602..244312a6e 100644 --- a/src/de/danoeh/antennapod/asynctask/FeedRemover.java +++ b/src/de/danoeh/antennapod/asynctask/FeedRemover.java @@ -7,7 +7,9 @@ import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.os.AsyncTask; import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedManager; +import de.danoeh.antennapod.storage.DBWriter; + +import java.util.concurrent.ExecutionException; /** Removes a feed in the background. */ public class FeedRemover extends AsyncTask<Void, Void, Void> { @@ -23,9 +25,14 @@ public class FeedRemover extends AsyncTask<Void, Void, Void> { @Override protected Void doInBackground(Void... params) { - FeedManager manager = FeedManager.getInstance(); - manager.deleteFeed(context, feed); - return null; + try { + DBWriter.deleteFeed(context, feed.getId()).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + return null; } @Override diff --git a/src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java b/src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java index 978f53ac6..e14e22917 100644 --- a/src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java +++ b/src/de/danoeh/antennapod/asynctask/OpmlExportWorker.java @@ -14,9 +14,9 @@ import android.os.AsyncTask; import android.util.Log; import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.opml.OpmlWriter; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.storage.DBReader; /** Writes an OPML file into the export directory in the background. */ public class OpmlExportWorker extends AsyncTask<Void, Void, Void> { @@ -51,8 +51,7 @@ public class OpmlExportWorker extends AsyncTask<Void, Void, Void> { } try { FileWriter writer = new FileWriter(output); - opmlWriter.writeDocument(Arrays.asList(FeedManager.getInstance().getFeedsArray()), - writer); + opmlWriter.writeDocument(DBReader.getFeedList(context), writer); writer.close(); } catch (IOException e) { e.printStackTrace(); diff --git a/src/de/danoeh/antennapod/feed/EventDistributor.java b/src/de/danoeh/antennapod/feed/EventDistributor.java index 1fc7e2c35..c538808e2 100644 --- a/src/de/danoeh/antennapod/feed/EventDistributor.java +++ b/src/de/danoeh/antennapod/feed/EventDistributor.java @@ -92,7 +92,7 @@ public class EventDistributor extends Observable { super.addObserver(observer); if (!(observer instanceof EventListener)) { throw new IllegalArgumentException( - "Observer must be instance of FeedManager.EventListener"); + "Observer must be instance of EventListener"); } } diff --git a/src/de/danoeh/antennapod/feed/Feed.java b/src/de/danoeh/antennapod/feed/Feed.java index 6220bde00..9cee1a86a 100644 --- a/src/de/danoeh/antennapod/feed/Feed.java +++ b/src/de/danoeh/antennapod/feed/Feed.java @@ -10,339 +10,363 @@ import de.danoeh.antennapod.util.EpisodeFilter; /** * Data Object for a whole feed - * + * * @author daniel - * */ public class Feed extends FeedFile { - public static final int FEEDFILETYPE_FEED = 0; - public static final String TYPE_RSS2 = "rss"; - public static final String TYPE_RSS091 = "rss"; - public static final String TYPE_ATOM1 = "atom"; - - private String title; - /** Contains 'id'-element in Atom feed. */ - private String feedIdentifier; - /** Link to the website. */ - private String link; - private String description; - private String language; - /** Name of the author */ - private String author; - private FeedImage image; - private List<FeedItem> items; - /** Date of last refresh. */ - private Date lastUpdate; - private String paymentLink; - /** Feed type, for example RSS 2 or Atom */ - private String type; - - public Feed(Date lastUpdate) { - super(); - items = Collections.synchronizedList(new ArrayList<FeedItem>()); - this.lastUpdate = lastUpdate; - } - - /** - * This constructor is used for requesting a feed download. It should NOT be - * used if the title of the feed is already known. - * */ - public Feed(String url, Date lastUpdate) { - this(lastUpdate); - this.download_url = url; - } - - /** - * This constructor is used for requesting a feed download. It should be - * used if the title of the feed is already known. - * */ - public Feed(String url, Date lastUpdate, String title) { - this(url, lastUpdate); - this.title = title; - } - - /** - * Returns the number of FeedItems where 'read' is false. If the 'display - * only episodes' - preference is set to true, this method will only count - * items with episodes. - * */ - public int getNumOfNewItems() { - int count = 0; - for (FeedItem item : items) { - if (item.getState() == FeedItem.State.NEW) { - if (!UserPreferences.isDisplayOnlyEpisodes() - || item.getMedia() != null) { - count++; - } - } - } - return count; - } - - /** - * Returns the number of FeedItems where the media started to play but - * wasn't finished yet. - * */ - public int getNumOfStartedItems() { - int count = 0; - - for (FeedItem item : items) { - FeedItem.State state = item.getState(); - if (state == FeedItem.State.IN_PROGRESS - || state == FeedItem.State.PLAYING) { - count++; - } - } - return count; - } - - /** - * Returns true if at least one item in the itemlist is unread. - * - * @param enableEpisodeFilter - * true if this method should only count items with episodes if - * the 'display only episodes' - preference is set to true by the - * user. - */ - public boolean hasNewItems(boolean enableEpisodeFilter) { - for (FeedItem item : items) { - if (item.getState() == FeedItem.State.NEW) { - if (!(enableEpisodeFilter && UserPreferences - .isDisplayOnlyEpisodes()) || item.getMedia() != null) { - return true; - } - } - } - return false; - } - - /** - * Returns the number of FeedItems. - * - * @param enableEpisodeFilter - * true if this method should only count items with episodes if - * the 'display only episodes' - preference is set to true by the - * user. - * */ - public int getNumOfItems(boolean enableEpisodeFilter) { - if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) { - return EpisodeFilter.countItemsWithEpisodes(items); - } else { - return items.size(); - } - } - - /** - * Returns the item at the specified index. - * - * @param enableEpisodeFilter - * true if this method should ignore items without episdodes if - * the episodes filter has been enabled by the user. - */ - public FeedItem getItemAtIndex(boolean enableEpisodeFilter, int position) { - if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) { - return EpisodeFilter.accessEpisodeByIndex(items, position); - } else { - return items.get(position); - } - } - - /** - * Returns the value that uniquely identifies this Feed. If the - * feedIdentifier attribute is not null, it will be returned. Else it will - * try to return the title. If the title is not given, it will use the link - * of the feed. - * */ - public String getIdentifyingValue() { - if (feedIdentifier != null && !feedIdentifier.isEmpty()) { - return feedIdentifier; - } else if (title != null && !title.isEmpty()) { - return title; - } else { - return link; - } - } - - @Override - public String getHumanReadableIdentifier() { - if (title != null) { - return title; - } else { - return download_url; - } - } - - /** Calls cacheDescriptions on all items. */ - protected void cacheDescriptionsOfItems() { - if (items != null) { - for (FeedItem item : items) { - item.cacheDescriptions(); - } - } - } - - public void updateFromOther(Feed other) { - super.updateFromOther(other); - if (other.title != null) { - title = other.title; - } - if (other.feedIdentifier != null) { - feedIdentifier = other.feedIdentifier; - } - if (other.link != null) { - link = other.link; - } - if (other.description != null) { - description = other.description; - } - if (other.language != null) { - language = other.language; - } - if (other.author != null) { - author = other.author; - } - if (other.paymentLink != null) { - paymentLink = other.paymentLink; - } - } - - public boolean compareWithOther(Feed other) { - if (super.compareWithOther(other)) { - return true; - } - if (!title.equals(other.title)) { - return true; - } - if (other.feedIdentifier != null) { - if (feedIdentifier == null - || !feedIdentifier.equals(other.feedIdentifier)) { - return true; - } - } - if (other.link != null) { - if (link == null || !link.equals(other.link)) { - return true; - } - } - if (other.description != null) { - if (description == null || !description.equals(other.description)) { - return true; - } - } - if (other.language != null) { - if (language == null || !language.equals(other.language)) { - return true; - } - } - if (other.author != null) { - if (author == null || !author.equals(other.author)) { - return true; - } - } - if (other.paymentLink != null) { - if (paymentLink == null || !paymentLink.equals(other.paymentLink)) { - return true; - } - } - return false; - } - - @Override - public int getTypeAsInt() { - return FEEDFILETYPE_FEED; - } - - 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; - } - - List<FeedItem> getItems() { - return items; - } - - public void setItems(ArrayList<FeedItem> items) { - this.items = Collections.synchronizedList(items); - } - - /** Returns an array that contains all the feeditems of this feed. */ - public FeedItem[] getItemsArray() { - return items.toArray(new FeedItem[items.size()]); - } - - public Date getLastUpdate() { - return lastUpdate; - } - - public void setLastUpdate(Date lastUpdate) { - this.lastUpdate = lastUpdate; - } - - public String getFeedIdentifier() { - return feedIdentifier; - } - - public void setFeedIdentifier(String feedIdentifier) { - this.feedIdentifier = feedIdentifier; - } - - 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; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } + public static final int FEEDFILETYPE_FEED = 0; + public static final String TYPE_RSS2 = "rss"; + public static final String TYPE_RSS091 = "rss"; + public static final String TYPE_ATOM1 = "atom"; + + private String title; + /** + * Contains 'id'-element in Atom feed. + */ + private String feedIdentifier; + /** + * Link to the website. + */ + private String link; + private String description; + private String language; + /** + * Name of the author + */ + private String author; + private FeedImage image; + private List<FeedItem> items; + /** + * Date of last refresh. + */ + private Date lastUpdate; + private String paymentLink; + /** + * Feed type, for example RSS 2 or Atom + */ + private String type; + + /** + * This constructor is used for restoring a feed from the database. + */ + public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink, + String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl, + String downloadUrl, boolean downloaded) { + super(fileUrl, downloadUrl, downloaded); + this.id = id; + this.title = title; + this.lastUpdate = lastUpdate; + this.link = link; + this.description = description; + this.paymentLink = paymentLink; + this.author = author; + this.language = language; + this.type = type; + this.feedIdentifier = feedIdentifier; + this.image = image; + + items = new ArrayList<FeedItem>(); + } + + /** + * This constructor can be used when parsing feed data. Only the 'lastUpdate' and 'items' field are initialized. + */ + public Feed() { + super(); + items = new ArrayList<FeedItem>(); + lastUpdate = new Date(); + } + + /** + * This constructor is used for requesting a feed download (it must not be used for anything else!). It should NOT be + * used if the title of the feed is already known. + */ + public Feed(String url, Date lastUpdate) { + super(null, url, false); + this.lastUpdate = lastUpdate; + } + + /** + * This constructor is used for requesting a feed download (it must not be used for anything else!). It should be + * used if the title of the feed is already known. + */ + public Feed(String url, Date lastUpdate, String title) { + this(url, lastUpdate); + this.title = title; + } + + /** + * Returns the number of FeedItems where 'read' is false. If the 'display + * only episodes' - preference is set to true, this method will only count + * items with episodes. + */ + public int getNumOfNewItems() { + int count = 0; + for (FeedItem item : items) { + if (item.getState() == FeedItem.State.NEW) { + if (!UserPreferences.isDisplayOnlyEpisodes() + || item.getMedia() != null) { + count++; + } + } + } + return count; + } + + /** + * Returns the number of FeedItems where the media started to play but + * wasn't finished yet. + */ + public int getNumOfStartedItems() { + int count = 0; + + for (FeedItem item : items) { + FeedItem.State state = item.getState(); + if (state == FeedItem.State.IN_PROGRESS + || state == FeedItem.State.PLAYING) { + count++; + } + } + return count; + } + + /** + * Returns true if at least one item in the itemlist is unread. + * + * @param enableEpisodeFilter true if this method should only count items with episodes if + * the 'display only episodes' - preference is set to true by the + * user. + */ + public boolean hasNewItems(boolean enableEpisodeFilter) { + for (FeedItem item : items) { + if (item.getState() == FeedItem.State.NEW) { + if (!(enableEpisodeFilter && UserPreferences + .isDisplayOnlyEpisodes()) || item.getMedia() != null) { + return true; + } + } + } + return false; + } + + /** + * Returns the number of FeedItems. + * + * @param enableEpisodeFilter true if this method should only count items with episodes if + * the 'display only episodes' - preference is set to true by the + * user. + */ + public int getNumOfItems(boolean enableEpisodeFilter) { + if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) { + return EpisodeFilter.countItemsWithEpisodes(items); + } else { + return items.size(); + } + } + + /** + * Returns the item at the specified index. + * + * @param enableEpisodeFilter true if this method should ignore items without episdodes if + * the episodes filter has been enabled by the user. + */ + public FeedItem getItemAtIndex(boolean enableEpisodeFilter, int position) { + if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) { + return EpisodeFilter.accessEpisodeByIndex(items, position); + } else { + return items.get(position); + } + } + + /** + * Returns the value that uniquely identifies this Feed. If the + * feedIdentifier attribute is not null, it will be returned. Else it will + * try to return the title. If the title is not given, it will use the link + * of the feed. + */ + public String getIdentifyingValue() { + if (feedIdentifier != null && !feedIdentifier.isEmpty()) { + return feedIdentifier; + } else if (title != null && !title.isEmpty()) { + return title; + } else { + return link; + } + } + + @Override + public String getHumanReadableIdentifier() { + if (title != null) { + return title; + } else { + return download_url; + } + } + + public void updateFromOther(Feed other) { + super.updateFromOther(other); + if (other.title != null) { + title = other.title; + } + if (other.feedIdentifier != null) { + feedIdentifier = other.feedIdentifier; + } + if (other.link != null) { + link = other.link; + } + if (other.description != null) { + description = other.description; + } + if (other.language != null) { + language = other.language; + } + if (other.author != null) { + author = other.author; + } + if (other.paymentLink != null) { + paymentLink = other.paymentLink; + } + } + + public boolean compareWithOther(Feed other) { + if (super.compareWithOther(other)) { + return true; + } + if (!title.equals(other.title)) { + return true; + } + if (other.feedIdentifier != null) { + if (feedIdentifier == null + || !feedIdentifier.equals(other.feedIdentifier)) { + return true; + } + } + if (other.link != null) { + if (link == null || !link.equals(other.link)) { + return true; + } + } + if (other.description != null) { + if (description == null || !description.equals(other.description)) { + return true; + } + } + if (other.language != null) { + if (language == null || !language.equals(other.language)) { + return true; + } + } + if (other.author != null) { + if (author == null || !author.equals(other.author)) { + return true; + } + } + if (other.paymentLink != null) { + if (paymentLink == null || !paymentLink.equals(other.paymentLink)) { + return true; + } + } + return false; + } + + @Override + public int getTypeAsInt() { + return FEEDFILETYPE_FEED; + } + + 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 List<FeedItem> getItems() { + return items; + } + + public void setItems(List<FeedItem> list) { + this.items = list; + } + + /** + * Returns an array that contains all the feeditems of this feed. + */ + public FeedItem[] getItemsArray() { + return items.toArray(new FeedItem[items.size()]); + } + + public Date getLastUpdate() { + return lastUpdate; + } + + public void setLastUpdate(Date lastUpdate) { + this.lastUpdate = lastUpdate; + } + + public String getFeedIdentifier() { + return feedIdentifier; + } + + public void setFeedIdentifier(String feedIdentifier) { + this.feedIdentifier = feedIdentifier; + } + + 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; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } } diff --git a/src/de/danoeh/antennapod/feed/FeedImage.java b/src/de/danoeh/antennapod/feed/FeedImage.java index 09595f5eb..3cc99d1c2 100644 --- a/src/de/danoeh/antennapod/feed/FeedImage.java +++ b/src/de/danoeh/antennapod/feed/FeedImage.java @@ -9,7 +9,7 @@ import org.apache.commons.io.IOUtils; import de.danoeh.antennapod.asynctask.ImageLoader; -; + public class FeedImage extends FeedFile implements ImageLoader.ImageWorkerTaskResource { diff --git a/src/de/danoeh/antennapod/feed/FeedItem.java b/src/de/danoeh/antennapod/feed/FeedItem.java index 0df384b60..54682397e 100644 --- a/src/de/danoeh/antennapod/feed/FeedItem.java +++ b/src/de/danoeh/antennapod/feed/FeedItem.java @@ -4,279 +4,276 @@ import java.io.InputStream; import java.lang.ref.SoftReference; import java.util.Date; import java.util.List; +import java.util.concurrent.Callable; +import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.asynctask.ImageLoader; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.util.ShownotesProvider; /** * Data Object for a XML message - * + * * @author daniel - * */ public class FeedItem extends FeedComponent implements - ImageLoader.ImageWorkerTaskResource { - - /** The id/guid that can be found in the rss/atom feed. Might not be set. */ - private String itemIdentifier; - private String title; - /** - * The description of a feeditem. This field should only be set by the - * parser. - */ - private String description; - /** - * The content of the content-encoded tag of a feeditem. This field should - * only be set by the parser. - */ - private String contentEncoded; - - private SoftReference<String> cachedDescription; - private SoftReference<String> cachedContentEncoded; - - private String link; - private Date pubDate; - private FeedMedia media; - private Feed feed; - private boolean read; - private String paymentLink; - private List<Chapter> chapters; - - public FeedItem() { - this.read = true; - } - - public void updateFromOther(FeedItem other) { - super.updateFromOther(other); - if (other.title != null) { - title = other.title; - } - if (other.getDescription() != null) { - description = other.getDescription(); - } - if (other.getContentEncoded() != null) { - contentEncoded = other.contentEncoded; - } - if (other.link != null) { - link = other.link; - } - if (other.pubDate != null && other.pubDate != pubDate) { - pubDate = other.pubDate; - } - if (other.media != null) { - if (media == null) { - media = other.media; - } else if (media.compareWithOther(other)) { - media.updateFromOther(other); - } - } - if (other.paymentLink != null) { - paymentLink = other.paymentLink; - } - if (other.chapters != null) { - if (chapters == null) { - chapters = other.chapters; - } - } - } - - /** - * Moves the 'description' and 'contentEncoded' field of feeditem to their - * SoftReference fields. - */ - protected void cacheDescriptions() { - if (description != null) { - cachedDescription = new SoftReference<String>(description); - } - if (contentEncoded != null) { - cachedContentEncoded = new SoftReference<String>(contentEncoded); - } - description = null; - contentEncoded = null; - } - - /** - * Returns the value that uniquely identifies this FeedItem. If the - * itemIdentifier attribute is not null, it will be returned. Else it will - * try to return the title. If the title is not given, it will use the link - * of the entry. - * */ - public String getIdentifyingValue() { - if (itemIdentifier != null) { - return itemIdentifier; - } else if (title != null) { - return title; - } else { - return link; - } - } - - public String getTitle() { - return title; - } - - public void setTitle(String title) { - this.title = title; - } - - public String getDescription() { - if (description == null && cachedDescription != null) { - return cachedDescription.get(); - } - 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 || isInProgress(); - } - - public void setRead(boolean read) { - this.read = read; - } - - private boolean isInProgress() { - return (media != null && media.isInProgress()); - } - - public String getContentEncoded() { - if (contentEncoded == null && cachedContentEncoded != null) { - return cachedContentEncoded.get(); - - } - return contentEncoded; - } - - public void setContentEncoded(String contentEncoded) { - this.contentEncoded = contentEncoded; - } - - public String getPaymentLink() { - return paymentLink; - } - - public void setPaymentLink(String paymentLink) { - this.paymentLink = paymentLink; - } - - public List<Chapter> getChapters() { - return chapters; - } - - public void setChapters(List<Chapter> chapters) { - this.chapters = chapters; - } - - public String getItemIdentifier() { - return itemIdentifier; - } - - public void setItemIdentifier(String itemIdentifier) { - this.itemIdentifier = itemIdentifier; - } - - public boolean hasMedia() { - return media != null; - } - - private boolean isPlaying() { - if (media != null) { - return media.isPlaying(); - } - return false; - } - - public void setCachedDescription(String d) { - cachedDescription = new SoftReference<String>(d); - } - - public void setCachedContentEncoded(String c) { - cachedContentEncoded = new SoftReference<String>(c); - } - - public enum State { - NEW, IN_PROGRESS, READ, PLAYING - } - - public State getState() { - if (hasMedia()) { - if (isPlaying()) { - return State.PLAYING; - } - if (isInProgress()) { - return State.IN_PROGRESS; - } - } - return (isRead() ? State.READ : State.NEW); - } - - @Override - public InputStream openImageInputStream() { - InputStream out = null; - if (hasMedia()) { - out = media.openImageInputStream(); - } - if (out == null && feed.getImage() != null) { - out = feed.getImage().openImageInputStream(); - } - return out; - } - - @Override - public InputStream reopenImageInputStream(InputStream input) { - InputStream out = null; - if (hasMedia()) { - out = media.reopenImageInputStream(input); - } - if (out == null && feed.getImage() != null) { - out = feed.getImage().reopenImageInputStream(input); - } - return out; - } - - @Override - public String getImageLoaderCacheKey() { - String out = null; - if (hasMedia()) { - out = media.getImageLoaderCacheKey(); - } - if (out == null && feed.getImage() != null) { - out = feed.getImage().getImageLoaderCacheKey(); - } - return out; - } + ImageLoader.ImageWorkerTaskResource, ShownotesProvider { + + /** + * The id/guid that can be found in the rss/atom feed. Might not be set. + */ + private String itemIdentifier; + private String title; + /** + * The description of a feeditem. + */ + private String description; + /** + * The content of the content-encoded tag of a feeditem. + */ + private String contentEncoded; + + private String link; + private Date pubDate; + private FeedMedia media; + + private Feed feed; + private long feedId; + + private boolean read; + private String paymentLink; + private List<Chapter> chapters; + + public FeedItem() { + this.read = true; + } + + public void updateFromOther(FeedItem other) { + super.updateFromOther(other); + if (other.title != null) { + title = other.title; + } + if (other.getDescription() != null) { + description = other.getDescription(); + } + if (other.getContentEncoded() != null) { + contentEncoded = other.contentEncoded; + } + if (other.link != null) { + link = other.link; + } + if (other.pubDate != null && other.pubDate != pubDate) { + pubDate = other.pubDate; + } + if (other.media != null) { + if (media == null) { + media = other.media; + } else if (media.compareWithOther(other)) { + media.updateFromOther(other); + } + } + if (other.paymentLink != null) { + paymentLink = other.paymentLink; + } + if (other.chapters != null) { + if (chapters == null) { + chapters = other.chapters; + } + } + } + + /** + * Returns the value that uniquely identifies this FeedItem. If the + * itemIdentifier attribute is not null, it will be returned. Else it will + * try to return the title. If the title is not given, it will use the link + * of the entry. + */ + public String getIdentifyingValue() { + if (itemIdentifier != null) { + return itemIdentifier; + } else if (title != null) { + return title; + } else { + return link; + } + } + + 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 || isInProgress(); + } + + public void setRead(boolean read) { + this.read = read; + } + + private boolean isInProgress() { + return (media != null && media.isInProgress()); + } + + 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 List<Chapter> getChapters() { + return chapters; + } + + public void setChapters(List<Chapter> chapters) { + this.chapters = chapters; + } + + public String getItemIdentifier() { + return itemIdentifier; + } + + public void setItemIdentifier(String itemIdentifier) { + this.itemIdentifier = itemIdentifier; + } + + public boolean hasMedia() { + return media != null; + } + + private boolean isPlaying() { + if (media != null) { + return media.isPlaying(); + } + return false; + } + + @Override + public Callable<String> loadShownotes() { + return new Callable<String>() { + @Override + public String call() throws Exception { + + if (contentEncoded == null || description == null) { + DBReader.loadExtraInformationOfFeedItem(PodcastApp.getInstance(), FeedItem.this); + + } + return (contentEncoded != null) ? contentEncoded : description; + } + }; + } + + public enum State { + NEW, IN_PROGRESS, READ, PLAYING + } + + public State getState() { + if (hasMedia()) { + if (isPlaying()) { + return State.PLAYING; + } + if (isInProgress()) { + return State.IN_PROGRESS; + } + } + return (isRead() ? State.READ : State.NEW); + } + + @Override + public InputStream openImageInputStream() { + InputStream out = null; + if (hasMedia()) { + out = media.openImageInputStream(); + } + if (out == null && feed.getImage() != null) { + out = feed.getImage().openImageInputStream(); + } + return out; + } + + @Override + public InputStream reopenImageInputStream(InputStream input) { + InputStream out = null; + if (hasMedia()) { + out = media.reopenImageInputStream(input); + } + if (out == null && feed.getImage() != null) { + out = feed.getImage().reopenImageInputStream(input); + } + return out; + } + + @Override + public String getImageLoaderCacheKey() { + String out = null; + if (hasMedia()) { + out = media.getImageLoaderCacheKey(); + } + if (out == null && feed.getImage() != null) { + out = feed.getImage().getImageLoaderCacheKey(); + } + return out; + } + + public long getFeedId() { + return feedId; + } + + public void setFeedId(long feedId) { + this.feedId = feedId; + } + } diff --git a/src/de/danoeh/antennapod/feed/FeedManager.java b/src/de/danoeh/antennapod/feed/FeedManager.java deleted file mode 100644 index a1a8c6c32..000000000 --- a/src/de/danoeh/antennapod/feed/FeedManager.java +++ /dev/null @@ -1,2014 +0,0 @@ -package de.danoeh.antennapod.feed; - -import java.io.File; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.Date; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.Comparator; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ThreadFactory; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.Intent; -import android.content.SharedPreferences; -import android.database.Cursor; -import android.os.AsyncTask; -import android.os.Handler; -import android.preference.PreferenceManager; -import android.util.Log; -import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.asynctask.DownloadStatus; -import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.service.PlaybackService; -import de.danoeh.antennapod.storage.DownloadRequestException; -import de.danoeh.antennapod.storage.DownloadRequester; -import de.danoeh.antennapod.storage.PodDBAdapter; -import de.danoeh.antennapod.util.DownloadError; -import de.danoeh.antennapod.util.EpisodeFilter; -import de.danoeh.antennapod.util.FeedtitleComparator; -import de.danoeh.antennapod.util.NetworkUtils; -import de.danoeh.antennapod.util.comparator.DownloadStatusComparator; -import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; -import de.danoeh.antennapod.util.comparator.PlaybackCompletionDateComparator; -import de.danoeh.antennapod.util.exception.MediaFileNotFoundException; - -/** - * Singleton class that - provides access to all Feeds and FeedItems and to - * several lists of FeedItems. - provides methods for modifying the - * application's data - takes care of updating the information stored in the - * database when something is modified - * - * An instance of this class can be retrieved via getInstance(). - * */ -public class FeedManager { - private static final String TAG = "FeedManager"; - - /** Number of completed Download status entries to store. */ - private static final int DOWNLOAD_LOG_SIZE = 50; - - private static FeedManager singleton; - - private List<Feed> feeds; - - /** Contains all items where 'read' is false */ - private List<FeedItem> unreadItems; - - /** Contains completed Download status entries */ - private List<DownloadStatus> downloadLog; - - /** Contains the queue of items to be played. */ - private List<FeedItem> queue; - - /** Contains the last played items */ - private List<FeedItem> playbackHistory; - - /** Maximum number of items in the playback history. */ - private static final int PLAYBACK_HISTORY_SIZE = 15; - - private DownloadRequester requester = DownloadRequester.getInstance(); - private EventDistributor eventDist = EventDistributor.getInstance(); - - /** - * Should be used to change the content of the arrays from another thread to - * ensure that arrays are only modified on the main thread. - */ - private Handler contentChanger; - - /** Ensures that there are no parallel db operations. */ - private Executor dbExec; - - /** Prevents user from starting several feed updates at the same time. */ - private static boolean isStartingFeedRefresh = false; - - private FeedManager() { - feeds = Collections.synchronizedList(new ArrayList<Feed>()); - unreadItems = Collections.synchronizedList(new ArrayList<FeedItem>()); - downloadLog = new ArrayList<DownloadStatus>(); - queue = Collections.synchronizedList(new ArrayList<FeedItem>()); - playbackHistory = Collections - .synchronizedList(new ArrayList<FeedItem>()); - contentChanger = new Handler(); - dbExec = Executors.newSingleThreadExecutor(new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - } - - /** Creates a new instance of this class if necessary and returns it. */ - public static FeedManager getInstance() { - if (singleton == null) { - singleton = new FeedManager(); - } - return singleton; - } - - /** - * Play FeedMedia and start the playback service + launch Mediaplayer - * Activity. The FeedItem will be added at the top of the queue if it isn't - * in there yet. - * - * @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 - * @param shouldStream - * if Mediaplayer should stream the file - */ - public void playMedia(Context context, FeedMedia media, boolean showPlayer, - boolean startWhenPrepared, boolean shouldStream) { - try { - if (!shouldStream) { - if (media.fileExists() == false) { - throw new MediaFileNotFoundException( - "No episode was found at " + media.getFile_url(), - media); - } - } - // Start playback Service - Intent launchIntent = new Intent(context, PlaybackService.class); - launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); - launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, - startWhenPrepared); - launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, - shouldStream); - launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, - true); - context.startService(launchIntent); - if (showPlayer) { - // Launch Mediaplayer - context.startActivity(PlaybackService.getPlayerActivityIntent( - context, media)); - } - if (!queue.contains(media.getItem())) { - addQueueItemAt(context, media.getItem(), 0, false); - } - } catch (MediaFileNotFoundException e) { - e.printStackTrace(); - if (media.isPlaying()) { - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - notifyMissingFeedMediaFile(context, media); - } - } - - /** 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); - - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context); - if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA) { - if (media.getId() == PlaybackPreferences - .getCurrentlyPlayingFeedMediaId()) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, - true); - editor.commit(); - } - if (PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == media - .getId()) { - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - } - } - if (AppConfig.DEBUG) - Log.d(TAG, "Deleting File. Result: " + result); - return result; - } - - /** Remove a feed with all its items and media files and its image. */ - public void deleteFeed(final Context context, final Feed feed) { - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context.getApplicationContext()); - if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA - && PlaybackPreferences.getLastPlayedFeedId() == feed.getId()) { - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - SharedPreferences.Editor editor = prefs.edit(); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - -1); - editor.commit(); - } - - contentChanger.post(new Runnable() { - - @Override - public void run() { - feeds.remove(feed); - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - DownloadRequester requester = DownloadRequester - .getInstance(); - 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(); - } else if (requester.isDownloadingFile(feed - .getImage())) { - requester.cancelDownload(context, - feed.getImage()); - } - } - // delete stored media files and mark them as read - for (FeedItem item : feed.getItems()) { - if (item.getState() == FeedItem.State.NEW) { - unreadItems.remove(item); - } - if (queue.contains(item)) { - removeQueueItem(item, adapter); - } - removeItemFromPlaybackHistory(context, item); - if (item.getMedia() != null - && item.getMedia().isDownloaded()) { - File mediaFile = new File(item.getMedia() - .getFile_url()); - mediaFile.delete(); - } else if (item.getMedia() != null - && requester.isDownloadingFile(item - .getMedia())) { - requester.cancelDownload(context, - item.getMedia()); - } - } - - adapter.removeFeed(feed); - adapter.close(); - eventDist.sendFeedUpdateBroadcast(); - } - - }); - } - }); - - } - - /** - * Makes sure that playback history is sorted and is not larger than - * PLAYBACK_HISTORY_SIZE. - * - * @return an array of all feeditems that were remove from the playback - * history or null if no items were removed. - */ - private FeedItem[] cleanupPlaybackHistory() { - if (AppConfig.DEBUG) - Log.d(TAG, "Cleaning up playback history."); - - Collections.sort(playbackHistory, - new PlaybackCompletionDateComparator()); - final int initialSize = playbackHistory.size(); - if (initialSize > PLAYBACK_HISTORY_SIZE) { - FeedItem[] removed = new FeedItem[initialSize - - PLAYBACK_HISTORY_SIZE]; - - for (int i = 0; i < removed.length; i++) { - removed[i] = playbackHistory.remove(playbackHistory.size() - 1); - } - if (AppConfig.DEBUG) - Log.d(TAG, "Removed " + removed.length - + " items from playback history."); - return removed; - } - return null; - } - - /** - * Executes cleanupPlaybackHistory and deletes the playbackCompletionDate of - * all item that were removed from the history. - */ - private void cleanupPlaybackHistoryWithDBCleanup(final Context context) { - final FeedItem[] removedItems = cleanupPlaybackHistory(); - if (removedItems != null) { - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - for (FeedItem item : removedItems) { - if (item.getMedia() != null) { - item.getMedia().setPlaybackCompletionDate(null); - adapter.setMedia(item.getMedia()); - } - } - adapter.close(); - } - }); - } - } - - /** Removes all items from the playback history. */ - public void clearPlaybackHistory(final Context context) { - if (!playbackHistory.isEmpty()) { - if (AppConfig.DEBUG) - Log.d(TAG, "Clearing playback history."); - final FeedItem[] items = playbackHistory - .toArray(new FeedItem[playbackHistory.size()]); - playbackHistory.clear(); - eventDist.sendPlaybackHistoryUpdateBroadcast(); - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - for (FeedItem item : items) { - if (item.getMedia() != null - && item.getMedia().getPlaybackCompletionDate() != null) { - item.getMedia().setPlaybackCompletionDate(null); - adapter.setMedia(item.getMedia()); - } - } - adapter.close(); - } - }); - } - } - - /** Adds a FeedItem to the playback history. */ - public void addItemToPlaybackHistory(Context context, FeedItem item) { - if (item.getMedia() != null - && item.getMedia().getPlaybackCompletionDate() != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Adding new item to playback history"); - if (!playbackHistory.contains(item)) { - playbackHistory.add(item); - } - cleanupPlaybackHistoryWithDBCleanup(context); - eventDist.sendPlaybackHistoryUpdateBroadcast(); - } - } - - private void removeItemFromPlaybackHistory(Context context, FeedItem item) { - playbackHistory.remove(item); - eventDist.sendPlaybackHistoryUpdateBroadcast(); - } - - /** - * Sets the 'read'-attribute of a FeedItem. Should be used by all Classes - * instead of the setters of FeedItem. - */ - public void markItemRead(final Context context, final FeedItem item, - final boolean read, boolean resetMediaPosition) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting item with title " + item.getTitle() - + " as read/unread"); - - item.setRead(read); - if (item.hasMedia() && resetMediaPosition) { - item.getMedia().setPosition(0); - } - setFeedItem(context, item); - if (item.hasMedia() && resetMediaPosition) - setFeedMedia(context, item.getMedia()); - - contentChanger.post(new Runnable() { - - @Override - public void run() { - if (read == true) { - unreadItems.remove(item); - } else { - unreadItems.add(item); - Collections.sort(unreadItems, - new FeedItemPubdateComparator()); - } - eventDist.sendUnreadItemsUpdateBroadcast(); - } - }); - - } - - /** - * Sets the 'read' attribute of all FeedItems of a specific feed to true - */ - public void markFeedRead(Context context, Feed feed) { - for (FeedItem item : feed.getItems()) { - if (unreadItems.contains(item)) { - markItemRead(context, item, true, false); - } - } - } - - /** Marks all items in the unread items list as read */ - public void markAllItemsRead(final Context context) { - if (AppConfig.DEBUG) - Log.d(TAG, "marking all items as read"); - for (FeedItem item : unreadItems) { - item.setRead(true); - } - final ArrayList<FeedItem> unreadItemsCopy = new ArrayList<FeedItem>( - unreadItems); - unreadItems.clear(); - eventDist.sendUnreadItemsUpdateBroadcast(); - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - for (FeedItem item : unreadItemsCopy) { - setFeedItem(item, adapter); - if (item.hasMedia()) - setFeedMedia(context, item.getMedia()); - } - adapter.close(); - } - }); - - } - - /** Updates all feeds in the feed list. */ - public void refreshAllFeeds(final Context context) { - if (AppConfig.DEBUG) - Log.d(TAG, "Refreshing all feeds."); - refreshFeeds(context, feeds); - } - - /** Updates all feeds in the feed list. */ - public void refreshExpiredFeeds(final Context context) { - long millis = UserPreferences.getUpdateInterval(); - - if (AppConfig.DEBUG) - Log.d(TAG, "Refreshing expired feeds, " + millis + " ms"); - - if (millis > 0) { - List<Feed> feedList = new ArrayList<Feed>(); - long now = Calendar.getInstance().getTime().getTime(); - - // Allow a 10 minute window - millis -= 10 * 60 * 1000; - for (Feed feed : feeds) { - Date date = feed.getLastUpdate(); - if (date != null) { - if (date.getTime() + millis <= now) { - if (AppConfig.DEBUG) { - Log.d(TAG, "Adding expired feed " + feed.getTitle()); - } - feedList.add(feed); - } else { - if (AppConfig.DEBUG) { - Log.d(TAG, "Skipping feed " + feed.getTitle()); - } - } - } - } - if (feedList.size() > 0) { - refreshFeeds(context, feedList); - } - } - } - - @SuppressLint("NewApi") - private void refreshFeeds(final Context context, final List<Feed> feedList) { - if (!isStartingFeedRefresh) { - isStartingFeedRefresh = true; - AsyncTask<Void, Void, Void> updateWorker = new AsyncTask<Void, Void, Void>() { - - @Override - protected void onPostExecute(Void result) { - if (AppConfig.DEBUG) - Log.d(TAG, - "All feeds have been sent to the downloadmanager"); - isStartingFeedRefresh = false; - } - - @Override - protected Void doInBackground(Void... params) { - for (Feed feed : feedList) { - try { - refreshFeed(context, feed); - } catch (DownloadRequestException e) { - e.printStackTrace(); - addDownloadStatus( - context, - new DownloadStatus(feed, feed - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, - false, e.getMessage())); - } - } - return null; - } - - }; - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - updateWorker.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - updateWorker.execute(); - } - } - - } - - /** - * 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"); - try { - requester.downloadImage(context, image); - } catch (DownloadRequestException e) { - e.printStackTrace(); - Log.w(TAG, "Failed to download invalid feed image"); - } - } - - /** - * Notifies the feed manager that a downloaded episode doesn't exist - * anymore. It will update the values of the FeedMedia object accordingly. - */ - public void notifyMissingFeedMediaFile(Context context, FeedMedia media) { - Log.i(TAG, - "The feedmanager was notified about a missing episode. It will update its database now."); - media.setDownloaded(false); - media.setFile_url(null); - setFeedMedia(context, media); - eventDist.sendFeedUpdateBroadcast(); - } - - /** Updates a specific feed. */ - public void refreshFeed(Context context, Feed feed) - throws DownloadRequestException { - requester.downloadFeed(context, new Feed(feed.getDownload_url(), - new Date(), feed.getTitle())); - } - - /** Adds a download status object to the download log. */ - public void addDownloadStatus(final Context context, - final DownloadStatus status) { - contentChanger.post(new Runnable() { - - @Override - public void run() { - downloadLog.add(status); - Collections.sort(downloadLog, new DownloadStatusComparator()); - final DownloadStatus removedStatus; - if (downloadLog.size() > DOWNLOAD_LOG_SIZE) { - removedStatus = downloadLog.remove(downloadLog.size() - 1); - } else { - removedStatus = null; - } - eventDist.sendDownloadLogUpdateBroadcast(); - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - if (removedStatus != null) { - adapter.removeDownloadStatus(removedStatus); - } - adapter.setDownloadStatus(status); - adapter.close(); - } - }); - } - }); - - } - - /** Downloads all items in the queue that have not been downloaded yet. */ - public void downloadAllItemsInQueue(final Context context) { - if (!queue.isEmpty()) { - try { - downloadFeedItem(context, - queue.toArray(new FeedItem[queue.size()])); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } - } - } - - public void downloadFeedItem(final Context context, FeedItem... items) - throws DownloadRequestException { - downloadFeedItem(true, context, items); - } - - /** Downloads FeedItems if they have not been downloaded yet. */ - private void downloadFeedItem(boolean performAutoCleanup, - final Context context, final FeedItem... items) - throws DownloadRequestException { - if (performAutoCleanup) { - new Thread() { - - @Override - public void run() { - performAutoCleanup(context, - getPerformAutoCleanupArgs(items.length)); - } - - }.start(); - } - for (FeedItem item : items) { - if (item.getMedia() != null - && !requester.isDownloadingFile(item.getMedia()) - && !item.getMedia().isDownloaded()) { - if (items.length > 1) { - try { - requester.downloadMedia(context, item.getMedia()); - } catch (DownloadRequestException e) { - e.printStackTrace(); - addDownloadStatus(context, - new DownloadStatus(item.getMedia(), item - .getMedia() - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, - false, e.getMessage())); - } - } else { - requester.downloadMedia(context, item.getMedia()); - } - } - } - } - - /** - * This method will try to download undownloaded items in the queue or the - * unread items list. If not enough space is available, an episode cleanup - * will be performed first. - * - * This method will not try to download the currently playing item. - */ - public void autodownloadUndownloadedItems(Context context) { - if (AppConfig.DEBUG) - Log.d(TAG, "Performing auto-dl of undownloaded episodes"); - if (NetworkUtils.autodownloadNetworkAvailable(context) - && UserPreferences.isEnableAutodownload()) { - int undownloadedEpisodes = getNumberOfUndownloadedEpisodes(); - int downloadedEpisodes = getNumberOfDownloadedEpisodes(); - int deletedEpisodes = performAutoCleanup(context, - getPerformAutoCleanupArgs(undownloadedEpisodes)); - int episodeSpaceLeft = undownloadedEpisodes; - boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences - .getEpisodeCacheSizeUnlimited(); - - if (!cacheIsUnlimited - && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes - + undownloadedEpisodes) { - episodeSpaceLeft = UserPreferences.getEpisodeCacheSize() - - (downloadedEpisodes - deletedEpisodes); - } - - List<FeedItem> itemsToDownload = new ArrayList<FeedItem>(); - if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { - for (int i = 0; i < queue.size(); i++) { // ignore playing item - FeedItem item = queue.get(i); - if (item.hasMedia() && !item.getMedia().isDownloaded() - && !item.getMedia().isPlaying()) { - itemsToDownload.add(item); - episodeSpaceLeft--; - undownloadedEpisodes--; - if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { - break; - } - } - } - } - if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { - for (FeedItem item : unreadItems) { - if (item.hasMedia() && !item.getMedia().isDownloaded()) { - itemsToDownload.add(item); - episodeSpaceLeft--; - undownloadedEpisodes--; - if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { - break; - } - } - } - } - if (AppConfig.DEBUG) - Log.d(TAG, "Enqueueing " + itemsToDownload.size() - + " items for download"); - - try { - downloadFeedItem(false, context, - itemsToDownload.toArray(new FeedItem[itemsToDownload - .size()])); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } - - } - } - - /** - * This method will determine the number of episodes that have to be deleted - * depending on a given number of episodes. - * - * @return The argument that has to be passed to performAutoCleanup() so - * that the number of episodes fits into the episode cache. - * */ - private int getPerformAutoCleanupArgs(final int episodeNumber) { - if (episodeNumber >= 0 - && UserPreferences.getEpisodeCacheSize() != UserPreferences - .getEpisodeCacheSizeUnlimited()) { - int downloadedEpisodes = getNumberOfDownloadedEpisodes(); - if (downloadedEpisodes + episodeNumber >= UserPreferences - .getEpisodeCacheSize()) { - - return downloadedEpisodes + episodeNumber - - UserPreferences.getEpisodeCacheSize(); - } - } - return 0; - } - - /** - * Performs an auto-cleanup so that the number of downloaded episodes is - * below or equal to the episode cache size. The method will be executed in - * the caller's thread. - */ - public void performAutoCleanup(Context context) { - performAutoCleanup(context, getPerformAutoCleanupArgs(0)); - } - - /** - * This method will try to delete a given number of episodes. An episode - * will only be deleted if it is not in the queue. - * - * @return The number of episodes that were actually deleted - * */ - private int performAutoCleanup(Context context, final int episodeNumber) { - List<FeedItem> candidates = new ArrayList<FeedItem>(); - List<FeedItem> delete; - for (Feed feed : feeds) { - for (FeedItem item : feed.getItems()) { - if (item.hasMedia() && item.getMedia().isDownloaded() - && !isInQueue(item) && item.isRead()) { - candidates.add(item); - } - } - } - - Collections.sort(candidates, new Comparator<FeedItem>() { - @Override - public int compare(FeedItem lhs, FeedItem rhs) { - Date l = lhs.getMedia().getPlaybackCompletionDate(); - Date r = rhs.getMedia().getPlaybackCompletionDate(); - - if (l == null) { - l = new Date(0); - } - if (r == null) { - r = new Date(0); - } - return l.compareTo(r); - } - }); - - if (candidates.size() > episodeNumber) { - delete = candidates.subList(0, episodeNumber); - } else { - delete = candidates; - } - - for (FeedItem item : delete) { - deleteFeedMedia(context, item.getMedia()); - } - - int counter = delete.size(); - - if (AppConfig.DEBUG) - Log.d(TAG, String.format( - "Auto-delete deleted %d episodes (%d requested)", counter, - episodeNumber)); - - return counter; - } - - /** - * Counts items in the queue and the unread items list which haven't been - * downloaded yet. - * - * This method will not count the playing item - */ - private int getNumberOfUndownloadedEpisodes() { - int counter = 0; - for (FeedItem item : queue) { - if (item.hasMedia() && !item.getMedia().isDownloaded() - && !item.getMedia().isPlaying()) { - counter++; - } - } - for (FeedItem item : unreadItems) { - if (item.hasMedia() && !item.getMedia().isDownloaded()) { - counter++; - } - } - return counter; - - } - - /** Counts all downloaded items. */ - private int getNumberOfDownloadedEpisodes() { - int counter = 0; - for (Feed feed : feeds) { - for (FeedItem item : feed.getItems()) { - if (item.hasMedia() && item.getMedia().isDownloaded()) { - counter++; - } - } - } - if (AppConfig.DEBUG) - Log.d(TAG, "Number of downloaded episodes: " + counter); - return counter; - } - - /** - * Enqueues all items that are currently in the unreadItems list and marks - * them as 'read'. - */ - public void enqueueAllNewItems(final Context context) { - if (!unreadItems.isEmpty()) { - addQueueItem(context, - unreadItems.toArray(new FeedItem[unreadItems.size()])); - markAllItemsRead(context); - } - } - - /** - * Adds a feeditem to the queue at the specified index if it is not in the - * queue yet. The item is marked as 'read'. - */ - public void addQueueItemAt(final Context context, final FeedItem item, - final int index, final boolean performAutoDownload) { - contentChanger.post(new Runnable() { - - @Override - public void run() { - if (!queue.contains(item)) { - queue.add(index, item); - if (!item.isRead()) { - markItemRead(context, item, true, false); - } - } - eventDist.sendQueueUpdateBroadcast(); - - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setQueue(queue); - adapter.close(); - } - }); - if (performAutoDownload) { - new Thread() { - @Override - public void run() { - autodownloadUndownloadedItems(context); - } - }.start(); - } - } - }); - - } - - /** - * Adds FeedItems to the queue if they are not in the queue yet. The items - * are marked as 'read'. - */ - public void addQueueItem(final Context context, final FeedItem... items) { - if (items.length > 0) { - contentChanger.post(new Runnable() { - - @Override - public void run() { - for (FeedItem item : items) { - if (!queue.contains(item)) { - queue.add(item); - if (!item.isRead()) { - markItemRead(context, item, true, false); - } - } - } - eventDist.sendQueueUpdateBroadcast(); - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setQueue(queue); - adapter.close(); - } - }); - new Thread() { - @Override - public void run() { - autodownloadUndownloadedItems(context); - } - }.start(); - } - }); - } - - } - - /** - * Return the item that comes after this item in the queue or null if this - * item is not in the queue or if this item has no successor. - */ - public FeedItem getQueueSuccessorOfItem(FeedItem item) { - if (isInQueue(item)) { - int itemIndex = queue.indexOf(item); - if (itemIndex != -1 && itemIndex < (queue.size() - 1)) { - return queue.get(itemIndex + 1); - } - } - return null; - } - - /** Removes all items in queue */ - public void clearQueue(final Context context) { - if (AppConfig.DEBUG) - Log.d(TAG, "Clearing queue"); - Iterator<FeedItem> iter = queue.iterator(); - while (iter.hasNext()) { - FeedItem item = iter.next(); - if (item.getState() != FeedItem.State.PLAYING) { - iter.remove(); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, - "FeedItem is playing and is therefore not removed from the queue"); - } - } - eventDist.sendQueueUpdateBroadcast(); - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setQueue(queue); - adapter.close(); - } - }); - - } - - /** Removes a FeedItem from the queue. Uses external PodDBAdapter. */ - private void removeQueueItem(FeedItem item, PodDBAdapter adapter) { - boolean removed = queue.remove(item); - if (removed) { - adapter.setQueue(queue); - } - } - - /** Removes a FeedItem from the queue. */ - public void removeQueueItem(final Context context, FeedItem item, - final boolean performAutoDownload) { - boolean removed = queue.remove(item); - if (removed) { - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setQueue(queue); - adapter.close(); - } - }); - - } - if (performAutoDownload) { - new Thread() { - @Override - public void run() { - autodownloadUndownloadedItems(context); - } - }.start(); - } - eventDist.sendQueueUpdateBroadcast(); - } - - /** - * Moves the queue item at the specified index to another position. If the - * indices are out of range, no operation will be performed. - * - * @param from - * index of the item that is going to be moved - * @param to - * destination index of item - * @param broadcastUpdate - * true if the method should send a queue update broadcast after - * the operation has been performed. This should be set to false - * if the order of the queue is changed through drag & drop - * reordering to avoid visual glitches. - */ - public void moveQueueItem(final Context context, int from, int to, - boolean broadcastUpdate) { - if (AppConfig.DEBUG) - Log.d(TAG, "Moving queue item from index " + from + " to index " - + to); - if (from >= 0 && from < queue.size() && to >= 0 && to < queue.size()) { - FeedItem item = queue.remove(from); - queue.add(to, item); - dbExec.execute(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setQueue(queue); - adapter.close(); - } - }); - if (broadcastUpdate) { - eventDist.sendQueueUpdateBroadcast(); - } - } - } - - /** Returns true if the specified item is in the queue. */ - public boolean isInQueue(FeedItem item) { - return queue.contains(item); - } - - /** - * Returns the FeedItem at the beginning of the queue or null if the queue - * is empty. - */ - public FeedItem getFirstQueueItem() { - if (queue.isEmpty()) { - return null; - } else { - return queue.get(0); - } - } - - private void addNewFeed(final Context context, final Feed feed) { - contentChanger.post(new Runnable() { - - @Override - public void run() { - feeds.add(feed); - Collections.sort(feeds, new FeedtitleComparator()); - eventDist.sendFeedUpdateBroadcast(); - } - }); - setCompleteFeed(context, feed); - } - - /** - * 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(final Context context, final Feed newFeed) { - // Look up feed in the feedslist - final Feed savedFeed = searchFeedByIdentifyingValue(newFeed - .getIdentifyingValue()); - if (savedFeed == null) { - if (AppConfig.DEBUG) - Log.d(TAG, - "Found no existing Feed with title " - + newFeed.getTitle() + ". Adding as new one."); - // Add a new Feed - addNewFeed(context, newFeed); - return newFeed; - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "Feed with title " + newFeed.getTitle() - + " already exists. Syncing new with existing one."); - if (savedFeed.compareWithOther(newFeed)) { - if (AppConfig.DEBUG) - Log.d(TAG, - "Feed has updated attribute values. Updating old feed's attributes"); - savedFeed.updateFromOther(newFeed); - } - // Look for new or updated Items - for (int idx = 0; idx < newFeed.getItems().size(); idx++) { - final FeedItem item = newFeed.getItems().get(idx); - FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed, - item.getIdentifyingValue()); - if (oldItem == null) { - // item is new - final int i = idx; - item.setFeed(savedFeed); - contentChanger.post(new Runnable() { - @Override - public void run() { - savedFeed.getItems().add(i, item); - - } - }); - markItemRead(context, item, false, false); - } else { - oldItem.updateFromOther(item); - } - } - // update attributes - savedFeed.setLastUpdate(newFeed.getLastUpdate()); - savedFeed.setType(newFeed.getType()); - setCompleteFeed(context, savedFeed); - new Thread() { - @Override - public void run() { - autodownloadUndownloadedItems(context); - } - }.start(); - return savedFeed; - } - - } - - /** Get a Feed by its identifying value. */ - private Feed searchFeedByIdentifyingValue(String identifier) { - for (Feed feed : feeds) { - if (feed.getIdentifyingValue().equals(identifier)) { - return feed; - } - } - return null; - } - - /** - * Returns true if a feed with the given download link is already in the - * feedlist. - */ - public boolean feedExists(String downloadUrl) { - for (Feed feed : feeds) { - if (feed.getDownload_url().equals(downloadUrl)) { - return true; - } - } - return false; - } - - /** Get a FeedItem by its identifying value. */ - private FeedItem searchFeedItemByIdentifyingValue(Feed feed, - String identifier) { - for (FeedItem item : feed.getItems()) { - if (item.getIdentifyingValue().equals(identifier)) { - return item; - } - } - return null; - } - - /** Updates Information of an existing Feed. Uses external adapter. */ - private void setFeed(Feed feed, PodDBAdapter adapter) { - if (adapter != null) { - adapter.setFeed(feed); - feed.cacheDescriptionsOfItems(); - } else { - Log.w(TAG, "Adapter in setFeed was null"); - } - } - - /** Updates Information of an existing Feeditem. Uses external adapter. */ - private void setFeedItem(FeedItem item, PodDBAdapter adapter) { - if (adapter != null) { - adapter.setSingleFeedItem(item); - } else { - Log.w(TAG, "Adapter in setFeedItem was null"); - } - } - - /** Updates Information of an existing Feedimage. Uses external adapter. */ - private void setFeedImage(FeedImage image, PodDBAdapter adapter) { - if (adapter != null) { - adapter.setImage(image); - } else { - Log.w(TAG, "Adapter in setFeedImage was null"); - } - } - - /** - * Updates Information of an existing Feedmedia object. Uses external - * adapter. - */ - private void setFeedImage(FeedMedia media, PodDBAdapter adapter) { - if (adapter != null) { - adapter.setMedia(media); - } else { - Log.w(TAG, "Adapter in setFeedMedia was null"); - } - } - - /** - * Updates Information of an existing Feed. Creates and opens its own - * adapter. - */ - public void setFeed(final Context context, final Feed feed) { - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeed(feed); - feed.cacheDescriptionsOfItems(); - adapter.close(); - } - }); - - } - - /** - * Updates Information of an existing Feed and its FeedItems. Creates and - * opens its own adapter. - */ - public void setCompleteFeed(final Context context, final Feed feed) { - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setCompleteFeed(feed); - feed.cacheDescriptionsOfItems(); - adapter.close(); - } - }); - - } - - /** - * Updates information of an existing FeedItem. Creates and opens its own - * adapter. - */ - public void setFeedItem(final Context context, final FeedItem item) { - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setSingleFeedItem(item); - adapter.close(); - } - }); - - } - - /** - * Updates information of an existing FeedImage. Creates and opens its own - * adapter. - */ - public void setFeedImage(final Context context, final FeedImage image) { - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setImage(image); - adapter.close(); - } - }); - - } - - /** - * Updates information of an existing FeedMedia object. Creates and opens - * its own adapter. - */ - public void setFeedMedia(final Context context, final FeedMedia media) { - dbExec.execute(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setMedia(media); - adapter.close(); - } - }); - - } - - /** 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) { - if (feed != null) { - 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 FeedItem by its id and the id of its feed. */ - public FeedItem getFeedItem(long itemId, long feedId) { - Feed feed = getFeed(feedId); - if (feed != null && feed.getItems() != null) { - for (FeedItem item : feed.getItems()) { - if (item.getId() == itemId) { - return item; - } - } - } - 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() != null && 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; - } - - /** Get a download status object from the download log by its FeedFile. */ - public DownloadStatus getDownloadStatus(FeedFile feedFile) { - for (DownloadStatus status : downloadLog) { - if (status.getFeedFile() == feedFile) { - return status; - } - } - return null; - } - - /** Reads the database */ - public void loadDBData(Context context) { - feeds.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()); - cleanupPlaybackHistory(); - } - - private void extractFeedlistFromCursor(Context context, PodDBAdapter adapter) { - if (AppConfig.DEBUG) - Log.d(TAG, "Extracting Feedlist"); - Cursor feedlistCursor = adapter.getAllFeedsCursor(); - if (feedlistCursor.moveToFirst()) { - do { - Date lastUpdate = new Date( - feedlistCursor - .getLong(PodDBAdapter.KEY_LAST_UPDATE_INDEX)); - Feed feed = new Feed(lastUpdate); - - feed.id = feedlistCursor.getLong(PodDBAdapter.KEY_ID_INDEX); - feed.setTitle(feedlistCursor - .getString(PodDBAdapter.KEY_TITLE_INDEX)); - feed.setLink(feedlistCursor - .getString(PodDBAdapter.KEY_LINK_INDEX)); - feed.setDescription(feedlistCursor - .getString(PodDBAdapter.KEY_DESCRIPTION_INDEX)); - feed.setPaymentLink(feedlistCursor - .getString(PodDBAdapter.KEY_PAYMENT_LINK_INDEX)); - feed.setAuthor(feedlistCursor - .getString(PodDBAdapter.KEY_AUTHOR_INDEX)); - feed.setLanguage(feedlistCursor - .getString(PodDBAdapter.KEY_LANGUAGE_INDEX)); - feed.setType(feedlistCursor - .getString(PodDBAdapter.KEY_TYPE_INDEX)); - feed.setFeedIdentifier(feedlistCursor - .getString(PodDBAdapter.KEY_FEED_IDENTIFIER_INDEX)); - long imageIndex = feedlistCursor - .getLong(PodDBAdapter.KEY_IMAGE_INDEX); - if (imageIndex != 0) { - feed.setImage(adapter.getFeedImage(imageIndex)); - feed.getImage().setFeed(feed); - } - feed.file_url = feedlistCursor - .getString(PodDBAdapter.KEY_FILE_URL_INDEX); - feed.download_url = feedlistCursor - .getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX); - feed.setDownloaded(feedlistCursor - .getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 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) { - if (AppConfig.DEBUG) - Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle()); - ArrayList<FeedItem> items = new ArrayList<FeedItem>(); - ArrayList<String> mediaIds = new ArrayList<String>(); - - if (itemlistCursor.moveToFirst()) { - do { - FeedItem item = new FeedItem(); - - item.id = itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_ID); - item.setFeed(feed); - item.setTitle(itemlistCursor - .getString(PodDBAdapter.IDX_FI_SMALL_TITLE)); - item.setLink(itemlistCursor - .getString(PodDBAdapter.IDX_FI_SMALL_LINK)); - item.setPubDate(new Date(itemlistCursor - .getLong(PodDBAdapter.IDX_FI_SMALL_PUBDATE))); - item.setPaymentLink(itemlistCursor - .getString(PodDBAdapter.IDX_FI_SMALL_PAYMENT_LINK)); - long mediaId = itemlistCursor - .getLong(PodDBAdapter.IDX_FI_SMALL_MEDIA); - if (mediaId != 0) { - mediaIds.add(String.valueOf(mediaId)); - item.setMedia(new FeedMedia(mediaId, item)); - } - item.setRead((itemlistCursor - .getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0) ? true - : false); - item.setItemIdentifier(itemlistCursor - .getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER)); - if (item.getState() == FeedItem.State.NEW) { - unreadItems.add(item); - } - - // extract chapters - boolean hasSimpleChapters = itemlistCursor - .getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0; - if (hasSimpleChapters) { - Cursor chapterCursor = adapter - .getSimpleChaptersOfFeedItemCursor(item); - if (chapterCursor.moveToFirst()) { - item.setChapters(new ArrayList<Chapter>()); - do { - int chapterType = chapterCursor - .getInt(PodDBAdapter.KEY_CHAPTER_TYPE_INDEX); - Chapter chapter = null; - long start = chapterCursor - .getLong(PodDBAdapter.KEY_CHAPTER_START_INDEX); - String title = chapterCursor - .getString(PodDBAdapter.KEY_TITLE_INDEX); - String link = chapterCursor - .getString(PodDBAdapter.KEY_CHAPTER_LINK_INDEX); - - switch (chapterType) { - case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER: - chapter = new SimpleChapter(start, title, item, - link); - break; - case ID3Chapter.CHAPTERTYPE_ID3CHAPTER: - chapter = new ID3Chapter(start, title, item, - link); - break; - case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER: - chapter = new VorbisCommentChapter(start, - title, item, link); - break; - } - chapter.setId(chapterCursor - .getLong(PodDBAdapter.KEY_ID_INDEX)); - item.getChapters().add(chapter); - } while (chapterCursor.moveToNext()); - } - chapterCursor.close(); - } - items.add(item); - } while (itemlistCursor.moveToNext()); - } - extractMediafromFeedItemlist(adapter, items, mediaIds); - Collections.sort(items, new FeedItemPubdateComparator()); - return items; - } - - private void extractMediafromFeedItemlist(PodDBAdapter adapter, - ArrayList<FeedItem> items, ArrayList<String> mediaIds) { - ArrayList<FeedItem> itemsCopy = new ArrayList<FeedItem>(items); - Cursor cursor = adapter.getFeedMediaCursor(mediaIds - .toArray(new String[mediaIds.size()])); - if (cursor.moveToFirst()) { - do { - long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); - // find matching feed item - FeedItem item = getMatchingItemForMedia(mediaId, itemsCopy); - itemsCopy.remove(item); - if (item != null) { - Date playbackCompletionDate = null; - long playbackCompletionTime = cursor - .getLong(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE_INDEX); - if (playbackCompletionTime > 0) { - playbackCompletionDate = new Date( - playbackCompletionTime); - } - - item.setMedia(new FeedMedia( - mediaId, - item, - cursor.getInt(PodDBAdapter.KEY_DURATION_INDEX), - cursor.getInt(PodDBAdapter.KEY_POSITION_INDEX), - cursor.getLong(PodDBAdapter.KEY_SIZE_INDEX), - cursor.getString(PodDBAdapter.KEY_MIME_TYPE_INDEX), - cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX), - cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX), - cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0, - playbackCompletionDate)); - if (playbackCompletionDate != null) { - playbackHistory.add(item); - } - - } - } while (cursor.moveToNext()); - cursor.close(); - } - } - - private FeedItem getMatchingItemForMedia(long mediaId, - ArrayList<FeedItem> items) { - for (FeedItem item : items) { - if (item.getMedia() != null && item.getMedia().getId() == mediaId) { - return item; - } - } - return null; - } - - private void extractDownloadLogFromCursor(Context context, - PodDBAdapter adapter) { - if (AppConfig.DEBUG) - Log.d(TAG, "Extracting DownloadLog"); - Cursor logCursor = adapter.getDownloadLogCursor(); - if (logCursor.moveToFirst()) { - do { - long id = logCursor.getLong(PodDBAdapter.KEY_ID_INDEX); - FeedFile feedfile = null; - - long feedfileId = logCursor - .getLong(PodDBAdapter.KEY_FEEDFILE_INDEX); - int feedfileType = logCursor - .getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX); - if (feedfileId != 0) { - switch (feedfileType) { - case Feed.FEEDFILETYPE_FEED: - feedfile = getFeed(feedfileId); - break; - case FeedImage.FEEDFILETYPE_FEEDIMAGE: - feedfile = getFeedImage(feedfileId); - break; - case FeedMedia.FEEDFILETYPE_FEEDMEDIA: - feedfile = getFeedMedia(feedfileId); - } - } - boolean successful = logCursor - .getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0; - int reason = logCursor.getInt(PodDBAdapter.KEY_REASON_INDEX); - String reasonDetailed = logCursor - .getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX); - String title = logCursor - .getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX); - Date completionDate = new Date( - logCursor - .getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX)); - downloadLog.add(new DownloadStatus(id, title, feedfile, - feedfileType, successful, reason, completionDate, - reasonDetailed)); - - } while (logCursor.moveToNext()); - } - logCursor.close(); - Collections.sort(downloadLog, new DownloadStatusComparator()); - } - - private void extractQueueFromCursor(Context context, PodDBAdapter adapter) { - if (AppConfig.DEBUG) - Log.d(TAG, "Extracting Queue"); - Cursor cursor = adapter.getQueueCursor(); - - // Sort cursor results by ID with TreeMap - TreeMap<Integer, FeedItem> map = new TreeMap<Integer, FeedItem>(); - - if (cursor.moveToFirst()) { - do { - int index = cursor.getInt(PodDBAdapter.KEY_ID_INDEX); - Feed feed = getFeed(cursor - .getLong(PodDBAdapter.KEY_QUEUE_FEED_INDEX)); - if (feed != null) { - FeedItem item = getFeedItem( - cursor.getLong(PodDBAdapter.KEY_FEEDITEM_INDEX), - feed); - if (item != null) { - map.put(index, item); - } - } - } while (cursor.moveToNext()); - } - cursor.close(); - - for (Map.Entry<Integer, FeedItem> entry : map.entrySet()) { - FeedItem item = entry.getValue(); - queue.add(item); - } - } - - /** - * Loads description and contentEncoded values from the database and caches - * it in the feeditem. The task callback will contain a String-array with - * the description at index 0 and the value of contentEncoded at index 1. - */ - public void loadExtraInformationOfItem(final Context context, - final FeedItem item, FeedManager.TaskCallback<String[]> callback) { - if (AppConfig.DEBUG) { - Log.d(TAG, - "Loading extra information of item with id " + item.getId()); - if (item.getTitle() != null) { - Log.d(TAG, "Title: " + item.getTitle()); - } - } - dbExec.execute(new FeedManager.Task<String[]>(new Handler(), callback) { - - @Override - public void execute() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - Cursor extraCursor = adapter.getExtraInformationOfItem(item); - if (extraCursor.moveToFirst()) { - String description = extraCursor - .getString(PodDBAdapter.IDX_FI_EXTRA_DESCRIPTION); - String contentEncoded = extraCursor - .getString(PodDBAdapter.IDX_FI_EXTRA_CONTENT_ENCODED); - item.setCachedDescription(description); - item.setCachedContentEncoded(contentEncoded); - setResult(new String[] { description, contentEncoded }); - } - adapter.close(); - } - }); - } - - /** - * Searches the descriptions of FeedItems of a specific feed for a given - * string. - * - * @param feed - * The feed whose items should be searched. - * @param query - * The search string - * @param callback - * A callback which will be used to return the search result - * */ - public void searchFeedItemDescription(final Context context, - final Feed feed, final String query, - FeedManager.QueryTaskCallback callback) { - dbExec.execute(new FeedManager.QueryTask(context, new Handler(), - callback) { - - @Override - public void execute(PodDBAdapter adapter) { - Cursor searchResult = adapter.searchItemDescriptions(feed, - query); - setResult(searchResult); - } - }); - } - - /** - * Searches the 'contentEncoded' field of FeedItems of a specific feed for a - * given string. - * - * @param feed - * The feed whose items should be searched. - * @param query - * The search string - * @param callback - * A callback which will be used to return the search result - * */ - public void searchFeedItemContentEncoded(final Context context, - final Feed feed, final String query, - FeedManager.QueryTaskCallback callback) { - dbExec.execute(new FeedManager.QueryTask(context, new Handler(), - callback) { - - @Override - public void execute(PodDBAdapter adapter) { - Cursor searchResult = adapter.searchItemContentEncoded(feed, - query); - setResult(searchResult); - } - }); - } - - /** Returns the number of feeds that are currently in the feeds list. */ - public int getFeedsSize() { - return feeds.size(); - } - - /** Returns the feed at the specified index of the feeds list. */ - public Feed getFeedAtIndex(int index) { - return feeds.get(index); - } - - /** Returns an array that contains all feeds of the feed manager. */ - public Feed[] getFeedsArray() { - return feeds.toArray(new Feed[feeds.size()]); - } - - List<Feed> getFeeds() { - return feeds; - } - - /** - * Returns the number of items that are currently in the queue. - * - * @param enableEpisodeFilter - * true if items without episodes should be ignored by this - * method if the episode filter was enabled by the user. - * */ - public int getQueueSize(boolean enableEpisodeFilter) { - if (UserPreferences.isDisplayOnlyEpisodes() && enableEpisodeFilter) { - return EpisodeFilter.countItemsWithEpisodes(queue); - } else { - return queue.size(); - } - } - - /** - * Returns the FeedItem at the specified index of the queue. - * - * @param enableEpisodeFilter - * true if items without episodes should be ignored by this - * method if the episode filter was enabled by the user. - * - * @throws IndexOutOfBoundsException - * if index is out of range - * */ - public FeedItem getQueueItemAtIndex(int index, boolean enableEpisodeFilter) { - if (UserPreferences.isDisplayOnlyEpisodes() && enableEpisodeFilter) { - return EpisodeFilter.accessEpisodeByIndex(queue, index); - } else { - return queue.get(index); - } - } - - /** - * Returns the index of the episode that is currently being played in the - * queue or -1 if the queue is empty or no episode in the queue is being - * played. - * */ - public int getQueuePlayingEpisodeIndex() { - FeedManager manager = FeedManager.getInstance(); - int queueSize = manager.getQueueSize(true); - if (queueSize == 0) { - return -1; - } else { - for (int x = 0; x < queueSize; x++) { - FeedItem item = getQueueItemAtIndex(x, true); - if (item.getState() == FeedItem.State.PLAYING) { - return x; - } - } - return -1; - } - } - - /** - * Returns the number of unread items. - * - * @param enableEpisodeFilter - * true if items without episodes should be ignored by this - * method if the episode filter was enabled by the user. - * */ - public int getUnreadItemsSize(boolean enableEpisodeFilter) { - if (UserPreferences.isDisplayOnlyEpisodes() && enableEpisodeFilter) { - return EpisodeFilter.countItemsWithEpisodes(unreadItems); - } else { - return unreadItems.size(); - } - } - - /** - * Returns the FeedItem at the specified index of the unread items list. - * - * @param enableEpisodeFilter - * true if items without episodes should be ignored by this - * method if the episode filter was enabled by the user. - * - * @throws IndexOutOfBoundsException - * if index is out of range - * */ - public FeedItem getUnreadItemAtIndex(int index, boolean enableEpisodeFilter) { - if (UserPreferences.isDisplayOnlyEpisodes() && enableEpisodeFilter) { - return EpisodeFilter.accessEpisodeByIndex(unreadItems, index); - } else { - return unreadItems.get(index); - } - } - - /** - * Returns the number of items in the playback history. - * */ - public int getPlaybackHistorySize() { - return playbackHistory.size(); - } - - /** - * Returns the FeedItem at the specified index of the playback history. - * - * @throws IndexOutOfBoundsException - * if index is out of range - * */ - public FeedItem getPlaybackHistoryItemIndex(int index) { - return playbackHistory.get(index); - } - - /** Returns the number of items in the download log */ - public int getDownloadLogSize() { - return downloadLog.size(); - } - - /** Returns the download status at the specified index of the download log. */ - public DownloadStatus getDownloadStatusFromLogAtIndex(int index) { - return downloadLog.get(index); - } - - /** Is called by a FeedManagerTask after completion. */ - public interface TaskCallback<V> { - void onCompletion(V result); - } - - /** Is called by a FeedManager.QueryTask after completion. */ - public interface QueryTaskCallback { - void handleResult(Cursor result); - - void onCompletion(); - } - - /** A runnable that can post a callback to a handler after completion. */ - abstract class Task<V> implements Runnable { - private Handler handler; - private TaskCallback<V> callback; - private V result; - - /** - * Standard contructor. No callbacks are going to be posted to a - * handler. - */ - public Task() { - super(); - } - - /** - * The Task will post a Runnable to 'handler' that will execute the - * 'callback' after completion. - */ - public Task(Handler handler, TaskCallback<V> callback) { - super(); - this.handler = handler; - this.callback = callback; - } - - @Override - public final void run() { - execute(); - if (handler != null && callback != null) { - handler.post(new Runnable() { - @Override - public void run() { - callback.onCompletion(result); - } - }); - } - } - - /** This method will be executed in the same thread as the run() method. */ - public abstract void execute(); - - public void setResult(V result) { - this.result = result; - } - } - - /** - * A runnable which should be used for database queries. The onCompletion - * method is executed on the database executor to handle Cursors correctly. - * This class automatically creates a PodDBAdapter object and closes it when - * it is no longer in use. - */ - abstract class QueryTask implements Runnable { - private QueryTaskCallback callback; - private Cursor result; - private Context context; - private Handler handler; - - public QueryTask(Context context, Handler handler, - QueryTaskCallback callback) { - this.callback = callback; - this.context = context; - this.handler = handler; - } - - @Override - public final void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - execute(adapter); - callback.handleResult(result); - if (result != null && !result.isClosed()) { - result.close(); - } - adapter.close(); - if (handler != null && callback != null) { - handler.post(new Runnable() { - - @Override - public void run() { - callback.onCompletion(); - } - - }); - } - } - - public abstract void execute(PodDBAdapter adapter); - - protected void setResult(Cursor c) { - result = c; - } - } - -} diff --git a/src/de/danoeh/antennapod/feed/FeedMedia.java b/src/de/danoeh/antennapod/feed/FeedMedia.java index 1368cf854..2e0eac7a4 100644 --- a/src/de/danoeh/antennapod/feed/FeedMedia.java +++ b/src/de/danoeh/antennapod/feed/FeedMedia.java @@ -4,6 +4,7 @@ import java.io.FileInputStream; import java.io.InputStream; import java.util.Date; import java.util.List; +import java.util.concurrent.Callable; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; @@ -11,358 +12,383 @@ import android.os.Parcel; import android.os.Parcelable; import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.preferences.PlaybackPreferences; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.util.ChapterUtils; import de.danoeh.antennapod.util.playback.Playable; public class FeedMedia extends FeedFile implements Playable { - public static final int FEEDFILETYPE_FEEDMEDIA = 2; - public static final int PLAYABLE_TYPE_FEEDMEDIA = 1; - - public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId"; - public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId"; - - private int duration; - private int position; // Current position in file - private long size; // File size in Byte - private String mime_type; - private FeedItem item; - private Date playbackCompletionDate; - - 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, Date playbackCompletionDate) { - 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; - this.playbackCompletionDate = playbackCompletionDate; - } - - public FeedMedia(long id, FeedItem item) { - super(); - this.id = id; - this.item = item; - } - - @Override - public String getHumanReadableIdentifier() { - if (item != null && item.getTitle() != null) { - return item.getTitle(); - } else { - return download_url; - } - } - - /** Uses mimetype to determine the type of media. */ - public MediaType getMediaType() { - if (mime_type == null || mime_type.isEmpty()) { - return MediaType.UNKNOWN; - } else { - if (mime_type.startsWith("audio")) { - return MediaType.AUDIO; - } else if (mime_type.startsWith("video")) { - return MediaType.VIDEO; - } else if (mime_type.equals("application/ogg")) { - return MediaType.AUDIO; - } - } - return MediaType.UNKNOWN; - } - - public void updateFromOther(FeedMedia other) { - super.updateFromOther(other); - if (other.size > 0) { - size = other.size; - } - if (other.mime_type != null) { - mime_type = other.mime_type; - } - } - - public boolean compareWithOther(FeedMedia other) { - if (super.compareWithOther(other)) { - return true; - } - if (other.mime_type != null) { - if (mime_type == null || !mime_type.equals(other.mime_type)) { - return true; - } - } - if (other.size > 0 && other.size != size) { - return true; - } - return false; - } - - /** - * Reads playback preferences to determine whether this FeedMedia object is - * currently being played. - */ - public boolean isPlaying() { - return PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA - && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == id; - } - - @Override - public int getTypeAsInt() { - return FEEDFILETYPE_FEEDMEDIA; - } - - 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; - } - - public Date getPlaybackCompletionDate() { - return playbackCompletionDate; - } - - public void setPlaybackCompletionDate(Date playbackCompletionDate) { - this.playbackCompletionDate = playbackCompletionDate; - } - - public boolean isInProgress() { - return (this.position > 0); - } - - public FeedImage getImage() { - if (item != null && item.getFeed() != null) { - return item.getFeed().getImage(); - } - return null; - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeLong(item.getFeed().getId()); - dest.writeLong(item.getId()); - } - - @Override - public void writeToPreferences(Editor prefEditor) { - prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId()); - prefEditor.putLong(PREF_MEDIA_ID, id); - } - - @Override - public void loadMetadata() throws PlayableException { - } - - @Override - public void loadChapterMarks() { - if (getChapters() == null && !localFileAvailable()) { - ChapterUtils.loadChaptersFromStreamUrl(this); - if (getChapters() != null) { - FeedManager.getInstance().setFeedItem(PodcastApp.getInstance(), - item); - } - } - - } - - @Override - public String getEpisodeTitle() { - if (getItem().getTitle() != null) { - return getItem().getTitle(); - } else { - return getItem().getIdentifyingValue(); - } - } - - @Override - public List<Chapter> getChapters() { - return getItem().getChapters(); - } - - @Override - public String getWebsiteLink() { - return getItem().getLink(); - } - - @Override - public String getFeedTitle() { - return getItem().getFeed().getTitle(); - } - - @Override - public Object getIdentifier() { - return id; - } - - @Override - public String getLocalMediaUrl() { - return file_url; - } - - @Override - public String getStreamUrl() { - return download_url; - } - - @Override - public boolean localFileAvailable() { - return isDownloaded() && file_url != null; - } - - @Override - public boolean streamAvailable() { - return download_url != null; - } - - @Override - public void saveCurrentPosition(SharedPreferences pref, int newPosition) { - position = newPosition; - FeedManager.getInstance().setFeedMedia(PodcastApp.getInstance(), this); - } - - @Override - public void onPlaybackStart() { - } - - @Override - public void onPlaybackCompleted() { - - } - - @Override - public int getPlayableType() { - return PLAYABLE_TYPE_FEEDMEDIA; - } - - @Override - public void setChapters(List<Chapter> chapters) { - getItem().setChapters(chapters); - } - - @Override - public String getPaymentLink() { - return getItem().getPaymentLink(); - } - - @Override - public void loadShownotes(final ShownoteLoaderCallback callback) { - String contentEncoded = item.getContentEncoded(); - if (item.getDescription() == null || contentEncoded == null) { - FeedManager.getInstance().loadExtraInformationOfItem( - PodcastApp.getInstance(), item, - new FeedManager.TaskCallback<String[]>() { - @Override - public void onCompletion(String[] result) { - if (result[1] != null) { - callback.onShownotesLoaded(result[1]); - } else { - callback.onShownotesLoaded(result[0]); - - } - - } - }); - } else { - callback.onShownotesLoaded(contentEncoded); - } - } - - public static final Parcelable.Creator<FeedMedia> CREATOR = new Parcelable.Creator<FeedMedia>() { - public FeedMedia createFromParcel(Parcel in) { - long feedId = in.readLong(); - long itemId = in.readLong(); - FeedItem item = FeedManager.getInstance().getFeedItem(itemId, - feedId); - if (item != null) { - return item.getMedia(); - } else { - return null; - } - } - - public FeedMedia[] newArray(int size) { - return new FeedMedia[size]; - } - }; - - @Override - public InputStream openImageInputStream() { - InputStream out = new Playable.DefaultPlayableImageLoader(this) - .openImageInputStream(); - if (out == null) { - if (item.getFeed().getImage() != null) { - return item.getFeed().getImage().openImageInputStream(); - } - } - return out; - } - - @Override - public String getImageLoaderCacheKey() { - String out = new Playable.DefaultPlayableImageLoader(this) - .getImageLoaderCacheKey(); - if (out == null) { - if (item.getFeed().getImage() != null) { - return item.getFeed().getImage().getImageLoaderCacheKey(); - } - } - return out; - } - - @Override - public InputStream reopenImageInputStream(InputStream input) { - if (input instanceof FileInputStream) { - return item.getFeed().getImage().reopenImageInputStream(input); - } else { - return new Playable.DefaultPlayableImageLoader(this) - .reopenImageInputStream(input); - } - } + public static final int FEEDFILETYPE_FEEDMEDIA = 2; + public static final int PLAYABLE_TYPE_FEEDMEDIA = 1; + + public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId"; + public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId"; + + private int duration; + private int position; // Current position in file + private long size; // File size in Byte + private String mime_type; + private volatile FeedItem item; + private Date playbackCompletionDate; + + /* Used for loading item when restoring from parcel. */ + private long itemID; + + 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, Date playbackCompletionDate) { + 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; + this.playbackCompletionDate = playbackCompletionDate; + } + + public FeedMedia(long id, FeedItem item) { + super(); + this.id = id; + this.item = item; + } + + @Override + public String getHumanReadableIdentifier() { + if (item != null && item.getTitle() != null) { + return item.getTitle(); + } else { + return download_url; + } + } + + /** + * Uses mimetype to determine the type of media. + */ + public MediaType getMediaType() { + if (mime_type == null || mime_type.isEmpty()) { + return MediaType.UNKNOWN; + } else { + if (mime_type.startsWith("audio")) { + return MediaType.AUDIO; + } else if (mime_type.startsWith("video")) { + return MediaType.VIDEO; + } else if (mime_type.equals("application/ogg")) { + return MediaType.AUDIO; + } + } + return MediaType.UNKNOWN; + } + + public void updateFromOther(FeedMedia other) { + super.updateFromOther(other); + if (other.size > 0) { + size = other.size; + } + if (other.mime_type != null) { + mime_type = other.mime_type; + } + } + + public boolean compareWithOther(FeedMedia other) { + if (super.compareWithOther(other)) { + return true; + } + if (other.mime_type != null) { + if (mime_type == null || !mime_type.equals(other.mime_type)) { + return true; + } + } + if (other.size > 0 && other.size != size) { + return true; + } + return false; + } + + /** + * Reads playback preferences to determine whether this FeedMedia object is + * currently being played. + */ + public boolean isPlaying() { + return PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA + && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == id; + } + + @Override + public int getTypeAsInt() { + return FEEDFILETYPE_FEEDMEDIA; + } + + 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; + } + + public Date getPlaybackCompletionDate() { + return playbackCompletionDate; + } + + public void setPlaybackCompletionDate(Date playbackCompletionDate) { + this.playbackCompletionDate = playbackCompletionDate; + } + + public boolean isInProgress() { + return (this.position > 0); + } + + public FeedImage getImage() { + if (item != null && item.getFeed() != null) { + return item.getFeed().getImage(); + } + return null; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(id); + dest.writeLong(item.getId()); + + dest.writeInt(duration); + dest.writeInt(position); + dest.writeLong(size); + dest.writeString(mime_type); + dest.writeString(file_url); + dest.writeString(download_url); + dest.writeByte((byte) ((downloaded) ? 1 : 0)); + dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0); + } + + @Override + public void writeToPreferences(Editor prefEditor) { + prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId()); + prefEditor.putLong(PREF_MEDIA_ID, id); + } + + @Override + public void loadMetadata() throws PlayableException { + if (item == null && itemID != 0) { + item = DBReader.getFeedItem(PodcastApp.getInstance(), itemID); + } + } + + @Override + public void loadChapterMarks() { + if (getChapters() == null && !localFileAvailable()) { + ChapterUtils.loadChaptersFromStreamUrl(this); + if (getChapters() != null && item != null) { + DBWriter.setFeedItem(PodcastApp.getInstance(), + item); + } + } + + } + + @Override + public String getEpisodeTitle() { + if (item == null) { + return null; + } + if (getItem().getTitle() != null) { + return getItem().getTitle(); + } else { + return getItem().getIdentifyingValue(); + } + } + + @Override + public List<Chapter> getChapters() { + if (item == null) { + return null; + } + return getItem().getChapters(); + } + + @Override + public String getWebsiteLink() { + if (item == null) { + return null; + } + return getItem().getLink(); + } + + @Override + public String getFeedTitle() { + if (item == null) { + return null; + } + return getItem().getFeed().getTitle(); + } + + @Override + public Object getIdentifier() { + return id; + } + + @Override + public String getLocalMediaUrl() { + return file_url; + } + + @Override + public String getStreamUrl() { + return download_url; + } + + @Override + public String getPaymentLink() { + if (item == null) { + return null; + } + return getItem().getPaymentLink(); + } + + @Override + public boolean localFileAvailable() { + return isDownloaded() && file_url != null; + } + + @Override + public boolean streamAvailable() { + return download_url != null; + } + + @Override + public void saveCurrentPosition(SharedPreferences pref, int newPosition) { + position = newPosition; + DBWriter.setFeedMediaPosition(PodcastApp.getInstance(), this); + } + + @Override + public void onPlaybackStart() { + } + + @Override + public void onPlaybackCompleted() { + + } + + @Override + public int getPlayableType() { + return PLAYABLE_TYPE_FEEDMEDIA; + } + + @Override + public void setChapters(List<Chapter> chapters) { + getItem().setChapters(chapters); + } + + @Override + public Callable<String> loadShownotes() { + return new Callable<String>() { + @Override + public String call() throws Exception { + if (item == null) { + item = DBReader.getFeedItem(PodcastApp.getInstance(), itemID); + } + if (item.getContentEncoded() == null || item.getDescription() == null) { + DBReader.loadExtraInformationOfFeedItem(PodcastApp.getInstance(), item); + + } + return (item.getContentEncoded() != null) ? item.getContentEncoded() : item.getDescription(); + } + }; + } + + public static final Parcelable.Creator<FeedMedia> CREATOR = new Parcelable.Creator<FeedMedia>() { + public FeedMedia createFromParcel(Parcel in) { + final long id = in.readLong(); + final long itemID = in.readLong(); + FeedMedia result = new FeedMedia(id, null, in.readInt(), in.readInt(), in.readLong(), in.readString(), in.readString(), + in.readString(), in.readByte() != 0, new Date(in.readLong())); + result.itemID = itemID; + return result; + } + + public FeedMedia[] newArray(int size) { + return new FeedMedia[size]; + } + }; + + @Override + public InputStream openImageInputStream() { + InputStream out = new Playable.DefaultPlayableImageLoader(this) + .openImageInputStream(); + if (out == null) { + if (item.getFeed().getImage() != null) { + return item.getFeed().getImage().openImageInputStream(); + } + } + return out; + } + + @Override + public String getImageLoaderCacheKey() { + String out = new Playable.DefaultPlayableImageLoader(this) + .getImageLoaderCacheKey(); + if (out == null) { + if (item.getFeed().getImage() != null) { + return item.getFeed().getImage().getImageLoaderCacheKey(); + } + } + return out; + } + + @Override + public InputStream reopenImageInputStream(InputStream input) { + if (input instanceof FileInputStream) { + return item.getFeed().getImage().reopenImageInputStream(input); + } else { + return new Playable.DefaultPlayableImageLoader(this) + .reopenImageInputStream(input); + } + } } diff --git a/src/de/danoeh/antennapod/feed/FeedSearcher.java b/src/de/danoeh/antennapod/feed/FeedSearcher.java deleted file mode 100644 index ab7c174bc..000000000 --- a/src/de/danoeh/antennapod/feed/FeedSearcher.java +++ /dev/null @@ -1,253 +0,0 @@ -package de.danoeh.antennapod.feed; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import android.content.Context; -import android.database.Cursor; -import android.os.Looper; -import android.util.Log; -import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.storage.PodDBAdapter; -import de.danoeh.antennapod.util.comparator.SearchResultValueComparator; - -/** Performs search on Feeds and FeedItems */ -public class FeedSearcher { - private static final String TAG = "FeedSearcher"; - - // Search result values - private static final int VALUE_FEED_TITLE = 3; - private static final int VALUE_ITEM_TITLE = 2; - private static final int VALUE_ITEM_CHAPTER = 1; - private static final int VALUE_ITEM_DESCRIPTION = 0; - private static final int VALUE_WORD_MATCH = 4; - - /** Performs a search in all feeds or one specific feed. */ - public static ArrayList<SearchResult> performSearch(final Context context, - final String query, final Feed selectedFeed) { - final String lcQuery = query.toLowerCase(); - final ArrayList<SearchResult> result = new ArrayList<SearchResult>(); - if (selectedFeed == null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Performing global search"); - if (AppConfig.DEBUG) - Log.d(TAG, "Searching Feed titles"); - searchFeedtitles(lcQuery, result); - } else if (AppConfig.DEBUG) { - Log.d(TAG, "Performing search on specific feed"); - } - - if (AppConfig.DEBUG) - Log.d(TAG, "Searching Feeditem titles"); - searchFeedItemTitles(lcQuery, result, selectedFeed); - - if (AppConfig.DEBUG) - Log.d(TAG, "Searching item-chaptertitles"); - searchFeedItemChapters(lcQuery, result, selectedFeed); - - final FeedManager manager = FeedManager.getInstance(); - Looper.prepare(); - manager.searchFeedItemDescription(context, selectedFeed, lcQuery, - new FeedManager.QueryTaskCallback() { - - @Override - public void handleResult(Cursor cResult) { - searchFeedItemContentEncodedCursor(lcQuery, result, - selectedFeed, cResult); - - } - - @Override - public void onCompletion() { - manager.searchFeedItemContentEncoded(context, - selectedFeed, lcQuery, - new FeedManager.QueryTaskCallback() { - - @Override - public void handleResult(Cursor cResult) { - searchFeedItemDescriptionCursor( - lcQuery, result, selectedFeed, - cResult); - } - - @Override - public void onCompletion() { - Looper.myLooper().quit(); - } - }); - } - }); - - Looper.loop(); - if (AppConfig.DEBUG) - Log.d(TAG, "Sorting results"); - Collections.sort(result, new SearchResultValueComparator()); - - return result; - } - - private static void searchFeedtitles(String query, - ArrayList<SearchResult> destination) { - FeedManager manager = FeedManager.getInstance(); - for (Feed feed : manager.getFeeds()) { - SearchResult result = createSearchResult(feed, query, feed - .getTitle().toLowerCase(), VALUE_FEED_TITLE); - if (result != null) { - destination.add(result); - } - } - } - - private static void searchFeedItemTitles(String query, - ArrayList<SearchResult> destination, Feed selectedFeed) { - FeedManager manager = FeedManager.getInstance(); - if (selectedFeed == null) { - for (Feed feed : manager.getFeeds()) { - searchFeedItemTitlesSingleFeed(query, destination, feed); - } - } else { - searchFeedItemTitlesSingleFeed(query, destination, selectedFeed); - } - } - - private static void searchFeedItemTitlesSingleFeed(String query, - ArrayList<SearchResult> destination, Feed feed) { - for (FeedItem item : feed.getItems()) { - SearchResult result = createSearchResult(item, query, item - .getTitle().toLowerCase(), VALUE_ITEM_TITLE); - if (result != null) { - result.setSubtitle(PodcastApp.getInstance().getString( - R.string.found_in_title_label)); - destination.add(result); - } - - } - } - - private static void searchFeedItemChapters(String query, - ArrayList<SearchResult> destination, Feed selectedFeed) { - FeedManager manager = FeedManager.getInstance(); - if (selectedFeed == null) { - for (Feed feed : manager.getFeeds()) { - searchFeedItemChaptersSingleFeed(query, destination, feed); - } - } else { - searchFeedItemChaptersSingleFeed(query, destination, selectedFeed); - } - } - - private static void searchFeedItemChaptersSingleFeed(String query, - ArrayList<SearchResult> destination, Feed feed) { - for (FeedItem item : feed.getItems()) { - if (item.getChapters() != null) { - for (Chapter sc : item.getChapters()) { - SearchResult result = createSearchResult(item, query, sc - .getTitle().toLowerCase(), VALUE_ITEM_CHAPTER); - if (result != null) { - result.setSubtitle(PodcastApp.getInstance().getString( - R.string.found_in_chapters_label)); - destination.add(result); - } - } - } - } - } - - private static void searchFeedItemDescriptionCursor(String query, - ArrayList<SearchResult> destination, Feed feed, Cursor cursor) { - FeedManager manager = FeedManager.getInstance(); - if (cursor.moveToFirst()) { - do { - final long itemId = cursor - .getLong(PodDBAdapter.IDX_FI_EXTRA_ID); - String content = cursor - .getString(PodDBAdapter.IDX_FI_EXTRA_DESCRIPTION); - if (content != null) { - content = content.toLowerCase(); - final long feedId = cursor - .getLong(PodDBAdapter.IDX_FI_EXTRA_FEED); - FeedItem item = null; - if (feed == null) { - item = manager.getFeedItem(itemId, feedId); - } else { - item = manager.getFeedItem(itemId, feed); - } - if (item != null) { - SearchResult searchResult = createSearchResult(item, - query, content, VALUE_ITEM_DESCRIPTION); - if (searchResult != null) { - searchResult.setSubtitle(PodcastApp.getInstance() - .getString( - R.string.found_in_shownotes_label)); - destination.add(searchResult); - - } - } - } - - } while (cursor.moveToNext()); - } - } - - private static void searchFeedItemContentEncodedCursor(String query, - ArrayList<SearchResult> destination, Feed feed, Cursor cursor) { - FeedManager manager = FeedManager.getInstance(); - if (cursor.moveToFirst()) { - do { - final long itemId = cursor - .getLong(PodDBAdapter.IDX_FI_EXTRA_ID); - String content = cursor - .getString(PodDBAdapter.IDX_FI_EXTRA_CONTENT_ENCODED); - if (content != null) { - content = content.toLowerCase(); - - final long feedId = cursor - .getLong(PodDBAdapter.IDX_FI_EXTRA_FEED); - FeedItem item = null; - if (feed == null) { - item = manager.getFeedItem(itemId, feedId); - } else { - item = manager.getFeedItem(itemId, feed); - } - if (item != null) { - SearchResult searchResult = createSearchResult(item, - query, content, VALUE_ITEM_DESCRIPTION); - if (searchResult != null) { - searchResult.setSubtitle(PodcastApp.getInstance() - .getString( - R.string.found_in_shownotes_label)); - destination.add(searchResult); - } - } - } - } while (cursor.moveToNext()); - } - } - - private static SearchResult createSearchResult(FeedComponent component, - String query, String text, int baseValue) { - int bonus = 0; - boolean found = false; - // try word search - Pattern word = Pattern.compile("\b" + query + "\b"); - Matcher matcher = word.matcher(text); - found = matcher.find(); - if (found) { - bonus = VALUE_WORD_MATCH; - } else { - // search for other occurence - found = text.contains(query); - } - - if (found) { - return new SearchResult(component, baseValue + bonus); - } else { - return null; - } - } - -} diff --git a/src/de/danoeh/antennapod/feed/SearchResult.java b/src/de/danoeh/antennapod/feed/SearchResult.java index b4016f2e8..1cba389ec 100644 --- a/src/de/danoeh/antennapod/feed/SearchResult.java +++ b/src/de/danoeh/antennapod/feed/SearchResult.java @@ -7,10 +7,11 @@ public class SearchResult { /** Higher value means more importance */ private int value; - public SearchResult(FeedComponent component, int value) { + public SearchResult(FeedComponent component, int value, String subtitle) { super(); this.component = component; this.value = value; + this.subtitle = subtitle; } public FeedComponent getComponent() { diff --git a/src/de/danoeh/antennapod/fragment/CoverFragment.java b/src/de/danoeh/antennapod/fragment/CoverFragment.java index 6be76f515..791315719 100644 --- a/src/de/danoeh/antennapod/fragment/CoverFragment.java +++ b/src/de/danoeh/antennapod/fragment/CoverFragment.java @@ -1,14 +1,13 @@ package de.danoeh.antennapod.fragment; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; -import com.actionbarsherlock.app.SherlockFragment; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.AudioplayerActivity.AudioplayerContentFragment; @@ -16,7 +15,7 @@ import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.util.playback.Playable; /** Displays the cover and the title of a FeedItem. */ -public class CoverFragment extends SherlockFragment implements +public class CoverFragment extends Fragment implements AudioplayerContentFragment { private static final String TAG = "CoverFragment"; private static final String ARG_PLAYABLE = "arg.playable"; diff --git a/src/de/danoeh/antennapod/fragment/EpisodesFragment.java b/src/de/danoeh/antennapod/fragment/EpisodesFragment.java index 4039235e0..a99056c9a 100644 --- a/src/de/danoeh/antennapod/fragment/EpisodesFragment.java +++ b/src/de/danoeh/antennapod/fragment/EpisodesFragment.java @@ -1,20 +1,16 @@ package de.danoeh.antennapod.fragment; +import android.content.Context; import android.content.Intent; +import android.os.AsyncTask; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.util.Log; -import android.view.ContextMenu; +import android.view.*; import android.view.ContextMenu.ContextMenuInfo; -import android.view.LayoutInflater; -import android.view.MenuInflater; -import android.view.View; -import android.view.ViewGroup; import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; -import com.actionbarsherlock.app.SherlockFragment; -import com.actionbarsherlock.view.Menu; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.ItemviewActivity; @@ -24,11 +20,16 @@ import de.danoeh.antennapod.adapter.ExternalEpisodesListAdapter; import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.DBTasks; +import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.storage.DownloadRequestException; +import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.menuhandler.FeedItemMenuHandler; -public class EpisodesFragment extends SherlockFragment { +import java.util.List; + +public class EpisodesFragment extends Fragment { private static final String TAG = "EpisodesFragment"; private static final int EVENTS = EventDistributor.QUEUE_UPDATE @@ -40,6 +41,9 @@ public class EpisodesFragment extends SherlockFragment { private ExpandableListView listView; private ExternalEpisodesListAdapter adapter; + private List<FeedItem> queue; + private List<FeedItem> unreadItems; + protected FeedItem selectedItem = null; protected long selectedGroupId = -1; protected boolean contextMenuClosed = true; @@ -92,7 +96,7 @@ public class EpisodesFragment extends SherlockFragment { public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); adapter = new ExternalEpisodesListAdapter(getActivity(), - adapterCallback, groupActionCallback); + adapterCallback, groupActionCallback, itemAccess); listView.setAdapter(adapter); listView.expandGroup(ExternalEpisodesListAdapter.GROUP_POS_QUEUE); listView.expandGroup(ExternalEpisodesListAdapter.GROUP_POS_UNREAD); @@ -117,9 +121,73 @@ public class EpisodesFragment extends SherlockFragment { return true; } }); + loadData(); registerForContextMenu(listView); + } + ExternalEpisodesListAdapter.ItemAccess itemAccess = new ExternalEpisodesListAdapter.ItemAccess() { + + @Override + public int getQueueSize() { + return (queue != null) ? queue.size() : 0; + } + + @Override + public int getUnreadItemsSize() { + return (unreadItems != null) ? unreadItems.size() : 0; + } + + @Override + public FeedItem getQueueItemAt(int position) { + return (queue != null) ? queue.get(position) : null; + } + + @Override + public FeedItem getUnreadItemAt(int position) { + return (unreadItems != null) ? unreadItems.get(position) : null; + } + }; + + private void loadData() { + AsyncTask<Void, Void, Void> loadTask = new AsyncTask<Void, Void, Void>() { + private volatile List<FeedItem> queueRef; + private volatile List<FeedItem> unreadItemsRef; + + @Override + protected Void doInBackground(Void... voids) { + if (AppConfig.DEBUG) Log.d(TAG, "Starting to load list data"); + Context context = EpisodesFragment.this.getActivity(); + if (context != null) { + queueRef = DBReader.getQueue(context); + unreadItemsRef = DBReader.getUnreadItemsList(context); + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + if (queueRef != null && unreadItemsRef != null) { + if (AppConfig.DEBUG) Log.d(TAG, "Done loading list data"); + queue = queueRef; + unreadItems = unreadItemsRef; + if (adapter != null) { + adapter.notifyDataSetChanged(); + } + } else { + if (queueRef == null) { + Log.e(TAG, "Could not load queue"); + } + if (unreadItemsRef == null) { + Log.e(TAG, "Could not load unread items"); + } + } + } + }; + loadTask.execute(); + } + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { @Override @@ -127,7 +195,7 @@ public class EpisodesFragment extends SherlockFragment { if ((EVENTS & arg) != 0) { if (AppConfig.DEBUG) Log.d(TAG, "Received contentUpdate Intent."); - adapter.notifyDataSetChanged(); + loadData(); } } }; @@ -146,13 +214,13 @@ public class EpisodesFragment extends SherlockFragment { menu.setHeaderTitle(selectedItem.getTitle()); FeedItemMenuHandler.onPrepareMenu( - new FeedItemMenuHandler.MenuInterface() { + new FeedItemMenuHandler.MenuInterface() { - @Override - public void setItemVisibility(int id, boolean visible) { - menu.findItem(id).setVisible(visible); - } - }, selectedItem, false); + @Override + public void setItemVisibility(int id, boolean visible) { + menu.findItem(id).setVisible(visible); + } + }, selectedItem, false, QueueAccess.ItemListAccess(queue)); } else if (selectedGroupId == ExternalEpisodesListAdapter.GROUP_POS_QUEUE) { menu.add(Menu.NONE, R.id.organize_queue_item, Menu.NONE, @@ -172,11 +240,10 @@ public class EpisodesFragment extends SherlockFragment { @Override public boolean onContextItemSelected(android.view.MenuItem item) { boolean handled = false; - FeedManager manager = FeedManager.getInstance(); if (selectedItem != null) { try { handled = FeedItemMenuHandler.onMenuItemClicked( - getSherlockActivity(), item.getItemId(), selectedItem); + getActivity(), item.getItemId(), selectedItem); } catch (DownloadRequestException e) { e.printStackTrace(); DownloadRequestErrorDialogCreator.newRequestErrorDialog( @@ -191,10 +258,10 @@ public class EpisodesFragment extends SherlockFragment { OrganizeQueueActivity.class)); break; case R.id.clear_queue_item: - manager.clearQueue(getActivity()); + DBWriter.clearQueue(getActivity()); break; case R.id.download_all_item: - manager.downloadAllItemsInQueue(getActivity()); + DBTasks.downloadAllItemsInQueue(getActivity()); break; default: handled = false; @@ -203,10 +270,10 @@ public class EpisodesFragment extends SherlockFragment { handled = true; switch (item.getItemId()) { case R.id.mark_all_read_item: - manager.markAllItemsRead(getActivity()); + DBWriter.markAllItemsRead(getActivity()); break; case R.id.enqueue_all_item: - manager.enqueueAllNewItems(getActivity()); + DBTasks.enqueueAllNewItems(getActivity()); break; default: handled = false; diff --git a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java index a50e820b6..10312b20b 100644 --- a/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java +++ b/src/de/danoeh/antennapod/fragment/ExternalPlayerFragment.java @@ -1,6 +1,7 @@ package de.danoeh.antennapod.fragment; import android.os.Bundle; +import android.support.v4.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -10,8 +11,6 @@ import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; -import com.actionbarsherlock.app.SherlockFragment; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.ImageLoader; @@ -24,7 +23,7 @@ import de.danoeh.antennapod.util.playback.PlaybackController; * Fragment which is supposed to be displayed outside of the MediaplayerActivity * if the PlaybackService is running */ -public class ExternalPlayerFragment extends SherlockFragment { +public class ExternalPlayerFragment extends Fragment { private static final String TAG = "ExternalPlayerFragment"; private ViewGroup fragmentLayout; diff --git a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java index c3034c2af..3e8679bca 100644 --- a/src/de/danoeh/antennapod/fragment/FeedlistFragment.java +++ b/src/de/danoeh/antennapod/fragment/FeedlistFragment.java @@ -1,23 +1,18 @@ package de.danoeh.antennapod.fragment; +import java.util.List; + import android.annotation.SuppressLint; -import android.app.Activity; import android.content.DialogInterface; import android.content.Intent; +import android.os.AsyncTask; import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBarActivity; +import android.support.v7.view.ActionMode; import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AdapterView; -import android.widget.GridView; -import android.widget.ListView; -import android.widget.TextView; - -import com.actionbarsherlock.app.SherlockFragment; -import com.actionbarsherlock.view.ActionMode; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuItem; +import android.view.*; +import android.widget.*; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; @@ -28,201 +23,261 @@ import de.danoeh.antennapod.dialog.ConfirmationDialog; import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedManager; +import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DownloadRequestException; +import de.danoeh.antennapod.storage.FeedItemStatistics; import de.danoeh.antennapod.util.menuhandler.FeedMenuHandler; -public class FeedlistFragment extends SherlockFragment implements +public class FeedlistFragment extends Fragment implements ActionMode.Callback, AdapterView.OnItemClickListener, AdapterView.OnItemLongClickListener { private static final String TAG = "FeedlistFragment"; - private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED - | EventDistributor.DOWNLOAD_QUEUED - | EventDistributor.FEED_LIST_UPDATE - | EventDistributor.UNREAD_ITEMS_UPDATE; - - public static final String EXTRA_SELECTED_FEED = "extra.de.danoeh.antennapod.activity.selected_feed"; - - private FeedManager manager; - private FeedlistAdapter fla; - - private Feed selectedFeed; - private ActionMode mActionMode; - - private GridView gridView; - private ListView listView; - private TextView txtvEmpty; - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - } - - @Override - public void onDetach() { - super.onDetach(); - } - - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (AppConfig.DEBUG) - Log.d(TAG, "Creating"); - manager = FeedManager.getInstance(); - fla = new FeedlistAdapter(getActivity()); - - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View result = inflater.inflate(R.layout.feedlist, container, false); - listView = (ListView) result.findViewById(android.R.id.list); - gridView = (GridView) result.findViewById(R.id.grid); - txtvEmpty = (TextView) result.findViewById(android.R.id.empty); - - return result; - - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if (listView != null) { - listView.setOnItemClickListener(this); - listView.setOnItemLongClickListener(this); - listView.setAdapter(fla); - listView.setEmptyView(txtvEmpty); - if (AppConfig.DEBUG) - Log.d(TAG, "Using ListView"); - } else { - gridView.setOnItemClickListener(this); - gridView.setOnItemLongClickListener(this); - gridView.setAdapter(fla); - gridView.setEmptyView(txtvEmpty); - if (AppConfig.DEBUG) - Log.d(TAG, "Using GridView"); - } - } - - @Override - public void onResume() { - super.onResume(); - if (AppConfig.DEBUG) - Log.d(TAG, "Resuming"); - EventDistributor.getInstance().register(contentUpdate); - fla.notifyDataSetChanged(); - } - - @Override - public void onPause() { - super.onPause(); - EventDistributor.getInstance().unregister(contentUpdate); - if (mActionMode != null) { - mActionMode.finish(); - } - } - - private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { - - @Override - public void update(EventDistributor eventDistributor, Integer arg) { - if ((EVENTS & arg) != 0) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received contentUpdate Intent."); - fla.notifyDataSetChanged(); - } - } - }; - - @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); - } - - @SuppressLint("NewApi") - @Override - public boolean onActionItemClicked(ActionMode mode, MenuItem item) { - try { - if (FeedMenuHandler.onOptionsItemClicked(getSherlockActivity(), - item, selectedFeed)) { - fla.notifyDataSetChanged(); - } else { - switch (item.getItemId()) { - case R.id.remove_item: - final FeedRemover remover = new FeedRemover( - getSherlockActivity(), selectedFeed) { - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - fla.notifyDataSetChanged(); - } - }; - ConfirmationDialog conDialog = new ConfirmationDialog( - getActivity(), R.string.remove_feed_label, - R.string.feed_delete_confirmation_msg) { - - @Override - public void onConfirmButtonPressed( - DialogInterface dialog) { - dialog.dismiss(); - remover.executeAsync(); - } - }; - conDialog.createNewDialog().show(); - break; - } - } - } catch (DownloadRequestException e) { - e.printStackTrace(); - DownloadRequestErrorDialogCreator.newRequestErrorDialog( - getActivity(), e.getMessage()); - } - mode.finish(); - return true; - } - - @Override - public void onDestroyActionMode(ActionMode mode) { - mActionMode = null; - selectedFeed = null; - fla.setSelectedItemIndex(FeedlistAdapter.SELECTION_NONE); - } - - @Override - public void onItemClick(AdapterView<?> arg0, View arg1, int position, - long id) { - Feed selection = fla.getItem(position); - Intent showFeed = new Intent(getActivity(), FeedItemlistActivity.class); - showFeed.putExtra(EXTRA_SELECTED_FEED, selection.getId()); - - getActivity().startActivity(showFeed); - } - - @Override - public boolean onItemLongClick(AdapterView<?> parent, View view, - int position, long id) { - Feed selection = fla.getItem(position); - if (AppConfig.DEBUG) - 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; - } + private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED + | EventDistributor.DOWNLOAD_QUEUED + | EventDistributor.FEED_LIST_UPDATE + | EventDistributor.UNREAD_ITEMS_UPDATE; + + public static final String EXTRA_SELECTED_FEED = "extra.de.danoeh.antennapod.activity.selected_feed"; + + private FeedlistAdapter fla; + private List<Feed> feeds; + private List<FeedItemStatistics> feedItemStatistics; + + private Feed selectedFeed; + private ActionMode mActionMode; + + private GridView gridView; + private ListView listView; + private TextView emptyView; + + private FeedlistAdapter.ItemAccess itemAccess = new FeedlistAdapter.ItemAccess() { + + @Override + public Feed getItem(int position) { + if (feeds != null) { + return feeds.get(position); + } else { + return null; + } + } + + @Override + public FeedItemStatistics getFeedItemStatistics(int position) { + if (feedItemStatistics != null && position < feedItemStatistics.size()) { + return feedItemStatistics.get(position); + } else { + return null; + } + } + + @Override + public int getCount() { + if (feeds != null) { + return feeds.size(); + } else { + return 0; + } + } + }; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (AppConfig.DEBUG) + Log.d(TAG, "Creating"); + fla = new FeedlistAdapter(getActivity(), itemAccess); + loadFeeds(); + } + + private void loadFeeds() { + AsyncTask<Void, Void, List[]> loadTask = new AsyncTask<Void, Void, List[]>() { + @Override + protected List[] doInBackground(Void... params) { + return new List[]{DBReader.getFeedList(getActivity()), + DBReader.getFeedStatisticsList(getActivity())}; + } + + + + @Override + protected void onPostExecute(List[] result) { + super.onPostExecute(result); + if (result != null) { + feeds = result[0]; + feedItemStatistics = result[1]; + setEmptyViewIfListIsEmpty(); + if (fla != null) { + fla.notifyDataSetChanged(); + } + } else { + Log.e(TAG, "Failed to load feeds"); + } + } + }; + loadTask.execute(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View result = inflater.inflate(R.layout.feedlist, container, false); + listView = (ListView) result.findViewById(android.R.id.list); + gridView = (GridView) result.findViewById(R.id.grid); + emptyView = (TextView) result.findViewById(android.R.id.empty); + + return result; + + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + if (listView != null) { + listView.setOnItemClickListener(this); + listView.setOnItemLongClickListener(this); + listView.setAdapter(fla); + listView.setEmptyView(emptyView); + if (AppConfig.DEBUG) + Log.d(TAG, "Using ListView"); + } else { + gridView.setOnItemClickListener(this); + gridView.setOnItemLongClickListener(this); + gridView.setAdapter(fla); + gridView.setEmptyView(emptyView); + if (AppConfig.DEBUG) + Log.d(TAG, "Using GridView"); + } + setEmptyViewIfListIsEmpty(); + } + + @Override + public void onResume() { + super.onResume(); + if (AppConfig.DEBUG) + Log.d(TAG, "Resuming"); + EventDistributor.getInstance().register(contentUpdate); + } + + @Override + public void onPause() { + super.onPause(); + EventDistributor.getInstance().unregister(contentUpdate); + if (mActionMode != null) { + mActionMode.finish(); + } + } + + private EventDistributor.EventListener contentUpdate = new EventDistributor.EventListener() { + + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((EVENTS & arg) != 0) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received contentUpdate Intent."); + loadFeeds(); + } + } + }; + + @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); + } + + @SuppressLint("NewApi") + @Override + public boolean onActionItemClicked(ActionMode mode, MenuItem item) { + try { + if (FeedMenuHandler.onOptionsItemClicked(getActivity(), + item, selectedFeed)) { + loadFeeds(); + } else { + switch (item.getItemId()) { + case R.id.remove_item: + final FeedRemover remover = new FeedRemover( + getActivity(), selectedFeed) { + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + loadFeeds(); + } + }; + ConfirmationDialog conDialog = new ConfirmationDialog( + getActivity(), R.string.remove_feed_label, + R.string.feed_delete_confirmation_msg) { + + @Override + public void onConfirmButtonPressed( + DialogInterface dialog) { + dialog.dismiss(); + remover.executeAsync(); + } + }; + conDialog.createNewDialog().show(); + break; + } + } + } catch (DownloadRequestException e) { + e.printStackTrace(); + DownloadRequestErrorDialogCreator.newRequestErrorDialog( + getActivity(), e.getMessage()); + } + mode.finish(); + return true; + } + + @Override + public void onDestroyActionMode(ActionMode mode) { + mActionMode = null; + selectedFeed = null; + fla.setSelectedItemIndex(FeedlistAdapter.SELECTION_NONE); + } + + @Override + public void onItemClick(AdapterView<?> arg0, View arg1, int position, + long id) { + Feed selection = fla.getItem(position); + Intent showFeed = new Intent(getActivity(), FeedItemlistActivity.class); + showFeed.putExtra(EXTRA_SELECTED_FEED, selection.getId()); + + getActivity().startActivity(showFeed); + } + + @Override + public boolean onItemLongClick(AdapterView<?> parent, View view, + int position, long id) { + Feed selection = fla.getItem(position); + if (AppConfig.DEBUG) + Log.d(TAG, "Selected Feed with title " + selection.getTitle()); + if (selection != null) { + if (mActionMode != null) { + mActionMode.finish(); + } + fla.setSelectedItemIndex(position); + selectedFeed = selection; + mActionMode = ((ActionBarActivity) getActivity()).startSupportActionMode(FeedlistFragment.this); + + } + return true; + } + + private AbsListView getMainView() { + return (listView != null) ? listView : gridView; + } + + private void setEmptyViewIfListIsEmpty() { + if (getMainView() != null && emptyView != null && feeds != null) { + if (feeds.isEmpty()) { + emptyView.setText(R.string.no_feeds_label); + } + } + } } diff --git a/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java b/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java index 10f43718f..9183180c1 100644 --- a/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java +++ b/src/de/danoeh/antennapod/fragment/ItemDescriptionFragment.java @@ -1,5 +1,10 @@ package de.danoeh.antennapod.fragment; +import android.support.v4.app.Fragment; +import android.support.v7.app.ActionBarActivity; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.util.ShownotesProvider; import org.apache.commons.lang3.StringEscapeUtils; import android.annotation.SuppressLint; @@ -27,442 +32,424 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.Toast; -import com.actionbarsherlock.app.SherlockFragment; - import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.util.ShareUtils; import de.danoeh.antennapod.util.playback.Playable; +import java.util.concurrent.Callable; + /** Displays the description of a Playable object in a Webview. */ -public class ItemDescriptionFragment extends SherlockFragment { - - private static final String TAG = "ItemDescriptionFragment"; - - private static final String PREF = "ItemDescriptionFragmentPrefs"; - private static final String PREF_SCROLL_Y = "prefScrollY"; - private static final String PREF_PLAYABLE_ID = "prefPlayableId"; - - private static final String ARG_PLAYABLE = "arg.playable"; - - private static final String ARG_FEED_ID = "arg.feedId"; - private static final String ARG_FEED_ITEM_ID = "arg.feeditemId"; - private static final String ARG_SAVE_STATE = "arg.saveState"; - - private WebView webvDescription; - private Playable media; - - private FeedItem item; - - private AsyncTask<Void, Void, Void> webViewLoader; - - private String shownotes; - - /** URL that was selected via long-press. */ - private String selectedURL; - - /** - * True if Fragment should save its state (e.g. scrolling position) in a - * shared preference. - */ - private boolean saveState; - - public static ItemDescriptionFragment newInstance(Playable media, - boolean saveState) { - ItemDescriptionFragment f = new ItemDescriptionFragment(); - Bundle args = new Bundle(); - args.putParcelable(ARG_PLAYABLE, media); - args.putBoolean(ARG_SAVE_STATE, saveState); - f.setArguments(args); - return f; - } - - public static ItemDescriptionFragment newInstance(FeedItem item, - boolean saveState) { - ItemDescriptionFragment f = new ItemDescriptionFragment(); - Bundle args = new Bundle(); - args.putLong(ARG_FEED_ID, item.getFeed().getId()); - args.putLong(ARG_FEED_ITEM_ID, item.getId()); - args.putBoolean(ARG_SAVE_STATE, saveState); - f.setArguments(args); - return f; - } - - @SuppressLint("NewApi") - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - if (AppConfig.DEBUG) - Log.d(TAG, "Creating view"); - webvDescription = new WebView(getActivity()); - if (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) { - if (Build.VERSION.SDK_INT >= 11 - && Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { - webvDescription.setLayerType(View.LAYER_TYPE_SOFTWARE, null); - } - webvDescription.setBackgroundColor(getResources().getColor( - R.color.black)); - } - webvDescription.getSettings().setUseWideViewPort(false); - webvDescription.getSettings().setLayoutAlgorithm( - LayoutAlgorithm.NARROW_COLUMNS); - webvDescription.getSettings().setLoadWithOverviewMode(true); - webvDescription.setOnLongClickListener(webViewLongClickListener); - webvDescription.setWebViewClient(new WebViewClient() { - - @Override - public boolean shouldOverrideUrlLoading(WebView view, String url) { - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivity(intent); - return true; - } - - @Override - public void onPageFinished(WebView view, String url) { - super.onPageFinished(view, url); - if (AppConfig.DEBUG) - Log.d(TAG, "Page finished"); - // Restoring the scroll position might not always work - view.postDelayed(new Runnable() { - - @Override - public void run() { - restoreFromPreference(); - } - - }, 50); - } - - }); - registerForContextMenu(webvDescription); - return webvDescription; - } - - @Override - public void onDestroyView() { - super.onDestroyView(); - } - - @Override - public void onAttach(Activity activity) { - super.onAttach(activity); - if (AppConfig.DEBUG) - Log.d(TAG, "Fragment attached"); - } - - @Override - public void onDetach() { - super.onDetach(); - if (AppConfig.DEBUG) - Log.d(TAG, "Fragment detached"); - if (webViewLoader != null) { - webViewLoader.cancel(true); - } - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (AppConfig.DEBUG) - Log.d(TAG, "Fragment destroyed"); - if (webViewLoader != null) { - webViewLoader.cancel(true); - } - } - - @SuppressLint("NewApi") - @Override - public void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (AppConfig.DEBUG) - Log.d(TAG, "Creating fragment"); - Bundle args = getArguments(); - saveState = args.getBoolean(ARG_SAVE_STATE, false); - if (args.containsKey(ARG_PLAYABLE)) { - media = args.getParcelable(ARG_PLAYABLE); - } else if (args.containsKey(ARG_FEED_ID) - && args.containsKey(ARG_FEED_ITEM_ID)) { - long feedId = args.getLong(ARG_FEED_ID); - long itemId = args.getLong(ARG_FEED_ITEM_ID); - FeedItem f = FeedManager.getInstance().getFeedItem(itemId, feedId); - if (f != null) { - item = f; - } - } - } - - @Override - public void onViewCreated(View view, Bundle savedInstanceState) { - super.onViewCreated(view, savedInstanceState); - if (media != null) { - media.loadShownotes(new Playable.ShownoteLoaderCallback() { - - @Override - public void onShownotesLoaded(String shownotes) { - ItemDescriptionFragment.this.shownotes = shownotes; - if (ItemDescriptionFragment.this.shownotes != null) { - startLoader(); - } - } - }); - } else if (item != null) { - if (item.getDescription() == null - || item.getContentEncoded() == null) { - FeedManager.getInstance().loadExtraInformationOfItem( - PodcastApp.getInstance(), item, - new FeedManager.TaskCallback<String[]>() { - @Override - public void onCompletion(String[] result) { - if (result[1] != null) { - shownotes = result[1]; - } else { - shownotes = result[0]; - } - if (shownotes != null) { - startLoader(); - } - - } - }); - } else { - shownotes = item.getContentEncoded(); - startLoader(); - } - } else { - Log.e(TAG, "Error in onViewCreated: Item and media were null"); - } - } - - @Override - public void onResume() { - super.onResume(); - } - - @SuppressLint("NewApi") - private void startLoader() { - webViewLoader = createLoader(); - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - webViewLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - webViewLoader.execute(); - } - } - - /** - * Return the CSS style of the Webview. - * - * @param textColor - * the default color to use for the text in the webview. This - * value is inserted directly into the CSS String. - * */ - private String applyWebviewStyle(String textColor, String data) { - final String WEBVIEW_STYLE = "<html><head><style type=\"text/css\"> * { color: %s; font-family: Helvetica; line-height: 1.5em; font-size: 11pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }</style></head><body>%s</body></html>"; - final int pageMargin = (int) TypedValue.applyDimension( - TypedValue.COMPLEX_UNIT_DIP, 8, getResources() - .getDisplayMetrics()); - return String.format(WEBVIEW_STYLE, textColor, "100%", pageMargin, - pageMargin, pageMargin, pageMargin, data); - } - - private View.OnLongClickListener webViewLongClickListener = new View.OnLongClickListener() { - - @Override - public boolean onLongClick(View v) { - WebView.HitTestResult r = webvDescription.getHitTestResult(); - if (r != null - && r.getType() == WebView.HitTestResult.SRC_ANCHOR_TYPE) { - if (AppConfig.DEBUG) - Log.d(TAG, - "Link of webview was long-pressed. Extra: " - + r.getExtra()); - selectedURL = r.getExtra(); - webvDescription.showContextMenu(); - return true; - } - selectedURL = null; - return false; - } - }; - - @SuppressWarnings("deprecation") - @SuppressLint("NewApi") - @Override - public boolean onContextItemSelected(MenuItem item) { - boolean handled = selectedURL != null; - if (selectedURL != null) { - switch (item.getItemId()) { - case R.id.open_in_browser_item: - Uri uri = Uri.parse(selectedURL); - getActivity() - .startActivity(new Intent(Intent.ACTION_VIEW, uri)); - break; - case R.id.share_url_item: - ShareUtils.shareLink(getActivity(), selectedURL); - break; - case R.id.copy_url_item: - if (android.os.Build.VERSION.SDK_INT >= 11) { - ClipData clipData = ClipData.newPlainText(selectedURL, - selectedURL); - android.content.ClipboardManager cm = (android.content.ClipboardManager) getActivity() - .getSystemService(Context.CLIPBOARD_SERVICE); - cm.setPrimaryClip(clipData); - } else { - android.text.ClipboardManager cm = (android.text.ClipboardManager) getActivity() - .getSystemService(Context.CLIPBOARD_SERVICE); - cm.setText(selectedURL); - } - Toast t = Toast.makeText(getActivity(), - R.string.copied_url_msg, Toast.LENGTH_SHORT); - t.show(); - break; - default: - handled = false; - break; - - } - selectedURL = null; - } - return handled; - - } - - @Override - public void onCreateContextMenu(ContextMenu menu, View v, - ContextMenuInfo menuInfo) { - if (selectedURL != null) { - super.onCreateContextMenu(menu, v, menuInfo); - menu.add(Menu.NONE, R.id.open_in_browser_item, Menu.NONE, - R.string.open_in_browser_label); - menu.add(Menu.NONE, R.id.copy_url_item, Menu.NONE, - R.string.copy_url_label); - menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE, - R.string.share_url_label); - menu.setHeaderTitle(selectedURL); - } - } - - 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"); - if (getSherlockActivity() != null) { - getSherlockActivity() - .setSupportProgressBarIndeterminateVisibility(false); - } - if (AppConfig.DEBUG) - Log.d(TAG, "Webview loaded"); - webViewLoader = null; - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - if (getSherlockActivity() != null) { - getSherlockActivity() - .setSupportProgressBarIndeterminateVisibility(true); - } - } - - @Override - protected Void doInBackground(Void... params) { - if (AppConfig.DEBUG) - Log.d(TAG, "Loading Webview"); - data = ""; - data = StringEscapeUtils.unescapeHtml4(shownotes); - Activity activity = getActivity(); - if (activity != null) { - TypedArray res = getActivity() - .getTheme() - .obtainStyledAttributes( - new int[] { android.R.attr.textColorPrimary }); - int colorResource = res.getColor(0, 0); - String colorString = String.format("#%06X", - 0xFFFFFF & colorResource); - Log.i(TAG, "text color: " + colorString); - res.recycle(); - data = applyWebviewStyle(colorString, data); - } else { - cancel(true); - } - return null; - } - - }; - } - - @Override - public void onPause() { - super.onPause(); - savePreference(); - } - - private void savePreference() { - if (saveState) { - if (AppConfig.DEBUG) - Log.d(TAG, "Saving preferences"); - SharedPreferences prefs = getActivity().getSharedPreferences(PREF, - Activity.MODE_PRIVATE); - SharedPreferences.Editor editor = prefs.edit(); - if (media != null && webvDescription != null) { - if (AppConfig.DEBUG) - Log.d(TAG, - "Saving scroll position: " - + webvDescription.getScrollY()); - editor.putInt(PREF_SCROLL_Y, webvDescription.getScrollY()); - editor.putString(PREF_PLAYABLE_ID, media.getIdentifier() - .toString()); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, - "savePreferences was called while media or webview was null"); - editor.putInt(PREF_SCROLL_Y, -1); - editor.putString(PREF_PLAYABLE_ID, ""); - } - editor.commit(); - } - } - - private boolean restoreFromPreference() { - if (saveState) { - if (AppConfig.DEBUG) - Log.d(TAG, "Restoring from preferences"); - Activity activity = getActivity(); - if (activity != null) { - SharedPreferences prefs = activity.getSharedPreferences( - PREF, Activity.MODE_PRIVATE); - String id = prefs.getString(PREF_PLAYABLE_ID, ""); - int scrollY = prefs.getInt(PREF_SCROLL_Y, -1); - if (scrollY != -1 && media != null - && id.equals(media.getIdentifier().toString()) - && webvDescription != null) { - if (AppConfig.DEBUG) - Log.d(TAG, "Restored scroll Position: " + scrollY); - webvDescription.scrollTo(webvDescription.getScrollX(), - scrollY); - return true; - } - } - } - return false; - } +public class ItemDescriptionFragment extends Fragment { + + private static final String TAG = "ItemDescriptionFragment"; + + private static final String PREF = "ItemDescriptionFragmentPrefs"; + private static final String PREF_SCROLL_Y = "prefScrollY"; + private static final String PREF_PLAYABLE_ID = "prefPlayableId"; + + private static final String ARG_PLAYABLE = "arg.playable"; + private static final String ARG_FEEDITEM_ID = "arg.feeditem"; + + private static final String ARG_SAVE_STATE = "arg.saveState"; + + private WebView webvDescription; + + private ShownotesProvider shownotesProvider; + private Playable media; + + + private AsyncTask<Void, Void, Void> webViewLoader; + + /** + * URL that was selected via long-press. + */ + private String selectedURL; + + /** + * True if Fragment should save its state (e.g. scrolling position) in a + * shared preference. + */ + private boolean saveState; + + public static ItemDescriptionFragment newInstance(Playable media, + boolean saveState) { + ItemDescriptionFragment f = new ItemDescriptionFragment(); + Bundle args = new Bundle(); + args.putParcelable(ARG_PLAYABLE, media); + args.putBoolean(ARG_SAVE_STATE, saveState); + f.setArguments(args); + return f; + } + + public static ItemDescriptionFragment newInstance(FeedItem item, boolean saveState) { + ItemDescriptionFragment f = new ItemDescriptionFragment(); + Bundle args = new Bundle(); + args.putLong(ARG_FEEDITEM_ID, item.getId()); + args.putBoolean(ARG_SAVE_STATE, saveState); + f.setArguments(args); + return f; + } + + @SuppressLint("NewApi") + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + if (AppConfig.DEBUG) + Log.d(TAG, "Creating view"); + webvDescription = new WebView(getActivity()); + if (UserPreferences.getTheme() == R.style.Theme_AntennaPod_Dark) { + if (Build.VERSION.SDK_INT >= 11 + && Build.VERSION.SDK_INT <= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { + webvDescription.setLayerType(View.LAYER_TYPE_SOFTWARE, null); + } + webvDescription.setBackgroundColor(getResources().getColor( + R.color.black)); + } + webvDescription.getSettings().setUseWideViewPort(false); + webvDescription.getSettings().setLayoutAlgorithm( + LayoutAlgorithm.NARROW_COLUMNS); + webvDescription.getSettings().setLoadWithOverviewMode(true); + webvDescription.setOnLongClickListener(webViewLongClickListener); + webvDescription.setWebViewClient(new WebViewClient() { + + @Override + public boolean shouldOverrideUrlLoading(WebView view, String url) { + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivity(intent); + return true; + } + + @Override + public void onPageFinished(WebView view, String url) { + super.onPageFinished(view, url); + if (AppConfig.DEBUG) + Log.d(TAG, "Page finished"); + // Restoring the scroll position might not always work + view.postDelayed(new Runnable() { + + @Override + public void run() { + restoreFromPreference(); + } + + }, 50); + } + + }); + registerForContextMenu(webvDescription); + return webvDescription; + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (AppConfig.DEBUG) + Log.d(TAG, "Fragment attached"); + } + + @Override + public void onDetach() { + super.onDetach(); + if (AppConfig.DEBUG) + Log.d(TAG, "Fragment detached"); + if (webViewLoader != null) { + webViewLoader.cancel(true); + } + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (AppConfig.DEBUG) + Log.d(TAG, "Fragment destroyed"); + if (webViewLoader != null) { + webViewLoader.cancel(true); + } + } + + @SuppressLint("NewApi") + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (AppConfig.DEBUG) + Log.d(TAG, "Creating fragment"); + Bundle args = getArguments(); + saveState = args.getBoolean(ARG_SAVE_STATE, false); + + } + + @Override + public void onViewCreated(View view, Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Bundle args = getArguments(); + if (args.containsKey(ARG_PLAYABLE)) { + media = args.getParcelable(ARG_PLAYABLE); + shownotesProvider = media; + startLoader(); + } else if (args.containsKey(ARG_FEEDITEM_ID)) { + AsyncTask<Void, Void, FeedItem> itemLoadTask = new AsyncTask<Void, Void, FeedItem>() { + + @Override + protected FeedItem doInBackground(Void... voids) { + return DBReader.getFeedItem(getActivity(), getArguments().getLong(ARG_FEEDITEM_ID)); + } + + @Override + protected void onPostExecute(FeedItem feedItem) { + super.onPostExecute(feedItem); + shownotesProvider = feedItem; + startLoader(); + } + }; + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + itemLoadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + itemLoadTask.execute(); + } + } + + + } + + @Override + public void onResume() { + super.onResume(); + } + + @SuppressLint("NewApi") + private void startLoader() { + webViewLoader = createLoader(); + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + webViewLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + webViewLoader.execute(); + } + } + + /** + * Return the CSS style of the Webview. + * + * @param textColor the default color to use for the text in the webview. This + * value is inserted directly into the CSS String. + */ + private String applyWebviewStyle(String textColor, String data) { + final String WEBVIEW_STYLE = "<html><head><style type=\"text/css\"> * { color: %s; font-family: Helvetica; line-height: 1.5em; font-size: 11pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }</style></head><body>%s</body></html>"; + final int pageMargin = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 8, getResources() + .getDisplayMetrics()); + return String.format(WEBVIEW_STYLE, textColor, "100%", pageMargin, + pageMargin, pageMargin, pageMargin, data); + } + + private View.OnLongClickListener webViewLongClickListener = new View.OnLongClickListener() { + + @Override + public boolean onLongClick(View v) { + WebView.HitTestResult r = webvDescription.getHitTestResult(); + if (r != null + && r.getType() == WebView.HitTestResult.SRC_ANCHOR_TYPE) { + if (AppConfig.DEBUG) + Log.d(TAG, + "Link of webview was long-pressed. Extra: " + + r.getExtra()); + selectedURL = r.getExtra(); + webvDescription.showContextMenu(); + return true; + } + selectedURL = null; + return false; + } + }; + + @SuppressWarnings("deprecation") + @SuppressLint("NewApi") + @Override + public boolean onContextItemSelected(MenuItem item) { + boolean handled = selectedURL != null; + if (selectedURL != null) { + switch (item.getItemId()) { + case R.id.open_in_browser_item: + Uri uri = Uri.parse(selectedURL); + getActivity() + .startActivity(new Intent(Intent.ACTION_VIEW, uri)); + break; + case R.id.share_url_item: + ShareUtils.shareLink(getActivity(), selectedURL); + break; + case R.id.copy_url_item: + if (android.os.Build.VERSION.SDK_INT >= 11) { + ClipData clipData = ClipData.newPlainText(selectedURL, + selectedURL); + android.content.ClipboardManager cm = (android.content.ClipboardManager) getActivity() + .getSystemService(Context.CLIPBOARD_SERVICE); + cm.setPrimaryClip(clipData); + } else { + android.text.ClipboardManager cm = (android.text.ClipboardManager) getActivity() + .getSystemService(Context.CLIPBOARD_SERVICE); + cm.setText(selectedURL); + } + Toast t = Toast.makeText(getActivity(), + R.string.copied_url_msg, Toast.LENGTH_SHORT); + t.show(); + break; + default: + handled = false; + break; + + } + selectedURL = null; + } + return handled; + + } + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenuInfo menuInfo) { + if (selectedURL != null) { + super.onCreateContextMenu(menu, v, menuInfo); + menu.add(Menu.NONE, R.id.open_in_browser_item, Menu.NONE, + R.string.open_in_browser_label); + menu.add(Menu.NONE, R.id.copy_url_item, Menu.NONE, + R.string.copy_url_label); + menu.add(Menu.NONE, R.id.share_url_item, Menu.NONE, + R.string.share_url_label); + menu.setHeaderTitle(selectedURL); + } + } + + private AsyncTask<Void, Void, Void> createLoader() { + return new AsyncTask<Void, Void, Void>() { + @Override + protected void onCancelled() { + super.onCancelled(); + if (getActivity() != null) { + ((ActionBarActivity) getActivity()) + .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"); + if (getActivity() != null) { + ((ActionBarActivity) getActivity()) + .setSupportProgressBarIndeterminateVisibility(false); + } + if (AppConfig.DEBUG) + Log.d(TAG, "Webview loaded"); + webViewLoader = null; + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (getActivity() != null) { + ((ActionBarActivity) getActivity()) + .setSupportProgressBarIndeterminateVisibility(false); + } + } + + @Override + protected Void doInBackground(Void... params) { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading Webview"); + try { + Callable<String> shownotesLoadTask = shownotesProvider.loadShownotes(); + final String shownotes = shownotesLoadTask.call(); + + data = ""; + data = StringEscapeUtils.unescapeHtml4(shownotes); + Activity activity = getActivity(); + if (activity != null) { + TypedArray res = getActivity() + .getTheme() + .obtainStyledAttributes( + new int[]{android.R.attr.textColorPrimary}); + int colorResource = res.getColor(0, 0); + String colorString = String.format("#%06X", + 0xFFFFFF & colorResource); + Log.i(TAG, "text color: " + colorString); + res.recycle(); + data = applyWebviewStyle(colorString, data); + } else { + cancel(true); + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + + }; + } + + @Override + public void onPause() { + super.onPause(); + savePreference(); + } + + private void savePreference() { + if (saveState) { + if (AppConfig.DEBUG) + Log.d(TAG, "Saving preferences"); + SharedPreferences prefs = getActivity().getSharedPreferences(PREF, + Activity.MODE_PRIVATE); + SharedPreferences.Editor editor = prefs.edit(); + if (media != null && webvDescription != null) { + if (AppConfig.DEBUG) + Log.d(TAG, + "Saving scroll position: " + + webvDescription.getScrollY()); + editor.putInt(PREF_SCROLL_Y, webvDescription.getScrollY()); + editor.putString(PREF_PLAYABLE_ID, media.getIdentifier() + .toString()); + } else { + if (AppConfig.DEBUG) + Log.d(TAG, + "savePreferences was called while media or webview was null"); + editor.putInt(PREF_SCROLL_Y, -1); + editor.putString(PREF_PLAYABLE_ID, ""); + } + editor.commit(); + } + } + + private boolean restoreFromPreference() { + if (saveState) { + if (AppConfig.DEBUG) + Log.d(TAG, "Restoring from preferences"); + Activity activity = getActivity(); + if (activity != null) { + SharedPreferences prefs = activity.getSharedPreferences( + PREF, Activity.MODE_PRIVATE); + String id = prefs.getString(PREF_PLAYABLE_ID, ""); + int scrollY = prefs.getInt(PREF_SCROLL_Y, -1); + if (scrollY != -1 && media != null + && id.equals(media.getIdentifier().toString()) + && webvDescription != null) { + if (AppConfig.DEBUG) + Log.d(TAG, "Restored scroll Position: " + scrollY); + webvDescription.scrollTo(webvDescription.getScrollX(), + scrollY); + return true; + } + } + } + return false; + } } diff --git a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java index 6265694f6..40637544d 100644 --- a/src/de/danoeh/antennapod/fragment/ItemlistFragment.java +++ b/src/de/danoeh/antennapod/fragment/ItemlistFragment.java @@ -1,8 +1,12 @@ package de.danoeh.antennapod.fragment; import android.annotation.SuppressLint; +import android.content.Context; import android.content.Intent; +import android.os.AsyncTask; import android.os.Bundle; +import android.support.v4.app.ListFragment; +import android.support.v7.app.ActionBarActivity; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; @@ -12,27 +16,30 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ListView; -import com.actionbarsherlock.app.SherlockListFragment; +import android.widget.TextView; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.ItemviewActivity; import de.danoeh.antennapod.adapter.ActionButtonCallback; -import de.danoeh.antennapod.adapter.DefaultFeedItemlistAdapter; import de.danoeh.antennapod.adapter.InternalFeedItemlistAdapter; import de.danoeh.antennapod.dialog.DownloadRequestErrorDialogCreator; import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.feed.Feed; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.service.download.DownloadService; +import de.danoeh.antennapod.storage.DBReader; import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.storage.DownloadRequester; +import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.menuhandler.FeedItemMenuHandler; +import java.util.Iterator; +import java.util.List; + /** Displays a list of FeedItems. */ @SuppressLint("ValidFragment") -public class ItemlistFragment extends SherlockListFragment { +public class ItemlistFragment extends ListFragment { private static final String TAG = "ItemlistFragment"; private static final int EVENTS = EventDistributor.DOWNLOAD_HANDLED @@ -43,12 +50,10 @@ public class ItemlistFragment extends SherlockListFragment { 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 InternalFeedItemlistAdapter fila; - protected FeedManager manager = FeedManager.getInstance(); protected DownloadRequester requester = DownloadRequester.getInstance(); - private DefaultFeedItemlistAdapter.ItemAccess itemAccess; - private Feed feed; + protected List<Long> queue; protected FeedItem selectedItem = null; protected boolean contextMenuClosed = true; @@ -56,10 +61,8 @@ public class ItemlistFragment extends SherlockListFragment { /** Argument for FeeditemlistAdapter */ protected boolean showFeedtitle; - public ItemlistFragment(DefaultFeedItemlistAdapter.ItemAccess itemAccess, - boolean showFeedtitle) { + public ItemlistFragment(boolean showFeedtitle) { super(); - this.itemAccess = itemAccess; this.showFeedtitle = showFeedtitle; } @@ -83,6 +86,30 @@ public class ItemlistFragment extends SherlockListFragment { return i; } + private InternalFeedItemlistAdapter.ItemAccess itemAccessRef; + protected InternalFeedItemlistAdapter.ItemAccess itemAccess() { + if (itemAccessRef == null) { + itemAccessRef = new InternalFeedItemlistAdapter.ItemAccess() { + + @Override + public FeedItem getItem(int position) { + return (feed != null) ? feed.getItemAtIndex(true, position) : null; + } + + @Override + public int getCount() { + return (feed != null) ? feed.getNumOfItems(true) : 0; + } + + @Override + public boolean isInQueue(FeedItem item) { + return (queue != null) && queue.contains(item.getId()); + } + }; + } + return itemAccessRef; + } + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -92,27 +119,71 @@ public class ItemlistFragment extends SherlockListFragment { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (itemAccess == null) { - long feedId = getArguments().getLong(ARGUMENT_FEED_ID); - final Feed feed = FeedManager.getInstance().getFeed(feedId); - this.feed = feed; - itemAccess = new DefaultFeedItemlistAdapter.ItemAccess() { - - @Override - public FeedItem getItem(int position) { - return feed.getItemAtIndex(true, position); - } - - @Override - public int getCount() { - return feed.getNumOfItems(true); - } - }; - } + loadData(); } + protected void loadData() { + final long feedId; + if (feed == null) { + feedId = getArguments().getLong(ARGUMENT_FEED_ID); + } else { + feedId = feed.getId(); + } + AsyncTask<Long, Void, Feed> loadTask = new AsyncTask<Long, Void, Feed>(){ + private volatile List<Long> queueRef; + + @Override + protected Feed doInBackground(Long... longs) { + Context context = ItemlistFragment.this.getActivity(); + if (context != null) { + Feed result = DBReader.getFeed(context, longs[0]); + if (result != null) { + result.setItems(DBReader.getFeedItemList(context, result)); + queueRef = DBReader.getQueueIDList(context); + return result; + } + } + return null; + } + + @Override + protected void onPostExecute(Feed result) { + super.onPostExecute(result); + if (result != null && result.getItems() != null) { + feed = result; + if (queueRef != null) { + queue = queueRef; + } else { + Log.e(TAG, "Could not load queue"); + } + if (result.getItems().isEmpty()) { + } + setEmptyViewIfListIsEmpty(); + if (fila != null) { + fila.notifyDataSetChanged(); + } + } else { + if (result == null) { + Log.e(TAG, "Could not load feed with id " + feedId); + } else if (result.getItems() == null) { + Log.e(TAG, "Could not load feed items"); + } + } + } + }; + loadTask.execute(feedId); + } + + private void setEmptyViewIfListIsEmpty() { + if (getListView() != null && feed != null && feed.getItems() != null) { + if (feed.getItems().isEmpty()) { + ((TextView) getActivity().findViewById(android.R.id.empty)).setText(R.string.no_items_label); + } + } + } + protected InternalFeedItemlistAdapter createListAdapter() { - return new InternalFeedItemlistAdapter(getActivity(), itemAccess, + return new InternalFeedItemlistAdapter(getActivity(), itemAccess(), adapterCallback, showFeedtitle); } @@ -162,7 +233,9 @@ public class ItemlistFragment extends SherlockListFragment { if ((EventDistributor.DOWNLOAD_QUEUED & arg) != 0) { updateProgressBarVisibility(); } else { - fila.notifyDataSetChanged(); + if (feed != null) { + loadData(); + } updateProgressBarVisibility(); } } @@ -173,13 +246,13 @@ public class ItemlistFragment extends SherlockListFragment { if (feed != null) { if (DownloadService.isRunning && DownloadRequester.getInstance().isDownloadingFile(feed)) { - getSherlockActivity() + ((ActionBarActivity) getActivity()) .setSupportProgressBarIndeterminateVisibility(true); } else { - getSherlockActivity() + ((ActionBarActivity) getActivity()) .setSupportProgressBarIndeterminateVisibility(false); } - getSherlockActivity().supportInvalidateOptionsMenu(); + getActivity().supportInvalidateOptionsMenu(); } } @@ -218,13 +291,13 @@ public class ItemlistFragment extends SherlockListFragment { menu.setHeaderTitle(selectedItem.getTitle()); FeedItemMenuHandler.onPrepareMenu( - new FeedItemMenuHandler.MenuInterface() { + new FeedItemMenuHandler.MenuInterface() { - @Override - public void setItemVisibility(int id, boolean visible) { - menu.findItem(id).setVisible(visible); - } - }, selectedItem, false); + @Override + public void setItemVisibility(int id, boolean visible) { + menu.findItem(id).setVisible(visible); + } + }, selectedItem, false, QueueAccess.IDListAccess(queue)); } } @@ -237,7 +310,7 @@ public class ItemlistFragment extends SherlockListFragment { try { handled = FeedItemMenuHandler.onMenuItemClicked( - getSherlockActivity(), item.getItemId(), selectedItem); + getActivity(), item.getItemId(), selectedItem); } catch (DownloadRequestException e) { e.printStackTrace(); DownloadRequestErrorDialogCreator.newRequestErrorDialog( diff --git a/src/de/danoeh/antennapod/fragment/MiroGuideChannellistFragment.java b/src/de/danoeh/antennapod/fragment/MiroGuideChannellistFragment.java index c378c0acd..c6901ad17 100644 --- a/src/de/danoeh/antennapod/fragment/MiroGuideChannellistFragment.java +++ b/src/de/danoeh/antennapod/fragment/MiroGuideChannellistFragment.java @@ -10,6 +10,7 @@ import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; +import android.support.v4.app.ListFragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -17,8 +18,6 @@ import android.widget.AbsListView; import android.widget.AbsListView.OnScrollListener; import android.widget.ListView; -import com.actionbarsherlock.app.SherlockListFragment; - import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MiroGuideChannelViewActivity; @@ -33,7 +32,7 @@ import de.danoeh.antennapod.miroguide.model.MiroGuideChannel; * entries will be loaded until all entries have been loaded or the maximum * number of channels has been reached. * */ -public class MiroGuideChannellistFragment extends SherlockListFragment { +public class MiroGuideChannellistFragment extends ListFragment { private static final String TAG = "MiroGuideChannellistFragment"; private static final String ARG_FILTER = "filter"; diff --git a/src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java b/src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java index b471d5303..d20cb63c4 100644 --- a/src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java +++ b/src/de/danoeh/antennapod/fragment/PlaybackHistoryFragment.java @@ -1,33 +1,53 @@ package de.danoeh.antennapod.fragment; +import android.content.Context; +import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.adapter.DefaultFeedItemlistAdapter; +import de.danoeh.antennapod.adapter.InternalFeedItemlistAdapter; import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; +import de.danoeh.antennapod.storage.DBReader; + +import java.util.Iterator; +import java.util.List; public class PlaybackHistoryFragment extends ItemlistFragment { private static final String TAG = "PlaybackHistoryFragment"; + private List<FeedItem> playbackHistory; + public PlaybackHistoryFragment() { - super(new DefaultFeedItemlistAdapter.ItemAccess() { + super(true); + } - @Override - public FeedItem getItem(int position) { - return FeedManager.getInstance().getPlaybackHistoryItemIndex( - position); - } + InternalFeedItemlistAdapter.ItemAccess itemAccessRef; + @Override + protected InternalFeedItemlistAdapter.ItemAccess itemAccess() { + if (itemAccessRef == null) { + itemAccessRef = new InternalFeedItemlistAdapter.ItemAccess() { - @Override - public int getCount() { - return FeedManager.getInstance().getPlaybackHistorySize(); - } - }, true); - } + @Override + public FeedItem getItem(int position) { + return (playbackHistory != null) ? playbackHistory.get(position) : null; + } - @Override + @Override + public int getCount() { + return (playbackHistory != null) ? playbackHistory.size() : 0; + } + + @Override + public boolean isInQueue(FeedItem item) { + return (queue != null) ? queue.contains(item.getId()) : false; + } + }; + } + return itemAccessRef; + } + + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); EventDistributor.getInstance().register(historyUpdate); @@ -46,10 +66,48 @@ public class PlaybackHistoryFragment extends ItemlistFragment { if ((EventDistributor.PLAYBACK_HISTORY_UPDATE & arg) != 0) { if (AppConfig.DEBUG) Log.d(TAG, "Received content update"); - fila.notifyDataSetChanged(); + loadData(); } } }; + @Override + protected void loadData() { + AsyncTask<Void, Void, Void> loadTask = new AsyncTask<Void, Void, Void>() { + private volatile List<FeedItem> phRef; + private volatile List<Long> queueRef; + + @Override + protected Void doInBackground(Void... voids) { + Context context = PlaybackHistoryFragment.this.getActivity(); + if (context != null) { + queueRef = DBReader.getQueueIDList(context); + phRef = DBReader.getPlaybackHistory(context); + } + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + super.onPostExecute(aVoid); + if (queueRef != null && phRef != null) { + queue = queueRef; + playbackHistory = phRef; + Log.i(TAG, "Number of items in playback history: " + playbackHistory.size()); + if (fila != null) { + fila.notifyDataSetChanged(); + } + } else { + if (queueRef == null) { + Log.e(TAG, "Could not load queue"); + } + if (phRef == null) { + Log.e(TAG, "Could not load playback history"); + } + } + } + }; + loadTask.execute(); + } } diff --git a/src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java b/src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java index b58527130..aebe5a681 100644 --- a/src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java +++ b/src/de/danoeh/antennapod/receiver/ConnectivityActionReceiver.java @@ -7,7 +7,7 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.util.Log; import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.feed.FeedManager; +import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.NetworkUtils; @@ -27,7 +27,7 @@ public class ConnectivityActionReceiver extends BroadcastReceiver { new Thread() { @Override public void run() { - FeedManager.getInstance() + DBTasks .autodownloadUndownloadedItems(context); } }.start(); diff --git a/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java b/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java index 821ade4b0..fdbaa97f0 100644 --- a/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java +++ b/src/de/danoeh/antennapod/receiver/FeedUpdateReceiver.java @@ -7,8 +7,8 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.util.Log; import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.storage.DBTasks; /** Refreshes all feeds when it receives an intent */ public class FeedUpdateReceiver extends BroadcastReceiver { @@ -22,7 +22,7 @@ public class FeedUpdateReceiver extends BroadcastReceiver { Log.d(TAG, "Received intent"); boolean mobileUpdate = UserPreferences.isAllowMobileUpdate(); if (mobileUpdate || connectedToWifi(context)) { - FeedManager.getInstance().refreshExpiredFeeds(context); + DBTasks.refreshExpiredFeeds(context); } else { if (AppConfig.DEBUG) Log.d(TAG, diff --git a/src/de/danoeh/antennapod/service/PlaybackService.java b/src/de/danoeh/antennapod/service/PlaybackService.java index e6cf6ee82..7c306984c 100644 --- a/src/de/danoeh/antennapod/service/PlaybackService.java +++ b/src/de/danoeh/antennapod/service/PlaybackService.java @@ -2,13 +2,8 @@ package de.danoeh.antennapod.service; import java.io.IOException; import java.util.Date; -import java.util.concurrent.Future; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.List; +import java.util.concurrent.*; import android.annotation.SuppressLint; import android.app.Notification; @@ -40,366 +35,406 @@ import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.AudioplayerActivity; import de.danoeh.antennapod.activity.VideoplayerActivity; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.FeedComponent; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.feed.MediaType; +import de.danoeh.antennapod.feed.*; import de.danoeh.antennapod.preferences.PlaybackPreferences; import de.danoeh.antennapod.preferences.UserPreferences; import de.danoeh.antennapod.receiver.MediaButtonReceiver; import de.danoeh.antennapod.receiver.PlayerWidget; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.storage.DBTasks; +import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.util.BitmapDecoder; +import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.flattr.FlattrUtils; import de.danoeh.antennapod.util.playback.Playable; import de.danoeh.antennapod.util.playback.Playable.PlayableException; +import de.danoeh.antennapod.util.playback.PlaybackController; -/** Controls the MediaPlayer that plays a FeedMedia-file */ +/** + * Controls the MediaPlayer that plays a FeedMedia-file + */ public class PlaybackService extends Service { - /** Logging tag */ - private static final String TAG = "PlaybackService"; - - /** Parcelable of type Playable. */ - public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; - /** 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 EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately"; - - public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged"; - private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; - - 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"; - - /** - * If the PlaybackService receives this action, it will stop playback and - * try to shutdown. - */ - public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService"; - - /** - * If the PlaybackService receives this action, it will end playback of the - * current episode and load the next episode if there is one available. - * */ - public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode"; - - /** Used in NOTIFICATION_TYPE_RELOAD. */ - public static final int EXTRA_CODE_AUDIO = 1; - public static final int EXTRA_CODE_VIDEO = 2; - - 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; - /** The state of the sleeptimer changed. */ - public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; - public static final int NOTIFICATION_TYPE_BUFFER_START = 5; - public static final int NOTIFICATION_TYPE_BUFFER_END = 6; - /** No more episodes are going to be played. */ - public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; - - /** - * Returned by getPositionSafe() or getDurationSafe() if the playbackService - * is in an invalid state. - */ - public static final int INVALID_TIME = -1; - - /** Is true if service is running. */ - public static boolean isRunning = false; - - private static final int NOTIFICATION_ID = 1; - - private AudioManager audioManager; - private ComponentName mediaButtonReceiver; - - private MediaPlayer player; - private RemoteControlClient remoteControlClient; - - private Playable media; - - /** True if media should be streamed (Extracted from Intent Extra) . */ - private boolean shouldStream; - - /** True if service should prepare playback after it has been initialized */ - private boolean prepareImmediately; - private boolean startWhenPrepared; - private FeedManager manager; - private PlayerStatus status; - - private PositionSaver positionSaver; - private ScheduledFuture positionSaverFuture; - - private WidgetUpdateWorker widgetUpdater; - private ScheduledFuture widgetUpdaterFuture; - - private SleepTimer sleepTimer; - private Future sleepTimerFuture; - - private static final int SCHED_EX_POOL_SIZE = 3; - private ScheduledThreadPoolExecutor schedExecutor; - - private volatile PlayerStatus statusBeforeSeek; - - private static boolean playingVideo; - - /** True if mediaplayer was paused because it lost audio focus temporarily */ - private boolean pausedBecauseOfTransientAudiofocusLoss; - - private Thread chapterLoader; - - private final IBinder mBinder = new LocalBinder(); - - public class LocalBinder extends Binder { - public PlaybackService getService() { - return PlaybackService.this; - } - } - - @Override - public boolean onUnbind(Intent intent) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received onUnbind event"); - return super.onUnbind(intent); - } - - /** - * Returns an intent which starts an audio- or videoplayer, depending on the - * type of media that is being played. If the playbackservice is not - * running, the type of the last played media will be looked up. - * */ - public static Intent getPlayerActivityIntent(Context context) { - if (isRunning) { - if (playingVideo) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } else { - if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } - } - - /** - * Same as getPlayerActivityIntent(context), but here the type of activity - * depends on the FeedMedia that is provided as an argument. - */ - public static Intent getPlayerActivityIntent(Context context, Playable media) { - MediaType mt = media.getMediaType(); - if (mt == MediaType.VIDEO) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } - - @SuppressLint("NewApi") - @Override - public void onCreate() { - super.onCreate(); - if (AppConfig.DEBUG) - Log.d(TAG, "Service created."); - isRunning = true; - pausedBecauseOfTransientAudiofocusLoss = false; - status = PlayerStatus.STOPPED; - audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); - manager = FeedManager.getInstance(); - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }, new RejectedExecutionHandler() { - - @Override - public void rejectedExecution(Runnable r, - ThreadPoolExecutor executor) { - Log.w(TAG, "SchedEx rejected submission of new task"); - } - }); - player = createMediaPlayer(); - - mediaButtonReceiver = new ComponentName(getPackageName(), - MediaButtonReceiver.class.getName()); - audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver); - if (android.os.Build.VERSION.SDK_INT >= 14) { - audioManager - .registerRemoteControlClient(setupRemoteControlClient()); - } - registerReceiver(headsetDisconnected, new IntentFilter( - Intent.ACTION_HEADSET_PLUG)); - registerReceiver(shutdownReceiver, new IntentFilter( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - registerReceiver(audioBecomingNoisy, new IntentFilter( - AudioManager.ACTION_AUDIO_BECOMING_NOISY)); - registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( - ACTION_SKIP_CURRENT_EPISODE)); - - } - - private MediaPlayer createMediaPlayer() { - return createMediaPlayer(new MediaPlayer()); - } - - private MediaPlayer createMediaPlayer(MediaPlayer mp) { - if (mp != null) { - mp.setOnPreparedListener(preparedListener); - mp.setOnCompletionListener(completionListener); - mp.setOnSeekCompleteListener(onSeekCompleteListener); - mp.setOnErrorListener(onErrorListener); - mp.setOnBufferingUpdateListener(onBufferingUpdateListener); - mp.setOnInfoListener(onInfoListener); - } - return mp; - } - - @SuppressLint("NewApi") - @Override - public void onDestroy() { - super.onDestroy(); - if (AppConfig.DEBUG) - Log.d(TAG, "Service is about to be destroyed"); - isRunning = false; - if (chapterLoader != null) { - chapterLoader.interrupt(); - } - disableSleepTimer(); - unregisterReceiver(headsetDisconnected); - unregisterReceiver(shutdownReceiver); - unregisterReceiver(audioBecomingNoisy); - unregisterReceiver(skipCurrentEpisodeReceiver); - if (android.os.Build.VERSION.SDK_INT >= 14) { - audioManager.unregisterRemoteControlClient(remoteControlClient); - } - audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver); - audioManager.abandonAudioFocus(audioFocusChangeListener); - player.release(); - stopWidgetUpdater(); - updateWidget(); - } - - @Override - public IBinder onBind(Intent intent) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received onBind event"); - return mBinder; - } - - private final OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() { - - @Override - public void onAudioFocusChange(int focusChange) { - switch (focusChange) { - case AudioManager.AUDIOFOCUS_LOSS: - if (AppConfig.DEBUG) - Log.d(TAG, "Lost audio focus"); - pause(true, false); - stopSelf(); - break; - case AudioManager.AUDIOFOCUS_GAIN: - if (AppConfig.DEBUG) - Log.d(TAG, "Gained audio focus"); - if (pausedBecauseOfTransientAudiofocusLoss) { - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_RAISE, 0); - play(); - } - break; - case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: - if (status == PlayerStatus.PLAYING) { - if (AppConfig.DEBUG) - 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: - if (status == PlayerStatus.PLAYING) { - if (AppConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); - pause(false, false); - pausedBecauseOfTransientAudiofocusLoss = true; - } - } - } - }; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - - if (AppConfig.DEBUG) - Log.d(TAG, "OnStartCommand called"); - int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); - if (keycode != -1) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received media button event"); - handleKeycode(keycode); - } else { - - Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); - boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, - true); - if (playable == null) { - Log.e(TAG, "Playable extra wasn't sent to the service"); - if (media == null) { - stopSelf(); - } - // Intent values appear to be valid - // check if already playing and playbackType is the same - } else if (media == null - || !playable.getIdentifier().equals(media.getIdentifier()) - || playbackType != shouldStream) { - pause(true, false); - player.reset(); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - if (media == null - || !playable.getIdentifier().equals( - media.getIdentifier())) { - media = playable; - } - - if (media != null) { - shouldStream = playbackType; - startWhenPrepared = intent.getBooleanExtra( - EXTRA_START_WHEN_PREPARED, false); - prepareImmediately = intent.getBooleanExtra( - EXTRA_PREPARE_IMMEDIATELY, false); - initMediaplayer(); - - } 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; - } + /** + * Logging tag + */ + private static final String TAG = "PlaybackService"; + + /** + * Parcelable of type Playable. + */ + public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; + /** + * 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 EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately"; + + public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged"; + private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; + + 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"; + + /** + * If the PlaybackService receives this action, it will stop playback and + * try to shutdown. + */ + public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService"; + + /** + * If the PlaybackService receives this action, it will end playback of the + * current episode and load the next episode if there is one available. + */ + public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode"; + + /** + * Used in NOTIFICATION_TYPE_RELOAD. + */ + public static final int EXTRA_CODE_AUDIO = 1; + public static final int EXTRA_CODE_VIDEO = 2; + + 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; + + /** + * Receivers of this intent should update their information about the curently playing media + */ + public static final int NOTIFICATION_TYPE_RELOAD = 3; + /** + * The state of the sleeptimer changed. + */ + public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; + public static final int NOTIFICATION_TYPE_BUFFER_START = 5; + public static final int NOTIFICATION_TYPE_BUFFER_END = 6; + /** + * No more episodes are going to be played. + */ + public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; + + /** + * Returned by getPositionSafe() or getDurationSafe() if the playbackService + * is in an invalid state. + */ + public static final int INVALID_TIME = -1; + + /** + * Is true if service is running. + */ + public static boolean isRunning = false; + + private static final int NOTIFICATION_ID = 1; + + private AudioManager audioManager; + private ComponentName mediaButtonReceiver; + + private MediaPlayer player; + private RemoteControlClient remoteControlClient; + + private Playable media; + + /** + * True if media should be streamed (Extracted from Intent Extra) . + */ + private boolean shouldStream; + + private boolean startWhenPrepared; + private PlayerStatus status; + + private PositionSaver positionSaver; + private ScheduledFuture positionSaverFuture; + + private WidgetUpdateWorker widgetUpdater; + private ScheduledFuture widgetUpdaterFuture; + + private SleepTimer sleepTimer; + private Future sleepTimerFuture; + + private static final int SCHED_EX_POOL_SIZE = 3; + private ScheduledThreadPoolExecutor schedExecutor; + private ExecutorService dbLoaderExecutor; + + private volatile PlayerStatus statusBeforeSeek; + + private static boolean playingVideo; + + /** + * True if mediaplayer was paused because it lost audio focus temporarily + */ + private boolean pausedBecauseOfTransientAudiofocusLoss; + + private Thread chapterLoader; + + private final IBinder mBinder = new LocalBinder(); + + private volatile List<FeedItem> queue; + + public class LocalBinder extends Binder { + public PlaybackService getService() { + return PlaybackService.this; + } + } + + @Override + public boolean onUnbind(Intent intent) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received onUnbind event"); + return super.onUnbind(intent); + } + + /** + * Returns an intent which starts an audio- or videoplayer, depending on the + * type of media that is being played. If the playbackservice is not + * running, the type of the last played media will be looked up. + */ + public static Intent getPlayerActivityIntent(Context context) { + if (isRunning) { + if (playingVideo) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } else { + if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } + } + + /** + * Same as getPlayerActivityIntent(context), but here the type of activity + * depends on the FeedMedia that is provided as an argument. + */ + public static Intent getPlayerActivityIntent(Context context, Playable media) { + MediaType mt = media.getMediaType(); + if (mt == MediaType.VIDEO) { + return new Intent(context, VideoplayerActivity.class); + } else { + return new Intent(context, AudioplayerActivity.class); + } + } + + @SuppressLint("NewApi") + @Override + public void onCreate() { + super.onCreate(); + if (AppConfig.DEBUG) + Log.d(TAG, "Service created."); + isRunning = true; + pausedBecauseOfTransientAudiofocusLoss = false; + status = PlayerStatus.STOPPED; + audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE); + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, + new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }, new RejectedExecutionHandler() { + + @Override + public void rejectedExecution(Runnable r, + ThreadPoolExecutor executor) { + Log.w(TAG, "SchedEx rejected submission of new task"); + } + } + ); + dbLoaderExecutor = Executors.newSingleThreadExecutor(); + player = createMediaPlayer(); + + mediaButtonReceiver = new ComponentName(getPackageName(), + MediaButtonReceiver.class.getName()); + audioManager.registerMediaButtonEventReceiver(mediaButtonReceiver); + if (android.os.Build.VERSION.SDK_INT >= 14) { + audioManager + .registerRemoteControlClient(setupRemoteControlClient()); + } + registerReceiver(headsetDisconnected, new IntentFilter( + Intent.ACTION_HEADSET_PLUG)); + registerReceiver(shutdownReceiver, new IntentFilter( + ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + registerReceiver(audioBecomingNoisy, new IntentFilter( + AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( + ACTION_SKIP_CURRENT_EPISODE)); + EventDistributor.getInstance().register(eventDistributorListener); + loadQueue(); + } + + private MediaPlayer createMediaPlayer() { + return createMediaPlayer(new MediaPlayer()); + } + + private MediaPlayer createMediaPlayer(MediaPlayer mp) { + if (mp != null) { + mp.setOnPreparedListener(preparedListener); + mp.setOnCompletionListener(completionListener); + mp.setOnSeekCompleteListener(onSeekCompleteListener); + mp.setOnErrorListener(onErrorListener); + mp.setOnBufferingUpdateListener(onBufferingUpdateListener); + mp.setOnInfoListener(onInfoListener); + } + return mp; + } + + @SuppressLint("NewApi") + @Override + public void onDestroy() { + super.onDestroy(); + if (AppConfig.DEBUG) + Log.d(TAG, "Service is about to be destroyed"); + isRunning = false; + if (chapterLoader != null) { + chapterLoader.interrupt(); + } + disableSleepTimer(); + unregisterReceiver(headsetDisconnected); + unregisterReceiver(shutdownReceiver); + unregisterReceiver(audioBecomingNoisy); + unregisterReceiver(skipCurrentEpisodeReceiver); + EventDistributor.getInstance().unregister(eventDistributorListener); + if (android.os.Build.VERSION.SDK_INT >= 14) { + audioManager.unregisterRemoteControlClient(remoteControlClient); + } + audioManager.unregisterMediaButtonEventReceiver(mediaButtonReceiver); + audioManager.abandonAudioFocus(audioFocusChangeListener); + player.release(); + stopWidgetUpdater(); + updateWidget(); + } + + @Override + public IBinder onBind(Intent intent) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received onBind event"); + return mBinder; + } + + private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((EventDistributor.QUEUE_UPDATE & arg) != 0) { + loadQueue(); + } + } + }; + + private final OnAudioFocusChangeListener audioFocusChangeListener = new OnAudioFocusChangeListener() { + + @Override + public void onAudioFocusChange(int focusChange) { + switch (focusChange) { + case AudioManager.AUDIOFOCUS_LOSS: + if (AppConfig.DEBUG) + Log.d(TAG, "Lost audio focus"); + pause(true, false); + stopSelf(); + break; + case AudioManager.AUDIOFOCUS_GAIN: + if (AppConfig.DEBUG) + Log.d(TAG, "Gained audio focus"); + if (pausedBecauseOfTransientAudiofocusLoss) { + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_RAISE, 0); + play(); + } + break; + case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: + if (status == PlayerStatus.PLAYING) { + if (AppConfig.DEBUG) + 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: + if (status == PlayerStatus.PLAYING) { + if (AppConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + pause(false, false); + pausedBecauseOfTransientAudiofocusLoss = true; + } + } + } + }; + + /** + * 1. Check type of intent + * 1.1 Keycode -> handle keycode -> done + * 1.2 Playable -> Step 2 + * 2. Handle playable + * 2.1 Check current status + * 2.1.1 Not playing -> play new playable + * 2.1.2 Playing, new playable is the same -> play if playback is currently paused + * 2.1.3 Playing, new playable different -> Stop playback of old media + * + * @param intent + * @param flags + * @param startId + * @return + */ + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + + if (AppConfig.DEBUG) + Log.d(TAG, "OnStartCommand called"); + final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); + final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); + if (keycode == -1 && playable == null) { + Log.e(TAG, "PlaybackService was started with no arguments"); + stopSelf(); + } + + if (keycode != -1) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received media button event"); + handleKeycode(keycode); + } else { + boolean playbackType = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, + true); + if (media == null) { + media = playable; + shouldStream = playbackType; + startWhenPrepared = intent.getBooleanExtra( + EXTRA_START_WHEN_PREPARED, false); + initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false)); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + } + if (media != null) { + if (!playable.getIdentifier().equals(media.getIdentifier())) { + // different media or different playback type + pause(true, false); + player.reset(); + media = playable; + shouldStream = playbackType; + startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); + initMediaplayer(intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false)); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + } else { + // same media and same playback type + if (status == PlayerStatus.PAUSED) { + play(); + } + } + } + } + + return Service.START_NOT_STICKY; + } /** Handles media button events */ private void handleKeycode(int keycode) { @@ -432,166 +467,180 @@ public class PlaybackService extends Service { pause(true, true); } break; + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { + seekDelta(PlaybackController.DEFAULT_SEEK_DELTA); + break; } + case KeyEvent.KEYCODE_MEDIA_REWIND: { + seekDelta(-PlaybackController.DEFAULT_SEEK_DELTA); + break; + } + } } - /** - * Called by a mediaplayer Activity as soon as it has prepared its - * mediaplayer. - */ - public void setVideoSurface(SurfaceHolder sh) { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting display"); - player.setDisplay(null); - player.setDisplay(sh); - if (status == PlayerStatus.STOPPED - || status == PlayerStatus.AWAITING_VIDEO_SURFACE) { - try { - InitTask initTask = new InitTask() { - - @Override - protected void onPostExecute(Playable result) { - if (status == PlayerStatus.INITIALIZING) { - if (result != null) { - try { - if (shouldStream) { - player.setDataSource(media - .getStreamUrl()); - setStatus(PlayerStatus.PREPARING); - player.prepareAsync(); - } else { - player.setDataSource(media - .getLocalMediaUrl()); - setStatus(PlayerStatus.PREPARING); - player.prepareAsync(); - } - } catch (IOException e) { - e.printStackTrace(); - } - } else { - setStatus(PlayerStatus.ERROR); - sendBroadcast(new Intent( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - } - } - - @Override - protected void onPreExecute() { - setStatus(PlayerStatus.INITIALIZING); - } - - }; - initTask.executeAsync(media); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } catch (SecurityException e) { - e.printStackTrace(); - } catch (IllegalStateException e) { - e.printStackTrace(); - } - } - - } - - /** Called when the surface holder of the mediaplayer has to be changed. */ - private void resetVideoSurface() { - if (AppConfig.DEBUG) - Log.d(TAG, "Resetting video surface"); - cancelPositionSaver(); - player.setDisplay(null); - player.reset(); - player.release(); - player = createMediaPlayer(); - status = PlayerStatus.STOPPED; - if (media != null) { - initMediaplayer(); - } - } + /** + * Called by a mediaplayer Activity as soon as it has prepared its + * mediaplayer. + */ + public void setVideoSurface(SurfaceHolder sh) { + if (AppConfig.DEBUG) + Log.d(TAG, "Setting display"); + player.setDisplay(null); + player.setDisplay(sh); + if (status == PlayerStatus.STOPPED + || status == PlayerStatus.AWAITING_VIDEO_SURFACE) { + try { + InitTask initTask = new InitTask() { + + @Override + protected void onPostExecute(Playable result) { + if (status == PlayerStatus.INITIALIZING) { + if (result != null) { + try { + if (shouldStream) { + player.setDataSource(media + .getStreamUrl()); + setStatus(PlayerStatus.PREPARING); + player.prepareAsync(); + } else { + player.setDataSource(media + .getLocalMediaUrl()); + setStatus(PlayerStatus.PREPARING); + player.prepareAsync(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } else { + setStatus(PlayerStatus.ERROR); + sendBroadcast(new Intent( + ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + } + } + } + + @Override + protected void onPreExecute() { + setStatus(PlayerStatus.INITIALIZING); + } + + }; + initTask.executeAsync(media); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + } catch (SecurityException e) { + e.printStackTrace(); + } catch (IllegalStateException e) { + e.printStackTrace(); + } + } + + } + + /** + * Called when the surface holder of the mediaplayer has to be changed. + */ + private void resetVideoSurface() { + if (AppConfig.DEBUG) + Log.d(TAG, "Resetting video surface"); + cancelPositionSaver(); + player.setDisplay(null); + player.reset(); + player.release(); + player = createMediaPlayer(); + status = PlayerStatus.STOPPED; + } public void notifyVideoSurfaceAbandoned() { resetVideoSurface(); + if (media != null) { + initMediaplayer(true); + } } - /** Called after service has extracted the media it is supposed to play. */ - private void initMediaplayer() { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting up media player"); - try { - MediaType mediaType = media.getMediaType(); - if (mediaType == MediaType.AUDIO) { - if (AppConfig.DEBUG) - Log.d(TAG, "Mime type is audio"); - - InitTask initTask = new InitTask() { - - @Override - protected void onPostExecute(Playable result) { - // check if state of service has changed. If it has - // changed, assume that loaded metadata is not needed - // anymore. - if (status == PlayerStatus.INITIALIZING) { - if (result != null) { - playingVideo = false; - try { - if (shouldStream) { - player.setDataSource(media - .getStreamUrl()); - } else if (media.localFileAvailable()) { - player.setDataSource(media - .getLocalMediaUrl()); - } - - if (prepareImmediately) { - setStatus(PlayerStatus.PREPARING); - player.prepareAsync(); - } else { - setStatus(PlayerStatus.INITIALIZED); - } - } catch (IOException e) { - e.printStackTrace(); - media = null; - setStatus(PlayerStatus.ERROR); - sendBroadcast(new Intent( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - } else { - Log.e(TAG, "InitTask could not load metadata"); - media = null; - setStatus(PlayerStatus.ERROR); - sendBroadcast(new Intent( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } - } else { - if (AppConfig.DEBUG) - Log.d(TAG, - "Status of player has changed during initialization. Stopping init process."); - } - } - - @Override - protected void onPreExecute() { - setStatus(PlayerStatus.INITIALIZING); - } - - }; - initTask.executeAsync(media); - } else if (mediaType == MediaType.VIDEO) { - if (AppConfig.DEBUG) - Log.d(TAG, "Mime type is 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(); - } - } + /** + * Called after service has extracted the media it is supposed to play. + * + * @param prepareImmediately True if service should prepare playback after it has been initialized + */ + private void initMediaplayer(final boolean prepareImmediately) { + if (AppConfig.DEBUG) + Log.d(TAG, "Setting up media player"); + try { + MediaType mediaType = media.getMediaType(); + if (mediaType == MediaType.AUDIO) { + if (AppConfig.DEBUG) + Log.d(TAG, "Mime type is audio"); + + InitTask initTask = new InitTask() { + + @Override + protected void onPostExecute(Playable result) { + // check if state of service has changed. If it has + // changed, assume that loaded metadata is not needed + // anymore. + if (status == PlayerStatus.INITIALIZING) { + if (result != null) { + playingVideo = false; + try { + if (shouldStream) { + player.setDataSource(media + .getStreamUrl()); + } else if (media.localFileAvailable()) { + player.setDataSource(media + .getLocalMediaUrl()); + } + + if (prepareImmediately) { + setStatus(PlayerStatus.PREPARING); + player.prepareAsync(); + } else { + setStatus(PlayerStatus.INITIALIZED); + } + } catch (IOException e) { + e.printStackTrace(); + media = null; + setStatus(PlayerStatus.ERROR); + sendBroadcast(new Intent( + ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + } + } else { + Log.e(TAG, "InitTask could not load metadata"); + media = null; + setStatus(PlayerStatus.ERROR); + sendBroadcast(new Intent( + ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + } + } else { + if (AppConfig.DEBUG) + Log.d(TAG, + "Status of player has changed during initialization. Stopping init process."); + } + } + + @Override + protected void onPreExecute() { + setStatus(PlayerStatus.INITIALIZING); + } + + }; + initTask.executeAsync(media); + } else if (mediaType == MediaType.VIDEO) { + if (AppConfig.DEBUG) + Log.d(TAG, "Mime type is 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(); + } + } private void setupPositionSaver() { if (positionSaverFuture == null @@ -713,81 +762,82 @@ public class PlaybackService extends Service { } }; - private void endPlayback(boolean playNextEpisode) { - if (AppConfig.DEBUG) - Log.d(TAG, "Playback ended"); - audioManager.abandonAudioFocus(audioFocusChangeListener); - - // Save state - cancelPositionSaver(); - - boolean isInQueue = false; - FeedItem nextItem = null; - - if (media instanceof FeedMedia) { - FeedItem item = ((FeedMedia) media).getItem(); - ((FeedMedia) media).setPlaybackCompletionDate(new Date()); - manager.markItemRead(PlaybackService.this, item, true, true); - nextItem = manager.getQueueSuccessorOfItem(item); - isInQueue = media instanceof FeedMedia - && manager.isInQueue(((FeedMedia) media).getItem()); - if (isInQueue) { - manager.removeQueueItem(PlaybackService.this, item, true); - } - manager.addItemToPlaybackHistory(PlaybackService.this, item); - manager.setFeedMedia(PlaybackService.this, (FeedMedia) media); - long autoDeleteMediaId = ((FeedComponent) media).getId(); - if (shouldStream) { - autoDeleteMediaId = -1; - } - } - - // Load next episode if previous episode was in the queue and if there - // is an episode in the queue left. - // Start playback immediately if continuous playback is enabled - boolean loadNextItem = isInQueue && nextItem != null; - playNextEpisode = playNextEpisode && loadNextItem - && UserPreferences.isFollowQueue(); - if (loadNextItem) { - if (AppConfig.DEBUG) - Log.d(TAG, "Loading next item in queue"); - media = nextItem.getMedia(); - } - - if (playNextEpisode) { - if (AppConfig.DEBUG) - Log.d(TAG, "Playback of next episode will start immediately."); - prepareImmediately = startWhenPrepared = true; - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "No more episodes available to play"); - media = null; - prepareImmediately = startWhenPrepared = false; - stopForeground(true); - stopWidgetUpdater(); - } - - int notificationCode = 0; - if (media != null) { - shouldStream = !media.localFileAvailable(); - if (media.getMediaType() == MediaType.AUDIO) { - notificationCode = EXTRA_CODE_AUDIO; - playingVideo = false; - } else if (media.getMediaType() == MediaType.VIDEO) { - notificationCode = EXTRA_CODE_VIDEO; - } - } - writePlaybackPreferences(); - if (media != null) { - resetVideoSurface(); - refreshRemoteControlClientState(); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, - notificationCode); - } else { - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); - stopSelf(); - } - } + private void endPlayback(boolean playNextEpisode) { + if (AppConfig.DEBUG) + Log.d(TAG, "Playback ended"); + audioManager.abandonAudioFocus(audioFocusChangeListener); + + // Save state + cancelPositionSaver(); + + boolean isInQueue = false; + FeedItem nextItem = null; + + if (media instanceof FeedMedia) { + FeedItem item = ((FeedMedia) media).getItem(); + DBWriter.markItemRead(PlaybackService.this, item, true, true); + nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); + isInQueue = media instanceof FeedMedia + && QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId()); + if (isInQueue) { + DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); + } + DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); + DBWriter.setFeedMedia(PlaybackService.this, (FeedMedia) media); + long autoDeleteMediaId = ((FeedComponent) media).getId(); + if (shouldStream) { + autoDeleteMediaId = -1; + } + } + + // Load next episode if previous episode was in the queue and if there + // is an episode in the queue left. + // Start playback immediately if continuous playback is enabled + boolean loadNextItem = isInQueue && nextItem != null; + playNextEpisode = playNextEpisode && loadNextItem + && UserPreferences.isFollowQueue(); + if (loadNextItem) { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading next item in queue"); + media = nextItem.getMedia(); + } + final boolean prepareImmediately; + if (playNextEpisode) { + if (AppConfig.DEBUG) + Log.d(TAG, "Playback of next episode will start immediately."); + prepareImmediately = startWhenPrepared = true; + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "No more episodes available to play"); + media = null; + prepareImmediately = startWhenPrepared = false; + stopForeground(true); + stopWidgetUpdater(); + } + + int notificationCode = 0; + if (media != null) { + shouldStream = !media.localFileAvailable(); + if (media.getMediaType() == MediaType.AUDIO) { + notificationCode = EXTRA_CODE_AUDIO; + playingVideo = false; + } else if (media.getMediaType() == MediaType.VIDEO) { + notificationCode = EXTRA_CODE_VIDEO; + } + } + writePlaybackPreferences(); + if (media != null) { + resetVideoSurface(); + refreshRemoteControlClientState(); + initMediaplayer(prepareImmediately); + + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, + notificationCode); + } else { + sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); + stopSelf(); + } + } public void setSleepTimer(long waitingTime) { if (AppConfig.DEBUG) @@ -816,7 +866,7 @@ public class PlaybackService extends Service { * * @param abandonFocus * is true if the service should release audio focus - * @param reset + * @param reinit * is true if service should reinit after pausing if the media * file is being streamed */ @@ -871,8 +921,7 @@ public class PlaybackService extends Service { public void reinit() { player.reset(); player = createMediaPlayer(player); - prepareImmediately = false; - initMediaplayer(); + initMediaplayer(false); } @SuppressLint("NewApi") @@ -1239,8 +1288,10 @@ public class PlaybackService extends Service { i.putExtra("album", media.getFeedTitle()); i.putExtra("track", media.getEpisodeTitle()); i.putExtra("playing", isPlaying); - i.putExtra("ListSize", manager.getQueueSize(false)); - i.putExtra("duration", media.getDuration()); + if (queue != null) { + i.putExtra("ListSize", queue.size()); + } + i.putExtra("duration", media.getDuration()); i.putExtra("position", media.getPosition()); sendBroadcast(i); } @@ -1520,4 +1571,16 @@ public class PlaybackService extends Service { } } + + private void loadQueue() { + dbLoaderExecutor.submit(new QueueLoaderTask()); + } + + private class QueueLoaderTask implements Runnable { + @Override + public void run() { + List<FeedItem> queueRef = DBReader.getQueue(PlaybackService.this); + queue = queueRef; + } + } } diff --git a/src/de/danoeh/antennapod/service/download/DownloadRequest.java b/src/de/danoeh/antennapod/service/download/DownloadRequest.java new file mode 100644 index 000000000..1f4e32e1b --- /dev/null +++ b/src/de/danoeh/antennapod/service/download/DownloadRequest.java @@ -0,0 +1,177 @@ +package de.danoeh.antennapod.service.download; + +import android.os.Parcel; +import android.os.Parcelable; + +public class DownloadRequest implements Parcelable { + + private final String destination; + private final String source; + private final String title; + private final long feedfileId; + private final int feedfileType; + + protected int progressPercent; + protected long soFar; + protected long size; + protected int statusMsg; + + public DownloadRequest(String destination, String source, String title, + long feedfileId, int feedfileType) { + if (destination == null) { + throw new IllegalArgumentException("Destination must not be null"); + } + if (source == null) { + throw new IllegalArgumentException("Source must not be null"); + } + if (title == null) { + throw new IllegalArgumentException("Title must not be null"); + } + + this.destination = destination; + this.source = source; + this.title = title; + this.feedfileId = feedfileId; + this.feedfileType = feedfileType; + } + + private DownloadRequest(Parcel in) { + destination = in.readString(); + source = in.readString(); + title = in.readString(); + feedfileId = in.readLong(); + feedfileType = in.readInt(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(destination); + dest.writeString(source); + dest.writeString(title); + dest.writeLong(feedfileId); + dest.writeInt(feedfileType); + } + + public static final Parcelable.Creator<DownloadRequest> CREATOR = new Parcelable.Creator<DownloadRequest>() { + public DownloadRequest createFromParcel(Parcel in) { + return new DownloadRequest(in); + } + + public DownloadRequest[] newArray(int size) { + return new DownloadRequest[size]; + } + }; + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + + ((destination == null) ? 0 : destination.hashCode()); + result = prime * result + (int) (feedfileId ^ (feedfileId >>> 32)); + result = prime * result + feedfileType; + result = prime * result + progressPercent; + result = prime * result + (int) (size ^ (size >>> 32)); + result = prime * result + (int) (soFar ^ (soFar >>> 32)); + result = prime * result + ((source == null) ? 0 : source.hashCode()); + result = prime * result + statusMsg; + result = prime * result + ((title == null) ? 0 : title.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + DownloadRequest other = (DownloadRequest) obj; + if (destination == null) { + if (other.destination != null) + return false; + } else if (!destination.equals(other.destination)) + return false; + if (feedfileId != other.feedfileId) + return false; + if (feedfileType != other.feedfileType) + return false; + if (progressPercent != other.progressPercent) + return false; + if (size != other.size) + return false; + if (soFar != other.soFar) + return false; + if (source == null) { + if (other.source != null) + return false; + } else if (!source.equals(other.source)) + return false; + if (statusMsg != other.statusMsg) + return false; + if (title == null) { + if (other.title != null) + return false; + } else if (!title.equals(other.title)) + return false; + return true; + } + + public String getDestination() { + return destination; + } + + public String getSource() { + return source; + } + + public String getTitle() { + return title; + } + + public long getFeedfileId() { + return feedfileId; + } + + public int getFeedfileType() { + return feedfileType; + } + + public int getProgressPercent() { + return progressPercent; + } + + public void setProgressPercent(int progressPercent) { + this.progressPercent = progressPercent; + } + + public long getSoFar() { + return soFar; + } + + public void setSoFar(long soFar) { + this.soFar = soFar; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public int getStatusMsg() { + return statusMsg; + } + + public void setStatusMsg(int statusMsg) { + this.statusMsg = statusMsg; + } +} diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java index e1230e170..2056efab2 100644 --- a/src/de/danoeh/antennapod/service/download/DownloadService.java +++ b/src/de/danoeh/antennapod/service/download/DownloadService.java @@ -1,28 +1,17 @@ -/** - * Registers a DownloadReceiver and waits for all Downloads - * to complete, then stops - * */ - package de.danoeh.antennapod.service.download; import java.io.File; import java.io.IOException; -import java.lang.Thread.UncaughtExceptionHandler; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; import javax.xml.parsers.ParserConfigurationException; +import de.danoeh.antennapod.storage.*; import org.xml.sax.SAXException; import android.annotation.SuppressLint; @@ -41,8 +30,6 @@ import android.os.AsyncTask; import android.os.Binder; import android.os.Handler; import android.os.IBinder; -import android.os.Parcel; -import android.os.Parcelable; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.webkit.URLUtil; @@ -50,904 +37,888 @@ import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.DownloadActivity; import de.danoeh.antennapod.activity.DownloadLogActivity; -import de.danoeh.antennapod.asynctask.DownloadStatus; import de.danoeh.antennapod.feed.EventDistributor; import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedFile; import de.danoeh.antennapod.feed.FeedImage; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.storage.DownloadRequestException; -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.ChapterUtils; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.InvalidFeedException; +/** + * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent. + * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUEST field of + * the intent. + * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the + * type of the feedfile. + */ 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_ENQUEUE_DOWNLOAD = "action.de.danoeh.antennapod.service.enqueueDownload"; - public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.service.cancelDownload"; - public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.service.cancelAllDownloads"; - - /** Extra for ACTION_CANCEL_DOWNLOAD */ - public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; - - /** - * Sent by the DownloadService when the content of the downloads list - * changes. - */ - public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.service.downloadsContentChanged"; - - public static final String EXTRA_DOWNLOAD_ID = "extra.de.danoeh.antennapod.service.download_id"; - - /** Extra for ACTION_ENQUEUE_DOWNLOAD intent. */ - public static final String EXTRA_REQUEST = "request"; - - private CopyOnWriteArrayList<DownloadStatus> completedDownloads; - - private ExecutorService syncExecutor; - private ExecutorService downloadExecutor; - /** Number of threads of downloadExecutor. */ - private static final int NUM_PARALLEL_DOWNLOADS = 4; - - private DownloadRequester requester; - private FeedManager manager; - private NotificationCompat.Builder notificationCompatBuilder; - private Notification.BigTextStyle notificationBuilder; - private int NOTIFICATION_ID = 2; - private int REPORT_ID = 3; - - private List<Downloader> downloads; - - /** Number of completed downloads which are currently being handled. */ - private volatile int downloadsBeingHandled; - - private volatile boolean shutdownInitiated = false; - /** True if service is running. */ - public static boolean isRunning = false; - - private Handler handler; - - private NotificationUpdater notificationUpdater; - private ScheduledFuture notificationUpdaterFuture; - private static final int SCHED_EX_POOL_SIZE = 1; - private ScheduledThreadPoolExecutor schedExecutor; - - 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) { - if (intent.getParcelableExtra(EXTRA_REQUEST) != null) { - onDownloadQueued(intent); - } - return Service.START_NOT_STICKY; - } - - @SuppressLint("NewApi") - @Override - public void onCreate() { - if (AppConfig.DEBUG) - Log.d(TAG, "Service started"); - isRunning = true; - handler = new Handler(); - completedDownloads = new CopyOnWriteArrayList<DownloadStatus>( - new ArrayList<DownloadStatus>()); - downloads = new ArrayList<Downloader>(); - registerReceiver(downloadQueued, new IntentFilter( - ACTION_ENQUEUE_DOWNLOAD)); - - IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); - registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); - syncExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - t.setUncaughtExceptionHandler(new UncaughtExceptionHandler() { - - @Override - public void uncaughtException(Thread thread, Throwable ex) { - Log.e(TAG, "Thread exited with uncaught exception"); - ex.printStackTrace(); - downloadsBeingHandled -= 1; - queryDownloads(); - } - }); - return t; - } - }); - downloadExecutor = Executors.newFixedThreadPool(NUM_PARALLEL_DOWNLOADS, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }, new RejectedExecutionHandler() { - - @Override - public void rejectedExecution(Runnable r, - ThreadPoolExecutor executor) { - Log.w(TAG, "SchedEx rejected submission of new task"); - } - }); - setupNotificationBuilders(); - manager = FeedManager.getInstance(); - requester = DownloadRequester.getInstance(); - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - @Override - public void onDestroy() { - if (AppConfig.DEBUG) - Log.d(TAG, "Service shutting down"); - isRunning = false; - unregisterReceiver(cancelDownloadReceiver); - unregisterReceiver(downloadQueued); - } - - @SuppressLint("NewApi") - private void setupNotificationBuilders() { - PendingIntent pIntent = PendingIntent.getActivity(this, 0, new Intent( - this, DownloadActivity.class), - PendingIntent.FLAG_UPDATE_CURRENT); - - Bitmap icon = BitmapFactory.decodeResource(getResources(), - R.drawable.stat_notify_sync); - - if (android.os.Build.VERSION.SDK_INT >= 16) { - notificationBuilder = new Notification.BigTextStyle( - new Notification.Builder(this).setOngoing(true) - .setContentIntent(pIntent).setLargeIcon(icon) - .setSmallIcon(R.drawable.stat_notify_sync)); - } else { - notificationCompatBuilder = new NotificationCompat.Builder(this) - .setOngoing(true).setContentIntent(pIntent) - .setLargeIcon(icon) - .setSmallIcon(R.drawable.stat_notify_sync); - } - if (AppConfig.DEBUG) - Log.d(TAG, "Notification set up"); - } - - /** - * Updates the contents of the service's notifications. Should be called - * before setupNotificationBuilders. - */ - @SuppressLint("NewApi") - private Notification updateNotifications() { - String contentTitle = getString(R.string.download_notification_title); - String downloadsLeft = requester.getNumberOfDownloads() - + getString(R.string.downloads_left); - if (android.os.Build.VERSION.SDK_INT >= 16) { - - if (notificationBuilder != null) { - - StringBuilder bigText = new StringBuilder(""); - for (int i = 0; i < downloads.size(); i++) { - Downloader downloader = downloads.get(i); - if (downloader.getStatus() != null) { - FeedFile f = downloader.getStatus().getFeedFile(); - if (f.getClass() == Feed.class) { - Feed feed = (Feed) f; - if (feed.getTitle() != null) { - if (i > 0) { - bigText.append("\n"); - } - bigText.append("\u2022 " + feed.getTitle()); - } - } else if (f.getClass() == FeedMedia.class) { - FeedMedia media = (FeedMedia) f; - if (media.getItem().getTitle() != null) { - if (i > 0) { - bigText.append("\n"); - } - bigText.append("\u2022 " - + media.getItem().getTitle() - + " (" - + downloader.getStatus() - .getProgressPercent() + "%)"); - } - } - } - } - notificationBuilder.setSummaryText(downloadsLeft); - notificationBuilder.setBigContentTitle(contentTitle); - if (bigText != null) { - notificationBuilder.bigText(bigText.toString()); - } - return notificationBuilder.build(); - } - } else { - if (notificationCompatBuilder != null) { - notificationCompatBuilder.setContentTitle(contentTitle); - notificationCompatBuilder.setContentText(downloadsLeft); - return notificationCompatBuilder.getNotification(); - } - } - return null; - } - - private Downloader getDownloader(String downloadUrl) { - for (Downloader downloader : downloads) { - if (downloader.getStatus().getFeedFile().getDownload_url() - .equals(downloadUrl)) { - return downloader; - } - } - return null; - } - - private BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (intent.getAction().equals(ACTION_CANCEL_DOWNLOAD)) { - String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); - if (url == null) { - throw new IllegalArgumentException( - "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); - } - if (AppConfig.DEBUG) - Log.d(TAG, "Cancelling download with url " + url); - Downloader d = getDownloader(url); - if (d != null) { - d.cancel(); - removeDownload(d); - } else { - Log.e(TAG, "Could not cancel download with url " + url); - } - - } else if (intent.getAction().equals(ACTION_CANCEL_ALL_DOWNLOADS)) { - for (Downloader d : downloads) { - d.cancel(); - DownloadRequester.getInstance().removeDownload( - d.getStatus().getFeedFile()); - d.getStatus().getFeedFile().setFile_url(null); - if (AppConfig.DEBUG) - Log.d(TAG, "Cancelled all downloads"); - } - downloads.clear(); - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - - } - queryDownloads(); - } - - }; - - private void onDownloadQueued(Intent intent) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received enqueue request"); - Request request = intent.getParcelableExtra(EXTRA_REQUEST); - if (request == null) { - throw new IllegalArgumentException( - "ACTION_ENQUEUE_DOWNLOAD intent needs request extra"); - } - if (shutdownInitiated) { - if (AppConfig.DEBUG) - Log.d(TAG, "Cancelling shutdown; new download was queued"); - shutdownInitiated = false; - } - - DownloadRequester requester = DownloadRequester.getInstance(); - FeedFile feedfile = requester.getDownload(request.source); - if (feedfile != null) { - - DownloadStatus status = new DownloadStatus(feedfile, - feedfile.getHumanReadableIdentifier()); - Downloader downloader = getDownloader(status); - if (downloader != null) { - downloads.add(downloader); - downloadExecutor.submit(downloader); - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - } - } else { - Log.e(TAG, - "Could not find feedfile in download requester when trying to enqueue new download"); - } - queryDownloads(); - } - - private BroadcastReceiver downloadQueued = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - onDownloadQueued(intent); - } - - }; - - private Downloader getDownloader(DownloadStatus status) { - if (URLUtil.isHttpUrl(status.getFeedFile().getDownload_url())) { - return new HttpDownloader(new DownloaderCallback() { - - @Override - public void onDownloadCompleted(final Downloader downloader) { - handler.post(new Runnable() { - - @Override - public void run() { - DownloadService.this - .onDownloadCompleted(downloader); - } - }); - } - }, status); - } - Log.e(TAG, "Could not find appropriate downloader for " - + status.getFeedFile().getDownload_url()); - return null; - } - - @SuppressLint("NewApi") - public void onDownloadCompleted(final Downloader downloader) { - final AsyncTask<Void, Void, Void> handlerTask = new AsyncTask<Void, Void, Void>() { - boolean successful; - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - if (!successful) { - queryDownloads(); - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - removeDownload(downloader); - } - - @Override - protected Void doInBackground(Void... params) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received 'Download Complete' - message."); - downloadsBeingHandled += 1; - DownloadStatus status = downloader.getStatus(); - status.setCompletionDate(new Date()); - successful = status.isSuccessful(); - - FeedFile download = status.getFeedFile(); - if (download != null) { - if (successful) { - if (download.getClass() == Feed.class) { - handleCompletedFeedDownload(status); - } else if (download.getClass() == FeedImage.class) { - handleCompletedImageDownload(status); - } else if (download.getClass() == FeedMedia.class) { - handleCompletedFeedMediaDownload(status); - } - } else { - download.setFile_url(null); - download.setDownloaded(false); - if (!successful && !status.isCancelled()) { - Log.e(TAG, "Download failed"); - saveDownloadStatus(status); - } - sendDownloadHandledIntent(); - downloadsBeingHandled -= 1; - } - } - return null; - } - }; - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - handlerTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - handlerTask.execute(); - } - } - - /** - * Remove download from the DownloadRequester list and from the - * DownloadService list. - */ - private void removeDownload(final Downloader d) { - if (AppConfig.DEBUG) - Log.d(TAG, "Removing downloader: " - + d.getStatus().getFeedFile().getDownload_url()); - boolean rc = downloads.remove(d); - if (AppConfig.DEBUG) - Log.d(TAG, "Result of downloads.remove: " + rc); - DownloadRequester.getInstance().removeDownload( - d.getStatus().getFeedFile()); - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - } - - /** - * 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 void saveDownloadStatus(DownloadStatus status) { - completedDownloads.add(status); - manager.addDownloadStatus(this, status); - } - - private void sendDownloadHandledIntent() { - EventDistributor.getInstance().sendDownloadHandledBroadcast(); - } - - /** - * 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 successfully downloaded feeds is bigger than 1 - * or if there is at least one failed download which is not an image or if - * there is at least one downloaded media file. - */ - private void updateReport() { - // check if report should be created - boolean createReport = false; - int successfulDownloads = 0; - int failedDownloads = 0; - - // a download report is created if at least one download has failed - // (excluding failed image downloads) - for (DownloadStatus status : completedDownloads) { - if (status.isSuccessful()) { - successfulDownloads++; - } else if (!status.isCancelled()) { - if (status.getFeedFile().getClass() != FeedImage.class) { - createReport = true; - } - failedDownloads++; - } - } - - if (createReport) { - if (AppConfig.DEBUG) - Log.d(TAG, "Creating report"); - // 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( - String.format( - getString(R.string.download_report_content), - successfulDownloads, failedDownloads)) - .setSmallIcon(R.drawable.stat_notify_sync) - .setLargeIcon( - BitmapFactory.decodeResource(getResources(), - R.drawable.stat_notify_sync)) - .setContentIntent( - PendingIntent.getActivity(this, 0, new Intent(this, - DownloadLogActivity.class), 0)) - .setAutoCancel(true).getNotification(); - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(REPORT_ID, notification); - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "No report is created"); - } - completedDownloads.clear(); - } - - /** Check if there's something else to download, otherwise stop */ - void queryDownloads() { - int numOfDownloads = downloads.size(); - if (AppConfig.DEBUG) { - Log.d(TAG, numOfDownloads + " downloads left"); - Log.d(TAG, "Downloads being handled: " + downloadsBeingHandled); - Log.d(TAG, "ShutdownInitiated: " + shutdownInitiated); - } - - if (numOfDownloads == 0 && downloadsBeingHandled <= 0) { - if (AppConfig.DEBUG) - Log.d(TAG, "Starting shutdown"); - shutdownInitiated = true; - updateReport(); - cancelNotificationUpdater(); - stopForeground(true); - } else { - setupNotificationUpdater(); - startForeground(NOTIFICATION_ID, updateNotifications()); - } - } - - /** Is called whenever a Feed is downloaded */ - private void handleCompletedFeedDownload(DownloadStatus status) { - if (AppConfig.DEBUG) - Log.d(TAG, "Handling completed Feed Download"); - syncExecutor.execute(new FeedSyncThread(status)); - - } - - /** Is called whenever a Feed-Image is downloaded */ - private void handleCompletedImageDownload(DownloadStatus status) { - if (AppConfig.DEBUG) - Log.d(TAG, "Handling completed Image Download"); - syncExecutor.execute(new ImageHandlerThread(status)); - } - - /** Is called whenever a FeedMedia is downloaded. */ - private void handleCompletedFeedMediaDownload(DownloadStatus status) { - if (AppConfig.DEBUG) - Log.d(TAG, "Handling completed FeedMedia Download"); - syncExecutor.execute(new MediaHandlerThread(status)); - } - - /** - * 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 DownloadStatus status; - - private int reason; - private boolean successful; - - public FeedSyncThread(DownloadStatus status) { - this.feed = (Feed) status.getFeedFile(); - this.status = status; - } - - public void run() { - Feed savedFeed = null; - reason = 0; - String reasonDetailed = null; - successful = true; - final FeedManager manager = FeedManager.getInstance(); - FeedHandler feedHandler = new FeedHandler(); - feed.setDownloaded(true); - - try { - feed = feedHandler.parseFeed(feed); - if (AppConfig.DEBUG) - Log.d(TAG, feed.getTitle() + " parsed"); - if (checkFeedData(feed) == false) { - throw new InvalidFeedException(); - } - // Save information of feed in DB - savedFeed = manager.updateFeed(DownloadService.this, feed); - // Download Feed Image if provided and not downloaded - if (savedFeed.getImage() != null - && savedFeed.getImage().isDownloaded() == false) { - if (AppConfig.DEBUG) - Log.d(TAG, "Feed has image; Downloading...."); - savedFeed.getImage().setFeed(savedFeed); - final Feed savedFeedRef = savedFeed; - handler.post(new Runnable() { - - @Override - public void run() { - try { - requester.downloadImage(DownloadService.this, - savedFeedRef.getImage()); - } catch (DownloadRequestException e) { - e.printStackTrace(); - manager.addDownloadStatus( - DownloadService.this, - new DownloadStatus( - savedFeedRef.getImage(), - savedFeedRef - .getImage() - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, - false, e.getMessage())); - } - } - }); - - } - - } catch (SAXException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (IOException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (ParserConfigurationException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (UnsupportedFeedtypeException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_UNSUPPORTED_TYPE; - reasonDetailed = e.getMessage(); - } catch (InvalidFeedException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } - - // cleanup(); - if (savedFeed == null) { - savedFeed = feed; - } - - saveDownloadStatus(new DownloadStatus(savedFeed, - savedFeed.getHumanReadableIdentifier(), reason, successful, - reasonDetailed)); - sendDownloadHandledIntent(); - downloadsBeingHandled -= 1; - handler.post(new Runnable() { - - @Override - public void run() { - queryDownloads(); - - } - }); - } - - /** Checks if the feed was parsed correctly. */ - private boolean checkFeedData(Feed feed) { - if (feed.getTitle() == null) { - Log.e(TAG, "Feed has no title."); - return false; - } - if (!hasValidFeedItems(feed)) { - Log.e(TAG, "Feed has invalid items"); - return false; - } - if (AppConfig.DEBUG) - Log.d(TAG, "Feed appears to be valid."); - return true; - - } - - private boolean hasValidFeedItems(Feed feed) { - for (FeedItem item : feed.getItemsArray()) { - if (item.getTitle() == null) { - Log.e(TAG, "Item has no title"); - return false; - } - if (item.getPubDate() == null) { - Log.e(TAG, - "Item has no pubDate. Using current time as pubDate"); - if (item.getTitle() != null) { - Log.e(TAG, "Title of invalid item: " + item.getTitle()); - } - item.setPubDate(new Date()); - } - } - return true; - } - - /** Delete files that aren't needed anymore */ - private void cleanup() { - if (feed.getFile_url() != null) { - if (new File(feed.getFile_url()).delete()) - if (AppConfig.DEBUG) - Log.d(TAG, "Successfully deleted cache file."); - else - Log.e(TAG, "Failed to delete cache file."); - feed.setFile_url(null); - } else if (AppConfig.DEBUG) { - Log.d(TAG, "Didn't delete cache file: File url is not set."); - } - } - - } - - /** Handles a completed image download. */ - class ImageHandlerThread implements Runnable { - private FeedImage image; - private DownloadStatus status; - - public ImageHandlerThread(DownloadStatus status) { - this.image = (FeedImage) status.getFeedFile(); - this.status = status; - } - - @Override - public void run() { - image.setDownloaded(true); - - saveDownloadStatus(status); - sendDownloadHandledIntent(); - manager.setFeedImage(DownloadService.this, image); - if (image.getFeed() != null) { - manager.setFeed(DownloadService.this, image.getFeed()); - } else { - Log.e(TAG, - "Image has no feed, image might not be saved correctly!"); - } - downloadsBeingHandled -= 1; - handler.post(new Runnable() { - - @Override - public void run() { - queryDownloads(); - - } - }); - } - } - - /** Handles a completed media download. */ - class MediaHandlerThread implements Runnable { - private FeedMedia media; - private DownloadStatus status; - - public MediaHandlerThread(DownloadStatus status) { - super(); - this.media = (FeedMedia) status.getFeedFile(); - this.status = status; - } - - @Override - public void run() { - boolean chaptersRead = false; - - media.setDownloaded(true); - // Get duration - MediaPlayer mediaplayer = new MediaPlayer(); - try { - mediaplayer.setDataSource(media.getFile_url()); - mediaplayer.prepare(); - media.setDuration(mediaplayer.getDuration()); - if (AppConfig.DEBUG) - Log.d(TAG, "Duration of file is " + media.getDuration()); - mediaplayer.reset(); - } catch (IOException e) { - e.printStackTrace(); - } finally { - mediaplayer.release(); - } - - if (media.getItem().getChapters() == null) { - ChapterUtils.loadChaptersFromFileUrl(media); - if (media.getItem().getChapters() != null) { - chaptersRead = true; - } - } - - saveDownloadStatus(status); - sendDownloadHandledIntent(); - if (chaptersRead) { - manager.setFeedItem(DownloadService.this, media.getItem()); - } - manager.setFeedMedia(DownloadService.this, media); - - if (!FeedManager.getInstance().isInQueue(media.getItem())) { - FeedManager.getInstance().addQueueItem(DownloadService.this, - media.getItem()); - } - - downloadsBeingHandled -= 1; - handler.post(new Runnable() { - - @Override - public void run() { - queryDownloads(); - - } - }); - } - } - - /** Is used to request a new download. */ - public static class Request implements Parcelable { - private String destination; - private String source; - - public Request(String destination, String source) { - super(); - this.destination = destination; - this.source = source; - } - - private Request(Parcel in) { - destination = in.readString(); - source = in.readString(); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(destination); - dest.writeString(source); - } - - public static final Parcelable.Creator<Request> CREATOR = new Parcelable.Creator<Request>() { - public Request createFromParcel(Parcel in) { - return new Request(in); - } - - public Request[] newArray(int size) { - return new Request[size]; - } - }; - - public String getDestination() { - return destination; - } - - public String getSource() { - return source; - } - - } - - /** Schedules the notification updater task if it hasn't been scheduled yet. */ - private void setupNotificationUpdater() { - if (AppConfig.DEBUG) - Log.d(TAG, "Setting up notification updater"); - if (notificationUpdater == null) { - notificationUpdater = new NotificationUpdater(); - notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate( - notificationUpdater, 5L, 5L, TimeUnit.SECONDS); - } - } - - private void cancelNotificationUpdater() { - boolean result = false; - if (notificationUpdaterFuture != null) { - result = notificationUpdaterFuture.cancel(true); - } - notificationUpdater = null; - notificationUpdaterFuture = null; - Log.d(TAG, "NotificationUpdater cancelled. Result: " + result); - } - - private class NotificationUpdater implements Runnable { - public void run() { - handler.post(new Runnable() { - @Override - public void run() { - Notification n = updateNotifications(); - if (n != null) { - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, n); - } - } - }); - } - } - - public List<Downloader> getDownloads() { - return downloads; - } + private static final String TAG = "DownloadService"; + + /** + * Cancels one download. The intent MUST have an EXTRA_DOWNLOAD_URL extra that contains the download URL of the + * object whose download should be cancelled. + */ + public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.service.cancelDownload"; + + /** + * Cancels all running downloads. + */ + public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.service.cancelAllDownloads"; + + /** + * Extra for ACTION_CANCEL_DOWNLOAD + */ + public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; + + /** + * Sent by the DownloadService when the content of the downloads list + * changes. + */ + public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.service.downloadsContentChanged"; + + /** + * Extra for ACTION_ENQUEUE_DOWNLOAD intent. + */ + public static final String EXTRA_REQUEST = "request"; + + /** + * Stores DownloadStatus objects of completed downloads for creating a report at the end of the lifecylce. + */ + private List<DownloadStatus> completedDownloads; + + private ExecutorService syncExecutor; + private CompletionService<Downloader> downloadExecutor; + /** + * Number of threads of downloadExecutor. + */ + private static final int NUM_PARALLEL_DOWNLOADS = 4; + + private DownloadRequester requester; + + + private NotificationCompat.Builder notificationCompatBuilder; + private Notification.BigTextStyle notificationBuilder; + private int NOTIFICATION_ID = 2; + private int REPORT_ID = 3; + + /** + * Currently running downloads. + */ + private List<Downloader> downloads; + + /** + * Number of running downloads. + */ + private AtomicInteger numberOfDownloads; + + /** + * True if service is running. + */ + public static boolean isRunning = false; + + private Handler handler; + + private NotificationUpdater notificationUpdater; + private ScheduledFuture notificationUpdaterFuture; + private static final int SCHED_EX_POOL_SIZE = 1; + private ScheduledThreadPoolExecutor schedExecutor; + + private final IBinder mBinder = new LocalBinder(); + + public class LocalBinder extends Binder { + public DownloadService getService() { + return DownloadService.this; + } + } + + private Thread downloadCompletionThread = new Thread() { + private static final String TAG = "downloadCompletionThread"; + + @Override + public void run() { + if (AppConfig.DEBUG) Log.d(TAG, "downloadCompletionThread was started"); + while (!isInterrupted()) { + try { + Downloader downloader = downloadExecutor.take().get(); + if (AppConfig.DEBUG) + Log.d(TAG, "Received 'Download Complete' - message."); + removeDownload(downloader); + DownloadStatus status = downloader.getResult(); + boolean successful = status.isSuccessful(); + + final int type = status.getFeedfileType(); + if (successful) { + if (type == Feed.FEEDFILETYPE_FEED) { + handleCompletedFeedDownload(downloader + .getDownloadRequest()); + } else if (type == FeedImage.FEEDFILETYPE_FEEDIMAGE) { + handleCompletedImageDownload(status, downloader.getDownloadRequest()); + } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + handleCompletedFeedMediaDownload(status, downloader.getDownloadRequest()); + } + } else { + numberOfDownloads.decrementAndGet(); + if (!successful && !status.isCancelled()) { + Log.e(TAG, "Download failed"); + saveDownloadStatus(status); + } + sendDownloadHandledIntent(); + queryDownloadsAsync(); + } + } catch (InterruptedException e) { + if (AppConfig.DEBUG) Log.d(TAG, "DownloadCompletionThread was interrupted"); + } catch (ExecutionException e) { + e.printStackTrace(); + numberOfDownloads.decrementAndGet(); + } + } + if (AppConfig.DEBUG) Log.d(TAG, "End of downloadCompletionThread"); + } + }; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent.getParcelableExtra(EXTRA_REQUEST) != null) { + onDownloadQueued(intent); + } else if (numberOfDownloads.equals(0)) { + stopSelf(); + } + return Service.START_NOT_STICKY; + } + + @SuppressLint("NewApi") + @Override + public void onCreate() { + if (AppConfig.DEBUG) + Log.d(TAG, "Service started"); + isRunning = true; + handler = new Handler(); + completedDownloads = Collections.synchronizedList(new ArrayList<DownloadStatus>()); + downloads = new ArrayList<Downloader>(); + numberOfDownloads = new AtomicInteger(0); + + IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); + registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); + syncExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + downloadExecutor = new ExecutorCompletionService<Downloader>( + Executors.newFixedThreadPool(NUM_PARALLEL_DOWNLOADS, + new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + })); + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, + new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }, new RejectedExecutionHandler() { + + @Override + public void rejectedExecution(Runnable r, + ThreadPoolExecutor executor) { + Log.w(TAG, "SchedEx rejected submission of new task"); + } + } + ); + downloadCompletionThread.start(); + setupNotificationBuilders(); + requester = DownloadRequester.getInstance(); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public void onDestroy() { + if (AppConfig.DEBUG) + Log.d(TAG, "Service shutting down"); + isRunning = false; + updateReport(); + + stopForeground(true); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(NOTIFICATION_ID); + + downloadCompletionThread.interrupt(); + syncExecutor.shutdown(); + schedExecutor.shutdown(); + cancelNotificationUpdater(); + unregisterReceiver(cancelDownloadReceiver); + } + + @SuppressLint("NewApi") + private void setupNotificationBuilders() { + PendingIntent pIntent = PendingIntent.getActivity(this, 0, new Intent( + this, DownloadActivity.class), + PendingIntent.FLAG_UPDATE_CURRENT); + + Bitmap icon = BitmapFactory.decodeResource(getResources(), + R.drawable.stat_notify_sync); + + if (android.os.Build.VERSION.SDK_INT >= 16) { + notificationBuilder = new Notification.BigTextStyle( + new Notification.Builder(this).setOngoing(true) + .setContentIntent(pIntent).setLargeIcon(icon) + .setSmallIcon(R.drawable.stat_notify_sync)); + } else { + notificationCompatBuilder = new NotificationCompat.Builder(this) + .setOngoing(true).setContentIntent(pIntent) + .setLargeIcon(icon) + .setSmallIcon(R.drawable.stat_notify_sync); + } + if (AppConfig.DEBUG) + Log.d(TAG, "Notification set up"); + } + + /** + * Updates the contents of the service's notifications. Should be called + * before setupNotificationBuilders. + */ + @SuppressLint("NewApi") + private Notification updateNotifications() { + String contentTitle = getString(R.string.download_notification_title); + String downloadsLeft = requester.getNumberOfDownloads() + + getString(R.string.downloads_left); + if (android.os.Build.VERSION.SDK_INT >= 16) { + + if (notificationBuilder != null) { + + StringBuilder bigText = new StringBuilder(""); + for (int i = 0; i < downloads.size(); i++) { + Downloader downloader = downloads.get(i); + final DownloadRequest request = downloader + .getDownloadRequest(); + if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + if (request.getTitle() != null) { + if (i > 0) { + bigText.append("\n"); + } + bigText.append("\u2022 " + request.getTitle()); + } + } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + if (request.getTitle() != null) { + if (i > 0) { + bigText.append("\n"); + } + bigText.append("\u2022 " + request.getTitle() + + " (" + request.getProgressPercent() + + "%)"); + } + } + + } + notificationBuilder.setSummaryText(downloadsLeft); + notificationBuilder.setBigContentTitle(contentTitle); + if (bigText != null) { + notificationBuilder.bigText(bigText.toString()); + } + return notificationBuilder.build(); + } + } else { + if (notificationCompatBuilder != null) { + notificationCompatBuilder.setContentTitle(contentTitle); + notificationCompatBuilder.setContentText(downloadsLeft); + return notificationCompatBuilder.getNotification(); + } + } + return null; + } + + private Downloader getDownloader(String downloadUrl) { + for (Downloader downloader : downloads) { + if (downloader.getDownloadRequest().getSource().equals(downloadUrl)) { + return downloader; + } + } + return null; + } + + private BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction().equals(ACTION_CANCEL_DOWNLOAD)) { + String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); + if (url == null) { + throw new IllegalArgumentException( + "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); + } + if (AppConfig.DEBUG) + Log.d(TAG, "Cancelling download with url " + url); + Downloader d = getDownloader(url); + if (d != null) { + d.cancel(); + } else { + Log.e(TAG, "Could not cancel download with url " + url); + } + + } else if (intent.getAction().equals(ACTION_CANCEL_ALL_DOWNLOADS)) { + for (Downloader d : downloads) { + d.cancel(); + if (AppConfig.DEBUG) + Log.d(TAG, "Cancelled all downloads"); + } + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + + } + queryDownloads(); + } + + }; + + private void onDownloadQueued(Intent intent) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received enqueue request"); + DownloadRequest request = intent.getParcelableExtra(EXTRA_REQUEST); + if (request == null) { + throw new IllegalArgumentException( + "ACTION_ENQUEUE_DOWNLOAD intent needs request extra"); + } + + Downloader downloader = getDownloader(request); + if (downloader != null) { + numberOfDownloads.incrementAndGet(); + downloads.add(downloader); + downloadExecutor.submit(downloader); + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + } + + queryDownloads(); + } + + private Downloader getDownloader(DownloadRequest request) { + if (URLUtil.isHttpUrl(request.getSource())) { + return new HttpDownloader(request); + } + Log.e(TAG, + "Could not find appropriate downloader for " + + request.getSource()); + return null; + } + + @SuppressLint("NewApi") + public void onDownloadCompleted(final Downloader downloader) { + final AsyncTask<Void, Void, Void> handlerTask = new AsyncTask<Void, Void, Void>() { + boolean successful; + + @Override + protected void onPostExecute(Void result) { + super.onPostExecute(result); + if (!successful) { + queryDownloads(); + } + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + removeDownload(downloader); + } + + @Override + protected Void doInBackground(Void... params) { + + + return null; + } + }; + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + handlerTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + handlerTask.execute(); + } + } + + /** + * Remove download from the DownloadRequester list and from the + * DownloadService list. + */ + private void removeDownload(final Downloader d) { + if (AppConfig.DEBUG) + Log.d(TAG, "Removing downloader: " + + d.getDownloadRequest().getSource()); + boolean rc = downloads.remove(d); + if (AppConfig.DEBUG) + Log.d(TAG, "Result of downloads.remove: " + rc); + DownloadRequester.getInstance().removeDownload(d.getDownloadRequest()); + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + } + + /** + * 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 void saveDownloadStatus(DownloadStatus status) { + completedDownloads.add(status); + DBWriter.addDownloadStatus(this, status); + } + + private void sendDownloadHandledIntent() { + EventDistributor.getInstance().sendDownloadHandledBroadcast(); + } + + /** + * 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 successfully downloaded feeds is bigger than 1 + * or if there is at least one failed download which is not an image or if + * there is at least one downloaded media file. + */ + private void updateReport() { + // check if report should be created + boolean createReport = false; + int successfulDownloads = 0; + int failedDownloads = 0; + + // a download report is created if at least one download has failed + // (excluding failed image downloads) + for (DownloadStatus status : completedDownloads) { + if (status.isSuccessful()) { + successfulDownloads++; + } else if (!status.isCancelled()) { + if (status.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { + createReport = true; + } + failedDownloads++; + } + } + + if (createReport) { + if (AppConfig.DEBUG) + Log.d(TAG, "Creating report"); + // 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( + String.format( + getString(R.string.download_report_content), + successfulDownloads, failedDownloads)) + .setSmallIcon(R.drawable.stat_notify_sync) + .setLargeIcon( + BitmapFactory.decodeResource(getResources(), + R.drawable.stat_notify_sync)) + .setContentIntent( + PendingIntent.getActivity(this, 0, new Intent(this, + DownloadLogActivity.class), 0)) + .setAutoCancel(true).getNotification(); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(REPORT_ID, notification); + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "No report is created"); + } + completedDownloads.clear(); + } + + /** + * Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is + * used from a thread other than the main thread. + */ + void queryDownloadsAsync() { + handler.post(new Runnable() { + public void run() { + queryDownloads(); + ; + } + }); + } + + /** + * Check if there's something else to download, otherwise stop + */ + void queryDownloads() { + if (AppConfig.DEBUG) { + Log.d(TAG, numberOfDownloads.get() + " downloads left"); + } + + if (numberOfDownloads.get() <= 0) { + if (AppConfig.DEBUG) + Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); + stopSelf(); + } else { + setupNotificationUpdater(); + startForeground(NOTIFICATION_ID, updateNotifications()); + } + } + + /** + * Is called whenever a Feed is downloaded + */ + private void handleCompletedFeedDownload(DownloadRequest request) { + if (AppConfig.DEBUG) + Log.d(TAG, "Handling completed Feed Download"); + syncExecutor.execute(new FeedSyncThread(request)); + + } + + /** + * Is called whenever a Feed-Image is downloaded + */ + private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) { + if (AppConfig.DEBUG) + Log.d(TAG, "Handling completed Image Download"); + syncExecutor.execute(new ImageHandlerThread(status, request)); + } + + /** + * Is called whenever a FeedMedia is downloaded. + */ + private void handleCompletedFeedMediaDownload(DownloadStatus status, DownloadRequest request) { + if (AppConfig.DEBUG) + Log.d(TAG, "Handling completed FeedMedia Download"); + syncExecutor.execute(new MediaHandlerThread(status, request)); + } + + /** + * 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 DownloadRequest request; + + private DownloadError reason; + private boolean successful; + + public FeedSyncThread(DownloadRequest request) { + if (request == null) { + throw new IllegalArgumentException("Request must not be null"); + } + + this.request = request; + } + + public void run() { + Feed savedFeed = null; + + Feed feed = new Feed(request.getSource(), new Date()); + feed.setFile_url(request.getDestination()); + feed.setDownloaded(true); + + reason = null; + String reasonDetailed = null; + successful = true; + FeedHandler feedHandler = new FeedHandler(); + + try { + feed = feedHandler.parseFeed(feed); + if (AppConfig.DEBUG) + Log.d(TAG, feed.getTitle() + " parsed"); + if (checkFeedData(feed) == false) { + throw new InvalidFeedException(); + } + // Save information of feed in DB + savedFeed = DBTasks.updateFeed(DownloadService.this, feed); + // Download Feed Image if provided and not downloaded + if (savedFeed.getImage() != null + && savedFeed.getImage().isDownloaded() == false) { + if (AppConfig.DEBUG) + Log.d(TAG, "Feed has image; Downloading...."); + savedFeed.getImage().setFeed(savedFeed); + final Feed savedFeedRef = savedFeed; + handler.post(new Runnable() { + + @Override + public void run() { + try { + requester.downloadImage(DownloadService.this, + savedFeedRef.getImage()); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus( + DownloadService.this, + new DownloadStatus( + savedFeedRef.getImage(), + savedFeedRef + .getImage() + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, + false, e.getMessage())); + } + } + }); + + } + + } catch (SAXException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (IOException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (ParserConfigurationException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (UnsupportedFeedtypeException e) { + e.printStackTrace(); + successful = false; + reason = DownloadError.ERROR_UNSUPPORTED_TYPE; + reasonDetailed = e.getMessage(); + } catch (InvalidFeedException e) { + e.printStackTrace(); + successful = false; + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } + + // cleanup(); + if (savedFeed == null) { + savedFeed = feed; + } + + saveDownloadStatus(new DownloadStatus(savedFeed, + savedFeed.getHumanReadableIdentifier(), reason, successful, + reasonDetailed)); + sendDownloadHandledIntent(); + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + } + + /** + * Checks if the feed was parsed correctly. + */ + private boolean checkFeedData(Feed feed) { + if (feed.getTitle() == null) { + Log.e(TAG, "Feed has no title."); + return false; + } + if (!hasValidFeedItems(feed)) { + Log.e(TAG, "Feed has invalid items"); + return false; + } + if (AppConfig.DEBUG) + Log.d(TAG, "Feed appears to be valid."); + return true; + + } + + private boolean hasValidFeedItems(Feed feed) { + for (FeedItem item : feed.getItemsArray()) { + if (item.getTitle() == null) { + Log.e(TAG, "Item has no title"); + return false; + } + if (item.getPubDate() == null) { + Log.e(TAG, + "Item has no pubDate. Using current time as pubDate"); + if (item.getTitle() != null) { + Log.e(TAG, "Title of invalid item: " + item.getTitle()); + } + item.setPubDate(new Date()); + } + } + return true; + } + + /** + * Delete files that aren't needed anymore + */ + private void cleanup(Feed feed) { + if (feed.getFile_url() != null) { + if (new File(feed.getFile_url()).delete()) + if (AppConfig.DEBUG) + Log.d(TAG, "Successfully deleted cache file."); + else + Log.e(TAG, "Failed to delete cache file."); + feed.setFile_url(null); + } else if (AppConfig.DEBUG) { + Log.d(TAG, "Didn't delete cache file: File url is not set."); + } + } + + } + + /** + * Handles a completed image download. + */ + class ImageHandlerThread implements Runnable { + + private DownloadRequest request; + private DownloadStatus status; + + public ImageHandlerThread(DownloadStatus status, DownloadRequest request) { + if (status == null) { + throw new IllegalArgumentException("Status must not be null"); + } + if (request == null) { + throw new IllegalArgumentException("Request must not be null"); + } + this.status = status; + this.request = request; + } + + @Override + public void run() { + FeedImage image = DBReader.getFeedImage(DownloadService.this, request.getFeedfileId()); + if (image == null) { + throw new IllegalStateException("Could not find downloaded image in database"); + } + + image.setFile_url(request.getDestination()); + image.setDownloaded(true); + + saveDownloadStatus(status); + sendDownloadHandledIntent(); + DBWriter.setFeedImage(DownloadService.this, image); + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + } + } + + /** + * Handles a completed media download. + */ + class MediaHandlerThread implements Runnable { + + private DownloadRequest request; + private DownloadStatus status; + + public MediaHandlerThread(DownloadStatus status, DownloadRequest request) { + if (status == null) { + throw new IllegalArgumentException("Status must not be null"); + } + if (request == null) { + throw new IllegalArgumentException("Request must not be null"); + } + + this.status = status; + this.request = request; + } + + @Override + public void run() { + FeedMedia media = DBReader.getFeedMedia(DownloadService.this, + request.getFeedfileId()); + if (media == null) { + throw new IllegalStateException( + "Could not find downloaded media object in database"); + } + boolean chaptersRead = false; + media.setDownloaded(true); + media.setFile_url(request.getDestination()); + + // Get duration + MediaPlayer mediaplayer = new MediaPlayer(); + try { + mediaplayer.setDataSource(media.getFile_url()); + mediaplayer.prepare(); + media.setDuration(mediaplayer.getDuration()); + if (AppConfig.DEBUG) + Log.d(TAG, "Duration of file is " + media.getDuration()); + mediaplayer.reset(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + mediaplayer.release(); + } + + if (media.getItem().getChapters() == null) { + ChapterUtils.loadChaptersFromFileUrl(media); + if (media.getItem().getChapters() != null) { + chaptersRead = true; + } + } + + saveDownloadStatus(status); + sendDownloadHandledIntent(); + + try { + if (chaptersRead) { + DBWriter.setFeedItem(DownloadService.this, media.getItem()).get(); + } + DBWriter.setFeedMedia(DownloadService.this, media).get(); + } catch (ExecutionException e) { + e.printStackTrace(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + if (!DBTasks.isInQueue(DownloadService.this, media.getItem().getId())) { + DBWriter.addQueueItem(DownloadService.this, media.getItem().getId()); + } + + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + } + } + + /** + * Schedules the notification updater task if it hasn't been scheduled yet. + */ + private void setupNotificationUpdater() { + if (AppConfig.DEBUG) + Log.d(TAG, "Setting up notification updater"); + if (notificationUpdater == null) { + notificationUpdater = new NotificationUpdater(); + notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate( + notificationUpdater, 5L, 5L, TimeUnit.SECONDS); + } + } + + private void cancelNotificationUpdater() { + boolean result = false; + if (notificationUpdaterFuture != null) { + result = notificationUpdaterFuture.cancel(true); + } + notificationUpdater = null; + notificationUpdaterFuture = null; + Log.d(TAG, "NotificationUpdater cancelled. Result: " + result); + } + + private class NotificationUpdater implements Runnable { + public void run() { + handler.post(new Runnable() { + @Override + public void run() { + Notification n = updateNotifications(); + if (n != null) { + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, n); + } + } + }); + } + } + + public List<Downloader> getDownloads() { + return downloads; + } } diff --git a/src/de/danoeh/antennapod/service/download/DownloadStatus.java b/src/de/danoeh/antennapod/service/download/DownloadStatus.java new file mode 100644 index 000000000..62e54cbb4 --- /dev/null +++ b/src/de/danoeh/antennapod/service/download/DownloadStatus.java @@ -0,0 +1,181 @@ +package de.danoeh.antennapod.service.download; + +import java.util.Date; + +import de.danoeh.antennapod.feed.FeedFile; +import de.danoeh.antennapod.util.DownloadError; + +/** Contains status attributes for one download */ +public class DownloadStatus { + /** + * Downloaders should use this constant for the size attribute if necessary + * so that the listadapters etc. can react properly. + */ + public static final int SIZE_UNKNOWN = -1; + + // ----------------------------------- ATTRIBUTES STORED IN DB + /** Unique id for storing the object in database. */ + protected long id; + /** + * A human-readable string which is shown to the user so that he can + * identify the download. Should be the title of the item/feed/media or the + * URL if the download has no other title. + */ + protected String title; + protected DownloadError reason; + /** + * A message which can be presented to the user to give more information. + * Should be null if Download was successful. + */ + protected String reasonDetailed; + protected boolean successful; + protected Date completionDate; + protected long feedfileId; + /** + * Is used to determine the type of the feedfile even if the feedfile does + * not exist anymore. The value should be FEEDFILETYPE_FEED, + * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA + */ + protected int feedfileType; + + // ------------------------------------ NOT STORED IN DB + protected boolean done; + protected boolean cancelled; + + /** Constructor for restoring Download status entries from DB. */ + public DownloadStatus(long id, String title, long feedfileId, + int feedfileType, boolean successful, DownloadError reason, + Date completionDate, String reasonDetailed) { + this.id = id; + this.title = title; + this.done = true; + this.feedfileId = feedfileId; + this.reason = reason; + this.successful = successful; + this.completionDate = completionDate; + this.reasonDetailed = reasonDetailed; + this.feedfileType = feedfileType; + } + + public DownloadStatus(DownloadRequest request, DownloadError reason, + boolean successful, boolean cancelled, String reasonDetailed) { + if (request == null) { + throw new IllegalArgumentException("request must not be null"); + } + this.title = request.getTitle(); + this.feedfileId = request.getFeedfileId(); + this.feedfileType = request.getFeedfileType(); + this.reason = reason; + this.successful = successful; + this.cancelled = cancelled; + this.reasonDetailed = reasonDetailed; + this.completionDate = new Date(); + } + + /** Constructor for creating new completed downloads. */ + public DownloadStatus(FeedFile feedfile, String title, DownloadError reason, + boolean successful, String reasonDetailed) { + if (feedfile == null) { + throw new IllegalArgumentException("feedfile must not be null"); + } + + this.title = title; + this.done = true; + this.feedfileId = feedfile.getId(); + this.feedfileType = feedfile.getTypeAsInt(); + this.reason = reason; + this.successful = successful; + this.completionDate = new Date(); + this.reasonDetailed = reasonDetailed; + } + + /** Constructor for creating new completed downloads. */ + public DownloadStatus(long feedfileId, int feedfileType, String title, + DownloadError reason, boolean successful, String reasonDetailed) { + this.title = title; + this.done = true; + this.feedfileId = feedfileId; + this.feedfileType = feedfileType; + this.reason = reason; + this.successful = successful; + this.completionDate = new Date(); + this.reasonDetailed = reasonDetailed; + } + + @Override + public String toString() { + return "DownloadStatus [id=" + id + ", title=" + title + ", reason=" + + reason + ", reasonDetailed=" + reasonDetailed + + ", successful=" + successful + ", completionDate=" + + completionDate + ", feedfileId=" + feedfileId + + ", feedfileType=" + feedfileType + ", done=" + done + + ", cancelled=" + cancelled + "]"; + } + + public long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public DownloadError getReason() { + return reason; + } + + public String getReasonDetailed() { + return reasonDetailed; + } + + public boolean isSuccessful() { + return successful; + } + + public Date getCompletionDate() { + return completionDate; + } + + public long getFeedfileId() { + return feedfileId; + } + + public int getFeedfileType() { + return feedfileType; + } + + public boolean isDone() { + return done; + } + + public boolean isCancelled() { + return cancelled; + } + + public void setSuccessful() { + this.successful = true; + this.reason = DownloadError.SUCCESS; + this.done = true; + } + + public void setFailed(DownloadError reason, String reasonDetailed) { + this.successful = false; + this.reason = reason; + this.reasonDetailed = reasonDetailed; + } + + public void setCancelled() { + this.successful = false; + this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED; + this.done = true; + this.cancelled = true; + } + + public void setCompletionDate(Date completionDate) { + this.completionDate = completionDate; + } + + public void setId(long id) { + this.id = id; + } +}
\ No newline at end of file diff --git a/src/de/danoeh/antennapod/service/download/Downloader.java b/src/de/danoeh/antennapod/service/download/Downloader.java index 9ed9d9a76..84731fe9f 100644 --- a/src/de/danoeh/antennapod/service/download/Downloader.java +++ b/src/de/danoeh/antennapod/service/download/Downloader.java @@ -1,49 +1,50 @@ package de.danoeh.antennapod.service.download; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.DownloadStatus; + +import java.util.concurrent.Callable; /** Downloads files */ -public abstract class Downloader extends Thread { +public abstract class Downloader implements Callable<Downloader> { private static final String TAG = "Downloader"; - private DownloaderCallback downloaderCallback; - protected boolean finished; + protected volatile boolean finished; protected volatile boolean cancelled; - protected volatile DownloadStatus status; + protected DownloadRequest request; + protected DownloadStatus result; - public Downloader(DownloaderCallback downloaderCallback, - DownloadStatus status) { + public Downloader(DownloadRequest request) { super(); - this.downloaderCallback = downloaderCallback; - this.status = status; - this.status.setStatusMsg(R.string.download_pending); + this.request = request; + this.request.setStatusMsg(R.string.download_pending); this.cancelled = false; + this.result = new DownloadStatus(request, null, false, false, null); } - /** - * This method must be called when the download was completed, failed, or - * was cancelled - */ - protected void finish() { - if (!finished) { - finished = true; - downloaderCallback.onDownloadCompleted(this); + protected abstract void download(); + + public final Downloader call() { + download(); + if (result == null) { + throw new IllegalStateException( + "Downloader hasn't created DownloadStatus object"); } + finished = true; + return this; } - protected abstract void download(); + public DownloadRequest getDownloadRequest() { + return request; + } - @Override - public final void run() { - download(); - finish(); + public DownloadStatus getResult() { + return result; } - public DownloadStatus getStatus() { - return status; + public boolean isFinished() { + return finished; } public void cancel() { diff --git a/src/de/danoeh/antennapod/service/download/HttpDownloader.java b/src/de/danoeh/antennapod/service/download/HttpDownloader.java index f8f26f6fd..c9671ceb3 100644 --- a/src/de/danoeh/antennapod/service/download/HttpDownloader.java +++ b/src/de/danoeh/antennapod/service/download/HttpDownloader.java @@ -26,7 +26,6 @@ import android.util.Log; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.PodcastApp; import de.danoeh.antennapod.R; -import de.danoeh.antennapod.asynctask.DownloadStatus; import de.danoeh.antennapod.util.DownloadError; import de.danoeh.antennapod.util.StorageUtils; @@ -39,9 +38,8 @@ public class HttpDownloader extends Downloader { private static final int CONNECTION_TIMEOUT = 30000; private static final int SOCKET_TIMEOUT = 30000; - public HttpDownloader(DownloaderCallback downloaderCallback, - DownloadStatus status) { - super(downloaderCallback, status); + public HttpDownloader(DownloadRequest request) { + super(request); } private DefaultHttpClient createHttpClient() { @@ -66,8 +64,7 @@ public class HttpDownloader extends Downloader { OutputStream out = null; InputStream connection = null; try { - HttpGet httpGet = new HttpGet(status.getFeedFile() - .getDownload_url()); + HttpGet httpGet = new HttpGet(request.getSource()); httpClient = createHttpClient(); HttpResponse response = httpClient.execute(httpGet); HttpEntity httpEntity = response.getEntity(); @@ -76,8 +73,7 @@ public class HttpDownloader extends Downloader { Log.d(TAG, "Response code is " + responseCode); if (responseCode == HttpURLConnection.HTTP_OK && httpEntity != null) { if (StorageUtils.storageAvailable(PodcastApp.getInstance())) { - File destination = new File(status.getFeedFile() - .getFile_url()); + File destination = new File(request.getDestination()); if (!destination.exists()) { connection = AndroidHttpClient .getUngzippedContent(httpEntity); @@ -86,29 +82,30 @@ public class HttpDownloader extends Downloader { destination)); byte[] buffer = new byte[BUFFER_SIZE]; int count = 0; - status.setStatusMsg(R.string.download_running); + request.setStatusMsg(R.string.download_running); if (AppConfig.DEBUG) Log.d(TAG, "Getting size of download"); - status.setSize(httpEntity.getContentLength()); + request.setSize(httpEntity.getContentLength()); if (AppConfig.DEBUG) - Log.d(TAG, "Size is " + status.getSize()); - if (status.getSize() < 0) { - status.setSize(DownloadStatus.SIZE_UNKNOWN); + Log.d(TAG, "Size is " + request.getSize()); + if (request.getSize() < 0) { + request.setSize(DownloadStatus.SIZE_UNKNOWN); } long freeSpace = StorageUtils.getFreeSpaceAvailable(); if (AppConfig.DEBUG) Log.d(TAG, "Free space is " + freeSpace); - if (status.getSize() == DownloadStatus.SIZE_UNKNOWN - || status.getSize() <= freeSpace) { + if (request.getSize() == DownloadStatus.SIZE_UNKNOWN + || request.getSize() <= freeSpace) { if (AppConfig.DEBUG) Log.d(TAG, "Starting download"); while (!cancelled && (count = in.read(buffer)) != -1) { out.write(buffer, 0, count); - status.setSoFar(status.getSoFar() + count); - status.setProgressPercent((int) (((double) status - .getSoFar() / (double) status.getSize()) * 100)); + request.setSoFar(request.getSoFar() + count); + request.setProgressPercent((int) (((double) request + .getSoFar() / (double) request + .getSize()) * 100)); } if (cancelled) { onCancelled(); @@ -144,10 +141,8 @@ public class HttpDownloader extends Downloader { } catch (NullPointerException e) { // might be thrown by connection.getInputStream() e.printStackTrace(); - onFail(DownloadError.ERROR_CONNECTION_ERROR, status.getFeedFile() - .getDownload_url()); + onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); } finally { - IOUtils.closeQuietly(connection); IOUtils.closeQuietly(out); if (httpClient != null) { httpClient.getConnectionManager().shutdown(); @@ -158,36 +153,28 @@ public class HttpDownloader extends Downloader { private void onSuccess() { if (AppConfig.DEBUG) Log.d(TAG, "Download was successful"); - status.setSuccessful(true); - status.setDone(true); + result.setSuccessful(); } - private void onFail(int reason, String reasonDetailed) { + private void onFail(DownloadError reason, String reasonDetailed) { if (AppConfig.DEBUG) { Log.d(TAG, "Download failed"); } - status.setReason(reason); - status.setReasonDetailed(reasonDetailed); - status.setDone(true); - status.setSuccessful(false); + result.setFailed(reason, reasonDetailed); cleanup(); } private void onCancelled() { if (AppConfig.DEBUG) Log.d(TAG, "Download was cancelled"); - status.setReason(DownloadError.ERROR_DOWNLOAD_CANCELLED); - status.setDone(true); - status.setSuccessful(false); - status.setCancelled(true); + result.setCancelled(); cleanup(); } /** Deletes unfinished downloads. */ private void cleanup() { - if (status != null && status.getFeedFile() != null - && status.getFeedFile().getFile_url() != null) { - File dest = new File(status.getFeedFile().getFile_url()); + if (request.getDestination() != null) { + File dest = new File(request.getDestination()); if (dest.exists()) { boolean rc = dest.delete(); if (AppConfig.DEBUG) diff --git a/src/de/danoeh/antennapod/storage/DBReader.java b/src/de/danoeh/antennapod/storage/DBReader.java new file mode 100644 index 000000000..c96051874 --- /dev/null +++ b/src/de/danoeh/antennapod/storage/DBReader.java @@ -0,0 +1,755 @@ +package de.danoeh.antennapod.storage; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import android.content.Context; +import android.database.Cursor; +import android.database.SQLException; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.feed.Chapter; +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.feed.ID3Chapter; +import de.danoeh.antennapod.feed.SimpleChapter; +import de.danoeh.antennapod.feed.VorbisCommentChapter; +import de.danoeh.antennapod.service.download.*; +import de.danoeh.antennapod.util.DownloadError; +import de.danoeh.antennapod.util.comparator.DownloadStatusComparator; +import de.danoeh.antennapod.util.comparator.FeedItemPubdateComparator; + +/** + * Provides methods for reading data from the AntennaPod database. + * In general, all database calls in DBReader-methods are executed on the caller's thread. + * This means that the caller should make sure that DBReader-methods are not executed on the GUI-thread. + * This class will use the {@link de.danoeh.antennapod.feed.EventDistributor} to notify listeners about changes in the database. + + */ +public final class DBReader { + private static final String TAG = "DBReader"; + + /** + * Maximum size of the list returned by {@link #getPlaybackHistory(android.content.Context)}. + */ + public static final int PLAYBACK_HISTORY_SIZE = 50; + + /** + * Maximum size of the list returned by {@link #getDownloadLog(android.content.Context)}. + */ + public static final int DOWNLOAD_LOG_SIZE = 200; + + + private DBReader() { + } + + /** + * Returns a list of Feeds, sorted alphabetically by their title. + * + * @param context A context that is used for opening a database connection. + * @return A list of Feeds, sorted alphabetically by their title. A Feed-object + * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list + * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.feed.Feed)}. + */ + public static List<Feed> getFeedList(final Context context) { + if (AppConfig.DEBUG) + Log.d(TAG, "Extracting Feedlist"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor feedlistCursor = adapter.getAllFeedsCursor(); + List<Feed> feeds = new ArrayList<Feed>(feedlistCursor.getCount()); + + if (feedlistCursor.moveToFirst()) { + do { + Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor); + feeds.add(feed); + } while (feedlistCursor.moveToNext()); + } + feedlistCursor.close(); + return feeds; + } + + /** + * Returns a list of 'expired Feeds', i.e. Feeds that have not been updated for a certain amount of time. + * + * @param context A context that is used for opening a database connection. + * @param expirationTime Time that is used for determining whether a feed is outdated or not. + * A Feed is considered expired if 'lastUpdate < (currentTime - expirationTime)' evaluates to true. + * @return A list of Feeds, sorted alphabetically by their title. A Feed-object + * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list + * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.feed.Feed)}. + */ + static List<Feed> getExpiredFeedsList(final Context context, final long expirationTime) { + if (AppConfig.DEBUG) + Log.d(TAG, String.format("getExpiredFeedsList(%d)", expirationTime)); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor feedlistCursor = adapter.getExpiredFeedsCursor(expirationTime); + List<Feed> feeds = new ArrayList<Feed>(feedlistCursor.getCount()); + + if (feedlistCursor.moveToFirst()) { + do { + Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor); + feeds.add(feed); + } while (feedlistCursor.moveToNext()); + } + feedlistCursor.close(); + return feeds; + } + + /** + * Takes a list of FeedItems and loads their corresponding Feed-objects from the database. + * + * @param context A context that is used for opening a database connection. + * @param items The FeedItems whose Feed-objects should be loaded. + */ + public static void loadFeedDataOfFeedItemlist(Context context, + List<FeedItem> items) { + List<Feed> feeds = getFeedList(context); + for (FeedItem item : items) { + for (Feed feed : feeds) { + if (feed.getId() == item.getFeedId()) { + item.setFeed(feed); + break; + } + } + if (item.getFeed() == null) { + Log.w(TAG, "No match found for item with ID " + item.getId() + ". Feed ID was " + item.getFeedId()); + } + } + } + + /** + * Loads the list of FeedItems for a certain Feed-object. This method should NOT be used if the FeedItems are not + * used. In order to get information ABOUT the list of FeedItems, consider using {@link #getFeedStatisticsList(android.content.Context)} instead. + * + * @param context A context that is used for opening a database connection. + * @param feed The Feed whose items should be loaded + * @return A list with the FeedItems of the Feed. The Feed-attribute of the FeedItems will already be set correctly. + * The method does NOT change the items-attribute of the feed. + */ + public static List<FeedItem> getFeedItemList(Context context, + final Feed feed) { + if (AppConfig.DEBUG) + Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle()); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getAllItemsOfFeedCursor(feed); + List<FeedItem> items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + + Collections.sort(items, new FeedItemPubdateComparator()); + + adapter.close(); + + for (FeedItem item : items) { + item.setFeed(feed); + } + + return items; + } + + static List<FeedItem> extractItemlistFromCursor(Context context, Cursor itemlistCursor) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List<FeedItem> result = extractItemlistFromCursor(adapter, itemlistCursor); + adapter.close(); + return result; + } + + private static List<FeedItem> extractItemlistFromCursor( + PodDBAdapter adapter, Cursor itemlistCursor) { + ArrayList<String> itemIds = new ArrayList<String>(); + List<FeedItem> items = new ArrayList<FeedItem>( + itemlistCursor.getCount()); + + if (itemlistCursor.moveToFirst()) { + do { + FeedItem item = new FeedItem(); + + item.setId(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_ID)); + item.setTitle(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_TITLE)); + item.setLink(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_LINK)); + item.setPubDate(new Date(itemlistCursor + .getLong(PodDBAdapter.IDX_FI_SMALL_PUBDATE))); + item.setPaymentLink(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_PAYMENT_LINK)); + item.setFeedId(itemlistCursor + .getLong(PodDBAdapter.IDX_FI_SMALL_FEED)); + itemIds.add(String.valueOf(item.getId())); + + item.setRead((itemlistCursor + .getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0)); + item.setItemIdentifier(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER)); + + // extract chapters + boolean hasSimpleChapters = itemlistCursor + .getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0; + if (hasSimpleChapters) { + Cursor chapterCursor = adapter + .getSimpleChaptersOfFeedItemCursor(item); + if (chapterCursor.moveToFirst()) { + item.setChapters(new ArrayList<Chapter>()); + do { + int chapterType = chapterCursor + .getInt(PodDBAdapter.KEY_CHAPTER_TYPE_INDEX); + Chapter chapter = null; + long start = chapterCursor + .getLong(PodDBAdapter.KEY_CHAPTER_START_INDEX); + String title = chapterCursor + .getString(PodDBAdapter.KEY_TITLE_INDEX); + String link = chapterCursor + .getString(PodDBAdapter.KEY_CHAPTER_LINK_INDEX); + + switch (chapterType) { + case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER: + chapter = new SimpleChapter(start, title, item, + link); + break; + case ID3Chapter.CHAPTERTYPE_ID3CHAPTER: + chapter = new ID3Chapter(start, title, item, + link); + break; + case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER: + chapter = new VorbisCommentChapter(start, + title, item, link); + break; + } + chapter.setId(chapterCursor + .getLong(PodDBAdapter.KEY_ID_INDEX)); + item.getChapters().add(chapter); + } while (chapterCursor.moveToNext()); + } + chapterCursor.close(); + } + items.add(item); + } while (itemlistCursor.moveToNext()); + } + + extractMediafromItemlist(adapter, items, itemIds); + return items; + } + + private static void extractMediafromItemlist(PodDBAdapter adapter, + List<FeedItem> items, ArrayList<String> itemIds) { + + List<FeedItem> itemsCopy = new ArrayList<FeedItem>(items); + Cursor cursor = adapter.getFeedMediaCursorByItemID(itemIds + .toArray(new String[itemIds.size()])); + if (cursor.moveToFirst()) { + do { + long itemId = cursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); + // find matching feed item + FeedItem item = getMatchingItemForMedia(itemId, itemsCopy); + if (item != null) { + item.setMedia(extractFeedMediaFromCursorRow(cursor)); + item.getMedia().setItem(item); + } + } while (cursor.moveToNext()); + cursor.close(); + } + } + + private static FeedMedia extractFeedMediaFromCursorRow(final Cursor cursor) { + long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); + Date playbackCompletionDate = null; + long playbackCompletionTime = cursor + .getLong(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE_INDEX); + if (playbackCompletionTime > 0) { + playbackCompletionDate = new Date( + playbackCompletionTime); + } + + return new FeedMedia( + mediaId, + null, + cursor.getInt(PodDBAdapter.KEY_DURATION_INDEX), + cursor.getInt(PodDBAdapter.KEY_POSITION_INDEX), + cursor.getLong(PodDBAdapter.KEY_SIZE_INDEX), + cursor.getString(PodDBAdapter.KEY_MIME_TYPE_INDEX), + cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX), + cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX), + cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0, + playbackCompletionDate); + } + + private static Feed extractFeedFromCursorRow(PodDBAdapter adapter, + Cursor cursor) { + Date lastUpdate = new Date( + cursor.getLong(PodDBAdapter.KEY_LAST_UPDATE_INDEX)); + + final FeedImage image; + long imageIndex = cursor.getLong(PodDBAdapter.KEY_IMAGE_INDEX); + if (imageIndex != 0) { + image = getFeedImage(adapter, imageIndex); + } else { + image = null; + } + Feed feed = new Feed(cursor.getLong(PodDBAdapter.KEY_ID_INDEX), + lastUpdate, + cursor.getString(PodDBAdapter.KEY_TITLE_INDEX), + cursor.getString(PodDBAdapter.KEY_LINK_INDEX), + cursor.getString(PodDBAdapter.KEY_DESCRIPTION_INDEX), + cursor.getString(PodDBAdapter.KEY_PAYMENT_LINK_INDEX), + cursor.getString(PodDBAdapter.KEY_AUTHOR_INDEX), + cursor.getString(PodDBAdapter.KEY_LANGUAGE_INDEX), + cursor.getString(PodDBAdapter.KEY_TYPE_INDEX), + cursor.getString(PodDBAdapter.KEY_FEED_IDENTIFIER_INDEX), + image, + cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX), + cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX), + cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0); + + if (image != null) { + image.setFeed(feed); + } + return feed; + } + + private static FeedItem getMatchingItemForMedia(long itemId, + List<FeedItem> items) { + for (FeedItem item : items) { + if (item.getId() == itemId) { + return item; + } + } + return null; + } + + static List<FeedItem> getQueue(Context context, PodDBAdapter adapter) { + if (AppConfig.DEBUG) + Log.d(TAG, "Extracting queue"); + + Cursor itemlistCursor = adapter.getQueueCursor(); + List<FeedItem> items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + loadFeedDataOfFeedItemlist(context, items); + + return items; + } + + /** + * Loads the IDs of the FeedItems in the queue. This method should be preferred over + * {@link #getQueue(android.content.Context)} if the FeedItems of the queue are not needed. + * + * @param context A context that is used for opening a database connection. + * @return A list of IDs sorted by the same order as the queue. The caller can wrap the returned + * list in a {@link de.danoeh.antennapod.util.QueueAccess} object for easier access to the queue's properties. + */ + public static List<Long> getQueueIDList(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + + adapter.open(); + List<Long> result = getQueueIDList(adapter); + adapter.close(); + + return result; + } + + static List<Long> getQueueIDList(PodDBAdapter adapter) { + adapter.open(); + Cursor queueCursor = adapter.getQueueIDCursor(); + + List<Long> queueIds = new ArrayList<Long>(queueCursor.getCount()); + if (queueCursor.moveToFirst()) { + do { + queueIds.add(queueCursor.getLong(0)); + } while (queueCursor.moveToNext()); + } + return queueIds; + } + + + /** + * Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using + * {@link #getQueueIDList(android.content.Context)} instead. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItems sorted by the same order as the queue. The caller can wrap the returned + * list in a {@link de.danoeh.antennapod.util.QueueAccess} object for easier access to the queue's properties. + */ + public static List<FeedItem> getQueue(Context context) { + if (AppConfig.DEBUG) + Log.d(TAG, "Extracting queue"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List<FeedItem> items = getQueue(context, adapter); + adapter.close(); + return items; + } + + /** + * Loads a list of FeedItems whose episode has been downloaded. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItems whose episdoe has been downloaded. + */ + public static List<FeedItem> getDownloadedItems(Context context) { + if (AppConfig.DEBUG) + Log.d(TAG, "Extracting downloaded items"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getDownloadedItemsCursor(); + List<FeedItem> items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + loadFeedDataOfFeedItemlist(context, items); + Collections.sort(items, new FeedItemPubdateComparator()); + + adapter.close(); + return items; + + } + + /** + * Loads a list of FeedItems whose 'read'-attribute is set to false. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItems whose 'read'-attribute it set to false. If the FeedItems in the list are not used, + * consider using {@link #getUnreadItemIds(android.content.Context)} instead. + */ + public static List<FeedItem> getUnreadItemsList(Context context) { + if (AppConfig.DEBUG) + Log.d(TAG, "Extracting unread items list"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getUnreadItemsCursor(); + List<FeedItem> items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + + loadFeedDataOfFeedItemlist(context, items); + + adapter.close(); + + return items; + } + + /** + * Loads the IDs of the FeedItems whose 'read'-attribute is set to false. + * + * @param context A context that is used for opening a database connection. + * @return A list of IDs of the FeedItems whose 'read'-attribute is set to false. This method should be preferred + * over {@link #getUnreadItemsList(android.content.Context)} if the FeedItems in the UnreadItems list are not used. + */ + public static long[] getUnreadItemIds(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getUnreadItemIdsCursor(); + long[] itemIds = new long[cursor.getCount()]; + int i = 0; + if (cursor.moveToFirst()) { + do { + itemIds[i] = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); + i++; + } while (cursor.moveToNext()); + } + return itemIds; + } + + /** + * Loads the playback history from the database. A FeedItem is in the playback history if playback of the correpsonding episode + * has been completed at least once. + * + * @param context A context that is used for opening a database connection. + * @return The playback history. The FeedItems are sorted by their media's playbackCompletionDate in descending order. + * The size of the returned list is limited by {@link #PLAYBACK_HISTORY_SIZE}. + */ + public static List<FeedItem> getPlaybackHistory(final Context context) { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading playback history"); + final int PLAYBACK_HISTORY_SIZE = 50; + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor mediaCursor = adapter.getCompletedMediaCursor(PLAYBACK_HISTORY_SIZE); + String[] itemIds = new String[mediaCursor.getCount()]; + for (int i = 0; i < itemIds.length && mediaCursor.moveToPosition(i); i++) { + itemIds[i] = Long.toString(mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX)); + } + mediaCursor.close(); + Cursor itemCursor = adapter.getFeedItemCursor(itemIds); + List<FeedItem> items = extractItemlistFromCursor(adapter, itemCursor); + loadFeedDataOfFeedItemlist(context, items); + itemCursor.close(); + + adapter.close(); + return items; + } + + /** + * Loads the download log from the database. + * + * @param context A context that is used for opening a database connection. + * @return A list with DownloadStatus objects that represent the download log. + * The size of the returned list is limited by {@link #DOWNLOAD_LOG_SIZE}. + */ + public static List<DownloadStatus> getDownloadLog(Context context) { + if (AppConfig.DEBUG) + Log.d(TAG, "Extracting DownloadLog"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor logCursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE); + List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>( + logCursor.getCount()); + + if (logCursor.moveToFirst()) { + do { + long id = logCursor.getLong(PodDBAdapter.KEY_ID_INDEX); + + long feedfileId = logCursor + .getLong(PodDBAdapter.KEY_FEEDFILE_INDEX); + int feedfileType = logCursor + .getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX); + boolean successful = logCursor + .getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0; + int reason = logCursor.getInt(PodDBAdapter.KEY_REASON_INDEX); + String reasonDetailed = logCursor + .getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX); + String title = logCursor + .getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX); + Date completionDate = new Date( + logCursor + .getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX)); + downloadLog.add(new DownloadStatus(id, title, feedfileId, + feedfileType, successful, DownloadError.fromCode(reason), completionDate, + reasonDetailed)); + + } while (logCursor.moveToNext()); + } + logCursor.close(); + Collections.sort(downloadLog, new DownloadStatusComparator()); + return downloadLog; + } + + /** + * Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over + * {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.feed.Feed)} if only metadata about + * the FeedItems is needed. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItemStatistics objects sorted alphabetically by their Feed's title. + */ + public static List<FeedItemStatistics> getFeedStatisticsList(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List<FeedItemStatistics> result = new ArrayList<FeedItemStatistics>(); + Cursor cursor = adapter.getFeedStatisticsCursor(); + if (cursor.moveToFirst()) { + do { + result.add(new FeedItemStatistics(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_FEED), + cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NUM_ITEMS), + cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NEW_ITEMS), + cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES), + new Date(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_LATEST_EPISODE)))); + } while (cursor.moveToNext()); + } + + cursor.close(); + adapter.close(); + return result; + } + + /** + * Loads a specific Feed from the database. + * + * @param context A context that is used for opening a database connection. + * @param feedId The ID of the Feed + * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the + * database and the items-attribute will be set correctly. + */ + public static Feed getFeed(final Context context, final long feedId) { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading feed with id " + feedId); + Feed feed = null; + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor feedCursor = adapter.getFeedCursor(feedId); + if (feedCursor.moveToFirst()) { + feed = extractFeedFromCursorRow(adapter, feedCursor); + feed.setItems(getFeedItemList(context, feed)); + } else { + Log.e(TAG, "getFeed could not find feed with id " + feedId); + } + feedCursor.close(); + adapter.close(); + return feed; + } + + static FeedItem getFeedItem(final Context context, final long itemId, PodDBAdapter adapter) { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading feeditem with id " + itemId); + FeedItem item = null; + + Cursor itemCursor = adapter.getFeedItemCursor(Long.toString(itemId)); + if (itemCursor.moveToFirst()) { + List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor); + if (list.size() > 0) { + item = list.get(0); + loadFeedDataOfFeedItemlist(context, list); + } + } + return item; + + } + + /** + * Loads a specific FeedItem from the database. + * + * @param context A context that is used for opening a database connection. + * @param itemId The ID of the FeedItem + * @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes of the FeedItem will + * also be loaded from the database. + */ + public static FeedItem getFeedItem(final Context context, final long itemId) { + if (AppConfig.DEBUG) + Log.d(TAG, "Loading feeditem with id " + itemId); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + FeedItem item = getFeedItem(context, itemId, adapter); + adapter.close(); + return item; + + } + + /** + * Loads additional information about a FeedItem, e.g. shownotes + * + * @param context A context that is used for opening a database connection. + * @param item The FeedItem + */ + public static void loadExtraInformationOfFeedItem(final Context context, final FeedItem item) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor extraCursor = adapter.getExtraInformationOfItem(item); + if (extraCursor.moveToFirst()) { + String description = extraCursor + .getString(PodDBAdapter.IDX_FI_EXTRA_DESCRIPTION); + String contentEncoded = extraCursor + .getString(PodDBAdapter.IDX_FI_EXTRA_CONTENT_ENCODED); + item.setDescription(description); + item.setContentEncoded(contentEncoded); + } + adapter.close(); + } + + /** + * Returns the number of downloaded episodes. + * + * @param context A context that is used for opening a database connection. + * @return The number of downloaded episodes. + */ + public static int getNumberOfDownloadedEpisodes(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final int result = adapter.getNumberOfDownloadedEpisodes(); + adapter.close(); + return result; + } + + /** + * Returns the number of unread items. + * + * @param context A context that is used for opening a database connection. + * @return The number of unread items. + */ + public static int getNumberOfUnreadItems(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final int result = adapter.getNumberOfUnreadItems(); + adapter.close(); + return result; + } + + /** + * Searches the DB for a FeedImage of the given id. + * + * @param context A context that is used for opening a database connection. + * @param imageId The id of the object + * @return The found object + */ + public static FeedImage getFeedImage(final Context context, final long imageId) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + FeedImage result = getFeedImage(adapter, imageId); + adapter.close(); + return result; + } + + /** + * Searches the DB for a FeedImage of the given id. + * + * @param id The id of the object + * @return The found object + */ + static FeedImage getFeedImage(PodDBAdapter adapter, final long id) { + Cursor cursor = adapter.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(PodDBAdapter.KEY_TITLE)), + cursor.getString(cursor + .getColumnIndex(PodDBAdapter.KEY_FILE_URL)), + cursor.getString(cursor + .getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL)), + cursor.getInt(cursor + .getColumnIndex(PodDBAdapter.KEY_DOWNLOADED)) > 0); + cursor.close(); + return image; + } + + /** + * Searches the DB for a FeedMedia of the given id. + * + * @param context A context that is used for opening a database connection. + * @param mediaId The id of the object + * @return The found object + */ + public static FeedMedia getFeedMedia(final Context context, final long mediaId) { + PodDBAdapter adapter = new PodDBAdapter(context); + + adapter.open(); + Cursor mediaCursor = adapter.getSingleFeedMediaCursor(mediaId); + + FeedMedia media = null; + if (mediaCursor.moveToFirst()) { + final long itemId = mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); + media = extractFeedMediaFromCursorRow(mediaCursor); + FeedItem item = getFeedItem(context, itemId); + if (media != null && item != null) { + media.setItem(item); + item.setMedia(media); + } + } + + mediaCursor.close(); + adapter.close(); + + return media; + } +} diff --git a/src/de/danoeh/antennapod/storage/DBTasks.java b/src/de/danoeh/antennapod/storage/DBTasks.java new file mode 100644 index 000000000..b1efda658 --- /dev/null +++ b/src/de/danoeh/antennapod/storage/DBTasks.java @@ -0,0 +1,731 @@ +package de.danoeh.antennapod.storage; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; +import java.util.concurrent.atomic.AtomicBoolean; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.feed.EventDistributor; +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.preferences.UserPreferences; +import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.download.DownloadStatus; +import de.danoeh.antennapod.util.DownloadError; +import de.danoeh.antennapod.util.NetworkUtils; +import de.danoeh.antennapod.util.QueueAccess; +import de.danoeh.antennapod.util.exception.MediaFileNotFoundException; + +/** + * Provides methods for doing common tasks that use DBReader and DBWriter. + */ +public final class DBTasks { + private static final String TAG = "DBTasks"; + + private DBTasks() { + } + + /** + * Starts playback of a FeedMedia object's file. This method will build an Intent based on the given parameters to + * start the {@link PlaybackService}. + * + * @param context Used for sending starting Services and Activities. + * @param media The FeedMedia object. + * @param showPlayer If true, starts the appropriate player activity ({@link de.danoeh.antennapod.activity.AudioplayerActivity} + * or {@link de.danoeh.antennapod.activity.VideoplayerActivity} + * @param startWhenPrepared Parameter for the {@link PlaybackService} start intent. If true, playback will start as + * soon as the PlaybackService has finished loading the FeedMedia object's file. + * @param shouldStream Parameter for the {@link PlaybackService} start intent. If true, the FeedMedia object's file + * will be streamed, otherwise the downloaded file will be used. If the downloaded file cannot be + * found, the PlaybackService will shutdown and the database entry of the FeedMedia object will be + * corrected. + */ + public static void playMedia(final Context context, final FeedMedia media, + boolean showPlayer, boolean startWhenPrepared, boolean shouldStream) { + try { + if (!shouldStream) { + if (media.fileExists() == false) { + throw new MediaFileNotFoundException( + "No episode was found at " + media.getFile_url(), + media); + } + } + // Start playback Service + Intent launchIntent = new Intent(context, PlaybackService.class); + launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, + startWhenPrepared); + launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, + shouldStream); + launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, + true); + context.startService(launchIntent); + if (showPlayer) { + // Launch media player + context.startActivity(PlaybackService.getPlayerActivityIntent( + context, media)); + } + DBWriter.addQueueItemAt(context, media.getItem().getId(), 0, false); + } catch (MediaFileNotFoundException e) { + e.printStackTrace(); + if (media.isPlaying()) { + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + } + notifyMissingFeedMediaFile(context, media); + } + } + + private static AtomicBoolean isRefreshing = new AtomicBoolean(false); + + /** + * Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still + * enqueuing Feeds for download from a previous call + * + * @param context Might be used for accessing the database + * @param feeds List of Feeds that should be refreshed. + */ + public static void refreshAllFeeds(final Context context, + final List<Feed> feeds) { + if (isRefreshing.compareAndSet(false, true)) { + new Thread() { + public void run() { + if (feeds != null) { + refreshFeeds(context, feeds); + } else { + refreshFeeds(context, DBReader.getFeedList(context)); + } + isRefreshing.set(false); + } + }.start(); + } else { + if (AppConfig.DEBUG) + Log.d(TAG, + "Ignoring request to refresh all feeds: Refresh lock is locked"); + } + } + + /** + * Refreshes expired Feeds in the list returned by the getExpiredFeedsList(Context, long) method in DBReader. + * The expiration date parameter is determined by the update interval specified in {@link UserPreferences}. + * + * @param context Used for DB access. + */ + public static void refreshExpiredFeeds(final Context context) { + if (AppConfig.DEBUG) + Log.d(TAG, "Refreshing expired feeds"); + + new Thread() { + public void run() { + long millis = UserPreferences.getUpdateInterval(); + + if (millis > 0) { + long now = Calendar.getInstance().getTime().getTime(); + + // Allow a 10 minute window + millis -= 10 * 60 * 1000; + List<Feed> feedList = DBReader.getExpiredFeedsList(context, + now - millis); + if (feedList.size() > 0) { + refreshFeeds(context, feedList); + } + } + } + }.start(); + } + + private static void refreshFeeds(final Context context, + final List<Feed> feedList) { + + for (Feed feed : feedList) { + try { + refreshFeed(context, feed); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus( + context, + new DownloadStatus(feed, feed + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, false, e + .getMessage())); + } + } + + } + + /** + * Updates a specific Feed. + * + * @param context Used for requesting the download. + * @param feed The Feed object. + */ + public static void refreshFeed(Context context, Feed feed) + throws DownloadRequestException { + DownloadRequester.getInstance().downloadFeed(context, + new Feed(feed.getDownload_url(), new Date(), feed.getTitle())); + } + + /** + * Notifies the database about a missing FeedImage file. This method will attempt to re-download the file. + * + * @param context Used for requesting the download. + * @param image The FeedImage object. + */ + public static void notifyInvalidImageFile(final Context context, + final FeedImage image) { + Log.i(TAG, + "The DB was notified about an invalid image download. It will now try to re-download the image file"); + try { + DownloadRequester.getInstance().downloadImage(context, image); + } catch (DownloadRequestException e) { + e.printStackTrace(); + Log.w(TAG, "Failed to download invalid feed image"); + } + } + + /** + * Notifies the database about a missing FeedMedia file. This method will correct the FeedMedia object's values in the + * DB and send a FeedUpdateBroadcast. + */ + public static void notifyMissingFeedMediaFile(final Context context, + final FeedMedia media) { + Log.i(TAG, + "The feedmanager was notified about a missing episode. It will update its database now."); + media.setDownloaded(false); + media.setFile_url(null); + DBWriter.setFeedMedia(context, media); + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + } + + /** + * Request the download of all objects in the queue. from a separate Thread. + * + * @param context Used for requesting the download an accessing the database. + */ + public static void downloadAllItemsInQueue(final Context context) { + new Thread() { + public void run() { + List<FeedItem> queue = DBReader.getQueue(context); + if (!queue.isEmpty()) { + try { + downloadFeedItems(context, + queue.toArray(new FeedItem[queue.size()])); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + } + }.start(); + } + + /** + * Requests the download of a list of FeedItem objects. + * + * @param context Used for requesting the download and accessing the DB. + * @param items The FeedItem objects. + */ + public static void downloadFeedItems(final Context context, + FeedItem... items) throws DownloadRequestException { + downloadFeedItems(true, context, items); + } + + private static void downloadFeedItems(boolean performAutoCleanup, + final Context context, final FeedItem... items) + throws DownloadRequestException { + final DownloadRequester requester = DownloadRequester.getInstance(); + + if (performAutoCleanup) { + new Thread() { + + @Override + public void run() { + performAutoCleanup(context, + getPerformAutoCleanupArgs(context, items.length)); + } + + }.start(); + } + for (FeedItem item : items) { + if (item.getMedia() != null + && !requester.isDownloadingFile(item.getMedia()) + && !item.getMedia().isDownloaded()) { + if (items.length > 1) { + try { + requester.downloadMedia(context, item.getMedia()); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus(context, + new DownloadStatus(item.getMedia(), item + .getMedia() + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, + false, e.getMessage())); + } + } else { + requester.downloadMedia(context, item.getMedia()); + } + } + } + } + + private static int getNumberOfUndownloadedEpisodes( + final List<FeedItem> queue, final List<FeedItem> unreadItems) { + int counter = 0; + for (FeedItem item : queue) { + if (item.hasMedia() && !item.getMedia().isDownloaded() + && !item.getMedia().isPlaying()) { + counter++; + } + } + for (FeedItem item : unreadItems) { + if (item.hasMedia() && !item.getMedia().isDownloaded()) { + counter++; + } + } + return counter; + } + + /** + * Looks for undownloaded episodes in the queue or list of unread items and request a download if + * 1. Network is available + * 2. There is free space in the episode cache + * This method should NOT be executed on the GUI thread. + * + * @param context Used for accessing the DB. + */ + public static void autodownloadUndownloadedItems(final Context context) { + if (AppConfig.DEBUG) + Log.d(TAG, "Performing auto-dl of undownloaded episodes"); + if (NetworkUtils.autodownloadNetworkAvailable(context) + && UserPreferences.isEnableAutodownload()) { + final List<FeedItem> queue = DBReader.getQueue(context); + final List<FeedItem> unreadItems = DBReader + .getUnreadItemsList(context); + + int undownloadedEpisodes = getNumberOfUndownloadedEpisodes(queue, + unreadItems); + int downloadedEpisodes = DBReader + .getNumberOfDownloadedEpisodes(context); + int deletedEpisodes = performAutoCleanup(context, + getPerformAutoCleanupArgs(context, undownloadedEpisodes)); + int episodeSpaceLeft = undownloadedEpisodes; + boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences + .getEpisodeCacheSizeUnlimited(); + + if (!cacheIsUnlimited + && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes + + undownloadedEpisodes) { + episodeSpaceLeft = UserPreferences.getEpisodeCacheSize() + - (downloadedEpisodes - deletedEpisodes); + } + + List<FeedItem> itemsToDownload = new ArrayList<FeedItem>(); + if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { + for (int i = 0; i < queue.size(); i++) { // ignore playing item + FeedItem item = queue.get(i); + if (item.hasMedia() && !item.getMedia().isDownloaded() + && !item.getMedia().isPlaying()) { + itemsToDownload.add(item); + episodeSpaceLeft--; + undownloadedEpisodes--; + if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { + break; + } + } + } + } + if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { + for (FeedItem item : unreadItems) { + if (item.hasMedia() && !item.getMedia().isDownloaded()) { + itemsToDownload.add(item); + episodeSpaceLeft--; + undownloadedEpisodes--; + if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { + break; + } + } + } + } + if (AppConfig.DEBUG) + Log.d(TAG, "Enqueueing " + itemsToDownload.size() + + " items for download"); + + try { + downloadFeedItems(false, context, + itemsToDownload.toArray(new FeedItem[itemsToDownload + .size()])); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + + } + } + + private static int getPerformAutoCleanupArgs(Context context, + final int episodeNumber) { + if (episodeNumber >= 0 + && UserPreferences.getEpisodeCacheSize() != UserPreferences + .getEpisodeCacheSizeUnlimited()) { + int downloadedEpisodes = DBReader + .getNumberOfDownloadedEpisodes(context); + if (downloadedEpisodes + episodeNumber >= UserPreferences + .getEpisodeCacheSize()) { + + return downloadedEpisodes + episodeNumber + - UserPreferences.getEpisodeCacheSize(); + } + } + return 0; + } + + /** + * Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller + * 'playbackCompletionDate'-value will be deleted first. + * <p/> + * This method should NOT be executed on the GUI thread. + * + * @param context Used for accessing the DB. + */ + public static void performAutoCleanup(final Context context) { + performAutoCleanup(context, getPerformAutoCleanupArgs(context, 0)); + } + + private static int performAutoCleanup(final Context context, + final int episodeNumber) { + List<FeedItem> candidates = DBReader.getDownloadedItems(context); + List<FeedItem> queue = DBReader.getQueue(context); + List<FeedItem> delete; + for (FeedItem item : candidates) { + if (item.hasMedia() && item.getMedia().isDownloaded() + && !queue.contains(item) && item.isRead()) { + candidates.add(item); + } + + } + + Collections.sort(candidates, new Comparator<FeedItem>() { + @Override + public int compare(FeedItem lhs, FeedItem rhs) { + Date l = lhs.getMedia().getPlaybackCompletionDate(); + Date r = rhs.getMedia().getPlaybackCompletionDate(); + + if (l == null) { + l = new Date(0); + } + if (r == null) { + r = new Date(0); + } + return l.compareTo(r); + } + }); + + if (candidates.size() > episodeNumber) { + delete = candidates.subList(0, episodeNumber); + } else { + delete = candidates; + } + + for (FeedItem item : delete) { + DBWriter.deleteFeedMediaOfItem(context, item.getId()); + } + + int counter = delete.size(); + + if (AppConfig.DEBUG) + Log.d(TAG, String.format( + "Auto-delete deleted %d episodes (%d requested)", counter, + episodeNumber)); + + return counter; + } + + /** + * Adds all FeedItem objects whose 'read'-attribute is false to the queue in a separate thread. + */ + public static void enqueueAllNewItems(final Context context) { + long[] unreadItems = DBReader.getUnreadItemIds(context); + DBWriter.addQueueItem(context, unreadItems); + } + + /** + * Returns the successor of a FeedItem in the queue. + * + * @param context Used for accessing the DB. + * @param itemId ID of the FeedItem + * @param queue Used for determining the successor of the item. If this parameter is null, the method will load + * the queue from the database in the same thread. + * @return Successor of the FeedItem or null if the FeedItem is not in the queue or has no successor. + */ + public static FeedItem getQueueSuccessorOfItem(Context context, + final long itemId, List<FeedItem> queue) { + FeedItem result = null; + if (queue == null) { + queue = DBReader.getQueue(context); + } + if (queue != null) { + Iterator<FeedItem> iterator = queue.iterator(); + while (iterator.hasNext()) { + FeedItem item = iterator.next(); + if (item.getId() == itemId) { + if (iterator.hasNext()) { + result = iterator.next(); + } + break; + } + } + } + return result; + } + + /** + * Loads the queue from the database and checks if the specified FeedItem is in the queue. + * This method should NOT be executed in the GUI thread. + * + * @param context Used for accessing the DB. + * @param feedItemId ID of the FeedItem + */ + public static boolean isInQueue(Context context, final long feedItemId) { + List<Long> queue = DBReader.getQueueIDList(context); + return QueueAccess.IDListAccess(queue).contains(feedItemId); + } + + private static Feed searchFeedByIdentifyingValue(Context context, + String identifier) { + List<Feed> feeds = DBReader.getFeedList(context); + for (Feed feed : feeds) { + if (feed.getIdentifyingValue().equals(identifier)) { + return feed; + } + } + return null; + } + + /** + * Get a FeedItem by its identifying value. + */ + private static FeedItem searchFeedItemByIdentifyingValue(Feed feed, + String identifier) { + for (FeedItem item : feed.getItems()) { + if (item.getIdentifyingValue().equals(identifier)) { + return item; + } + } + return null; + } + + /** + * Adds a new Feed to the database or updates the old version if it already exists. If another Feed with the same + * identifying value already exists, this method will add new FeedItems from the new Feed to the existing Feed. + * These FeedItems will be marked as unread. + * This method should NOT be executed on the GUI thread. + * + * @param context Used for accessing the DB. + * @param newFeed The new Feed object. + * @return The updated Feed from the database if it already existed, or the new Feed from the parameters otherwise. + */ + public static synchronized Feed updateFeed(final Context context, + final Feed newFeed) { + // Look up feed in the feedslist + final Feed savedFeed = searchFeedByIdentifyingValue(context, + newFeed.getIdentifyingValue()); + if (savedFeed == null) { + if (AppConfig.DEBUG) + Log.d(TAG, + "Found no existing Feed with title " + + newFeed.getTitle() + ". Adding as new one."); + // Add a new Feed + try { + DBWriter.addNewFeed(context, newFeed).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + return newFeed; + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "Feed with title " + newFeed.getTitle() + + " already exists. Syncing new with existing one."); + + savedFeed.setItems(DBReader.getFeedItemList(context, savedFeed)); + if (savedFeed.compareWithOther(newFeed)) { + if (AppConfig.DEBUG) + Log.d(TAG, + "Feed has updated attribute values. Updating old feed's attributes"); + savedFeed.updateFromOther(newFeed); + } + // Look for new or updated Items + for (int idx = 0; idx < newFeed.getItems().size(); idx++) { + final FeedItem item = newFeed.getItems().get(idx); + FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed, + item.getIdentifyingValue()); + if (oldItem == null) { + // item is new + final int i = idx; + item.setFeed(savedFeed); + savedFeed.getItems().add(i, item); + DBWriter.markItemRead(context, item.getId(), false); + } else { + oldItem.updateFromOther(item); + } + } + // update attributes + savedFeed.setLastUpdate(newFeed.getLastUpdate()); + savedFeed.setType(newFeed.getType()); + try { + DBWriter.setCompleteFeed(context, savedFeed).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + new Thread() { + @Override + public void run() { + autodownloadUndownloadedItems(context); + } + }.start(); + return savedFeed; + } + } + + /** + * Searches the titles of FeedItems of a specific Feed for a given + * string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string. + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask<List<FeedItem>> searchFeedItemTitle(final Context context, + final long feedID, final String query) { + return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemTitles(feedID, + query); + List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * Searches the descriptions of FeedItems of a specific Feed for a given + * string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask<List<FeedItem>> searchFeedItemDescription(final Context context, + final long feedID, final String query) { + return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemDescriptions(feedID, + query); + List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * Searches the contentEncoded-value of FeedItems of a specific Feed for a given + * string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask<List<FeedItem>> searchFeedItemContentEncoded(final Context context, + final long feedID, final String query) { + return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemContentEncoded(feedID, + query); + List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * Searches chapters of the FeedItems of a specific Feed for a given string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask<List<FeedItem>> searchFeedItemChapters(final Context context, + final long feedID, final String query) { + return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemChapters(feedID, + query); + List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * A runnable which should be used for database queries. The onCompletion + * method is executed on the database executor to handle Cursors correctly. + * This class automatically creates a PodDBAdapter object and closes it when + * it is no longer in use. + */ + static abstract class QueryTask<T> implements Callable<T> { + private T result; + private Context context; + + public QueryTask(Context context) { + this.context = context; + } + + @Override + public T call() throws Exception { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + execute(adapter); + adapter.close(); + return result; + } + + public abstract void execute(PodDBAdapter adapter); + + protected void setResult(T result) { + this.result = result; + } + } + +} diff --git a/src/de/danoeh/antennapod/storage/DBWriter.java b/src/de/danoeh/antennapod/storage/DBWriter.java new file mode 100644 index 000000000..5cdd6aee4 --- /dev/null +++ b/src/de/danoeh/antennapod/storage/DBWriter.java @@ -0,0 +1,724 @@ +package de.danoeh.antennapod.storage; + +import java.io.File; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.preference.PreferenceManager; +import android.util.Log; +import de.danoeh.antennapod.AppConfig; +import de.danoeh.antennapod.feed.*; +import de.danoeh.antennapod.preferences.PlaybackPreferences; +import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.service.download.DownloadStatus; +import de.danoeh.antennapod.util.QueueAccess; + +/** + * Provides methods for writing data to AntennaPod's database. + * In general, DBWriter-methods will be executed on an internal ExecutorService. + * Some methods return a Future-object which the caller can use for waiting for the method's completion. The returned Future's + * will NOT contain any results. + * The caller can also use the {@link EventDistributor} in order to be notified about the method's completion asynchronously. + * This class will use the {@link EventDistributor} to notify listeners about changes in the database. + */ +public class DBWriter { + private static final String TAG = "DBWriter"; + + private static final ExecutorService dbExec; + + static { + dbExec = Executors.newSingleThreadExecutor(new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + } + + private DBWriter() { + } + + /** + * Deletes a downloaded FeedMedia file from the storage device. + * + * @param context A context that is used for opening a database connection. + * @param mediaId ID of the FeedMedia object whose downloaded file should be deleted. + */ + public static Future<?> deleteFeedMediaOfItem(final Context context, + final long mediaId) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + + final FeedMedia media = DBReader.getFeedMedia(context, mediaId); + if (media != null) { + boolean result = false; + if (media.isDownloaded()) { + // delete downloaded media file + File mediaFile = new File(media.getFile_url()); + if (mediaFile.exists()) { + result = mediaFile.delete(); + } + media.setDownloaded(false); + media.setFile_url(null); + setFeedMedia(context, media); + + // If media is currently being played, change playback + // type to 'stream' and shutdown playback service + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA) { + if (media.getId() == PlaybackPreferences + .getCurrentlyPlayingFeedMediaId()) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean( + PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, + true); + editor.commit(); + } + if (PlaybackPreferences + .getCurrentlyPlayingFeedMediaId() == media + .getId()) { + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + } + } + } + if (AppConfig.DEBUG) + Log.d(TAG, "Deleting File. Result: " + result); + } + } + }); + } + + /** + * Deletes a Feed and all downloaded files of its components like images and downloaded episodes. + * + * @param context A context that is used for opening a database connection. + * @param feedId ID of the Feed that should be deleted. + */ + public static Future<?> deleteFeed(final Context context, final long feedId) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + DownloadRequester requester = DownloadRequester.getInstance(); + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context + .getApplicationContext()); + final Feed feed = DBReader.getFeed(context, feedId); + if (feed != null) { + if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA + && PlaybackPreferences.getLastPlayedFeedId() == feed + .getId()) { + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + SharedPreferences.Editor editor = prefs.edit(); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + -1); + editor.commit(); + } + + // 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(); + } else if (requester.isDownloadingFile(feed.getImage())) { + requester.cancelDownload(context, feed.getImage()); + } + } + // delete stored media files and mark them as read + List<FeedItem> queue = DBReader.getQueue(context); + boolean queueWasModified = false; + if (feed.getItems() == null) { + DBReader.getFeedItemList(context, feed); + } + + for (FeedItem item : feed.getItems()) { + queueWasModified |= queue.remove(item); + if (item.getMedia() != null + && item.getMedia().isDownloaded()) { + File mediaFile = new File(item.getMedia() + .getFile_url()); + mediaFile.delete(); + } else if (item.getMedia() != null + && requester.isDownloadingFile(item.getMedia())) { + requester.cancelDownload(context, item.getMedia()); + } + } + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + if (queueWasModified) { + adapter.setQueue(queue); + } + adapter.removeFeed(feed); + adapter.close(); + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + } + } + }); + } + + /** + * Deletes the entire playback history. + * + * @param context A context that is used for opening a database connection. + */ + public static Future<?> clearPlaybackHistory(final Context context) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.clearPlaybackHistory(); + adapter.close(); + EventDistributor.getInstance() + .sendPlaybackHistoryUpdateBroadcast(); + } + }); + } + + /** + * Adds a FeedMedia object to the playback history. A FeedMedia object is in the playback history if + * its playback completion date is set to a non-null value. This method will set the playback completion date to the + * current date regardless of the current value. + * + * @param context A context that is used for opening a database connection. + * @param media FeedMedia that should be added to the playback history. + */ + public static Future<?> addItemToPlaybackHistory(final Context context, + final FeedMedia media) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + if (AppConfig.DEBUG) + Log.d(TAG, "Adding new item to playback history"); + media.setPlaybackCompletionDate(new Date()); + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setMedia(media); + adapter.close(); + EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast(); + + } + }); + } + + private static void cleanupDownloadLog(final PodDBAdapter adapter) { + final long logSize = adapter.getDownloadLogSize(); + if (logSize > DBReader.DOWNLOAD_LOG_SIZE) { + if (AppConfig.DEBUG) + Log.d(TAG, "Cleaning up download log"); + adapter.removeDownloadLogItems(logSize - DBReader.DOWNLOAD_LOG_SIZE); + } + } + + /** + * Adds a Download status object to the download log. + * + * @param context A context that is used for opening a database connection. + * @param status The DownloadStatus object. + */ + public static Future<?> addDownloadStatus(final Context context, + final DownloadStatus status) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + adapter.setDownloadStatus(status); + cleanupDownloadLog(adapter); + adapter.close(); + EventDistributor.getInstance().sendDownloadLogUpdateBroadcast(); + } + }); + + } + + /** + * Inserts a FeedItem in the queue at the specified index. The 'read'-attribute of the FeedItem will be set to + * true. If the FeedItem is already in the queue, the queue will not be modified. + * + * @param context A context that is used for opening a database connection. + * @param itemId ID of the FeedItem that should be added to the queue. + * @param index Destination index. Must be in range 0..queue.size() + * @param performAutoDownload True if an auto-download process should be started after the operation + * @throws IndexOutOfBoundsException if index < 0 || index >= queue.size() + */ + public static Future<?> addQueueItemAt(final Context context, final long itemId, + final int index, final boolean performAutoDownload) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List<FeedItem> queue = DBReader + .getQueue(context, adapter); + FeedItem item = null; + + if (queue != null) { + boolean queueModified = false; + boolean unreadItemsModified = false; + + if (!itemListContains(queue, itemId)) { + item = DBReader.getFeedItem(context, itemId); + if (item != null) { + queue.add(index, item); + queueModified = true; + if (!item.isRead()) { + item.setRead(true); + unreadItemsModified = true; + } + } + } + if (queueModified) { + adapter.setQueue(queue); + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } + if (unreadItemsModified && item != null) { + adapter.setSingleFeedItem(item); + EventDistributor.getInstance() + .sendUnreadItemsUpdateBroadcast(); + } + } + adapter.close(); + if (performAutoDownload) { + + new Thread() { + @Override + public void run() { + DBTasks.autodownloadUndownloadedItems(context); + + } + }.start(); + } + + } + }); + + } + + /** + * Appends FeedItem objects to the end of the queue. The 'read'-attribute of all items will be set to true. + * If a FeedItem is already in the queue, the FeedItem will not change its position in the queue. + * + * @param context A context that is used for opening a database connection. + * @param itemIds IDs of the FeedItem objects that should be added to the queue. + */ + public static Future<?> addQueueItem(final Context context, + final long... itemIds) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + if (itemIds.length > 0) { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List<FeedItem> queue = DBReader.getQueue(context, + adapter); + + if (queue != null) { + boolean queueModified = false; + boolean unreadItemsModified = false; + List<FeedItem> itemsToSave = new LinkedList<FeedItem>(); + for (int i = 0; i < itemIds.length; i++) { + if (!itemListContains(queue, itemIds[i])) { + final FeedItem item = DBReader.getFeedItem( + context, itemIds[i]); + + if (item != null) { + queue.add(item); + queueModified = true; + if (!item.isRead()) { + item.setRead(true); + itemsToSave.add(item); + unreadItemsModified = true; + } + } + } + } + if (queueModified) { + adapter.setQueue(queue); + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } + if (unreadItemsModified) { + adapter.setFeedItemlist(itemsToSave); + EventDistributor.getInstance() + .sendUnreadItemsUpdateBroadcast(); + } + } + adapter.close(); + new Thread() { + @Override + public void run() { + DBTasks.autodownloadUndownloadedItems(context); + + } + }.start(); + } + } + }); + + } + + /** + * Removes all FeedItem objects from the queue. + * + * @param context A context that is used for opening a database connection. + */ + public static Future<?> clearQueue(final Context context) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.clearQueue(); + adapter.close(); + + EventDistributor.getInstance().sendQueueUpdateBroadcast(); + } + }); + } + + /** + * Removes a FeedItem object from the queue. + * + * @param context A context that is used for opening a database connection. + * @param itemId ID of the FeedItem that should be removed. + * @param performAutoDownload true if an auto-download process should be started after the operation. + */ + public static Future<?> removeQueueItem(final Context context, + final long itemId, final boolean performAutoDownload) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List<FeedItem> queue = DBReader + .getQueue(context, adapter); + FeedItem item = null; + + if (queue != null) { + boolean queueModified = false; + QueueAccess queueAccess = QueueAccess.ItemListAccess(queue); + if (queueAccess.contains(itemId)) { + item = DBReader.getFeedItem(context, itemId); + if (item != null) { + queueModified = queueAccess.remove(itemId); + } + } + if (queueModified) { + adapter.setQueue(queue); + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } else { + Log.w(TAG, "Queue was not modified by call to removeQueueItem"); + } + } else { + Log.e(TAG, "removeQueueItem: Could not load queue"); + } + adapter.close(); + if (performAutoDownload) { + + new Thread() { + @Override + public void run() { + DBTasks.autodownloadUndownloadedItems(context); + + } + }.start(); + } + } + }); + + } + + /** + * Changes the position of a FeedItem in the queue. + * + * @param context A context that is used for opening a database connection. + * @param from Source index. Must be in range 0..queue.size()-1. + * @param to Destination index. Must be in range 0..queue.size()-1. + * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to + * false if the caller wants to avoid unexpected updates of the GUI. + * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) + */ + public static Future<?> moveQueueItem(final Context context, final int from, + final int to, final boolean broadcastUpdate) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List<FeedItem> queue = DBReader + .getQueue(context, adapter); + + if (queue != null) { + if (from >= 0 && from < queue.size() && to >= 0 + && to < queue.size()) { + + final FeedItem item = queue.remove(from); + queue.add(to, item); + + adapter.setQueue(queue); + if (broadcastUpdate) { + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } + + } + } else { + Log.e(TAG, "moveQueueItem: Could not load queue"); + } + adapter.close(); + } + }); + } + + /** + * Sets the 'read'-attribute of a FeedItem to the specified value. + * + * @param context A context that is used for opening a database connection. + * @param item The FeedItem object + * @param read New value of the 'read'-attribute + * @param resetMediaPosition true if this method should also reset the position of the FeedItem's FeedMedia object. + * If the FeedItem has no FeedMedia object, this parameter will be ignored. + */ + public static Future<?> markItemRead(Context context, FeedItem item, boolean read, boolean resetMediaPosition) { + long mediaId = (item.hasMedia()) ? item.getMedia().getId() : 0; + return markItemRead(context, item.getId(), read, mediaId, resetMediaPosition); + } + + /** + * Sets the 'read'-attribute of a FeedItem to the specified value. + * + * @param context A context that is used for opening a database connection. + * @param itemId ID of the FeedItem + * @param read New value of the 'read'-attribute + */ + public static Future<?> markItemRead(final Context context, final long itemId, + final boolean read) { + return markItemRead(context, itemId, read, 0, false); + } + + private static Future<?> markItemRead(final Context context, final long itemId, + final boolean read, final long mediaId, + final boolean resetMediaPosition) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedItemRead(read, itemId, mediaId, + resetMediaPosition); + adapter.close(); + + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + } + }); + } + + /** + * Sets the 'read'-attribute of all FeedItems of a specific Feed to true. + * + * @param context A context that is used for opening a database connection. + * @param feedId ID of the Feed. + */ + public static Future<?> markFeedRead(final Context context, final long feedId) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor itemCursor = adapter.getAllItemsOfFeedCursor(feedId); + long[] itemIds = new long[itemCursor.getCount()]; + itemCursor.moveToFirst(); + for (int i = 0; i < itemIds.length; i++) { + itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX); + } + itemCursor.close(); + adapter.setFeedItemRead(true, itemIds); + adapter.close(); + + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + } + }); + + } + + /** + * Sets the 'read'-attribute of all FeedItems to true. + * + * @param context A context that is used for opening a database connection. + */ + public static Future<?> markAllItemsRead(final Context context) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor itemCursor = adapter.getUnreadItemsCursor(); + long[] itemIds = new long[itemCursor.getCount()]; + itemCursor.moveToFirst(); + for (int i = 0; i < itemIds.length; i++) { + itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX); + } + itemCursor.close(); + adapter.setFeedItemRead(true, itemIds); + adapter.close(); + + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + } + }); + + } + + static Future<?> addNewFeed(final Context context, final Feed feed) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + } + }); + } + + static Future<?> setCompleteFeed(final Context context, final Feed feed) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + } + }); + + } + + /** + * Saves a FeedMedia object in the database. This method will save all attributes of the FeedMedia object. The + * contents of FeedComponent-attributes (e.g. the FeedMedia's 'item'-attribute) will not be saved. + * + * @param context A context that is used for opening a database connection. + * @param media The FeedMedia object. + */ + public static Future<?> setFeedMedia(final Context context, + final FeedMedia media) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setMedia(media); + adapter.close(); + } + }); + } + + /** + * Saves only value of the 'position'-attribute of a FeedMedia object. + * + * @param context A context that is used for opening a database connection. + * @param media The FeedMedia object. + */ + public static Future<?> setFeedMediaPosition(final Context context, final FeedMedia media) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedMediaPosition(media); + adapter.close(); + } + }); + } + + /** + * Saves a FeedItem object in the database. This method will save all attributes of the FeedItem object including + * the content of FeedComponent-attributes. + * + * @param context A context that is used for opening a database connection. + * @param item The FeedItem object. + */ + public static Future<?> setFeedItem(final Context context, + final FeedItem item) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setSingleFeedItem(item); + adapter.close(); + } + }); + } + + /** + * Saves a FeedImage object in the database. This method will save all attributes of the FeedImage object. The + * contents of FeedComponent-attributes (e.g. the FeedImages's 'feed'-attribute) will not be saved. + * + * @param context A context that is used for opening a database connection. + * @param image The FeedImage object. + */ + public static Future<?> setFeedImage(final Context context, + final FeedImage image) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setImage(image); + adapter.close(); + } + }); + } + + private static boolean itemListContains(List<FeedItem> items, long itemId) { + for (FeedItem item : items) { + if (item.getId() == itemId) { + return true; + } + } + return false; + } +} diff --git a/src/de/danoeh/antennapod/storage/DownloadRequester.java b/src/de/danoeh/antennapod/storage/DownloadRequester.java index 29bd764dd..4e74d5e98 100644 --- a/src/de/danoeh/antennapod/storage/DownloadRequester.java +++ b/src/de/danoeh/antennapod/storage/DownloadRequester.java @@ -5,6 +5,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.io.FilenameUtils; +import org.apache.commons.lang3.StringUtils; import android.content.Context; import android.content.Intent; @@ -17,280 +18,283 @@ import de.danoeh.antennapod.feed.FeedFile; import de.danoeh.antennapod.feed.FeedImage; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.preferences.UserPreferences; +import de.danoeh.antennapod.service.download.DownloadRequest; import de.danoeh.antennapod.service.download.DownloadService; import de.danoeh.antennapod.util.FileNameGenerator; import de.danoeh.antennapod.util.URLChecker; public class DownloadRequester { - private static final String TAG = "DownloadRequester"; - - public static String IMAGE_DOWNLOADPATH = "images/"; - public static String FEED_DOWNLOADPATH = "cache/"; - public static String MEDIA_DOWNLOADPATH = "media/"; - - private static DownloadRequester downloader; - - Map<String, FeedFile> downloads; - - private DownloadRequester() { - downloads = new ConcurrentHashMap<String, FeedFile>(); - } - - public static DownloadRequester getInstance() { - if (downloader == null) { - downloader = new DownloadRequester(); - } - return downloader; - } - - private void download(Context context, FeedFile item, File dest, - boolean overwriteIfExists) { - if (!isDownloadingFile(item)) { - if (!isFilenameAvailable(dest.toString()) || dest.exists()) { - if (AppConfig.DEBUG) - Log.d(TAG, "Filename already used."); - if (isFilenameAvailable(dest.toString()) && overwriteIfExists) { - boolean result = dest.delete(); - if (AppConfig.DEBUG) - Log.d(TAG, "Deleting file. Result: " + result); - } else { - // find different name - File newDest = null; - for (int i = 1; i < Integer.MAX_VALUE; i++) { - String newName = FilenameUtils.getBaseName(dest - .getName()) - + "-" - + i - + "." - + FilenameUtils.getExtension(dest.getName()); - if (AppConfig.DEBUG) - Log.d(TAG, "Testing filename " + newName); - newDest = new File(dest.getParent(), newName); - if (!newDest.exists() - && isFilenameAvailable(newDest.toString())) { - if (AppConfig.DEBUG) - Log.d(TAG, "File doesn't exist yet. Using " - + newName); - break; - } - } - if (newDest != null) { - dest = newDest; - } - } - } - if (AppConfig.DEBUG) - Log.d(TAG, - "Requesting download of url " + item.getDownload_url()); - item.setDownload_url(URLChecker.prepareURL(item.getDownload_url())); - item.setFile_url(dest.toString()); - downloads.put(item.getDownload_url(), item); - - DownloadService.Request request = new DownloadService.Request( - item.getFile_url(), item.getDownload_url()); - - if (!DownloadService.isRunning) { - Intent launchIntent = new Intent(context, DownloadService.class); - launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request); - context.startService(launchIntent); - } else { - Intent queueIntent = new Intent( - DownloadService.ACTION_ENQUEUE_DOWNLOAD); - queueIntent.putExtra(DownloadService.EXTRA_REQUEST, request); - context.sendBroadcast(queueIntent); - } - EventDistributor.getInstance().sendDownloadQueuedBroadcast(); - } else { - Log.e(TAG, "URL " + item.getDownload_url() - + " is already being downloaded"); - } - } - - /** - * Returns true if a filename is available and false if it has already been - * taken by another requested download. - */ - private boolean isFilenameAvailable(String path) { - for (String key : downloads.keySet()) { - FeedFile f = downloads.get(key); - if (f.getFile_url() != null && f.getFile_url().equals(path)) { - if (AppConfig.DEBUG) - Log.d(TAG, path - + " is already used by another requested download"); - return false; - } - } - if (AppConfig.DEBUG) - Log.d(TAG, path + " is available as a download destination"); - return true; - } - - public void downloadFeed(Context context, Feed feed) - throws DownloadRequestException { - if (feedFileValid(feed)) { - download(context, feed, new File(getFeedfilePath(context), - getFeedfileName(feed)), true); - } - } - - public void downloadImage(Context context, FeedImage image) - throws DownloadRequestException { - if (feedFileValid(image)) { - download(context, image, new File(getImagefilePath(context), - getImagefileName(image)), true); - } - } - - public void downloadMedia(Context context, FeedMedia feedmedia) - throws DownloadRequestException { - if (feedFileValid(feedmedia)) { - download(context, feedmedia, - new File(getMediafilePath(context, feedmedia), - getMediafilename(feedmedia)), false); - } - } - - /** - * Throws a DownloadRequestException if the feedfile or the download url of - * the feedfile is null. - * - * @throws DownloadRequestException - */ - private boolean feedFileValid(FeedFile f) throws DownloadRequestException { - if (f == null) { - throw new DownloadRequestException("Feedfile was null"); - } else if (f.getDownload_url() == null) { - throw new DownloadRequestException("File has no download URL"); - } else { - return true; - } - } - - /** - * Cancels a running download. - * */ - public void cancelDownload(final Context context, final FeedFile f) { - cancelDownload(context, f.getDownload_url()); - } - - /** - * Cancels a running download. - * */ - public void cancelDownload(final Context context, final String downloadUrl) { - if (AppConfig.DEBUG) - Log.d(TAG, "Cancelling download with url " + downloadUrl); - Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD); - cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, downloadUrl); - context.sendBroadcast(cancelIntent); - } - - /** Cancels all running downloads */ - public void cancelAllDownloads(Context context) { - if (AppConfig.DEBUG) - Log.d(TAG, "Cancelling all running downloads"); - context.sendBroadcast(new Intent( - DownloadService.ACTION_CANCEL_ALL_DOWNLOADS)); - } - - /** Returns true if there is at least one Feed in the downloads queue. */ - public boolean isDownloadingFeeds() { - for (FeedFile f : downloads.values()) { - if (f.getClass() == Feed.class) { - return true; - } - } - return false; - } - - /** Checks if feedfile is in the downloads list */ - public boolean isDownloadingFile(FeedFile item) { - if (item.getDownload_url() != null) { - return downloads.containsKey(item.getDownload_url()); - } - return false; - } - - public FeedFile getDownload(String downloadUrl) { - return downloads.get(downloadUrl); - } - - /** Checks if feedfile with the given download url is in the downloads list */ - public boolean isDownloadingFile(String downloadUrl) { - return downloads.get(downloadUrl) != null; - } - - public boolean hasNoDownloads() { - return downloads.isEmpty(); - } - - public FeedFile getDownloadAt(int index) { - return downloads.get(index); - } - - /** Remove an object from the downloads-list of the requester. */ - public void removeDownload(FeedFile f) { - if (downloads.remove(f.getDownload_url()) == null) { - Log.e(TAG, - "Could not remove object with url " + f.getDownload_url()); - } - } - - /** Get the number of uncompleted Downloads */ - public int getNumberOfDownloads() { - return downloads.size(); - } - - public String getFeedfilePath(Context context) - throws DownloadRequestException { - return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH) - .toString() + "/"; - } - - public String getFeedfileName(Feed feed) { - String filename = feed.getDownload_url(); - if (feed.getTitle() != null && !feed.getTitle().isEmpty()) { - filename = feed.getTitle(); - } - return "feed-" + FileNameGenerator.generateFileName(filename); - } - - public String getImagefilePath(Context context) - throws DownloadRequestException { - return getExternalFilesDirOrThrowException(context, IMAGE_DOWNLOADPATH) - .toString() + "/"; - } - - public String getImagefileName(FeedImage image) { - String filename = image.getDownload_url(); - if (image.getFeed() != null && image.getFeed().getTitle() != null) { - filename = image.getFeed().getTitle(); - } - return "image-" + FileNameGenerator.generateFileName(filename); - } - - public String getMediafilePath(Context context, FeedMedia media) - throws DownloadRequestException { - File externalStorage = getExternalFilesDirOrThrowException( - context, - MEDIA_DOWNLOADPATH - + FileNameGenerator.generateFileName(media.getItem() - .getFeed().getTitle()) + "/"); - return externalStorage.toString(); - } - - private File getExternalFilesDirOrThrowException(Context context, - String type) throws DownloadRequestException { - File result = UserPreferences.getDataFolder(context, type); - if (result == null) { - throw new DownloadRequestException( - "Failed to access external storage"); - } - return result; - } - - public String getMediafilename(FeedMedia media) { - return URLUtil.guessFileName(media.getDownload_url(), null, - media.getMime_type()); - } + private static final String TAG = "DownloadRequester"; + + public static String IMAGE_DOWNLOADPATH = "images/"; + public static String FEED_DOWNLOADPATH = "cache/"; + public static String MEDIA_DOWNLOADPATH = "media/"; + + private static DownloadRequester downloader; + + Map<String, DownloadRequest> downloads; + + private DownloadRequester() { + downloads = new ConcurrentHashMap<String, DownloadRequest>(); + } + + public static DownloadRequester getInstance() { + if (downloader == null) { + downloader = new DownloadRequester(); + } + return downloader; + } + + private void download(Context context, FeedFile item, File dest, + boolean overwriteIfExists) { + if (!isDownloadingFile(item)) { + if (!isFilenameAvailable(dest.toString()) || dest.exists()) { + if (AppConfig.DEBUG) + Log.d(TAG, "Filename already used."); + if (isFilenameAvailable(dest.toString()) && overwriteIfExists) { + boolean result = dest.delete(); + if (AppConfig.DEBUG) + Log.d(TAG, "Deleting file. Result: " + result); + } else { + // find different name + File newDest = null; + for (int i = 1; i < Integer.MAX_VALUE; i++) { + String newName = FilenameUtils.getBaseName(dest + .getName()) + + "-" + + i + + "." + + FilenameUtils.getExtension(dest.getName()); + if (AppConfig.DEBUG) + Log.d(TAG, "Testing filename " + newName); + newDest = new File(dest.getParent(), newName); + if (!newDest.exists() + && isFilenameAvailable(newDest.toString())) { + if (AppConfig.DEBUG) + Log.d(TAG, "File doesn't exist yet. Using " + + newName); + break; + } + } + if (newDest != null) { + dest = newDest; + } + } + } + if (AppConfig.DEBUG) + Log.d(TAG, + "Requesting download of url " + item.getDownload_url()); + item.setDownload_url(URLChecker.prepareURL(item.getDownload_url())); + + DownloadRequest request = new DownloadRequest(dest.toString(), + item.getDownload_url(), item.getHumanReadableIdentifier(), + item.getId(), item.getTypeAsInt()); + + downloads.put(request.getSource(), request); + + Intent launchIntent = new Intent(context, DownloadService.class); + launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request); + context.startService(launchIntent); + EventDistributor.getInstance().sendDownloadQueuedBroadcast(); + } else { + Log.e(TAG, "URL " + item.getDownload_url() + + " is already being downloaded"); + } + } + + /** + * Returns true if a filename is available and false if it has already been + * taken by another requested download. + */ + private boolean isFilenameAvailable(String path) { + for (String key : downloads.keySet()) { + DownloadRequest r = downloads.get(key); + if (StringUtils.equals(r.getDestination(), path)) { + if (AppConfig.DEBUG) + Log.d(TAG, path + + " is already used by another requested download"); + return false; + } + } + if (AppConfig.DEBUG) + Log.d(TAG, path + " is available as a download destination"); + return true; + } + + public void downloadFeed(Context context, Feed feed) + throws DownloadRequestException { + if (feedFileValid(feed)) { + download(context, feed, new File(getFeedfilePath(context), + getFeedfileName(feed)), true); + } + } + + public void downloadImage(Context context, FeedImage image) + throws DownloadRequestException { + if (feedFileValid(image)) { + download(context, image, new File(getImagefilePath(context), + getImagefileName(image)), true); + } + } + + public void downloadMedia(Context context, FeedMedia feedmedia) + throws DownloadRequestException { + if (feedFileValid(feedmedia)) { + download(context, feedmedia, + new File(getMediafilePath(context, feedmedia), + getMediafilename(feedmedia)), false); + } + } + + /** + * Throws a DownloadRequestException if the feedfile or the download url of + * the feedfile is null. + * + * @throws DownloadRequestException + */ + private boolean feedFileValid(FeedFile f) throws DownloadRequestException { + if (f == null) { + throw new DownloadRequestException("Feedfile was null"); + } else if (f.getDownload_url() == null) { + throw new DownloadRequestException("File has no download URL"); + } else { + return true; + } + } + + /** + * Cancels a running download. + */ + public void cancelDownload(final Context context, final FeedFile f) { + cancelDownload(context, f.getDownload_url()); + } + + /** + * Cancels a running download. + */ + public void cancelDownload(final Context context, final String downloadUrl) { + if (AppConfig.DEBUG) + Log.d(TAG, "Cancelling download with url " + downloadUrl); + Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD); + cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, downloadUrl); + context.sendBroadcast(cancelIntent); + } + + /** + * Cancels all running downloads + */ + public void cancelAllDownloads(Context context) { + if (AppConfig.DEBUG) + Log.d(TAG, "Cancelling all running downloads"); + context.sendBroadcast(new Intent( + DownloadService.ACTION_CANCEL_ALL_DOWNLOADS)); + } + + /** + * Returns true if there is at least one Feed in the downloads queue. + */ + public boolean isDownloadingFeeds() { + for (DownloadRequest r : downloads.values()) { + if (r.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + return true; + } + } + return false; + } + + /** + * Checks if feedfile is in the downloads list + */ + public boolean isDownloadingFile(FeedFile item) { + if (item.getDownload_url() != null) { + return downloads.containsKey(item.getDownload_url()); + } + return false; + } + + public DownloadRequest getDownload(String downloadUrl) { + return downloads.get(downloadUrl); + } + + /** + * Checks if feedfile with the given download url is in the downloads list + */ + public boolean isDownloadingFile(String downloadUrl) { + return downloads.get(downloadUrl) != null; + } + + public boolean hasNoDownloads() { + return downloads.isEmpty(); + } + + /** + * Remove an object from the downloads-list of the requester. + */ + public void removeDownload(DownloadRequest r) { + if (downloads.remove(r.getSource()) == null) { + Log.e(TAG, + "Could not remove object with url " + r.getSource()); + } + } + + /** + * Get the number of uncompleted Downloads + */ + public int getNumberOfDownloads() { + return downloads.size(); + } + + public String getFeedfilePath(Context context) + throws DownloadRequestException { + return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH) + .toString() + "/"; + } + + public String getFeedfileName(Feed feed) { + String filename = feed.getDownload_url(); + if (feed.getTitle() != null && !feed.getTitle().isEmpty()) { + filename = feed.getTitle(); + } + return "feed-" + FileNameGenerator.generateFileName(filename); + } + + public String getImagefilePath(Context context) + throws DownloadRequestException { + return getExternalFilesDirOrThrowException(context, IMAGE_DOWNLOADPATH) + .toString() + "/"; + } + + public String getImagefileName(FeedImage image) { + String filename = image.getDownload_url(); + if (image.getFeed() != null && image.getFeed().getTitle() != null) { + filename = image.getFeed().getTitle(); + } + return "image-" + FileNameGenerator.generateFileName(filename); + } + + public String getMediafilePath(Context context, FeedMedia media) + throws DownloadRequestException { + File externalStorage = getExternalFilesDirOrThrowException( + context, + MEDIA_DOWNLOADPATH + + FileNameGenerator.generateFileName(media.getItem() + .getFeed().getTitle()) + "/"); + return externalStorage.toString(); + } + + private File getExternalFilesDirOrThrowException(Context context, + String type) throws DownloadRequestException { + File result = UserPreferences.getDataFolder(context, type); + if (result == null) { + throw new DownloadRequestException( + "Failed to access external storage"); + } + return result; + } + + public String getMediafilename(FeedMedia media) { + return URLUtil.guessFileName(media.getDownload_url(), null, + media.getMime_type()); + } } diff --git a/src/de/danoeh/antennapod/storage/FeedItemStatistics.java b/src/de/danoeh/antennapod/storage/FeedItemStatistics.java new file mode 100644 index 000000000..17e838761 --- /dev/null +++ b/src/de/danoeh/antennapod/storage/FeedItemStatistics.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.storage; + +import java.util.Date; + +/** + * Contains information about a feed's items. + */ +public class FeedItemStatistics { + private long feedID; + private int numberOfItems; + private int numberOfNewItems; + private int numberOfInProgressItems; + private Date lastUpdate; + + public FeedItemStatistics(long feedID, int numberOfItems, int numberOfNewItems, int numberOfInProgressItems, Date lastUpdate) { + this.feedID = feedID; + this.numberOfItems = numberOfItems; + this.numberOfNewItems = numberOfNewItems; + this.numberOfInProgressItems = numberOfInProgressItems; + this.lastUpdate = lastUpdate; + } + + public long getFeedID() { + return feedID; + } + + public int getNumberOfItems() { + return numberOfItems; + } + + public int getNumberOfNewItems() { + return numberOfNewItems; + } + + public int getNumberOfInProgressItems() { + return numberOfInProgressItems; + } + + public Date getLastUpdate() { + return lastUpdate; + } +} diff --git a/src/de/danoeh/antennapod/storage/FeedSearcher.java b/src/de/danoeh/antennapod/storage/FeedSearcher.java new file mode 100644 index 000000000..e7aa93f83 --- /dev/null +++ b/src/de/danoeh/antennapod/storage/FeedSearcher.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.storage; + +import android.content.Context; +import de.danoeh.antennapod.R; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.feed.SearchResult; +import de.danoeh.antennapod.util.comparator.SearchResultValueComparator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +/** + * Performs search on Feeds and FeedItems + */ +public class FeedSearcher { + private static final String TAG = "FeedSearcher"; + + + /** + * Performs a search in all feeds or one specific feed. + */ + public static List<SearchResult> performSearch(final Context context, + final String query, final long selectedFeed) { + final int values[] = {0, 0, 1, 2}; + final String[] subtitles = {context.getString(R.string.found_in_shownotes_label), + context.getString(R.string.found_in_shownotes_label), + context.getString(R.string.found_in_chapters_label), + context.getString(R.string.found_in_title_label)}; + + List<SearchResult> result = new ArrayList<SearchResult>(); + + FutureTask<List<FeedItem>>[] tasks = new FutureTask[4]; + (tasks[0] = DBTasks.searchFeedItemContentEncoded(context, selectedFeed, query)).run(); + (tasks[1] = DBTasks.searchFeedItemDescription(context, selectedFeed, query)).run(); + (tasks[2] = DBTasks.searchFeedItemChapters(context, selectedFeed, query)).run(); + (tasks[3] = DBTasks.searchFeedItemTitle(context, selectedFeed, query)).run(); + try { + for (int i = 0; i < tasks.length; i++) { + FutureTask task = tasks[i]; + List<FeedItem> items = (List<FeedItem>) task.get(); + for (FeedItem item : items) { + result.add(new SearchResult(item, values[i], subtitles[i])); + } + + } + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + Collections.sort(result, new SearchResultValueComparator()); + return result; + } +} diff --git a/src/de/danoeh/antennapod/storage/PodDBAdapter.java b/src/de/danoeh/antennapod/storage/PodDBAdapter.java index 420264840..edcbe1cf4 100644 --- a/src/de/danoeh/antennapod/storage/PodDBAdapter.java +++ b/src/de/danoeh/antennapod/storage/PodDBAdapter.java @@ -10,776 +10,1075 @@ import android.database.DatabaseUtils; import android.database.MergeCursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; import android.util.Log; import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.asynctask.DownloadStatus; import de.danoeh.antennapod.feed.Chapter; 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.service.download.DownloadStatus; + +// TODO Remove media column from feeditem table /** * Implements methods for accessing the database - * */ + */ public class PodDBAdapter { - private static final String TAG = "PodDBAdapter"; - private static final int DATABASE_VERSION = 8; - private static final String DATABASE_NAME = "Antennapod.db"; - - /** Maximum number of arguments for IN-operator. */ - public static final int IN_OPERATOR_MAXIMUM = 800; - - // ----------- Column indices - // ----------- General indices - public static final int KEY_ID_INDEX = 0; - public static final int KEY_TITLE_INDEX = 1; - public static final int KEY_FILE_URL_INDEX = 2; - public static final int KEY_DOWNLOAD_URL_INDEX = 3; - public static final int KEY_DOWNLOADED_INDEX = 4; - public static final int KEY_LINK_INDEX = 5; - public static final int KEY_DESCRIPTION_INDEX = 6; - public static final int KEY_PAYMENT_LINK_INDEX = 7; - // ----------- Feed indices - public static final int KEY_LAST_UPDATE_INDEX = 8; - public static final int KEY_LANGUAGE_INDEX = 9; - public static final int KEY_AUTHOR_INDEX = 10; - public static final int KEY_IMAGE_INDEX = 11; - public static final int KEY_TYPE_INDEX = 12; - public static final int KEY_FEED_IDENTIFIER_INDEX = 13; - // ----------- FeedItem indices - public static final int KEY_CONTENT_ENCODED_INDEX = 2; - public static final int KEY_PUBDATE_INDEX = 3; - public static final int KEY_READ_INDEX = 4; - public static final int KEY_MEDIA_INDEX = 8; - public static final int KEY_FEED_INDEX = 9; - public static final int KEY_HAS_SIMPLECHAPTERS_INDEX = 10; - public static final int KEY_ITEM_IDENTIFIER_INDEX = 11; - // ---------- FeedMedia indices - public static final int KEY_DURATION_INDEX = 1; - public static final int KEY_POSITION_INDEX = 5; - public static final int KEY_SIZE_INDEX = 6; - public static final int KEY_MIME_TYPE_INDEX = 7; - public static final int KEY_PLAYBACK_COMPLETION_DATE_INDEX = 8; - // --------- Download log indices - public static final int KEY_FEEDFILE_INDEX = 1; - public static final int KEY_FEEDFILETYPE_INDEX = 2; - public static final int KEY_REASON_INDEX = 3; - public static final int KEY_SUCCESSFUL_INDEX = 4; - public static final int KEY_COMPLETION_DATE_INDEX = 5; - public static final int KEY_REASON_DETAILED_INDEX = 6; - public static final int KEY_DOWNLOADSTATUS_TITLE_INDEX = 7; - // --------- Queue indices - public static final int KEY_FEEDITEM_INDEX = 1; - public static final int KEY_QUEUE_FEED_INDEX = 2; - // --------- Chapters indices - public static final int KEY_CHAPTER_START_INDEX = 2; - public static final int KEY_CHAPTER_FEEDITEM_INDEX = 3; - public static final int KEY_CHAPTER_LINK_INDEX = 4; - public static final int KEY_CHAPTER_TYPE_INDEX = 5; - - // 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_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"; - public static final String KEY_HAS_CHAPTERS = "has_simple_chapters"; - public static final String KEY_TYPE = "type"; - public static final String KEY_ITEM_IDENTIFIER = "item_identifier"; - public static final String KEY_FEED_IDENTIFIER = "feed_identifier"; - public static final String KEY_REASON_DETAILED = "reason_detailed"; - public static final String KEY_DOWNLOADSTATUS_TITLE = "title"; - public static final String KEY_CHAPTER_TYPE = "type"; - public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date"; - - // 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_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_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT," - + KEY_DOWNLOADED + " INTEGER," + KEY_LINK + " TEXT," - + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," - + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR - + " TEXT," + KEY_IMAGE + " INTEGER," + KEY_TYPE + " TEXT," - + KEY_FEED_IDENTIFIER + " TEXT)";; - - private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " - + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE - + " TEXT," + KEY_CONTENT_ENCODED + " TEXT," + KEY_PUBDATE - + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT," - + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," - + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," - + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " 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_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL - + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_POSITION - + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT," - + KEY_PLAYBACK_COMPLETION_DATE + " 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," + KEY_REASON_DETAILED + " TEXT," - + KEY_DOWNLOADSTATUS_TITLE + " TEXT)"; - - 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," - + KEY_LINK + " TEXT," + KEY_CHAPTER_TYPE + " INTEGER)"; - - private SQLiteDatabase db; - private final Context context; - private PodDBHelper helper; - - /** - * Select all columns from the feeditems-table except description and - * content-encoded. - */ - private static final String[] SEL_FI_SMALL = { KEY_ID, KEY_TITLE, - KEY_PUBDATE, KEY_READ, KEY_LINK, KEY_PAYMENT_LINK, KEY_MEDIA, - KEY_FEED, KEY_HAS_CHAPTERS, KEY_ITEM_IDENTIFIER }; - - // column indices for SEL_FI_SMALL - - public static final int IDX_FI_SMALL_ID = 0; - public static final int IDX_FI_SMALL_TITLE = 1; - public static final int IDX_FI_SMALL_PUBDATE = 2; - public static final int IDX_FI_SMALL_READ = 3; - public static final int IDX_FI_SMALL_LINK = 4; - public static final int IDX_FI_SMALL_PAYMENT_LINK = 5; - public static final int IDX_FI_SMALL_MEDIA = 6; - public static final int IDX_FI_SMALL_FEED = 7; - public static final int IDX_FI_SMALL_HAS_CHAPTERS = 8; - public static final int IDX_FI_SMALL_ITEM_IDENTIFIER = 9; - - /** Select id, description and content-encoded column from feeditems. */ - public static final String[] SEL_FI_EXTRA = { KEY_ID, KEY_DESCRIPTION, - KEY_CONTENT_ENCODED, KEY_FEED }; - - // column indices for SEL_FI_EXTRA - - public static final int IDX_FI_EXTRA_ID = 0; - public static final int IDX_FI_EXTRA_DESCRIPTION = 1; - public static final int IDX_FI_EXTRA_CONTENT_ENCODED = 2; - public static final int IDX_FI_EXTRA_FEED = 3; - - 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()) { - if (AppConfig.DEBUG) - Log.d(TAG, "Opening DB"); - try { - db = helper.getWritableDatabase(); - } catch (SQLException ex) { - ex.printStackTrace(); - db = helper.getReadableDatabase(); - } - } - return this; - } - - public void close() { - if (AppConfig.DEBUG) - 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()); - } - - 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()); - values.put(KEY_TYPE, feed.getType()); - values.put(KEY_FEED_IDENTIFIER, feed.getFeedIdentifier()); - if (feed.getId() == 0) { - // Create new entry - if (AppConfig.DEBUG) - Log.d(this.toString(), "Inserting new Feed into db"); - feed.setId(db.insert(TABLE_NAME_FEEDS, null, values)); - } else { - if (AppConfig.DEBUG) - 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 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.getPlaybackCompletionDate() != null) { - values.put(KEY_PLAYBACK_COMPLETION_DATE, media - .getPlaybackCompletionDate().getTime()); - } else { - values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); - } - 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.getItemsArray()) { - 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()); - if (item.getDescription() != null) { - values.put(KEY_DESCRIPTION, item.getDescription()); - } - if (item.getContentEncoded() != null) { - 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()); - values.put(KEY_HAS_CHAPTERS, item.getChapters() != null); - values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); - if (item.getId() == 0) { - item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values)); - } else { - db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", - new String[] { String.valueOf(item.getId()) }); - } - if (item.getChapters() != null) { - setChapters(item); - } - return item.getId(); - } - - public void setChapters(FeedItem item) { - ContentValues values = new ContentValues(); - for (Chapter chapter : item.getChapters()) { - values.put(KEY_TITLE, chapter.getTitle()); - values.put(KEY_START, chapter.getStart()); - values.put(KEY_FEEDITEM, item.getId()); - values.put(KEY_LINK, chapter.getLink()); - values.put(KEY_CHAPTER_TYPE, chapter.getChapterType()); - 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) { - ContentValues values = new ContentValues(); - if (status.getFeedFile() != null) { - values.put(KEY_FEEDFILE, status.getFeedFile().getId()); - if (status.getFeedFile().getClass() == Feed.class) { - values.put(KEY_FEEDFILETYPE, Feed.FEEDFILETYPE_FEED); - } else if (status.getFeedFile().getClass() == FeedImage.class) { - values.put(KEY_FEEDFILETYPE, FeedImage.FEEDFILETYPE_FEEDIMAGE); - } else if (status.getFeedFile().getClass() == FeedMedia.class) { - values.put(KEY_FEEDFILETYPE, FeedMedia.FEEDFILETYPE_FEEDMEDIA); - } - } - values.put(KEY_REASON, status.getReason()); - values.put(KEY_SUCCESSFUL, status.isSuccessful()); - values.put(KEY_COMPLETION_DATE, status.getCompletionDate().getTime()); - values.put(KEY_REASON_DETAILED, status.getReasonDetailed()); - values.put(KEY_DOWNLOADSTATUS_TITLE, status.getTitle()); - 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(List<FeedItem> queue) { - ContentValues values = new ContentValues(); - db.beginTransaction(); - 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); - } - db.setTransactionSuccessful(); - db.endTransaction(); - } - - public void removeFeedMedia(FeedMedia media) { - db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?", - new String[] { String.valueOf(media.getId()) }); - } - - public void removeChaptersOfItem(FeedItem item) { - db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + "=?", - new String[] { String.valueOf(item.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()); - } - if (item.getChapters() != null) { - removeChaptersOfItem(item); - } - 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) { - db.beginTransaction(); - if (feed.getImage() != null) { - removeFeedImage(feed.getImage()); - } - for (FeedItem item : feed.getItemsArray()) { - removeFeedItem(item); - } - db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?", - new String[] { String.valueOf(feed.getId()) }); - db.setTransactionSuccessful(); - db.endTransaction(); - } - - public void removeDownloadStatus(DownloadStatus remove) { - db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_ID + "=?", - new String[] { String.valueOf(remove.getId()) }); - } - - /** - * 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. Uses SEL_FI_SMALL - * - * @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, SEL_FI_SMALL, KEY_FEED - + "=?", new String[] { String.valueOf(feed.getId()) }, null, - null, null); - return c; - } - - /** Return a cursor with the SEL_FI_EXTRA selection of a single feeditem. */ - public final Cursor getExtraInformationOfItem(final FeedItem item) { - open(); - Cursor c = db - .query(TABLE_NAME_FEED_ITEMS, SEL_FI_EXTRA, KEY_ID + "=?", - new String[] { String.valueOf(item.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_FEEDITEM - + "=?", 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; - } - - public final Cursor getFeedMediaCursor(String... mediaIds) { - int length = mediaIds.length; - if (length > IN_OPERATOR_MAXIMUM) { - Log.w(TAG, "Length of id array is larger than " - + IN_OPERATOR_MAXIMUM + ". Creating multiple cursors"); - int numCursors = (int) (((double) length) / (IN_OPERATOR_MAXIMUM)) + 1; - Cursor[] cursors = new Cursor[numCursors]; - for (int i = 0; i < numCursors; i++) { - int neededLength = 0; - String[] parts = null; - final int elementsLeft = length - i * IN_OPERATOR_MAXIMUM; - - if (elementsLeft >= IN_OPERATOR_MAXIMUM) { - neededLength = IN_OPERATOR_MAXIMUM; - parts = Arrays.copyOfRange(mediaIds, i - * IN_OPERATOR_MAXIMUM, (i + 1) - * IN_OPERATOR_MAXIMUM); - } else { - neededLength = elementsLeft; - parts = Arrays.copyOfRange(mediaIds, i - * IN_OPERATOR_MAXIMUM, (i * IN_OPERATOR_MAXIMUM) - + neededLength); - } - - cursors[i] = db.rawQuery("SELECT * FROM " - + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_ID + " IN " - + buildInOperator(neededLength), parts); - } - return new MergeCursor(cursors); - } else { - return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + " IN " - + buildInOperator(length), mediaIds, null, null, null); - } - } - - /** Builds an IN-operator argument depending on the number of items. */ - private String buildInOperator(int size) { - StringBuffer buffer = new StringBuffer("("); - for (int i = 0; i <= size; i++) { - buffer.append("?,"); - } - buffer.append("?)"); - return buffer.toString(); - } - - /** - * 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; - } - - /** - * Uses DatabaseUtils to escape a search query and removes ' at the - * beginning and the end of the string returned by the escape method. - */ - private String prepareSearchQuery(String query) { - StringBuilder builder = new StringBuilder(); - DatabaseUtils.appendEscapedSQLString(builder, query); - builder.deleteCharAt(0); - builder.deleteCharAt(builder.length() - 1); - return builder.toString(); - } - - /** - * Searches for the given query in the description of all items or the items - * of a specified feed. - * - * @return A cursor with all search results in SEL_FI_EXTRA selection. - * */ - public Cursor searchItemDescriptions(Feed feed, String query) { - if (feed != null) { - // search items in specific feed - return db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_EXTRA, KEY_FEED - + "=? AND " + KEY_DESCRIPTION + " LIKE '%" - + prepareSearchQuery(query) + "%'", - new String[] { String.valueOf(feed.getId()) }, null, null, - null); - } else { - // search through all items - return db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_EXTRA, - KEY_DESCRIPTION + " LIKE '%" + prepareSearchQuery(query) - + "%'", null, null, null, null); - } - } - - /** - * Searches for the given query in the content-encoded field of all items or - * the items of a specified feed. - * - * @return A cursor with all search results in SEL_FI_EXTRA selection. - * */ - public Cursor searchItemContentEncoded(Feed feed, String query) { - if (feed != null) { - // search items in specific feed - return db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_EXTRA, KEY_FEED - + "=? AND " + KEY_CONTENT_ENCODED + " LIKE '%" - + prepareSearchQuery(query) + "%'", - new String[] { String.valueOf(feed.getId()) }, null, null, - null); - } else { - // search through all items - return db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_EXTRA, - KEY_CONTENT_ENCODED + " LIKE '%" - + prepareSearchQuery(query) + "%'", null, null, - null, null); - } - } - - /** 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_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 + "."); - if (oldVersion <= 1) { - db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + " ADD COLUMN " - + KEY_TYPE + " TEXT"); - } - if (oldVersion <= 2) { - db.execSQL("ALTER TABLE " + TABLE_NAME_SIMPLECHAPTERS - + " ADD COLUMN " + KEY_LINK + " TEXT"); - } - if (oldVersion <= 3) { - db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS - + " ADD COLUMN " + KEY_ITEM_IDENTIFIER + " TEXT"); - } - if (oldVersion <= 4) { - db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + " ADD COLUMN " - + KEY_FEED_IDENTIFIER + " TEXT"); - } - if (oldVersion <= 5) { - db.execSQL("ALTER TABLE " + TABLE_NAME_DOWNLOAD_LOG - + " ADD COLUMN " + KEY_REASON_DETAILED + " TEXT"); - db.execSQL("ALTER TABLE " + TABLE_NAME_DOWNLOAD_LOG - + " ADD COLUMN " + KEY_DOWNLOADSTATUS_TITLE + " TEXT"); - } - if (oldVersion <= 6) { - db.execSQL("ALTER TABLE " + TABLE_NAME_SIMPLECHAPTERS - + " ADD COLUMN " + KEY_CHAPTER_TYPE + " INTEGER"); - } - if (oldVersion <= 7) { - db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA - + " ADD COLUMN " + KEY_PLAYBACK_COMPLETION_DATE - + " INTEGER"); - } - } - } + private static final String TAG = "PodDBAdapter"; + private static final int DATABASE_VERSION = 9; + public static final String DATABASE_NAME = "Antennapod.db"; + + /** + * Maximum number of arguments for IN-operator. + */ + public static final int IN_OPERATOR_MAXIMUM = 800; + + /** + * Maximum number of entries per search request. + */ + public static final int SEARCH_LIMIT = 30; + + // ----------- Column indices + // ----------- General indices + public static final int KEY_ID_INDEX = 0; + public static final int KEY_TITLE_INDEX = 1; + public static final int KEY_FILE_URL_INDEX = 2; + public static final int KEY_DOWNLOAD_URL_INDEX = 3; + public static final int KEY_DOWNLOADED_INDEX = 4; + public static final int KEY_LINK_INDEX = 5; + public static final int KEY_DESCRIPTION_INDEX = 6; + public static final int KEY_PAYMENT_LINK_INDEX = 7; + // ----------- Feed indices + public static final int KEY_LAST_UPDATE_INDEX = 8; + public static final int KEY_LANGUAGE_INDEX = 9; + public static final int KEY_AUTHOR_INDEX = 10; + public static final int KEY_IMAGE_INDEX = 11; + public static final int KEY_TYPE_INDEX = 12; + public static final int KEY_FEED_IDENTIFIER_INDEX = 13; + // ----------- FeedItem indices + public static final int KEY_CONTENT_ENCODED_INDEX = 2; + public static final int KEY_PUBDATE_INDEX = 3; + public static final int KEY_READ_INDEX = 4; + public static final int KEY_MEDIA_INDEX = 8; + public static final int KEY_FEED_INDEX = 9; + public static final int KEY_HAS_SIMPLECHAPTERS_INDEX = 10; + public static final int KEY_ITEM_IDENTIFIER_INDEX = 11; + // ---------- FeedMedia indices + public static final int KEY_DURATION_INDEX = 1; + public static final int KEY_POSITION_INDEX = 5; + public static final int KEY_SIZE_INDEX = 6; + public static final int KEY_MIME_TYPE_INDEX = 7; + public static final int KEY_PLAYBACK_COMPLETION_DATE_INDEX = 8; + public static final int KEY_MEDIA_FEEDITEM_INDEX = 9; + // --------- Download log indices + public static final int KEY_FEEDFILE_INDEX = 1; + public static final int KEY_FEEDFILETYPE_INDEX = 2; + public static final int KEY_REASON_INDEX = 3; + public static final int KEY_SUCCESSFUL_INDEX = 4; + public static final int KEY_COMPLETION_DATE_INDEX = 5; + public static final int KEY_REASON_DETAILED_INDEX = 6; + public static final int KEY_DOWNLOADSTATUS_TITLE_INDEX = 7; + // --------- Queue indices + public static final int KEY_FEEDITEM_INDEX = 1; + public static final int KEY_QUEUE_FEED_INDEX = 2; + // --------- Chapters indices + public static final int KEY_CHAPTER_START_INDEX = 2; + public static final int KEY_CHAPTER_FEEDITEM_INDEX = 3; + public static final int KEY_CHAPTER_LINK_INDEX = 4; + public static final int KEY_CHAPTER_TYPE_INDEX = 5; + + // 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_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"; + public static final String KEY_HAS_CHAPTERS = "has_simple_chapters"; + public static final String KEY_TYPE = "type"; + public static final String KEY_ITEM_IDENTIFIER = "item_identifier"; + public static final String KEY_FEED_IDENTIFIER = "feed_identifier"; + public static final String KEY_REASON_DETAILED = "reason_detailed"; + public static final String KEY_DOWNLOADSTATUS_TITLE = "title"; + public static final String KEY_CHAPTER_TYPE = "type"; + public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date"; + + // 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_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_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT," + + KEY_DOWNLOADED + " INTEGER," + KEY_LINK + " TEXT," + + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR + + " TEXT," + KEY_IMAGE + " INTEGER," + KEY_TYPE + " TEXT," + + KEY_FEED_IDENTIFIER + " TEXT)"; + ; + + private static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + + " TEXT," + KEY_CONTENT_ENCODED + " TEXT," + KEY_PUBDATE + + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT," + + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," + + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " 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_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_POSITION + + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT," + + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER," + + KEY_FEEDITEM + " 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," + KEY_REASON_DETAILED + " TEXT," + + KEY_DOWNLOADSTATUS_TITLE + " TEXT)"; + + 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," + + KEY_LINK + " TEXT," + KEY_CHAPTER_TYPE + " INTEGER)"; + + private SQLiteDatabase db; + private final Context context; + private PodDBHelper helper; + + /** + * Select all columns from the feeditems-table except description and + * content-encoded. + */ + private static final String[] SEL_FI_SMALL = { + TABLE_NAME_FEED_ITEMS + "." + KEY_ID, + TABLE_NAME_FEED_ITEMS + "." + KEY_TITLE, + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE, + TABLE_NAME_FEED_ITEMS + "." + KEY_READ, + TABLE_NAME_FEED_ITEMS + "." + KEY_LINK, + TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK, KEY_MEDIA, + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED, + TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS, + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER}; + + /** + * Contains SEL_FI_SMALL as comma-separated list. Useful for raw queries. + */ + private static final String SEL_FI_SMALL_STR; + + static { + String selFiSmall = Arrays.toString(SEL_FI_SMALL); + SEL_FI_SMALL_STR = selFiSmall.substring(1, selFiSmall.length() - 1); + } + + // column indices for SEL_FI_SMALL + + public static final int IDX_FI_SMALL_ID = 0; + public static final int IDX_FI_SMALL_TITLE = 1; + public static final int IDX_FI_SMALL_PUBDATE = 2; + public static final int IDX_FI_SMALL_READ = 3; + public static final int IDX_FI_SMALL_LINK = 4; + public static final int IDX_FI_SMALL_PAYMENT_LINK = 5; + public static final int IDX_FI_SMALL_MEDIA = 6; + public static final int IDX_FI_SMALL_FEED = 7; + public static final int IDX_FI_SMALL_HAS_CHAPTERS = 8; + public static final int IDX_FI_SMALL_ITEM_IDENTIFIER = 9; + + /** + * Select id, description and content-encoded column from feeditems. + */ + public static final String[] SEL_FI_EXTRA = {KEY_ID, KEY_DESCRIPTION, + KEY_CONTENT_ENCODED, KEY_FEED}; + + // column indices for SEL_FI_EXTRA + + public static final int IDX_FI_EXTRA_ID = 0; + public static final int IDX_FI_EXTRA_DESCRIPTION = 1; + public static final int IDX_FI_EXTRA_CONTENT_ENCODED = 2; + public static final int IDX_FI_EXTRA_FEED = 3; + + static PodDBHelper dbHelperSingleton; + + private static synchronized PodDBHelper getDbHelperSingleton(Context appContext) { + if (dbHelperSingleton == null) { + dbHelperSingleton = new PodDBHelper(appContext, DATABASE_NAME, null, DATABASE_VERSION); + } + return dbHelperSingleton; + } + + public PodDBAdapter(Context c) { + this.context = c; + helper = getDbHelperSingleton(c.getApplicationContext()); + } + + public PodDBAdapter open() { + if (db == null || !db.isOpen() || db.isReadOnly()) { + if (AppConfig.DEBUG) + Log.d(TAG, "Opening DB"); + try { + db = helper.getWritableDatabase(); + } catch (SQLException ex) { + ex.printStackTrace(); + db = helper.getReadableDatabase(); + } + } + return this; + } + + public void close() { + if (AppConfig.DEBUG) + 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()); + } + + 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()); + values.put(KEY_TYPE, feed.getType()); + values.put(KEY_FEED_IDENTIFIER, feed.getFeedIdentifier()); + if (feed.getId() == 0) { + // Create new entry + if (AppConfig.DEBUG) + Log.d(this.toString(), "Inserting new Feed into db"); + feed.setId(db.insert(TABLE_NAME_FEEDS, null, values)); + } else { + if (AppConfig.DEBUG) + Log.d(this.toString(), "Updating existing Feed in db"); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", + new String[]{String.valueOf(feed.getId())}); + } + return feed.getId(); + } + + /** + * Inserts or updates an image entry + * + * @return the id of the entry + */ + public long setImage(FeedImage image) { + db.beginTransaction(); + 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())}); + } + if (image.getFeed() != null && image.getFeed().getId() != 0) { + values.clear(); + values.put(KEY_IMAGE, image.getId()); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(image.getFeed().getId())}); + } + db.setTransactionSuccessful(); + db.endTransaction(); + 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.getPlaybackCompletionDate() != null) { + values.put(KEY_PLAYBACK_COMPLETION_DATE, media + .getPlaybackCompletionDate().getTime()); + } else { + values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); + } + if (media.getItem() != null) { + values.put(KEY_FEEDITEM, media.getItem().getId()); + } + 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(); + } + + public void setFeedMediaPosition(FeedMedia media) { + if (media.getId() != 0) { + ContentValues values = new ContentValues(); + values.put(KEY_POSITION, media.getPosition()); + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } else { + Log.e(TAG, "setFeedMediaPosition: ID of media was 0"); + } + } + + /** + * 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.getItemsArray()) { + setFeedItem(item); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void setFeedItemlist(List<FeedItem> items) { + db.beginTransaction(); + for (FeedItem item : items) { + 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()); + if (item.getDescription() != null) { + values.put(KEY_DESCRIPTION, item.getDescription()); + } + if (item.getContentEncoded() != null) { + values.put(KEY_CONTENT_ENCODED, item.getContentEncoded()); + } + values.put(KEY_PUBDATE, item.getPubDate().getTime()); + values.put(KEY_PAYMENT_LINK, item.getPaymentLink()); + if (item.getFeed() != null) { + setFeed(item.getFeed()); + } + values.put(KEY_FEED, item.getFeed().getId()); + values.put(KEY_READ, item.isRead()); + values.put(KEY_HAS_CHAPTERS, item.getChapters() != null); + values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); + if (item.getId() == 0) { + item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values)); + } else { + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", + new String[]{String.valueOf(item.getId())}); + } + if (item.getMedia() != null) { + setMedia(item.getMedia()); + } + if (item.getChapters() != null) { + setChapters(item); + } + return item.getId(); + } + + public void setFeedItemRead(boolean read, long itemId, long mediaId, + boolean resetMediaPosition) { + db.beginTransaction(); + ContentValues values = new ContentValues(); + + values.put(KEY_READ, read); + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(itemId)}); + + if (resetMediaPosition) { + values.clear(); + values.put(KEY_POSITION, 0); + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); + } + + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void setFeedItemRead(boolean read, long... itemIds) { + db.beginTransaction(); + ContentValues values = new ContentValues(); + for (long id : itemIds) { + values.clear(); + values.put(KEY_READ, read); + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(id)}); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void setChapters(FeedItem item) { + ContentValues values = new ContentValues(); + for (Chapter chapter : item.getChapters()) { + values.put(KEY_TITLE, chapter.getTitle()); + values.put(KEY_START, chapter.getStart()); + values.put(KEY_FEEDITEM, item.getId()); + values.put(KEY_LINK, chapter.getLink()); + values.put(KEY_CHAPTER_TYPE, chapter.getChapterType()); + 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) { + ContentValues values = new ContentValues(); + values.put(KEY_FEEDFILE, status.getFeedfileId()); + values.put(KEY_FEEDFILETYPE, status.getFeedfileType()); + values.put(KEY_REASON, status.getReason().getCode()); + values.put(KEY_SUCCESSFUL, status.isSuccessful()); + values.put(KEY_COMPLETION_DATE, status.getCompletionDate().getTime()); + values.put(KEY_REASON_DETAILED, status.getReasonDetailed()); + values.put(KEY_DOWNLOADSTATUS_TITLE, status.getTitle()); + 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 long getDownloadLogSize() { + Cursor result = db.rawQuery("SELECT COUNT(?) AS ? FROM ?", + new String[]{KEY_ID, KEY_ID, TABLE_NAME_DOWNLOAD_LOG}); + long count = result.getLong(KEY_ID_INDEX); + result.close(); + return count; + } + + public void removeDownloadLogItems(long count) { + if (count > 0) { + db.rawQuery("DELETE FROM ? ORDER BY ? ASC LIMIT ?", + new String[]{TABLE_NAME_DOWNLOAD_LOG, + KEY_COMPLETION_DATE, String.valueOf(count)}); + } + } + + public void setQueue(List<FeedItem> queue) { + ContentValues values = new ContentValues(); + db.beginTransaction(); + 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); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void clearQueue() { + db.delete(TABLE_NAME_QUEUE, null, null); + } + + public void removeFeedMedia(FeedMedia media) { + db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } + + public void removeChaptersOfItem(FeedItem item) { + db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + "=?", + new String[]{String.valueOf(item.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()); + } + if (item.getChapters() != null) { + removeChaptersOfItem(item); + } + 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) { + db.beginTransaction(); + if (feed.getImage() != null) { + removeFeedImage(feed.getImage()); + } + for (FeedItem item : feed.getItemsArray()) { + removeFeedItem(item); + } + db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?", + new String[]{String.valueOf(feed.getId())}); + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void removeDownloadStatus(DownloadStatus remove) { + db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_ID + "=?", + new String[]{String.valueOf(remove.getId())}); + } + + public void clearPlaybackHistory() { + ContentValues values = new ContentValues(); + values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); + db.update(TABLE_NAME_FEED_MEDIA, values, null, null); + } + + /** + * Get all Feeds from the Feed Table. + * + * @return The cursor of the query + */ + public final Cursor getAllFeedsCursor() { + Cursor c = db.query(TABLE_NAME_FEEDS, null, null, null, null, null, + KEY_TITLE + " ASC"); + return c; + } + + public final Cursor getExpiredFeedsCursor(long expirationTime) { + Cursor c = db.query(TABLE_NAME_FEEDS, null, "?<?", new String[]{ + KEY_LASTUPDATE, String.valueOf(System.currentTimeMillis() - expirationTime)}, null, null, + null); + return c; + } + + /** + * Returns a cursor with all FeedItems of a Feed. Uses SEL_FI_SMALL + * + * @param feed The feed you want to get the FeedItems from. + * @return The cursor of the query + */ + public final Cursor getAllItemsOfFeedCursor(final Feed feed) { + return getAllItemsOfFeedCursor(feed.getId()); + } + + public final Cursor getAllItemsOfFeedCursor(final long feedId) { + Cursor c = db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_SMALL, KEY_FEED + + "=?", new String[]{String.valueOf(feedId)}, null, null, + null); + return c; + } + + /** + * Return a cursor with the SEL_FI_EXTRA selection of a single feeditem. + */ + public final Cursor getExtraInformationOfItem(final FeedItem item) { + Cursor c = db + .query(TABLE_NAME_FEED_ITEMS, SEL_FI_EXTRA, KEY_ID + "=?", + new String[]{String.valueOf(item.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) { + 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) { + 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) { + Cursor c = db.query(TABLE_NAME_SIMPLECHAPTERS, null, KEY_FEEDITEM + + "=?", new String[]{String.valueOf(item.getId())}, null, + null, null); + return c; + } + + public final Cursor getDownloadLogCursor(final int limit) { + Cursor c = db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null, + null, KEY_COMPLETION_DATE + " DESC LIMIT " + limit); + return c; + } + + /** + * Returns a cursor which contains all feed items in the queue. The returned + * cursor uses the SEL_FI_SMALL selection. + */ + public final Cursor getQueueCursor() { + Object[] args = (Object[]) new String[]{ + SEL_FI_SMALL_STR + "," + TABLE_NAME_QUEUE + "." + KEY_ID, + TABLE_NAME_FEED_ITEMS, TABLE_NAME_QUEUE, + TABLE_NAME_FEED_ITEMS + "." + KEY_ID, + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM, + TABLE_NAME_QUEUE + "." + KEY_ID}; + String query = String.format( + "SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s", args); + Cursor c = db.rawQuery(query, null); + /* + * Cursor c = db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_SMALL, + * "INNER JOIN ? ON ?=?", new String[] { TABLE_NAME_QUEUE, + * TABLE_NAME_FEED_ITEMS + "." + KEY_ID, TABLE_NAME_QUEUE + "." + + * KEY_FEEDITEM }, null, null, TABLE_NAME_QUEUE + "." + KEY_FEEDITEM); + */ + return c; + } + + public Cursor getQueueIDCursor() { + Cursor c = db.query(TABLE_NAME_QUEUE, new String[]{KEY_FEEDITEM}, null, null, null, null, KEY_ID + " ASC", null); + return c; + } + + /** + * Returns a cursor which contains all feed items in the unread items list. + * The returned cursor uses the SEL_FI_SMALL selection. + */ + public final Cursor getUnreadItemsCursor() { + Cursor c = db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_SMALL, KEY_READ + + "=0", null, null, null, KEY_PUBDATE + " DESC"); + return c; + } + + public final Cursor getUnreadItemIdsCursor() { + Cursor c = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID}, + KEY_READ + "=0", null, null, null, KEY_PUBDATE + " DESC"); + return c; + + } + + public Cursor getDownloadedItemsCursor() { + final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_FEED_MEDIA + "." + KEY_ID + " WHERE " + + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0"; + Cursor c = db.rawQuery(query, null); + return c; + } + + /** + * Returns a cursor which contains feed media objects with a playback + * completion date in descending order. + * + * @param limit The maximum row count of the returned cursor. Must be an + * integer >= 0. + * @throws IllegalArgumentException if limit < 0 + */ + public final Cursor getCompletedMediaCursor(int limit) { + if (limit < 0) { + throw new IllegalArgumentException("Limit must be >= 0"); + } + Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, + KEY_PLAYBACK_COMPLETION_DATE + " > 0", null, null, + null, KEY_PLAYBACK_COMPLETION_DATE + " DESC LIMIT " + limit); + return c; + } + + public final Cursor getSingleFeedMediaCursor(long id) { + return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", new String[]{String.valueOf(id)}, null, null, null); + } + + public final Cursor getFeedMediaCursorByItemID(String... mediaIds) { + int length = mediaIds.length; + if (length > IN_OPERATOR_MAXIMUM) { + Log.w(TAG, "Length of id array is larger than " + + IN_OPERATOR_MAXIMUM + ". Creating multiple cursors"); + int numCursors = (int) (((double) length) / (IN_OPERATOR_MAXIMUM)) + 1; + Cursor[] cursors = new Cursor[numCursors]; + for (int i = 0; i < numCursors; i++) { + int neededLength = 0; + String[] parts = null; + final int elementsLeft = length - i * IN_OPERATOR_MAXIMUM; + + if (elementsLeft >= IN_OPERATOR_MAXIMUM) { + neededLength = IN_OPERATOR_MAXIMUM; + parts = Arrays.copyOfRange(mediaIds, i + * IN_OPERATOR_MAXIMUM, (i + 1) + * IN_OPERATOR_MAXIMUM); + } else { + neededLength = elementsLeft; + parts = Arrays.copyOfRange(mediaIds, i + * IN_OPERATOR_MAXIMUM, (i * IN_OPERATOR_MAXIMUM) + + neededLength); + } + + cursors[i] = db.rawQuery("SELECT * FROM " + + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_FEEDITEM + " IN " + + buildInOperator(neededLength), parts); + } + return new MergeCursor(cursors); + } else { + return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_FEEDITEM + " IN " + + buildInOperator(length), mediaIds, null, null, null); + } + } + + /** + * Builds an IN-operator argument depending on the number of items. + */ + private String buildInOperator(int size) { + if (size == 1) { + return "(?)"; + } + StringBuffer buffer = new StringBuffer("("); + for (int i = 0; i < size - 1; i++) { + buffer.append("?,"); + } + buffer.append("?)"); + return buffer.toString(); + } + + public final Cursor getFeedCursor(final long id) { + Cursor c = db.query(TABLE_NAME_FEEDS, null, KEY_ID + "=" + id, null, + null, null, null); + return c; + } + + public final Cursor getFeedItemCursor(final String... ids) { + if (ids.length > IN_OPERATOR_MAXIMUM) { + throw new IllegalArgumentException( + "number of IDs must not be larger than " + + IN_OPERATOR_MAXIMUM); + } + + return db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_SMALL, KEY_ID + " IN " + + buildInOperator(ids.length), ids, null, null, null); + + } + + public final int getNumberOfUnreadItems() { + final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_ITEMS + + " WHERE " + KEY_READ + " = 0"; + Cursor c = db.rawQuery(query, null); + int result = 0; + if (c.moveToFirst()) { + result = c.getInt(0); + } + c.close(); + return result; + } + + public final int getNumberOfDownloadedEpisodes() { + final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA + + " WHERE " + KEY_DOWNLOADED + " > 0"; + + Cursor c = db.rawQuery(query, null); + int result = 0; + if (c.moveToFirst()) { + result = c.getInt(0); + } + c.close(); + return result; + } + + /** + * Uses DatabaseUtils to escape a search query and removes ' at the + * beginning and the end of the string returned by the escape method. + */ + private String prepareSearchQuery(String query) { + StringBuilder builder = new StringBuilder(); + DatabaseUtils.appendEscapedSQLString(builder, query); + builder.deleteCharAt(0); + builder.deleteCharAt(builder.length() - 1); + return builder.toString(); + } + + /** + * Searches for the given query in the description of all items or the items + * of a specified feed. + * + * @return A cursor with all search results in SEL_FI_EXTRA selection. + */ + public Cursor searchItemDescriptions(long feedID, String query) { + if (feedID != 0) { + // search items in specific feed + return db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_SMALL, KEY_FEED + + "=? AND " + KEY_DESCRIPTION + " LIKE '%" + + prepareSearchQuery(query) + "%'", + new String[]{String.valueOf(feedID)}, null, null, + null); + } else { + // search through all items + return db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_SMALL, + KEY_DESCRIPTION + " LIKE '%" + prepareSearchQuery(query) + + "%'", null, null, null, null); + } + } + + /** + * Searches for the given query in the content-encoded field of all items or + * the items of a specified feed. + * + * @return A cursor with all search results in SEL_FI_EXTRA selection. + */ + public Cursor searchItemContentEncoded(long feedID, String query) { + if (feedID != 0) { + // search items in specific feed + return db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_SMALL, KEY_FEED + + "=? AND " + KEY_CONTENT_ENCODED + " LIKE '%" + + prepareSearchQuery(query) + "%'", + new String[]{String.valueOf(feedID)}, null, null, + null); + } else { + // search through all items + return db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_SMALL, + KEY_CONTENT_ENCODED + " LIKE '%" + + prepareSearchQuery(query) + "%'", null, null, + null, null); + } + } + + public Cursor searchItemTitles(long feedID, String query) { + if (feedID != 0) { + // search items in specific feed + return db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_SMALL, KEY_FEED + + "=? AND " + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(query) + "%'", + new String[]{String.valueOf(feedID)}, null, null, + null); + } else { + // search through all items + return db.query(TABLE_NAME_FEED_ITEMS, SEL_FI_SMALL, + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(query) + "%'", null, null, + null, null); + } + } + + public Cursor searchItemChapters(long feedID, String searchQuery) { + final String query; + if (feedID != 0) { + query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " + + TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + + feedID + " AND " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(searchQuery) + "%'"; + } else { + query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " + + TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(searchQuery) + "%'"; + } + return db.rawQuery(query, null); + } + + + public static final int IDX_FEEDSTATISTICS_FEED = 0; + public static final int IDX_FEEDSTATISTICS_NUM_ITEMS = 1; + public static final int IDX_FEEDSTATISTICS_NEW_ITEMS = 2; + public static final int IDX_FEEDSTATISTICS_LATEST_EPISODE = 3; + public static final int IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES = 4; + + /** + * Select number of items, new items, the date of the latest episode and the number of episodes in progress. The result + * is sorted by the title of the feed. + */ + private static final String FEED_STATISTICS_QUERY = "SELECT feed, num_items, new_items, latest_episode, in_progress FROM " + + "(SELECT feed,count(*) AS num_items," + + " COUNT(CASE WHEN read=0 THEN 1 END) AS new_items," + + " MAX(pubDate) AS latest_episode," + + " COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," + + " COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " + + " FROM FeedItems INNER JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" + + " INNER JOIN Feeds ON Feeds.id = feed ORDER BY Feeds.title;"; + + public Cursor getFeedStatisticsCursor() { + return db.rawQuery(FEED_STATISTICS_QUERY, null); + } + + /** + * 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_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 + "."); + if (oldVersion <= 1) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + " ADD COLUMN " + + KEY_TYPE + " TEXT"); + } + if (oldVersion <= 2) { + db.execSQL("ALTER TABLE " + TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + KEY_LINK + " TEXT"); + } + if (oldVersion <= 3) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + KEY_ITEM_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 4) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEEDS + " ADD COLUMN " + + KEY_FEED_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 5) { + db.execSQL("ALTER TABLE " + TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + KEY_REASON_DETAILED + " TEXT"); + db.execSQL("ALTER TABLE " + TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + KEY_DOWNLOADSTATUS_TITLE + " TEXT"); + } + if (oldVersion <= 6) { + db.execSQL("ALTER TABLE " + TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + KEY_CHAPTER_TYPE + " INTEGER"); + } + if (oldVersion <= 7) { + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + KEY_PLAYBACK_COMPLETION_DATE + + " INTEGER"); + } + if (oldVersion <= 8) { + final int KEY_ID_POSITION = 0; + final int KEY_MEDIA_POSITION = 1; + // Add feeditem column to feedmedia table + db.execSQL("ALTER TABLE " + TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + KEY_FEEDITEM + + " INTEGER"); + Cursor feeditemCursor = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID, KEY_MEDIA}, "? > 0", new String[]{KEY_MEDIA}, null, null, null); + if (feeditemCursor.moveToFirst()) { + db.beginTransaction(); + ContentValues contentValues = new ContentValues(); + do { + long mediaId = feeditemCursor.getLong(KEY_MEDIA_POSITION); + contentValues.put(KEY_FEEDITEM, feeditemCursor.getLong(KEY_ID_POSITION)); + db.update(TABLE_NAME_FEED_MEDIA, contentValues, KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); + contentValues.clear(); + } while (feeditemCursor.moveToNext()); + db.setTransactionSuccessful(); + db.endTransaction(); + } + feeditemCursor.close(); + } + } + } } diff --git a/src/de/danoeh/antennapod/util/ConnectionTester.java b/src/de/danoeh/antennapod/util/ConnectionTester.java index 2fd22d356..5d940d9e1 100644 --- a/src/de/danoeh/antennapod/util/ConnectionTester.java +++ b/src/de/danoeh/antennapod/util/ConnectionTester.java @@ -14,7 +14,7 @@ public class ConnectionTester implements Runnable { private static final String TAG = "ConnectionTester"; private String strUrl; private Callback callback; - private int reason; + private DownloadError reason; private Handler handler; @@ -68,10 +68,10 @@ public class ConnectionTester implements Runnable { public static abstract class Callback { public abstract void onConnectionSuccessful(); - public abstract void onConnectionFailure(int reason); + public abstract void onConnectionFailure(DownloadError reason); } - public int getReason() { + public DownloadError getReason() { return reason; } diff --git a/src/de/danoeh/antennapod/util/DownloadError.java b/src/de/danoeh/antennapod/util/DownloadError.java index 4723a521c..c37a14584 100644 --- a/src/de/danoeh/antennapod/util/DownloadError.java +++ b/src/de/danoeh/antennapod/util/DownloadError.java @@ -4,54 +4,46 @@ import android.content.Context; import de.danoeh.antennapod.R; /** 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; - public static final int ERROR_MALFORMED_URL = 4; - public static final int ERROR_IO_ERROR = 5; - public static final int ERROR_FILE_EXISTS = 6; - public static final int ERROR_DOWNLOAD_CANCELLED = 7; - public static final int ERROR_DEVICE_NOT_FOUND = 8; - public static final int ERROR_HTTP_DATA_ERROR = 9; - public static final int ERROR_NOT_ENOUGH_SPACE = 10; - public static final int ERROR_UNKNOWN_HOST = 11; - public static final int ERROR_REQUEST_ERROR = 12; - - /** Get a human-readable string for a specific error code. */ - public static String getErrorString(Context context, int code) { - int resId; - switch(code) { - case ERROR_NOT_ENOUGH_SPACE: - resId = R.string.download_error_insufficient_space; - break; - case ERROR_DEVICE_NOT_FOUND: - resId = R.string.download_error_device_not_found; - break; - case ERROR_IO_ERROR: - resId = R.string.download_error_io_error; - break; - case 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; - case ERROR_UNKNOWN_HOST: - resId = R.string.download_error_unknown_host; - break; - case ERROR_REQUEST_ERROR: - resId = R.string.download_error_request_error; - break; - default: - resId = R.string.download_error_error_unknown; +public enum DownloadError { + SUCCESS(0, R.string.download_successful), + ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception), + ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type), + ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error), + ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown), + ERROR_IO_ERROR(5, R.string.download_error_io_error), + ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown), + ERROR_DOWNLOAD_CANCELLED(7, R.string.download_error_error_unknown), + ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found), + ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error), + ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space), + ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host), + ERROR_REQUEST_ERROR(12, R.string.download_error_request_error); + + private final int code; + private final int resId; + + private DownloadError(int code, int resId) { + this.code = code; + this.resId = resId; + } + + /** Return DownloadError from its associated code. */ + public static DownloadError fromCode(int code) { + for (DownloadError reason : values()) { + if (reason.getCode() == code) { + return reason; + } } + throw new IllegalArgumentException("unknown code: " + code); + } + + /** Get machine-readable code. */ + public int getCode() { + return code; + } + + /** Get a human-readable string. */ + public String getErrorString(Context context) { return context.getString(resId); } diff --git a/src/de/danoeh/antennapod/util/QueueAccess.java b/src/de/danoeh/antennapod/util/QueueAccess.java new file mode 100644 index 000000000..ce9d11429 --- /dev/null +++ b/src/de/danoeh/antennapod/util/QueueAccess.java @@ -0,0 +1,92 @@ +package de.danoeh.antennapod.util; + +import de.danoeh.antennapod.feed.FeedItem; + +import java.util.Iterator; +import java.util.List; + +/** + * Provides methods for accessing the queue. It is possible to load only a part of the information about the queue that + * is stored in the database (e.g. sometimes the user just has to test if a specific item is contained in the List. + * QueueAccess provides an interface for accessing the queue without having to care about the type of the queue + * representation. + */ +public abstract class QueueAccess { + /** + * Returns true if the item is in the queue, false otherwise. + */ + public abstract boolean contains(long id); + + /** + * Removes the item from the queue. + * + * @return true if the queue was modified by this operation. + */ + public abstract boolean remove(long id); + + private QueueAccess() { + + } + + public static QueueAccess IDListAccess(final List<Long> ids) { + return new QueueAccess() { + @Override + public boolean contains(long id) { + return (ids != null) && ids.contains(id); + } + + @Override + public boolean remove(long id) { + return ids.remove(id); + } + + + }; + } + + public static QueueAccess ItemListAccess(final List<FeedItem> items) { + return new QueueAccess() { + @Override + public boolean contains(long id) { + if (items == null) { + return false; + } + Iterator<FeedItem> it = items.iterator(); + for (FeedItem i = it.next(); it.hasNext(); i = it.next()) { + if (i.getId() == id) { + return true; + } + } + return false; + } + + @Override + public boolean remove(long id) { + Iterator<FeedItem> it = items.iterator(); + for (FeedItem i = it.next(); it.hasNext(); i = it.next()) { + if (i.getId() == id) { + it.remove(); + return true; + } + } + return false; + } + }; + } + + public static QueueAccess NotInQueueAccess() { + return new QueueAccess() { + @Override + public boolean contains(long id) { + return false; + } + + @Override + public boolean remove(long id) { + return false; + } + }; + + } + +} diff --git a/src/de/danoeh/antennapod/util/ShownotesProvider.java b/src/de/danoeh/antennapod/util/ShownotesProvider.java new file mode 100644 index 000000000..d273e0b8f --- /dev/null +++ b/src/de/danoeh/antennapod/util/ShownotesProvider.java @@ -0,0 +1,17 @@ +package de.danoeh.antennapod.util; + +import java.util.concurrent.Callable; +import java.util.concurrent.FutureTask; + +/** + * Created by daniel on 04.08.13. + */ +public interface ShownotesProvider { + /** + * Loads shownotes. If the shownotes have to be loaded from a file or from a + * database, it should be done in a separate thread. After the shownotes + * have been loaded, callback.onShownotesLoaded should be called. + */ + public Callable<String> loadShownotes(); + +} diff --git a/src/de/danoeh/antennapod/util/UndoBarController.java b/src/de/danoeh/antennapod/util/UndoBarController.java index e726717a1..a0240e7ce 100644 --- a/src/de/danoeh/antennapod/util/UndoBarController.java +++ b/src/de/danoeh/antennapod/util/UndoBarController.java @@ -16,15 +16,17 @@ package de.danoeh.antennapod.util; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.text.TextUtils; import android.view.View; -import android.view.ViewPropertyAnimator; import android.widget.TextView; +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.AnimatorListenerAdapter; +import com.nineoldandroids.view.ViewHelper; +import com.nineoldandroids.view.ViewPropertyAnimator; +import static com.nineoldandroids.view.ViewPropertyAnimator.animate; import de.danoeh.antennapod.R; @@ -46,7 +48,7 @@ public class UndoBarController { public UndoBarController(View undoBarView, UndoListener undoListener) { mBarView = undoBarView; - mBarAnimator = mBarView.animate(); + mBarAnimator = animate(mBarView); mUndoListener = undoListener; mMessageView = (TextView) mBarView.findViewById(R.id.undobar_message); @@ -73,7 +75,7 @@ public class UndoBarController { mBarView.setVisibility(View.VISIBLE); if (immediate) { - mBarView.setAlpha(1); + ViewHelper.setAlpha(mBarView, 1); } else { mBarAnimator.cancel(); mBarAnimator @@ -89,7 +91,7 @@ public class UndoBarController { mHideHandler.removeCallbacks(mHideRunnable); if (immediate) { mBarView.setVisibility(View.GONE); - mBarView.setAlpha(0); + ViewHelper.setAlpha(mBarView, 0); mUndoMessage = null; mUndoToken = null; diff --git a/src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java b/src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java index 12f800565..2cfe52364 100644 --- a/src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java +++ b/src/de/danoeh/antennapod/util/comparator/DownloadStatusComparator.java @@ -2,7 +2,7 @@ package de.danoeh.antennapod.util.comparator; import java.util.Comparator; -import de.danoeh.antennapod.asynctask.DownloadStatus; +import de.danoeh.antennapod.service.download.*; /** Compares the completion date of two Downloadstatus objects. */ public class DownloadStatusComparator implements Comparator<DownloadStatus> { diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java index 472124bf7..aa029f672 100644 --- a/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java +++ b/src/de/danoeh/antennapod/util/menuhandler/FeedItemMenuHandler.java @@ -7,12 +7,16 @@ import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.asynctask.FlattrClickWorker; import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.service.PlaybackService; +import de.danoeh.antennapod.storage.DBTasks; +import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.storage.DownloadRequester; +import de.danoeh.antennapod.util.QueueAccess; import de.danoeh.antennapod.util.ShareUtils; +import java.util.List; + /** Handles interactions with the FeedItemMenu. */ public class FeedItemMenuHandler { private FeedItemMenuHandler() { @@ -21,7 +25,7 @@ public class FeedItemMenuHandler { /** * Used by the MenuHandler to access different types of menus through one - * interface (for example android.view.Menu and com.actionbarsherlock.Menu) + * interface */ public interface MenuInterface { /** @@ -45,11 +49,12 @@ public class FeedItemMenuHandler { * True if MenuItems that let the user share information about * the FeedItem and visit its website should be set visible. This * parameter should be set to false if the menu space is limited. + * @param queueAccess + * Used for testing if the queue contains the selected item * @return Always returns true * */ public static boolean onPrepareMenu(MenuInterface mi, - FeedItem selectedItem, boolean showExtendedMenu) { - FeedManager manager = FeedManager.getInstance(); + FeedItem selectedItem, boolean showExtendedMenu, QueueAccess queueAccess) { DownloadRequester requester = DownloadRequester.getInstance(); boolean hasMedia = selectedItem.getMedia() != null; boolean downloaded = hasMedia && selectedItem.getMedia().isDownloaded(); @@ -79,7 +84,7 @@ public class FeedItemMenuHandler { mi.setItemVisibility(R.id.cancel_download_item, false); } - boolean isInQueue = manager.isInQueue(selectedItem); + boolean isInQueue = queueAccess.contains(selectedItem.getId()); if (!isInQueue || isPlaying) { mi.setItemVisibility(R.id.remove_from_queue_item, false); } @@ -111,39 +116,38 @@ public class FeedItemMenuHandler { public static boolean onMenuItemClicked(Context context, int menuItemId, FeedItem selectedItem) throws DownloadRequestException { DownloadRequester requester = DownloadRequester.getInstance(); - FeedManager manager = FeedManager.getInstance(); switch (menuItemId) { case R.id.skip_episode_item: context.sendBroadcast(new Intent( PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); break; case R.id.download_item: - manager.downloadFeedItem(context, selectedItem); + DBTasks.downloadFeedItems(context, selectedItem); break; case R.id.play_item: - manager.playMedia(context, selectedItem.getMedia(), true, true, + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, false); break; case R.id.remove_item: - manager.deleteFeedMedia(context, selectedItem.getMedia()); + DBWriter.deleteFeedMediaOfItem(context, selectedItem.getId()); break; case R.id.cancel_download_item: requester.cancelDownload(context, selectedItem.getMedia()); break; case R.id.mark_read_item: - manager.markItemRead(context, selectedItem, true, true); + DBWriter.markItemRead(context, selectedItem, true, true); break; case R.id.mark_unread_item: - manager.markItemRead(context, selectedItem, false, true); + DBWriter.markItemRead(context, selectedItem, false, true); break; case R.id.add_to_queue_item: - manager.addQueueItem(context, selectedItem); + DBWriter.addQueueItem(context, selectedItem.getId()); break; case R.id.remove_from_queue_item: - manager.removeQueueItem(context, selectedItem, true); + DBWriter.removeQueueItem(context, selectedItem.getId(), true); break; case R.id.stream_item: - manager.playMedia(context, selectedItem.getMedia(), true, true, + DBTasks.playMedia(context, selectedItem.getMedia(), true, true, true); break; case R.id.visit_website_item: diff --git a/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java b/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java index af8538e83..843607617 100644 --- a/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java +++ b/src/de/danoeh/antennapod/util/menuhandler/FeedMenuHandler.java @@ -5,17 +5,17 @@ import android.content.Intent; import android.net.Uri; import android.util.Log; -import com.actionbarsherlock.view.Menu; -import com.actionbarsherlock.view.MenuInflater; -import com.actionbarsherlock.view.MenuItem; - +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.FeedInfoActivity; import de.danoeh.antennapod.asynctask.FlattrClickWorker; import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.service.download.DownloadService; +import de.danoeh.antennapod.storage.DBTasks; +import de.danoeh.antennapod.storage.DBWriter; import de.danoeh.antennapod.storage.DownloadRequestException; import de.danoeh.antennapod.storage.DownloadRequester; import de.danoeh.antennapod.util.ShareUtils; @@ -56,7 +56,6 @@ public class FeedMenuHandler { */ public static boolean onOptionsItemClicked(Context context, MenuItem item, Feed selectedFeed) throws DownloadRequestException { - FeedManager manager = FeedManager.getInstance(); switch (item.getItemId()) { case R.id.show_info_item: Intent startIntent = new Intent(context, FeedInfoActivity.class); @@ -65,10 +64,10 @@ public class FeedMenuHandler { context.startActivity(startIntent); break; case R.id.refresh_item: - manager.refreshFeed(context, selectedFeed); + DBTasks.refreshFeed(context, selectedFeed); break; case R.id.mark_all_read_item: - manager.markFeedRead(context, selectedFeed); + DBWriter.markFeedRead(context, selectedFeed.getId()); break; case R.id.visit_website_item: Uri uri = Uri.parse(selectedFeed.getLink()); diff --git a/src/de/danoeh/antennapod/util/playback/ExternalMedia.java b/src/de/danoeh/antennapod/util/playback/ExternalMedia.java index c0a92904b..1ada0ec03 100644 --- a/src/de/danoeh/antennapod/util/playback/ExternalMedia.java +++ b/src/de/danoeh/antennapod/util/playback/ExternalMedia.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.util.playback; import java.io.InputStream; import java.util.List; +import java.util.concurrent.Callable; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; @@ -95,8 +96,13 @@ public class ExternalMedia implements Playable { } @Override - public void loadShownotes(ShownoteLoaderCallback callback) { - callback.onShownotesLoaded(null); + public Callable<String> loadShownotes() { + return new Callable<String>() { + @Override + public String call() throws Exception { + return ""; + } + }; } @Override diff --git a/src/de/danoeh/antennapod/util/playback/Playable.java b/src/de/danoeh/antennapod/util/playback/Playable.java index 0404379e2..98d5fbb36 100644 --- a/src/de/danoeh/antennapod/util/playback/Playable.java +++ b/src/de/danoeh/antennapod/util/playback/Playable.java @@ -3,7 +3,11 @@ package de.danoeh.antennapod.util.playback; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.List; +import java.util.concurrent.FutureTask; +import android.content.Context; +import de.danoeh.antennapod.storage.DBReader; +import de.danoeh.antennapod.util.ShownotesProvider; import org.apache.commons.io.IOUtils; import android.content.SharedPreferences; @@ -12,262 +16,263 @@ import android.os.Parcelable; import android.util.Log; import de.danoeh.antennapod.asynctask.ImageLoader; import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.feed.MediaType; -/** Interface for objects that can be played by the PlaybackService. */ +/** + * Interface for objects that can be played by the PlaybackService. + */ public interface Playable extends Parcelable, - ImageLoader.ImageWorkerTaskResource { - - /** - * Save information about the playable in a preference so that it can be - * restored later via PlayableUtils.createInstanceFromPreferences. - * Implementations must NOT call commit() after they have written the values - * to the preferences file. - */ - public void writeToPreferences(SharedPreferences.Editor prefEditor); - - /** - * This method is called from a separate thread by the PlaybackService. - * Playable objects should load their metadata in this method. This method - * should execute as quickly as possible and NOT load chapter marks if no - * local file is available. - */ - public void loadMetadata() throws PlayableException; - - /** - * This method is called from a separate thread by the PlaybackService. - * Playable objects should load their chapter marks in this method if no - * local file was available when loadMetadata() was called. - */ - public void loadChapterMarks(); - - /** Returns the title of the episode that this playable represents */ - public String getEpisodeTitle(); - - /** - * Loads shownotes. If the shownotes have to be loaded from a file or from a - * database, it should be done in a separate thread. After the shownotes - * have been loaded, callback.onShownotesLoaded should be called. - */ - public void loadShownotes(ShownoteLoaderCallback callback); - - /** - * Returns a list of chapter marks or null if this Playable has no chapters. - */ - public List<Chapter> getChapters(); - - /** Returns a link to a website that is meant to be shown in a browser */ - public String getWebsiteLink(); - - public String getPaymentLink(); - - /** Returns the title of the feed this Playable belongs to. */ - public String getFeedTitle(); - - /** - * Returns a unique identifier, for example a file url or an ID from a - * database. - */ - public Object getIdentifier(); - - /** Return duration of object or 0 if duration is unknown. */ - public int getDuration(); - - /** Return position of object or 0 if position is unknown. */ - public int getPosition(); - - /** - * Returns the type of media. This method should return the correct value - * BEFORE loadMetadata() is called. - */ - public MediaType getMediaType(); - - /** - * Returns an url to a local file that can be played or null if this file - * does not exist. - */ - public String getLocalMediaUrl(); - - /** - * Returns an url to a file that can be streamed by the player or null if - * this url is not known. - */ - public String getStreamUrl(); - - /** - * Returns true if a local file that can be played is available. getFileUrl - * MUST return a non-null string if this method returns true. - */ - public boolean localFileAvailable(); - - /** - * Returns true if a streamable file is available. getStreamUrl MUST return - * a non-null string if this method returns true. - */ - public boolean streamAvailable(); - - /** - * Saves the current position of this object. Implementations can use the - * provided SharedPreference to save this information and retrieve it later - * via PlayableUtils.createInstanceFromPreferences. - */ - public void saveCurrentPosition(SharedPreferences pref, int newPosition); - - public void setPosition(int newPosition); - - public void setDuration(int newDuration); - - /** Is called by the PlaybackService when playback starts. */ - public void onPlaybackStart(); - - /** Is called by the PlaybackService when playback is completed. */ - public void onPlaybackCompleted(); - - /** - * Returns an integer that must be unique among all Playable classes. The - * return value is later used by PlayableUtils to determine the type of the - * Playable object that is restored. - */ - public int getPlayableType(); - - public void setChapters(List<Chapter> chapters); - - /** Provides utility methods for Playable objects. */ - public static class PlayableUtils { - private static final String TAG = "PlayableUtils"; - - /** - * Restores a playable object from a sharedPreferences file. - * - * @param type - * An integer that represents the type of the Playable object - * that is restored. - * @param pref - * The SharedPreferences file from which the Playable object - * is restored - * @return The restored Playable object - */ - public static Playable createInstanceFromPreferences(int type, - SharedPreferences pref) { - // ADD new Playable types here: - switch (type) { - case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA: - long feedId = pref.getLong(FeedMedia.PREF_FEED_ID, -1); - long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1); - if (feedId != -1 && mediaId != -1) { - Feed feed = FeedManager.getInstance().getFeed(feedId); - if (feed != null) { - return FeedManager.getInstance().getFeedMedia(mediaId, - feed); - } - } - break; - case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA: - String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, - null); - String mediaType = pref.getString( - ExternalMedia.PREF_MEDIA_TYPE, null); - if (source != null && mediaType != null) { - int position = pref.getInt(ExternalMedia.PREF_POSITION, 0); - return new ExternalMedia(source, - MediaType.valueOf(mediaType), position); - } - break; - } - Log.e(TAG, "Could not restore Playable object from preferences"); - return null; - } - } - - public static class PlayableException extends Exception { - private static final long serialVersionUID = 1L; - - public PlayableException() { - super(); - } - - public PlayableException(String detailMessage, Throwable throwable) { - super(detailMessage, throwable); - } - - public PlayableException(String detailMessage) { - super(detailMessage); - } - - public PlayableException(Throwable throwable) { - super(throwable); - } - - } - - public static interface ShownoteLoaderCallback { - void onShownotesLoaded(String shownotes); - } - - /** Uses local file as image resource if it is available. */ - public static class DefaultPlayableImageLoader implements - ImageLoader.ImageWorkerTaskResource { - private Playable playable; - - public DefaultPlayableImageLoader(Playable playable) { - if (playable == null) { - throw new IllegalArgumentException("Playable must not be null"); - } - this.playable = playable; - } - - @Override - public InputStream openImageInputStream() { - if (playable.localFileAvailable() - && playable.getLocalMediaUrl() != null) { - MediaMetadataRetriever mmr = new MediaMetadataRetriever(); - try { - mmr.setDataSource(playable.getLocalMediaUrl()); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - return null; - } - byte[] imgData = mmr.getEmbeddedPicture(); - if (imgData != null) { - return new PublicByteArrayInputStream(imgData); - - } - } - return null; - } - - @Override - public String getImageLoaderCacheKey() { - return playable.getLocalMediaUrl(); - } - - @Override - public InputStream reopenImageInputStream(InputStream input) { - if (input instanceof PublicByteArrayInputStream) { - IOUtils.closeQuietly(input); - byte[] imgData = ((PublicByteArrayInputStream) input) - .getByteArray(); - if (imgData != null) { - ByteArrayInputStream out = new ByteArrayInputStream(imgData); - return out; - } - - } - return null; - } - - private static class PublicByteArrayInputStream extends - ByteArrayInputStream { - public PublicByteArrayInputStream(byte[] buf) { - super(buf); - } - - public byte[] getByteArray() { - return buf; - } - } - } + ImageLoader.ImageWorkerTaskResource, ShownotesProvider { + + /** + * Save information about the playable in a preference so that it can be + * restored later via PlayableUtils.createInstanceFromPreferences. + * Implementations must NOT call commit() after they have written the values + * to the preferences file. + */ + public void writeToPreferences(SharedPreferences.Editor prefEditor); + + /** + * This method is called from a separate thread by the PlaybackService. + * Playable objects should load their metadata in this method. This method + * should execute as quickly as possible and NOT load chapter marks if no + * local file is available. + */ + public void loadMetadata() throws PlayableException; + + /** + * This method is called from a separate thread by the PlaybackService. + * Playable objects should load their chapter marks in this method if no + * local file was available when loadMetadata() was called. + */ + public void loadChapterMarks(); + + /** + * Returns the title of the episode that this playable represents + */ + public String getEpisodeTitle(); + + /** + * Returns a list of chapter marks or null if this Playable has no chapters. + */ + public List<Chapter> getChapters(); + + /** + * Returns a link to a website that is meant to be shown in a browser + */ + public String getWebsiteLink(); + + public String getPaymentLink(); + + /** + * Returns the title of the feed this Playable belongs to. + */ + public String getFeedTitle(); + + /** + * Returns a unique identifier, for example a file url or an ID from a + * database. + */ + public Object getIdentifier(); + + /** + * Return duration of object or 0 if duration is unknown. + */ + public int getDuration(); + + /** + * Return position of object or 0 if position is unknown. + */ + public int getPosition(); + + /** + * Returns the type of media. This method should return the correct value + * BEFORE loadMetadata() is called. + */ + public MediaType getMediaType(); + + /** + * Returns an url to a local file that can be played or null if this file + * does not exist. + */ + public String getLocalMediaUrl(); + + /** + * Returns an url to a file that can be streamed by the player or null if + * this url is not known. + */ + public String getStreamUrl(); + + /** + * Returns true if a local file that can be played is available. getFileUrl + * MUST return a non-null string if this method returns true. + */ + public boolean localFileAvailable(); + + /** + * Returns true if a streamable file is available. getStreamUrl MUST return + * a non-null string if this method returns true. + */ + public boolean streamAvailable(); + + /** + * Saves the current position of this object. Implementations can use the + * provided SharedPreference to save this information and retrieve it later + * via PlayableUtils.createInstanceFromPreferences. + */ + public void saveCurrentPosition(SharedPreferences pref, int newPosition); + + public void setPosition(int newPosition); + + public void setDuration(int newDuration); + + /** + * Is called by the PlaybackService when playback starts. + */ + public void onPlaybackStart(); + + /** + * Is called by the PlaybackService when playback is completed. + */ + public void onPlaybackCompleted(); + + /** + * Returns an integer that must be unique among all Playable classes. The + * return value is later used by PlayableUtils to determine the type of the + * Playable object that is restored. + */ + public int getPlayableType(); + + public void setChapters(List<Chapter> chapters); + + /** + * Provides utility methods for Playable objects. + */ + public static class PlayableUtils { + private static final String TAG = "PlayableUtils"; + + /** + * Restores a playable object from a sharedPreferences file. This method might load data from the database, + * depending on the type of playable that was restored. + * + * @param type An integer that represents the type of the Playable object + * that is restored. + * @param pref The SharedPreferences file from which the Playable object + * is restored + * @return The restored Playable object + */ + public static Playable createInstanceFromPreferences(Context context, int type, + SharedPreferences pref) { + // ADD new Playable types here: + switch (type) { + case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA: + long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1); + if (mediaId != -1) { + return DBReader.getFeedMedia(context, mediaId); + } + break; + case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA: + String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, + null); + String mediaType = pref.getString( + ExternalMedia.PREF_MEDIA_TYPE, null); + if (source != null && mediaType != null) { + int position = pref.getInt(ExternalMedia.PREF_POSITION, 0); + return new ExternalMedia(source, + MediaType.valueOf(mediaType), position); + } + break; + } + Log.e(TAG, "Could not restore Playable object from preferences"); + return null; + } + } + + public static class PlayableException extends Exception { + private static final long serialVersionUID = 1L; + + public PlayableException() { + super(); + } + + public PlayableException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public PlayableException(String detailMessage) { + super(detailMessage); + } + + public PlayableException(Throwable throwable) { + super(throwable); + } + + } + + /** + * Uses local file as image resource if it is available. + */ + public static class DefaultPlayableImageLoader implements + ImageLoader.ImageWorkerTaskResource { + private Playable playable; + + public DefaultPlayableImageLoader(Playable playable) { + if (playable == null) { + throw new IllegalArgumentException("Playable must not be null"); + } + this.playable = playable; + } + + @Override + public InputStream openImageInputStream() { + if (playable.localFileAvailable() + && playable.getLocalMediaUrl() != null) { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + try { + mmr.setDataSource(playable.getLocalMediaUrl()); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return null; + } + byte[] imgData = mmr.getEmbeddedPicture(); + if (imgData != null) { + return new PublicByteArrayInputStream(imgData); + + } + } + return null; + } + + @Override + public String getImageLoaderCacheKey() { + return playable.getLocalMediaUrl(); + } + + @Override + public InputStream reopenImageInputStream(InputStream input) { + if (input instanceof PublicByteArrayInputStream) { + IOUtils.closeQuietly(input); + byte[] imgData = ((PublicByteArrayInputStream) input) + .getByteArray(); + if (imgData != null) { + ByteArrayInputStream out = new ByteArrayInputStream(imgData); + return out; + } + + } + return null; + } + + private static class PublicByteArrayInputStream extends + ByteArrayInputStream { + public PublicByteArrayInputStream(byte[] buf) { + super(buf); + } + + public byte[] getByteArray() { + return buf; + } + } + } } diff --git a/src/de/danoeh/antennapod/util/playback/PlaybackController.java b/src/de/danoeh/antennapod/util/playback/PlaybackController.java index cebb11cf0..5a5b43a6e 100644 --- a/src/de/danoeh/antennapod/util/playback/PlaybackController.java +++ b/src/de/danoeh/antennapod/util/playback/PlaybackController.java @@ -16,6 +16,7 @@ import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.res.TypedArray; +import android.os.AsyncTask; import android.os.IBinder; import android.preference.PreferenceManager; import android.util.Log; @@ -28,11 +29,11 @@ import android.widget.TextView; import de.danoeh.antennapod.AppConfig; import de.danoeh.antennapod.R; import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.FeedManager; import de.danoeh.antennapod.feed.FeedMedia; import de.danoeh.antennapod.preferences.PlaybackPreferences; import de.danoeh.antennapod.service.PlaybackService; import de.danoeh.antennapod.service.PlayerStatus; +import de.danoeh.antennapod.storage.DBTasks; import de.danoeh.antennapod.util.Converter; import de.danoeh.antennapod.util.playback.Playable.PlayableUtils; @@ -41,661 +42,680 @@ import de.danoeh.antennapod.util.playback.Playable.PlayableUtils; * control playback instead of communicating with the PlaybackService directly. */ public abstract class PlaybackController { - private static final String TAG = "PlaybackController"; + private static final String TAG = "PlaybackController"; - static final int DEFAULT_SEEK_DELTA = 30000; + public static final int DEFAULT_SEEK_DELTA = 30000; public static final int INVALID_TIME = -1; - private Activity activity; - - private PlaybackService playbackService; - private Playable media; - private PlayerStatus status; - - private ScheduledThreadPoolExecutor schedExecutor; - private static final int SCHED_EX_POOLSIZE = 1; - - protected MediaPositionObserver positionObserver; - protected ScheduledFuture positionObserverFuture; - - private boolean mediaInfoLoaded = false; - private boolean released = false; - - /** - * True if controller should reinit playback service if 'pause' button is - * pressed. - */ - private boolean reinitOnPause; - - public PlaybackController(Activity activity, boolean reinitOnPause) { - this.activity = activity; - this.reinitOnPause = reinitOnPause; - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }, new RejectedExecutionHandler() { - - @Override - public void rejectedExecution(Runnable r, - ThreadPoolExecutor executor) { - Log.w(TAG, - "Rejected execution of runnable in schedExecutor"); - } - }); - } - - /** - * Creates a new connection to the playbackService. Should be called in the - * activity's onResume() method. - */ - public void init() { - activity.registerReceiver(statusUpdate, new IntentFilter( - PlaybackService.ACTION_PLAYER_STATUS_CHANGED)); - - activity.registerReceiver(notificationReceiver, new IntentFilter( - PlaybackService.ACTION_PLAYER_NOTIFICATION)); - - activity.registerReceiver(shutdownReceiver, new IntentFilter( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - - if (!released) { - bindToService(); - } else { - throw new IllegalStateException( - "Can't call init() after release() has been called"); - } - } - - /** - * Should be called if the PlaybackController is no longer needed, for - * example in the activity's onStop() method. - */ - public void release() { - if (AppConfig.DEBUG) - Log.d(TAG, "Releasing PlaybackController"); - - try { - activity.unregisterReceiver(statusUpdate); - } catch (IllegalArgumentException e) { - // ignore - } - - try { - activity.unregisterReceiver(notificationReceiver); - } catch (IllegalArgumentException e) { - // ignore - } - - try { - activity.unbindService(mConnection); - } catch (IllegalArgumentException e) { - // ignore - } - - try { - activity.unregisterReceiver(shutdownReceiver); - } catch (IllegalArgumentException e) { - // ignore - } - cancelPositionObserver(); - schedExecutor.shutdownNow(); - media = null; - released = true; - - } - - /** Should be called in the activity's onPause() method. */ - public void pause() { - mediaInfoLoaded = false; - if (playbackService != null && playbackService.isPlayingVideo()) { - playbackService.pause(true, true); - } - } - - /** - * Tries to establish a connection to the PlaybackService. If it isn't - * running, the PlaybackService will be started with the last played media - * as the arguments of the launch intent. - */ - private void bindToService() { - if (AppConfig.DEBUG) - Log.d(TAG, "Trying to connect to service"); - Intent serviceIntent = getPlayLastPlayedMediaIntent(); - boolean bound = false; - if (!PlaybackService.isRunning) { - if (serviceIntent != null) { - activity.startService(serviceIntent); - bound = activity.bindService(serviceIntent, mConnection, 0); - } else { - status = PlayerStatus.STOPPED; - setupGUI(); - handleStatus(); - } - } else { - if (AppConfig.DEBUG) - Log.d(TAG, - "PlaybackService is running, trying to connect without start command."); - bound = activity.bindService(new Intent(activity, - PlaybackService.class), mConnection, 0); - } - if (AppConfig.DEBUG) - Log.d(TAG, "Result for service binding: " + bound); - } - - /** - * Returns an intent that starts the PlaybackService and plays the last - * played media or null if no last played media could be found. - */ - private Intent getPlayLastPlayedMediaIntent() { - if (AppConfig.DEBUG) - Log.d(TAG, "Trying to restore last played media"); - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(activity.getApplicationContext()); - long currentlyPlayingMedia = PlaybackPreferences - .getCurrentlyPlayingMedia(); - if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) { - Playable media = PlayableUtils.createInstanceFromPreferences( - (int) currentlyPlayingMedia, prefs); - if (media != null) { - Intent serviceIntent = new Intent(activity, - PlaybackService.class); - serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); - serviceIntent.putExtra( - PlaybackService.EXTRA_START_WHEN_PREPARED, false); - serviceIntent.putExtra( - PlaybackService.EXTRA_PREPARE_IMMEDIATELY, false); - boolean fileExists = media.localFileAvailable(); - boolean lastIsStream = PlaybackPreferences - .getCurrentEpisodeIsStream(); - if (!fileExists && !lastIsStream && media instanceof FeedMedia) { - FeedManager.getInstance().notifyMissingFeedMediaFile( - activity, (FeedMedia) media); - } - serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, - lastIsStream || !fileExists); - return serviceIntent; - } - } - if (AppConfig.DEBUG) - Log.d(TAG, "No last played media found"); - return null; - } - - public abstract void setupGUI(); - - private void setupPositionObserver() { - if ((positionObserverFuture != null && positionObserverFuture - .isCancelled()) - || (positionObserverFuture != null && positionObserverFuture - .isDone()) || positionObserverFuture == null) { - - if (AppConfig.DEBUG) - Log.d(TAG, "Setting up position observer"); - positionObserver = new MediaPositionObserver(); - positionObserverFuture = schedExecutor.scheduleWithFixedDelay( - positionObserver, MediaPositionObserver.WAITING_INTERVALL, - MediaPositionObserver.WAITING_INTERVALL, - TimeUnit.MILLISECONDS); - } - } - - private void cancelPositionObserver() { - if (positionObserverFuture != null) { - boolean result = positionObserverFuture.cancel(true); - if (AppConfig.DEBUG) - Log.d(TAG, "PositionObserver cancelled. Result: " + result); - } - } - - public abstract void onPositionObserverUpdate(); - - private ServiceConnection mConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - playbackService = ((PlaybackService.LocalBinder) service) - .getService(); - - queryService(); - if (AppConfig.DEBUG) - Log.d(TAG, "Connection to Service established"); - } - - @Override - public void onServiceDisconnected(ComponentName name) { - playbackService = null; - if (AppConfig.DEBUG) - Log.d(TAG, "Disconnected from Service"); - - } - }; - - protected BroadcastReceiver statusUpdate = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (AppConfig.DEBUG) - Log.d(TAG, "Received statusUpdate Intent."); - if (isConnectedToPlaybackService()) { - status = playbackService.getStatus(); - handleStatus(); - } else { - Log.w(TAG, - "Couldn't receive status update: playbackService was null"); - bindToService(); - } - } - }; - - protected BroadcastReceiver notificationReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (isConnectedToPlaybackService()) { - 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: - float progress = ((float) code) / 100; - onBufferUpdate(progress); - break; - case PlaybackService.NOTIFICATION_TYPE_RELOAD: - cancelPositionObserver(); - mediaInfoLoaded = false; - onReloadNotification(intent.getIntExtra( - PlaybackService.EXTRA_NOTIFICATION_CODE, -1)); - queryService(); - - break; - case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE: - onSleepTimerUpdate(); - break; - case PlaybackService.NOTIFICATION_TYPE_BUFFER_START: - onBufferStart(); - break; - case PlaybackService.NOTIFICATION_TYPE_BUFFER_END: - onBufferEnd(); - break; - case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END: - onPlaybackEnd(); - break; - } - - } else { - if (AppConfig.DEBUG) - Log.d(TAG, "Bad arguments. Won't handle intent"); - } - } else { - bindToService(); - } - } - - }; - - private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (isConnectedToPlaybackService()) { - if (intent.getAction().equals( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - release(); - onShutdownNotification(); - } - } - } - }; - - public abstract void onShutdownNotification(); - - /** Called when the currently displayed information should be refreshed. */ - public abstract void onReloadNotification(int code); - - public abstract void onBufferStart(); - - public abstract void onBufferEnd(); - - public abstract void onBufferUpdate(float progress); - - public abstract void onSleepTimerUpdate(); - - public abstract void handleError(int code); - - public abstract void onPlaybackEnd(); - - /** - * Is called whenever the PlaybackService changes it's status. This method - * should be used to update the GUI or start/cancel background threads. - */ - private void handleStatus() { - TypedArray res = activity.obtainStyledAttributes(new int[] { - R.attr.av_play, R.attr.av_pause }); - final int playResource = res.getResourceId(0, R.drawable.av_play); - final int pauseResource = res.getResourceId(1, R.drawable.av_pause); - res.recycle(); - - switch (status) { - - case ERROR: - postStatusMsg(R.string.player_error_msg); - break; - case PAUSED: - clearStatusMsg(); - checkMediaInfoLoaded(); - cancelPositionObserver(); - updatePlayButtonAppearance(playResource); - break; - case PLAYING: - clearStatusMsg(); - checkMediaInfoLoaded(); - setupPositionObserver(); - updatePlayButtonAppearance(pauseResource); - break; - case PREPARING: - postStatusMsg(R.string.player_preparing_msg); - checkMediaInfoLoaded(); - if (playbackService != null) { - if (playbackService.isStartWhenPrepared()) { - updatePlayButtonAppearance(pauseResource); - } else { - updatePlayButtonAppearance(playResource); - } - } - break; - case STOPPED: - postStatusMsg(R.string.player_stopped_msg); - break; - case PREPARED: - checkMediaInfoLoaded(); - postStatusMsg(R.string.player_ready_msg); - updatePlayButtonAppearance(playResource); - break; - case SEEKING: - postStatusMsg(R.string.player_seeking_msg); - break; - case AWAITING_VIDEO_SURFACE: - onAwaitingVideoSurface(); - break; - case INITIALIZED: - checkMediaInfoLoaded(); - clearStatusMsg(); - updatePlayButtonAppearance(playResource); - break; - } - } - - private void checkMediaInfoLoaded() { - if (!mediaInfoLoaded) { - loadMediaInfo(); - } - mediaInfoLoaded = true; - } - - private void updatePlayButtonAppearance(int resource) { - ImageButton butPlay = getPlayButton(); - butPlay.setImageResource(resource); - } - - public abstract ImageButton getPlayButton(); - - public abstract void postStatusMsg(int msg); - - public abstract void clearStatusMsg(); - - public abstract void loadMediaInfo(); - - public abstract void onAwaitingVideoSurface(); - - /** - * Called when connection to playback service has been established or - * information has to be refreshed - */ - void queryService() { - if (AppConfig.DEBUG) - Log.d(TAG, "Querying service info"); - if (playbackService != null) { - status = playbackService.getStatus(); - media = playbackService.getMedia(); - if (media == null) { - Log.w(TAG, - "PlaybackService has no media object. Trying to restore last played media."); - Intent serviceIntent = getPlayLastPlayedMediaIntent(); - if (serviceIntent != null) { - activity.startService(serviceIntent); - } - } - onServiceQueried(); - - setupGUI(); - handleStatus(); - // make sure that new media is loaded if it's available - mediaInfoLoaded = false; - - } else { - Log.e(TAG, - "queryService() was called without an existing connection to playbackservice"); - } - } - - public abstract void onServiceQueried(); - - /** - * Should be used by classes which implement the OnSeekBarChanged interface. - */ - public float onSeekBarProgressChanged(SeekBar seekBar, int progress, - boolean fromUser, TextView txtvPosition) { - if (fromUser && playbackService != null) { - float prog = progress / ((float) seekBar.getMax()); - int duration = media.getDuration(); - txtvPosition.setText(Converter - .getDurationStringLong((int) (prog * duration))); - return prog; - } - return 0; - - } - - /** - * Should be used by classes which implement the OnSeekBarChanged interface. - */ - public void onSeekBarStartTrackingTouch(SeekBar seekBar) { - // interrupt position Observer, restart later - cancelPositionObserver(); - } - - /** - * Should be used by classes which implement the OnSeekBarChanged interface. - */ - public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) { - if (playbackService != null) { - playbackService.seek((int) (prog * media.getDuration())); - setupPositionObserver(); - } - } - - public OnClickListener newOnPlayButtonClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (playbackService != null) { - switch (status) { - case PLAYING: - playbackService.pause(true, reinitOnPause); - break; - case PAUSED: - case PREPARED: - playbackService.play(); - break; - case PREPARING: - playbackService.setStartWhenPrepared(!playbackService - .isStartWhenPrepared()); - if (reinitOnPause - && playbackService.isStartWhenPrepared() == false) { - playbackService.reinit(); - } - break; - case INITIALIZED: - playbackService.setStartWhenPrepared(true); - playbackService.prepare(); - break; - } - } else { - Log.w(TAG, - "Play/Pause button was pressed, but playbackservice was null!"); - } - } - - }; - } - - public OnClickListener newOnRevButtonClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (status == PlayerStatus.PLAYING) { - playbackService.seekDelta(-DEFAULT_SEEK_DELTA); - } - } - }; - } - - public OnClickListener newOnFFButtonClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (status == PlayerStatus.PLAYING) { - playbackService.seekDelta(DEFAULT_SEEK_DELTA); - } - } - }; - } - - public boolean serviceAvailable() { - return playbackService != null; - } - - public int getPosition() { - if (playbackService != null) { - return playbackService.getCurrentPositionSafe(); - } else { - return PlaybackService.INVALID_TIME; - } - } - - public int getDuration() { - if (playbackService != null) { - return playbackService.getDurationSafe(); - } else { - return PlaybackService.INVALID_TIME; - } - } - - public Playable getMedia() { - return media; - } - - public boolean sleepTimerActive() { - return playbackService != null && playbackService.sleepTimerActive(); - } - - public boolean sleepTimerNotActive() { - return playbackService != null && !playbackService.sleepTimerActive(); - } - - public void disableSleepTimer() { - if (playbackService != null) { - playbackService.disableSleepTimer(); - } - } - - public long getSleepTimerTimeLeft() { - if (playbackService != null) { - return playbackService.getSleepTimerTimeLeft(); - } else { - return INVALID_TIME; - } - } - - public void setSleepTimer(long time) { - if (playbackService != null) { - playbackService.setSleepTimer(time); - } - } - - public void seekToChapter(Chapter chapter) { - if (playbackService != null) { - playbackService.seekToChapter(chapter); - } - } - - public void setVideoSurface(SurfaceHolder holder) { - if (playbackService != null) { - playbackService.setVideoSurface(holder); - } - } - - public PlayerStatus getStatus() { - return status; - } - - public boolean isPlayingVideo() { - if (playbackService != null) { - return PlaybackService.isPlayingVideo(); - } - return false; - } - - /** - * Returns true if PlaybackController can communicate with the playback - * service. - */ - public boolean isConnectedToPlaybackService() { - return playbackService != null; - } - - public void notifyVideoSurfaceAbandoned() { - if (playbackService != null) { - playbackService.notifyVideoSurfaceAbandoned(); - } - } - - /** Move service into INITIALIZED state if it's paused to save bandwidth */ - public void reinitServiceIfPaused() { - if (playbackService != null - && playbackService.isShouldStream() - && (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService - .getStatus() == PlayerStatus.PREPARING && playbackService - .isStartWhenPrepared() == false))) { - playbackService.reinit(); - } - } - - /** Refreshes the current position of the media file that is playing. */ - public class MediaPositionObserver implements Runnable { - - public static final int WAITING_INTERVALL = 1000; - - @Override - public void run() { - if (playbackService != null && playbackService.getPlayer() != null - && playbackService.getPlayer().isPlaying()) { - activity.runOnUiThread(new Runnable() { - - @Override - public void run() { - onPositionObserverUpdate(); - } - }); - } - } - } + private Activity activity; + + private PlaybackService playbackService; + private Playable media; + private PlayerStatus status; + + private ScheduledThreadPoolExecutor schedExecutor; + private static final int SCHED_EX_POOLSIZE = 1; + + protected MediaPositionObserver positionObserver; + protected ScheduledFuture positionObserverFuture; + + private boolean mediaInfoLoaded = false; + private boolean released = false; + + /** + * True if controller should reinit playback service if 'pause' button is + * pressed. + */ + private boolean reinitOnPause; + + public PlaybackController(Activity activity, boolean reinitOnPause) { + this.activity = activity; + this.reinitOnPause = reinitOnPause; + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE, + new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }, new RejectedExecutionHandler() { + + @Override + public void rejectedExecution(Runnable r, + ThreadPoolExecutor executor) { + Log.w(TAG, + "Rejected execution of runnable in schedExecutor"); + } + } + ); + } + + /** + * Creates a new connection to the playbackService. Should be called in the + * activity's onResume() method. + */ + public void init() { + activity.registerReceiver(statusUpdate, new IntentFilter( + PlaybackService.ACTION_PLAYER_STATUS_CHANGED)); + + activity.registerReceiver(notificationReceiver, new IntentFilter( + PlaybackService.ACTION_PLAYER_NOTIFICATION)); + + activity.registerReceiver(shutdownReceiver, new IntentFilter( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + + if (!released) { + bindToService(); + } else { + throw new IllegalStateException( + "Can't call init() after release() has been called"); + } + } + + /** + * Should be called if the PlaybackController is no longer needed, for + * example in the activity's onStop() method. + */ + public void release() { + if (AppConfig.DEBUG) + Log.d(TAG, "Releasing PlaybackController"); + + try { + activity.unregisterReceiver(statusUpdate); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unregisterReceiver(notificationReceiver); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unbindService(mConnection); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unregisterReceiver(shutdownReceiver); + } catch (IllegalArgumentException e) { + // ignore + } + cancelPositionObserver(); + schedExecutor.shutdownNow(); + media = null; + released = true; + + } + + /** + * Should be called in the activity's onPause() method. + */ + public void pause() { + mediaInfoLoaded = false; + if (playbackService != null && playbackService.isPlayingVideo()) { + playbackService.pause(true, true); + } + } + + /** + * Tries to establish a connection to the PlaybackService. If it isn't + * running, the PlaybackService will be started with the last played media + * as the arguments of the launch intent. + */ + private void bindToService() { + if (AppConfig.DEBUG) + Log.d(TAG, "Trying to connect to service"); + AsyncTask<Void, Void, Intent> intentLoader = new AsyncTask<Void, Void, Intent>() { + @Override + protected Intent doInBackground(Void... voids) { + return getPlayLastPlayedMediaIntent(); + } + + @Override + protected void onPostExecute(Intent serviceIntent) { + boolean bound = false; + if (!PlaybackService.isRunning) { + if (serviceIntent != null) { + activity.startService(serviceIntent); + bound = activity.bindService(serviceIntent, mConnection, 0); + } else { + status = PlayerStatus.STOPPED; + setupGUI(); + handleStatus(); + } + } else { + if (AppConfig.DEBUG) + Log.d(TAG, + "PlaybackService is running, trying to connect without start command."); + bound = activity.bindService(new Intent(activity, + PlaybackService.class), mConnection, 0); + } + if (AppConfig.DEBUG) + Log.d(TAG, "Result for service binding: " + bound); + } + }; + intentLoader.execute(); + } + + /** + * Returns an intent that starts the PlaybackService and plays the last + * played media or null if no last played media could be found. + */ + private Intent getPlayLastPlayedMediaIntent() { + if (AppConfig.DEBUG) + Log.d(TAG, "Trying to restore last played media"); + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(activity.getApplicationContext()); + long currentlyPlayingMedia = PlaybackPreferences + .getCurrentlyPlayingMedia(); + if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) { + Playable media = PlayableUtils.createInstanceFromPreferences(activity, + (int) currentlyPlayingMedia, prefs); + if (media != null) { + Intent serviceIntent = new Intent(activity, + PlaybackService.class); + serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + serviceIntent.putExtra( + PlaybackService.EXTRA_START_WHEN_PREPARED, false); + serviceIntent.putExtra( + PlaybackService.EXTRA_PREPARE_IMMEDIATELY, false); + boolean fileExists = media.localFileAvailable(); + boolean lastIsStream = PlaybackPreferences + .getCurrentEpisodeIsStream(); + if (!fileExists && !lastIsStream && media instanceof FeedMedia) { + DBTasks.notifyMissingFeedMediaFile( + activity, (FeedMedia) media); + } + serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, + lastIsStream || !fileExists); + return serviceIntent; + } + } + if (AppConfig.DEBUG) + Log.d(TAG, "No last played media found"); + return null; + } + + public abstract void setupGUI(); + + private void setupPositionObserver() { + if ((positionObserverFuture != null && positionObserverFuture + .isCancelled()) + || (positionObserverFuture != null && positionObserverFuture + .isDone()) || positionObserverFuture == null) { + + if (AppConfig.DEBUG) + Log.d(TAG, "Setting up position observer"); + positionObserver = new MediaPositionObserver(); + positionObserverFuture = schedExecutor.scheduleWithFixedDelay( + positionObserver, MediaPositionObserver.WAITING_INTERVALL, + MediaPositionObserver.WAITING_INTERVALL, + TimeUnit.MILLISECONDS); + } + } + + private void cancelPositionObserver() { + if (positionObserverFuture != null) { + boolean result = positionObserverFuture.cancel(true); + if (AppConfig.DEBUG) + Log.d(TAG, "PositionObserver cancelled. Result: " + result); + } + } + + public abstract void onPositionObserverUpdate(); + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + playbackService = ((PlaybackService.LocalBinder) service) + .getService(); + + queryService(); + if (AppConfig.DEBUG) + Log.d(TAG, "Connection to Service established"); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + playbackService = null; + if (AppConfig.DEBUG) + Log.d(TAG, "Disconnected from Service"); + + } + }; + + protected BroadcastReceiver statusUpdate = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (AppConfig.DEBUG) + Log.d(TAG, "Received statusUpdate Intent."); + if (isConnectedToPlaybackService()) { + status = playbackService.getStatus(); + handleStatus(); + } else { + Log.w(TAG, + "Couldn't receive status update: playbackService was null"); + bindToService(); + } + } + }; + + protected BroadcastReceiver notificationReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (isConnectedToPlaybackService()) { + 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: + float progress = ((float) code) / 100; + onBufferUpdate(progress); + break; + case PlaybackService.NOTIFICATION_TYPE_RELOAD: + cancelPositionObserver(); + mediaInfoLoaded = false; + onReloadNotification(intent.getIntExtra( + PlaybackService.EXTRA_NOTIFICATION_CODE, -1)); + queryService(); + + break; + case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE: + onSleepTimerUpdate(); + break; + case PlaybackService.NOTIFICATION_TYPE_BUFFER_START: + onBufferStart(); + break; + case PlaybackService.NOTIFICATION_TYPE_BUFFER_END: + onBufferEnd(); + break; + case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END: + onPlaybackEnd(); + break; + } + + } else { + if (AppConfig.DEBUG) + Log.d(TAG, "Bad arguments. Won't handle intent"); + } + } else { + bindToService(); + } + } + + }; + + private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (isConnectedToPlaybackService()) { + if (intent.getAction().equals( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { + release(); + onShutdownNotification(); + } + } + } + }; + + public abstract void onShutdownNotification(); + + /** + * Called when the currently displayed information should be refreshed. + */ + public abstract void onReloadNotification(int code); + + public abstract void onBufferStart(); + + public abstract void onBufferEnd(); + + public abstract void onBufferUpdate(float progress); + + public abstract void onSleepTimerUpdate(); + + public abstract void handleError(int code); + + public abstract void onPlaybackEnd(); + + /** + * Is called whenever the PlaybackService changes it's status. This method + * should be used to update the GUI or start/cancel background threads. + */ + private void handleStatus() { + TypedArray res = activity.obtainStyledAttributes(new int[]{ + R.attr.av_play, R.attr.av_pause}); + final int playResource = res.getResourceId(0, R.drawable.av_play); + final int pauseResource = res.getResourceId(1, R.drawable.av_pause); + res.recycle(); + + switch (status) { + + case ERROR: + postStatusMsg(R.string.player_error_msg); + break; + case PAUSED: + clearStatusMsg(); + checkMediaInfoLoaded(); + cancelPositionObserver(); + updatePlayButtonAppearance(playResource); + break; + case PLAYING: + clearStatusMsg(); + checkMediaInfoLoaded(); + setupPositionObserver(); + updatePlayButtonAppearance(pauseResource); + break; + case PREPARING: + postStatusMsg(R.string.player_preparing_msg); + checkMediaInfoLoaded(); + if (playbackService != null) { + if (playbackService.isStartWhenPrepared()) { + updatePlayButtonAppearance(pauseResource); + } else { + updatePlayButtonAppearance(playResource); + } + } + break; + case STOPPED: + postStatusMsg(R.string.player_stopped_msg); + break; + case PREPARED: + checkMediaInfoLoaded(); + postStatusMsg(R.string.player_ready_msg); + updatePlayButtonAppearance(playResource); + break; + case SEEKING: + postStatusMsg(R.string.player_seeking_msg); + break; + case AWAITING_VIDEO_SURFACE: + onAwaitingVideoSurface(); + break; + case INITIALIZED: + checkMediaInfoLoaded(); + clearStatusMsg(); + updatePlayButtonAppearance(playResource); + break; + } + } + + private void checkMediaInfoLoaded() { + if (!mediaInfoLoaded) { + loadMediaInfo(); + } + mediaInfoLoaded = true; + } + + private void updatePlayButtonAppearance(int resource) { + ImageButton butPlay = getPlayButton(); + butPlay.setImageResource(resource); + } + + public abstract ImageButton getPlayButton(); + + public abstract void postStatusMsg(int msg); + + public abstract void clearStatusMsg(); + + public abstract void loadMediaInfo(); + + public abstract void onAwaitingVideoSurface(); + + /** + * Called when connection to playback service has been established or + * information has to be refreshed + */ + void queryService() { + if (AppConfig.DEBUG) + Log.d(TAG, "Querying service info"); + if (playbackService != null) { + status = playbackService.getStatus(); + media = playbackService.getMedia(); + if (media == null) { + Log.w(TAG, + "PlaybackService has no media object. Trying to restore last played media."); + Intent serviceIntent = getPlayLastPlayedMediaIntent(); + if (serviceIntent != null) { + activity.startService(serviceIntent); + } + } + onServiceQueried(); + + setupGUI(); + handleStatus(); + // make sure that new media is loaded if it's available + mediaInfoLoaded = false; + + } else { + Log.e(TAG, + "queryService() was called without an existing connection to playbackservice"); + } + } + + public abstract void onServiceQueried(); + + /** + * Should be used by classes which implement the OnSeekBarChanged interface. + */ + public float onSeekBarProgressChanged(SeekBar seekBar, int progress, + boolean fromUser, TextView txtvPosition) { + if (fromUser && playbackService != null) { + float prog = progress / ((float) seekBar.getMax()); + int duration = media.getDuration(); + txtvPosition.setText(Converter + .getDurationStringLong((int) (prog * duration))); + return prog; + } + return 0; + + } + + /** + * Should be used by classes which implement the OnSeekBarChanged interface. + */ + public void onSeekBarStartTrackingTouch(SeekBar seekBar) { + // interrupt position Observer, restart later + cancelPositionObserver(); + } + + /** + * Should be used by classes which implement the OnSeekBarChanged interface. + */ + public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) { + if (playbackService != null) { + playbackService.seek((int) (prog * media.getDuration())); + setupPositionObserver(); + } + } + + public OnClickListener newOnPlayButtonClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (playbackService != null) { + switch (status) { + case PLAYING: + playbackService.pause(true, reinitOnPause); + break; + case PAUSED: + case PREPARED: + playbackService.play(); + break; + case PREPARING: + playbackService.setStartWhenPrepared(!playbackService + .isStartWhenPrepared()); + if (reinitOnPause + && playbackService.isStartWhenPrepared() == false) { + playbackService.reinit(); + } + break; + case INITIALIZED: + playbackService.setStartWhenPrepared(true); + playbackService.prepare(); + break; + } + } else { + Log.w(TAG, + "Play/Pause button was pressed, but playbackservice was null!"); + } + } + + }; + } + + public OnClickListener newOnRevButtonClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (status == PlayerStatus.PLAYING) { + playbackService.seekDelta(-DEFAULT_SEEK_DELTA); + } + } + }; + } + + public OnClickListener newOnFFButtonClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (status == PlayerStatus.PLAYING) { + playbackService.seekDelta(DEFAULT_SEEK_DELTA); + } + } + }; + } + + public boolean serviceAvailable() { + return playbackService != null; + } + + public int getPosition() { + if (playbackService != null) { + return playbackService.getCurrentPositionSafe(); + } else { + return PlaybackService.INVALID_TIME; + } + } + + public int getDuration() { + if (playbackService != null) { + return playbackService.getDurationSafe(); + } else { + return PlaybackService.INVALID_TIME; + } + } + + public Playable getMedia() { + return media; + } + + public boolean sleepTimerActive() { + return playbackService != null && playbackService.sleepTimerActive(); + } + + public boolean sleepTimerNotActive() { + return playbackService != null && !playbackService.sleepTimerActive(); + } + + public void disableSleepTimer() { + if (playbackService != null) { + playbackService.disableSleepTimer(); + } + } + + public long getSleepTimerTimeLeft() { + if (playbackService != null) { + return playbackService.getSleepTimerTimeLeft(); + } else { + return INVALID_TIME; + } + } + + public void setSleepTimer(long time) { + if (playbackService != null) { + playbackService.setSleepTimer(time); + } + } + + public void seekToChapter(Chapter chapter) { + if (playbackService != null) { + playbackService.seekToChapter(chapter); + } + } + + public void setVideoSurface(SurfaceHolder holder) { + if (playbackService != null) { + playbackService.setVideoSurface(holder); + } + } + + public PlayerStatus getStatus() { + return status; + } + + public boolean isPlayingVideo() { + if (playbackService != null) { + return PlaybackService.isPlayingVideo(); + } + return false; + } + + /** + * Returns true if PlaybackController can communicate with the playback + * service. + */ + public boolean isConnectedToPlaybackService() { + return playbackService != null; + } + + public void notifyVideoSurfaceAbandoned() { + if (playbackService != null) { + playbackService.notifyVideoSurfaceAbandoned(); + } + } + + /** + * Move service into INITIALIZED state if it's paused to save bandwidth + */ + public void reinitServiceIfPaused() { + if (playbackService != null + && playbackService.isShouldStream() + && (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService + .getStatus() == PlayerStatus.PREPARING && playbackService + .isStartWhenPrepared() == false))) { + playbackService.reinit(); + } + } + + /** + * Refreshes the current position of the media file that is playing. + */ + public class MediaPositionObserver implements Runnable { + + public static final int WAITING_INTERVALL = 1000; + + @Override + public void run() { + if (playbackService != null && playbackService.getPlayer() != null + && playbackService.getPlayer().isPlaying()) { + activity.runOnUiThread(new Runnable() { + + @Override + public void run() { + onPositionObserverUpdate(); + } + }); + } + } + } } diff --git a/src/instrumentationTest/de/test/antennapod/AntennaPodTestRunner.java b/src/instrumentationTest/de/test/antennapod/AntennaPodTestRunner.java new file mode 100644 index 000000000..7aaa14909 --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/AntennaPodTestRunner.java @@ -0,0 +1,18 @@ +package instrumentationTest.de.test.antennapod; + +import android.test.InstrumentationTestRunner; +import android.test.suitebuilder.TestSuiteBuilder; +import android.util.Log; + +import instrumentationTest.de.test.antennapod.service.download.HttpDownloaderTest; +import instrumentationTest.de.test.antennapod.util.FilenameGeneratorTest; +import junit.framework.TestSuite; + +public class AntennaPodTestRunner extends InstrumentationTestRunner { + + @Override + public TestSuite getAllTests() { + return new TestSuiteBuilder(AntennaPodTestRunner.class).includeAllPackagesUnderHere().build(); + //return new TestSuiteBuilder(AntennaPodTestRunner.class).includeAllPackagesUnderHere().excludePackages("instrumentationTest.de.test.antennapod.syndication.handler").build(); + } +} diff --git a/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java b/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java new file mode 100644 index 000000000..2cd5734d5 --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/service/download/HttpDownloaderTest.java @@ -0,0 +1,144 @@ +package instrumentationTest.de.test.antennapod.service.download; + +import java.io.File; + +import de.danoeh.antennapod.feed.FeedFile; +import de.danoeh.antennapod.service.download.*; + +import android.test.AndroidTestCase; +import android.util.Log; + +public class HttpDownloaderTest extends AndroidTestCase { + private static final String TAG = "HttpDownloaderTest"; + private static final String DOWNLOAD_DIR = "testdownloads"; + + private static boolean successful = true; + + public HttpDownloaderTest() { + super(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + File externalDir = getContext().getExternalFilesDir(DOWNLOAD_DIR); + assertNotNull(externalDir); + File[] contents = externalDir.listFiles(); + for (File f : contents) { + assertTrue(f.delete()); + } + } + + private FeedFileImpl setupFeedFile(String downloadUrl, String title) { + FeedFileImpl feedfile = new FeedFileImpl(downloadUrl); + String fileUrl = new File(getContext().getExternalFilesDir(DOWNLOAD_DIR).getAbsolutePath(), title).getAbsolutePath(); + File file = new File(fileUrl); + Log.d(TAG, "Deleting file: " + file.delete()); + feedfile.setFile_url(fileUrl); + return feedfile; + } + + private void download(String url, String title, boolean expectedResult) { + FeedFile feedFile = setupFeedFile(url, title); + DownloadRequest request = new DownloadRequest(feedFile.getFile_url(), url, title, 0, feedFile.getTypeAsInt()); + Downloader downloader = new HttpDownloader(request); + downloader.call(); + DownloadStatus status = downloader.getResult(); + assertNotNull(status); + assertTrue(status.isSuccessful() == expectedResult); + assertTrue(status.isDone()); + assertTrue(new File(feedFile.getFile_url()).exists()); + } + + public void testRandomUrls() { + final String[] urls = { + "http://radiobox.omroep.nl/programme/read_programme_podcast/9168/read.rss", + "http://content.zdf.de/podcast/zdf_heute/heute_a.xml", + "http://rss.sciam.com/sciam/60secsciencepodcast", + "http://rss.sciam.com/sciam/60-second-mind", + "http://rss.sciam.com/sciam/60-second-space", + "http://rss.sciam.com/sciam/60-second-health", + "http://rss.sciam.com/sciam/60-second-tech", + "http://risky.biz/feeds/risky-business", + "http://risky.biz/feeds/rb2", + "http://podcast.hr-online.de/lateline/podcast.xml", + "http://bitlove.org/nsemak/mikrodilettanten/feed", + "http://bitlove.org/moepmoeporg/riotburnz/feed", + "http://bitlove.org/moepmoeporg/schachcast/feed", + "http://bitlove.org/moepmoeporg/sundaymoaning/feed", + "http://bitlove.org/motofunk/anekdotkast/feed", + "http://bitlove.org/motofunk/motofunk/feed", + "http://bitlove.org/nerdinand/zch/feed", + "http://podcast.homerj.de/podcasts.xml", + "http://www.dradio.de/rss/podcast/sendungen/wissenschaftundbildung/", + "http://www.dradio.de/rss/podcast/sendungen/wirtschaftundverbraucher/", + "http://www.dradio.de/rss/podcast/sendungen/literatur/", + "http://www.dradio.de/rss/podcast/sendungen/sport/", + "http://www.dradio.de/rss/podcast/sendungen/wirtschaftundgesellschaft/", + "http://www.dradio.de/rss/podcast/sendungen/filmederwoche/", + "http://www.blacksweetstories.com/feed/podcast/", + "http://feeds.5by5.tv/buildanalyze", + "http://bitlove.org/ranzzeit/ranz/feed" + }; + for (int i = 0; i < urls.length; i++) { + download(urls[i], Integer.toString(i), true); + } + } + + public void testRedirect() { + download("http://httpbin.org/redirect/4", "testRedirect", true); + } + + public void testRelativeRedirect() { + download("http://httpbin.org/relative-redirect/4", "testRelativeRedirect", true); + } + + public void testGzip() { + download("http://httpbin.org/gzip", "testGzip", true); + } + + public void test404() { + download("http://httpbin.org/status/404", "test404", false); + } + + public void testCancel() { + final String url = "http://httpbin.org/delay/3"; + FeedFileImpl feedFile = setupFeedFile(url, "delay"); + final Downloader downloader = new HttpDownloader(new DownloadRequest(feedFile.getFile_url(), url, "delay", 0, feedFile.getTypeAsInt())); + Thread t = new Thread() { + @Override + public void run() { + downloader.call(); + } + }; + downloader.cancel(); + try { + t.join(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + DownloadStatus result = downloader.getResult(); + assertTrue(result.isDone()); + assertFalse(result.isSuccessful()); + assertTrue(result.isCancelled()); + assertFalse(new File(feedFile.getFile_url()).exists()); + } + + private static class FeedFileImpl extends FeedFile { + public FeedFileImpl(String download_url) { + super(null, download_url, false); + } + + + @Override + public String getHumanReadableIdentifier() { + return download_url; + } + + @Override + public int getTypeAsInt() { + return 0; + } + } + +} diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java new file mode 100644 index 000000000..0fb733b67 --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/storage/DBReaderTest.java @@ -0,0 +1,11 @@ +package instrumentationTest.de.test.antennapod.storage; + +import android.test.InstrumentationTestCase; + +/** + * Test class for DBReader + */ +public class DBReaderTest extends InstrumentationTestCase { + + +} diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java new file mode 100644 index 000000000..ef48b8d5f --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/storage/DBTasksTest.java @@ -0,0 +1,9 @@ +package instrumentationTest.de.test.antennapod.storage; + +import android.test.InstrumentationTestCase; + +/** + * Test class for DBTasks + */ +public class DBTasksTest extends InstrumentationTestCase { +} diff --git a/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java b/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java new file mode 100644 index 000000000..dbec84370 --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/storage/DBWriterTest.java @@ -0,0 +1,473 @@ +package instrumentationTest.de.test.antennapod.storage; + +import android.content.Context; +import android.database.Cursor; +import android.test.InstrumentationTestCase; +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.storage.DBReader; +import de.danoeh.antennapod.storage.DBWriter; +import de.danoeh.antennapod.storage.PodDBAdapter; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Test class for DBWriter + */ +public class DBWriterTest extends InstrumentationTestCase { + private static final String TEST_FOLDER = "testDBWriter"; + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + final Context context = getInstrumentation().getTargetContext(); + context.deleteDatabase(PodDBAdapter.DATABASE_NAME); + + File testDir = context.getExternalFilesDir(TEST_FOLDER); + assertNotNull(testDir); + for (File f : testDir.listFiles()) { + f.delete(); + } + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + final Context context = getInstrumentation().getTargetContext(); + context.deleteDatabase(PodDBAdapter.DATABASE_NAME); + // make sure database is created + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.close(); + } + + public void testDeleteFeedMediaOfItemFileExists() throws IOException, ExecutionException, InterruptedException { + File dest = new File(getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER), "testFile"); + + assertTrue(dest.createNewFile()); + + Feed feed = new Feed("url", new Date(), "title"); + List<FeedItem> items = new ArrayList<FeedItem>(); + feed.setItems(items); + FeedItem item = new FeedItem(); + item.setTitle("title"); + item.setPubDate(new Date()); + item.setFeed(feed); + + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", dest.getAbsolutePath(), "download_url", true, null); + item.setMedia(media); + + items.add(item); + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + assertTrue(media.getId() != 0); + assertTrue(item.getId() != 0); + + DBWriter.deleteFeedMediaOfItem(getInstrumentation().getTargetContext(), media.getId()).get(); + media = DBReader.getFeedMedia(getInstrumentation().getTargetContext(), media.getId()); + assertNotNull(media); + assertFalse(dest.exists()); + assertFalse(media.isDownloaded()); + assertNull(media.getFile_url()); + } + + public void testDeleteFeedMediaOfItemFileDoesNotExists() throws IOException, ExecutionException, InterruptedException { + File dest = new File(getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER), "testFile"); + + Feed feed = new Feed("url", new Date(), "title"); + List<FeedItem> items = new ArrayList<FeedItem>(); + feed.setItems(items); + FeedItem item = new FeedItem(); + item.setTitle("title"); + item.setPubDate(new Date()); + item.setFeed(feed); + + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", dest.getAbsolutePath(), "download_url", false, null); + item.setMedia(media); + + items.add(item); + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getTargetContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + assertTrue(media.getId() != 0); + assertTrue(item.getId() != 0); + + DBWriter.deleteFeedMediaOfItem(getInstrumentation().getTargetContext(), media.getId()).get(); + media = DBReader.getFeedMedia(getInstrumentation().getTargetContext(), media.getId()); + assertNotNull(media); + assertFalse(dest.exists()); + assertFalse(media.isDownloaded()); + assertNull(media.getFile_url()); + } + + public void testDeleteFeed() throws IOException, ExecutionException, InterruptedException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed ("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + + // create Feed image + File imgFile = new File(destFolder, "image"); + assertTrue(imgFile.createNewFile()); + FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true); + image.setFeed(feed); + feed.setImage(image); + + List<File> itemFiles = new ArrayList<File>(); + // create items with downloaded media files + for (int i = 0; i < 10; i++) { + FeedItem item = new FeedItem(); + item.setTitle("Item " + i); + item.setPubDate(new Date(System.currentTimeMillis())); + item.setFeed(feed); + feed.getItems().add(item); + + File enc = new File(destFolder, "file " + i); + assertTrue(enc.createNewFile()); + itemFiles.add(enc); + + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null); + item.setMedia(media); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + assertTrue(feed.getImage().getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + assertTrue(item.getMedia().getId() != 0); + } + + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(5, TimeUnit.SECONDS); + + // check if files still exist + assertFalse(imgFile.exists()); + for (File f : itemFiles) { + assertFalse(f.exists()); + } + + adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageOfFeedCursor(image.getId()); + assertTrue(c.getCount() == 0); + c.close(); + for (FeedItem item : feed.getItems()) { + c = adapter.getFeedItemCursor(String.valueOf(item.getId())); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getSingleFeedMediaCursor(item.getMedia().getId()); + assertTrue(c.getCount() == 0); + c.close(); + } + } + + public void testDeleteFeedNoImage() throws ExecutionException, InterruptedException, IOException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed ("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + + feed.setImage(null); + + List<File> itemFiles = new ArrayList<File>(); + // create items with downloaded media files + for (int i = 0; i < 10; i++) { + FeedItem item = new FeedItem(); + item.setTitle("Item " + i); + item.setPubDate(new Date(System.currentTimeMillis())); + item.setFeed(feed); + feed.getItems().add(item); + + File enc = new File(destFolder, "file " + i); + assertTrue(enc.createNewFile()); + itemFiles.add(enc); + + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", true, null); + item.setMedia(media); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + assertTrue(item.getMedia().getId() != 0); + } + + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(5, TimeUnit.SECONDS); + + // check if files still exist + for (File f : itemFiles) { + assertFalse(f.exists()); + } + + adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + for (FeedItem item : feed.getItems()) { + c = adapter.getFeedItemCursor(String.valueOf(item.getId())); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getSingleFeedMediaCursor(item.getMedia().getId()); + assertTrue(c.getCount() == 0); + c.close(); + } + } + + public void testDeleteFeedNoItems() throws IOException, ExecutionException, InterruptedException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed ("url", new Date(), "title"); + feed.setItems(null); + + // create Feed image + File imgFile = new File(destFolder, "image"); + assertTrue(imgFile.createNewFile()); + FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true); + image.setFeed(feed); + feed.setImage(image); + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + assertTrue(feed.getImage().getId() != 0); + + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(5, TimeUnit.SECONDS); + + // check if files still exist + assertFalse(imgFile.exists()); + + adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageOfFeedCursor(image.getId()); + assertTrue(c.getCount() == 0); + c.close(); + } + + public void testDeleteFeedNoFeedMedia() throws IOException, ExecutionException, InterruptedException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed ("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + + // create Feed image + File imgFile = new File(destFolder, "image"); + assertTrue(imgFile.createNewFile()); + FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true); + image.setFeed(feed); + feed.setImage(image); + + // create items + for (int i = 0; i < 10; i++) { + FeedItem item = new FeedItem(); + item.setTitle("Item " + i); + item.setPubDate(new Date(System.currentTimeMillis())); + item.setFeed(feed); + feed.getItems().add(item); + + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + assertTrue(feed.getImage().getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + } + + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(5, TimeUnit.SECONDS); + + // check if files still exist + assertFalse(imgFile.exists()); + + adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageOfFeedCursor(image.getId()); + assertTrue(c.getCount() == 0); + c.close(); + for (FeedItem item : feed.getItems()) { + c = adapter.getFeedItemCursor(String.valueOf(item.getId())); + assertTrue(c.getCount() == 0); + c.close(); + } + } + + public void testDeleteFeedWithQueueItems() throws ExecutionException, InterruptedException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed ("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + + // create Feed image + File imgFile = new File(destFolder, "image"); + FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true); + image.setFeed(feed); + feed.setImage(image); + + List<File> itemFiles = new ArrayList<File>(); + // create items with downloaded media files + for (int i = 0; i < 10; i++) { + FeedItem item = new FeedItem(); + item.setTitle("Item " + i); + item.setPubDate(new Date(System.currentTimeMillis())); + item.setFeed(feed); + feed.getItems().add(item); + + File enc = new File(destFolder, "file " + i); + itemFiles.add(enc); + + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null); + item.setMedia(media); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + assertTrue(feed.getImage().getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + assertTrue(item.getMedia().getId() != 0); + } + + + List<FeedItem> queue = new ArrayList<FeedItem>(); + queue.addAll(feed.getItems()); + adapter.open(); + adapter.setQueue(queue); + + Cursor queueCursor = adapter.getQueueIDCursor(); + assertTrue(queueCursor.getCount() == queue.size()); + queueCursor.close(); + + adapter.close(); + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(5, TimeUnit.SECONDS); + adapter.open(); + + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageOfFeedCursor(image.getId()); + assertTrue(c.getCount() == 0); + c.close(); + for (FeedItem item : feed.getItems()) { + c = adapter.getFeedItemCursor(String.valueOf(item.getId())); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getSingleFeedMediaCursor(item.getMedia().getId()); + assertTrue(c.getCount() == 0); + c.close(); + } + c = adapter.getQueueCursor(); + assertTrue(c.getCount() == 0); + c.close(); + adapter.close(); + } + + public void testDeleteFeedNoDownloadedFiles() throws ExecutionException, InterruptedException, TimeoutException { + File destFolder = getInstrumentation().getTargetContext().getExternalFilesDir(TEST_FOLDER); + assertNotNull(destFolder); + + Feed feed = new Feed ("url", new Date(), "title"); + feed.setItems(new ArrayList<FeedItem>()); + + // create Feed image + File imgFile = new File(destFolder, "image"); + FeedImage image = new FeedImage(0, "image", imgFile.getAbsolutePath(), "url", true); + image.setFeed(feed); + feed.setImage(image); + + List<File> itemFiles = new ArrayList<File>(); + // create items with downloaded media files + for (int i = 0; i < 10; i++) { + FeedItem item = new FeedItem(); + item.setTitle("Item " + i); + item.setPubDate(new Date(System.currentTimeMillis())); + item.setFeed(feed); + feed.getItems().add(item); + + File enc = new File(destFolder, "file " + i); + itemFiles.add(enc); + + FeedMedia media = new FeedMedia(0, item, 1, 1, 1, "mime_type", enc.getAbsolutePath(), "download_url", false, null); + item.setMedia(media); + } + + PodDBAdapter adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + adapter.setCompleteFeed(feed); + adapter.close(); + + assertTrue(feed.getId() != 0); + assertTrue(feed.getImage().getId() != 0); + for (FeedItem item : feed.getItems()) { + assertTrue(item.getId() != 0); + assertTrue(item.getMedia().getId() != 0); + } + + DBWriter.deleteFeed(getInstrumentation().getTargetContext(), feed.getId()).get(5, TimeUnit.SECONDS); + + adapter = new PodDBAdapter(getInstrumentation().getContext()); + adapter.open(); + Cursor c = adapter.getFeedCursor(feed.getId()); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getImageOfFeedCursor(image.getId()); + assertTrue(c.getCount() == 0); + c.close(); + for (FeedItem item : feed.getItems()) { + c = adapter.getFeedItemCursor(String.valueOf(item.getId())); + assertTrue(c.getCount() == 0); + c.close(); + c = adapter.getSingleFeedMediaCursor(item.getMedia().getId()); + assertTrue(c.getCount() == 0); + c.close(); + } + } +} diff --git a/src/instrumentationTest/de/test/antennapod/syndication/handler/FeedHandlerTest.java b/src/instrumentationTest/de/test/antennapod/syndication/handler/FeedHandlerTest.java new file mode 100644 index 000000000..030df0fcb --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/syndication/handler/FeedHandlerTest.java @@ -0,0 +1,170 @@ +package instrumentationTest.de.test.antennapod.syndication.handler; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Date; + +import android.test.AndroidTestCase; +import android.util.Log; +import de.danoeh.antennapod.feed.Feed; +import de.danoeh.antennapod.feed.FeedItem; +import de.danoeh.antennapod.syndication.handler.FeedHandler; + +/** Enqueues a list of Feeds and tests if they are parsed correctly */ +public class FeedHandlerTest extends AndroidTestCase { + private static final String TAG = "FeedHandlerTest"; + private static final String FEEDS_DIR = "testfeeds"; + + private ArrayList<Feed> feeds; + + protected void setUp() throws Exception { + super.setUp(); + feeds = new ArrayList<Feed>(); + for (int i = 0; i < TestFeeds.urls.length; i++) { + Feed f = new Feed(TestFeeds.urls[i], new Date()); + f.setFile_url(new File(getContext().getExternalFilesDir(FEEDS_DIR) + .getAbsolutePath(), "R" + i).getAbsolutePath()); + feeds.add(f); + } + } + + private InputStream getInputStream(String url) + throws MalformedURLException, IOException { + HttpURLConnection connection = (HttpURLConnection) (new URL(url)) + .openConnection(); + int rc = connection.getResponseCode(); + if (rc == HttpURLConnection.HTTP_OK) { + return connection.getInputStream(); + } else { + return null; + } + } + + private boolean downloadFeed(Feed feed) throws IOException { + int num_retries = 20; + boolean successful = false; + + for (int i = 0; i < num_retries; i++) { + InputStream in = null; + BufferedOutputStream out = null; + try { + in = getInputStream(feed.getDownload_url()); + if (in == null) { + return false; + } + out = new BufferedOutputStream(new FileOutputStream( + feed.getFile_url())); + byte[] buffer = new byte[8 * 1024]; + int count = 0; + while ((count = in.read(buffer)) != -1) { + out.write(buffer, 0, count); + } + out.flush(); + successful = true; + return true; + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (in != null) { + in.close(); + } + if (out != null) { + out.close(); + } + if (successful) { + break; + } + } + } + if (!successful) { + Log.e(TAG, "Download failed after " + num_retries + " retries"); + throw new IOException(); + } else { + return true; + } + } + + private boolean isFeedValid(Feed feed) { + Log.i(TAG, "Checking if " + feed.getDownload_url() + " is valid"); + boolean result = false; + if (feed.getTitle() == null) { + Log.e(TAG, "Feed has no title"); + return false; + } + if (!hasValidFeedItems(feed)) { + Log.e(TAG, "Feed has invalid items"); + return false; + } + if (feed.getLink() == null) { + Log.e(TAG, "Feed has no link"); + return false; + } + if (feed.getLink() != null && feed.getLink().length() == 0) { + Log.e(TAG, "Feed has empty link"); + return false; + } + if (feed.getIdentifyingValue() == null) { + Log.e(TAG, "Feed has no identifying value"); + return false; + } + if (feed.getIdentifyingValue() != null + && feed.getIdentifyingValue().length() == 0) { + Log.e(TAG, "Feed has empty identifying value"); + return false; + } + return true; + } + + private boolean hasValidFeedItems(Feed feed) { + for (FeedItem item : feed.getItemsArray()) { + if (item.getTitle() == null) { + Log.e(TAG, "Item has no title"); + return false; + } + } + return true; + } + + public void testParseFeeds() { + Log.i(TAG, "Testing RSS feeds"); + while (!feeds.isEmpty()) { + Feed feed = feeds.get(0); + parseFeed(feed); + feeds.remove(0); + } + + Log.i(TAG, "RSS Test completed"); + } + + private void parseFeed(Feed feed) { + try { + Log.i(TAG, "Testing feed with url " + feed.getDownload_url()); + FeedHandler handler = new FeedHandler(); + if (downloadFeed(feed)) { + handler.parseFeed(feed); + assertTrue(isFeedValid(feed)); + } + } catch (Exception e) { + Log.e(TAG, "Error when trying to test " + feed.getDownload_url()); + e.printStackTrace(); + } + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + for (Feed feed : feeds) { + File f = new File(feed.getFile_url()); + f.delete(); + } + } + +} diff --git a/src/instrumentationTest/de/test/antennapod/syndication/handler/TestFeeds.java b/src/instrumentationTest/de/test/antennapod/syndication/handler/TestFeeds.java new file mode 100644 index 000000000..61990cccb --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/syndication/handler/TestFeeds.java @@ -0,0 +1,346 @@ +package instrumentationTest.de.test.antennapod.syndication.handler; + +public class TestFeeds { + + public static final String[] urls = { + "http://savoirsenmultimedia.ens.fr/podcast.php?id=30", + "http://bitlove.org/apollo40/ps3newsroom/feed", + "http://bitlove.org/beapirate/hauptstadtpiraten/feed", + "http://bitlove.org/benni/besondereumstaende/feed", + "http://bitlove.org/bennihahn/unicast/feed", + "http://bitlove.org/berndbloggt/wirschweifenab/feed", + "http://bitlove.org/bildungsangst/spoiler-alert-mp3/feed", + "http://bitlove.org/bildungsangst/spoiler-alert-ogg/feed", + "http://bitlove.org/bildungsangst/troja-alert-mp3/feed", + "http://bitlove.org/bildungsangst/troja-alert-ogg/feed", + "http://bitlove.org/binaergewitter/talk/feed", + "http://bitlove.org/binaergewitter/talk-ogg/feed", + "http://bitlove.org/bitgamers/bitgamerscast/feed", + "http://bitlove.org/boingsworld/boingsworld/feed", + "http://bitlove.org/boris/bam/feed", + "http://bitlove.org/bruhndsoweiter/anycast-aac/feed", + "http://bitlove.org/bruhndsoweiter/anycast-mp3/feed", + "http://bitlove.org/bruhndsoweiter/schnackennet-m4a/feed", + "http://bitlove.org/bruhndsoweiter/schnackennet-mp3/feed", + "http://bitlove.org/bruhndsoweiter/schnackennet-ogg/feed", + "http://bitlove.org/byteweise/bytecast/feed", + "http://bitlove.org/byteweise/byteweiseaac/feed", + "http://bitlove.org/c3d2/news/feed", + "http://bitlove.org/c3d2/pentacast/feed", + "http://bitlove.org/c3d2/pentamedia/feed", + "http://bitlove.org/c3d2/pentamusic/feed", + "http://bitlove.org/c3d2/pentaradio/feed", + "http://bitlove.org/campuscast_cc/campuscast_cc/feed", + "http://bitlove.org/carlito/schnittmuster/feed", + "http://bitlove.org/carlito/yaycomics/feed", + "http://bitlove.org/cccb/chaosradio/feed", + "http://bitlove.org/ccculm/chaosseminar-mp4-high/feed", + "http://bitlove.org/ccculm/chaosseminar-theora/feed", + "http://bitlove.org/channelcast/channelcast/feed", + "http://bitlove.org/chgrasse/freequency/feed", + "http://bitlove.org/chgrasse/kiezradio/feed", + "http://bitlove.org/christiansteiner/secondunit/feed", + "http://bitlove.org/cinext/cinext/feed", + "http://bitlove.org/ckater/schoeneecken/feed", + "http://bitlove.org/ckater/schoeneecken-mp3/feed", + "http://bitlove.org/cocoaheads/austria/feed", + "http://bitlove.org/compod/compod/feed", + "http://bitlove.org/consolmedia/consolpodcast/feed", + "http://bitlove.org/couchblog/computerfix/feed", + "http://bitlove.org/culinaricast/podcast/feed", + "http://bitlove.org/d3v/die-drei-vogonen/feed", + "http://bitlove.org/danielbuechele/luftpost/feed", + "http://bitlove.org/deimhart/mp3/feed", + "http://bitlove.org/deimhart/ogg/feed", + "http://bitlove.org/derbastard/podcast/feed", + "http://bitlove.org/derpoppe/poppeandpeople/feed", + "http://bitlove.org/derpoppe/stammtischphilosophen/feed", + "http://bitlove.org/devradio/devradio-music-mp3/feed", + "http://bitlove.org/devradio/devradio-music-ogg/feed", + "http://bitlove.org/devradio/devradio-nomusic-mp3/feed", + "http://bitlove.org/devradio/devradio-nomusic-ogg/feed", + "http://bitlove.org/die-halde/die-halde/feed", + "http://bitlove.org/dirtyminutesleft/m4a/feed", + "http://bitlove.org/dirtyminutesleft/mp3/feed", + "http://bitlove.org/dominik/knutsens/feed", + "http://bitlove.org/dominik/schnittchen/feed", + "http://bitlove.org/driveeo/podcast/feed", + "http://bitlove.org/einfachben/freibeuterhafen/feed", + "http://bitlove.org/eintr8podcast/aac/feed", + "http://bitlove.org/eintr8podcast/eptv/feed", + "http://bitlove.org/eintr8podcast/mp3/feed", + "http://bitlove.org/eteubert/satoripress-m4a/feed", + "http://bitlove.org/fabu/indie-fresse/feed", + "http://bitlove.org/faldrian/bofh-mp3/feed", + "http://bitlove.org/faldrian/bofh-oga/feed", + "http://bitlove.org/faldrian/faldriansfeierabend/feed", + "http://bitlove.org/filmtonpodcast/filmtonpodcast/feed", + "http://bitlove.org/firmadorsch/fahrradio/feed", + "http://bitlove.org/frequenz9/feed/feed", + "http://bitlove.org/gamefusion/feeds/feed", + "http://bitlove.org/gamesandmacs/podcast/feed", + "http://bitlove.org/geekweek/techpodcast/feed", + "http://bitlove.org/germanstudent/apfelnet/feed", + "http://bitlove.org/germanstudent/bruellaffencouch-enhanced/feed", + "http://bitlove.org/germanstudent/bruellaffencouch-mp3/feed", + "http://bitlove.org/germanstudent/kauderwelschavantgarde/feed", + "http://bitlove.org/germanstudent/podccast-enhanced/feed", + "http://bitlove.org/germanstudent/podccast-mp3/feed", + "http://bitlove.org/geschichtendose/love/feed", + "http://bitlove.org/gfm/atzbach/feed", + "http://bitlove.org/gfm/rumsendende/feed", + "http://bitlove.org/grizze/vtlive/feed", + "http://bitlove.org/hackerfunk/hf-mp3/feed", + "http://bitlove.org/hackerfunk/hf-ogg/feed", + "http://bitlove.org/hasencore/podcast/feed", + "http://bitlove.org/hoaxmaster/hoaxilla/feed", + "http://bitlove.org/hoaxmaster/psychotalk/feed", + "http://bitlove.org/hoaxmaster/skeptoskop/feed", + "http://bitlove.org/hoersuppe/vorcast/feed", + "http://bitlove.org/holgi/wrint/feed", + "http://bitlove.org/ich-bin-radio/fir/feed", + "http://bitlove.org/ich-bin-radio/rsff/feed", + "http://bitlove.org/incerio/podcast/feed", + "http://bitlove.org/janlelis/rubykraut/feed", + "http://bitlove.org/jed/feed1/feed", + "http://bitlove.org/jupiterbroadcasting/coderradio/feed", + "http://bitlove.org/jupiterbroadcasting/fauxshowhd/feed", + "http://bitlove.org/jupiterbroadcasting/fauxshowmobile/feed", + "http://bitlove.org/jupiterbroadcasting/lashd/feed", + "http://bitlove.org/jupiterbroadcasting/lasmobile/feed", + "http://bitlove.org/jupiterbroadcasting/scibytehd/feed", + "http://bitlove.org/jupiterbroadcasting/scibytemobile/feed", + "http://bitlove.org/jupiterbroadcasting/techsnap60/feed", + "http://bitlove.org/jupiterbroadcasting/techsnapmobile/feed", + "http://bitlove.org/jupiterbroadcasting/unfilterhd/feed", + "http://bitlove.org/jupiterbroadcasting/unfiltermobile/feed", + "http://bitlove.org/kassettenkind/trollfunk/feed", + "http://bitlove.org/klangkammermedia/abgekuppelt/feed", + "http://bitlove.org/klangkammermedia/derbalkoncast/feed", + "http://bitlove.org/klangkammermedia/wortundstille/feed", + "http://bitlove.org/kurzpod/kurzpod/feed", + "http://bitlove.org/kurzpod/kurzpodm4a/feed", + "http://bitlove.org/kurzpod/ogg/feed", + "http://bitlove.org/langpod/lp/feed", + "http://bitlove.org/legrex/videli-noch/feed", + "http://bitlove.org/lisnewsnetcasts/listen/feed", + "http://bitlove.org/logenzuschlag/cinecast/feed", + "http://bitlove.org/maha/1337kultur/feed", + "http://bitlove.org/maha/klabautercast/feed", + "http://bitlove.org/map/fanboys/feed", + "http://bitlove.org/map/fanboys-mp3/feed", + "http://bitlove.org/map/retrozirkel/feed", + "http://bitlove.org/map/retrozirkel-mp3/feed", + "http://bitlove.org/markus/robotiklabor/feed", + "http://bitlove.org/martinschmidt/freiklettern/feed", + "http://bitlove.org/mespotine/mespotine_sessions/feed", + "http://bitlove.org/meszner/aether/feed", + "http://bitlove.org/meszner/kulturwissenschaften/feed", + "http://bitlove.org/metaebene/cre/feed", + "http://bitlove.org/metaebene/der-lautsprecher/feed", + "http://bitlove.org/metaebene/kolophon/feed", + "http://bitlove.org/metaebene/logbuch-netzpolitik/feed", + "http://bitlove.org/metaebene/mobilemacs/feed", + "http://bitlove.org/metaebene/newz-of-the-world/feed", + "http://bitlove.org/metaebene/not-safe-for-work/feed", + "http://bitlove.org/metaebene/raumzeit/feed", + "http://bitlove.org/metaebene/raumzeit-mp3/feed", + "http://bitlove.org/metagamer/metagamer/feed", + "http://bitlove.org/mfromm/collaborativerockers/feed", + "http://bitlove.org/mfromm/explorism/feed", + "http://bitlove.org/mfromm/transientesichten/feed", + "http://bitlove.org/mhpod/pofacs/feed", + "http://bitlove.org/mintcast/podcast/feed", + "http://bitlove.org/mitgezwitschert/brandung/feed", + "http://bitlove.org/moepmoeporg/anonnewsde/feed", + "http://bitlove.org/moepmoeporg/contentcast/feed", + "http://bitlove.org/moepmoeporg/dieseminarren/feed", + "http://bitlove.org/moepmoeporg/emcast/feed", + "http://bitlove.org/moepmoeporg/fhainalex/feed", + "http://bitlove.org/moepmoeporg/fruehstueck/feed", + "http://bitlove.org/moepmoeporg/galanoir/feed", + "http://bitlove.org/moepmoeporg/julespodcasts/feed", + "http://bitlove.org/moepmoeporg/knorkpod/feed", + "http://bitlove.org/moepmoeporg/lecast/feed", + "http://bitlove.org/moepmoeporg/moepspezial/feed", + "http://bitlove.org/moepmoeporg/podsprech/feed", + "http://bitlove.org/moepmoeporg/pottcast/feed", + "http://bitlove.org/moepmoeporg/riotburnz/feed", + "http://bitlove.org/moepmoeporg/schachcast/feed", + "http://bitlove.org/moepmoeporg/sundaymoaning/feed", + "http://bitlove.org/motofunk/anekdotkast/feed", + "http://bitlove.org/motofunk/motofunk/feed", + "http://bitlove.org/netzpolitik/netzpolitik-podcast/feed", + "http://bitlove.org/netzpolitik/netzpolitik-tv/feed", + "http://bitlove.org/nischenkultur/soziopod/feed", + "http://bitlove.org/nitramred/staatsbuergerkunde/feed", + "http://bitlove.org/nitramred/staatsbuergerkunde-mp3/feed", + "http://bitlove.org/nsemak/elementarfragen/feed", + "http://bitlove.org/nsemak/mikrodilettanten/feed", + "http://bitlove.org/oaad/oaad/feed", + "http://bitlove.org/ohrkrampfteam/ohr/feed", + "http://bitlove.org/omegatau/all/feed", + "http://bitlove.org/omegatau/english-only/feed", + "http://bitlove.org/omegatau/german-only/feed", + "http://bitlove.org/onatcer/oal/feed", + "http://bitlove.org/panikcast/panikcast/feed", + "http://bitlove.org/panxatony/macnemotv/feed", + "http://bitlove.org/pattex/megamagisch_m4a/feed", + "http://bitlove.org/pattex/megamagisch_mp3/feed", + "http://bitlove.org/pausengespraeche/pausengespraeche/feed", + "http://bitlove.org/pck/ez-und-geschlecht/feed", + "http://bitlove.org/pck/gender-kolleg-marburg/feed", + "http://bitlove.org/pck/genderzentrum_mr/feed", + "http://bitlove.org/pck/hh/feed", + "http://bitlove.org/pck/hh_mp3/feed", + "http://bitlove.org/pck/hh_ogg/feed", + "http://bitlove.org/pck/intercast/feed", + "http://bitlove.org/pck/peachnerdznohero/feed", + "http://bitlove.org/pck/peachnerdznohero_mp3/feed", + "http://bitlove.org/philip/einfach-schwul/feed", + "http://bitlove.org/philip/faselcast2/feed", + "http://bitlove.org/philipbanse/kuechenradio/feed", + "http://bitlove.org/philipbanse/medienradio/feed", + "http://bitlove.org/philipbanse/studienwahl_tv_audio/feed", + "http://bitlove.org/philipbanse/studienwahl_tv_video/feed", + "http://bitlove.org/podsafepilot/pmp/feed", + "http://bitlove.org/podsafepilot/psid/feed", + "http://bitlove.org/pratfm/strunt/feed", + "http://bitlove.org/pressrecord/podcast/feed", + "http://bitlove.org/qbi/datenkanal-mp3/feed", + "http://bitlove.org/qbi/datenkanal-ogg/feed", + "http://bitlove.org/quotidianitaet/quotidianitaet/feed", + "http://bitlove.org/radiotux/radiotux-all/feed", + "http://bitlove.org/randgruppenfunk/mediale_subkultur/feed", + "http://bitlove.org/ranzzeit/ranz/feed", + "http://bitlove.org/relet/lifeartificial_partone/feed", + "http://bitlove.org/retinacast/pilotenpruefung/feed", + "http://bitlove.org/retinacast/podcast/feed", + "http://bitlove.org/retinacast/podcast-aac/feed", + "http://bitlove.org/retinacast/retinauten/feed", + "http://bitlove.org/retinacast/rtc/feed", + "http://bitlove.org/revolutionarts/mehrspielerquote/feed", + "http://bitlove.org/ronsens/machtdose/feed", + "http://bitlove.org/rooby/fressefreiheit/feed", + "http://bitlove.org/rundumpodcast/rundum/feed", + "http://bitlove.org/ryuu/riesencast/feed", + "http://bitlove.org/ryuu/ryuus_labercast/feed", + "http://bitlove.org/schmalsprech/schmalsprech_m4a/feed", + "http://bitlove.org/schmalsprech/schmalsprech_mp3/feed", + "http://bitlove.org/schmidtlepp/houroflauer/feed", + "http://bitlove.org/schmidtlepp/lauerinformiert/feed", + "http://bitlove.org/sebastiansimon/wertungsfrei/feed", + "http://bitlove.org/sebseb7/vimeo/feed", + "http://bitlove.org/sirtomate/comichoehle/feed", + "http://bitlove.org/smartphone7/windowsphonepodcast/feed", + "http://bitlove.org/smcpodcast/feed/feed", + "http://bitlove.org/sneakpod/cocktailpodcast/feed", + "http://bitlove.org/sneakpod/sneakpod/feed", + "http://bitlove.org/socialhack/hoerensagen/feed", + "http://bitlove.org/socialhack/netzkinder/feed", + "http://bitlove.org/socialhack/netzkinder_mp3/feed", + "http://bitlove.org/socialhack/netzkinder_ogg/feed", + "http://bitlove.org/socialhack/talking_anthropology/feed", + "http://bitlove.org/sprechwaisen/sw/feed", + "http://bitlove.org/sublab/aboutradio/feed", + "http://bitlove.org/sysops/elektrisch/feed", + "http://bitlove.org/sysops/hd/feed", + "http://bitlove.org/taschencasts/taschencasts/feed", + "http://bitlove.org/tcmanila/ae-podcast/feed", + "http://bitlove.org/teezeit/kulturbuechse/feed", + "http://bitlove.org/teezeit/kulturbuechse-mp3/feed", + "http://bitlove.org/teezeit/teezeittalkradio/feed", + "http://bitlove.org/teezeit/teezeittalkradio-mp3/feed", + "http://bitlove.org/tinkengil/playtogether/feed", + "http://bitlove.org/tobi_s/alleswirdgut/feed", + "http://bitlove.org/toby/einschlafenenhanced/feed", + "http://bitlove.org/toby/einschlafenpodcast/feed", + "http://bitlove.org/toby/pubkameraden/feed", + "http://bitlove.org/toby/pubkameradenaac/feed", + "http://bitlove.org/tom/radioanstalt/feed", + "http://bitlove.org/tvallgaeu/beitraege/feed", + "http://bitlove.org/tvallgaeu/freizeit/feed", + "http://bitlove.org/tvallgaeu/sendung/feed", + "http://bitlove.org/ubahnverleih/teepodcast/feed", + "http://bitlove.org/umunsherum/sammelcast/feed", + "http://bitlove.org/umunsherum/spielonauten/feed", + "http://bitlove.org/umunsherum/unteruns/feed", + "http://bitlove.org/umunsherum/wasmachstdu/feed", + "http://bitlove.org/uwe/nettesfrettchen/feed", + "http://bitlove.org/webdev/wdr/feed", + "http://bitlove.org/weezerle/brandung/feed", + "http://bitlove.org/weezerle/guestcast/feed", + "http://bitlove.org/weezerle/stupalog/feed", + "http://bitlove.org/wikigeeks/mp3/feed", + "http://bitlove.org/workingdraft/revisionen/feed", + "http://bitlove.org/wunderlich/podcast/feed", + "http://www.guardian.co.uk/global-development/series/global-development-podcast/rss", + "http://rss.sciam.com/sciam/60secsciencepodcast", + "http://rss.sciam.com/sciam/60-second-mind", + "http://rss.sciam.com/sciam/60-second-space", + "http://rss.sciam.com/sciam/60-second-health", + "http://rss.sciam.com/sciam/60-second-tech", + "http://risky.biz/feeds/risky-business", + "http://risky.biz/feeds/rb2", + "http://podcast.hr-online.de/lateline/podcast.xml", + "http://bitlove.org/nsemak/mikrodilettanten/feed", + "http://bitlove.org/moepmoeporg/riotburnz/feed", + "http://bitlove.org/moepmoeporg/schachcast/feed", + "http://bitlove.org/moepmoeporg/sundaymoaning/feed", + "http://bitlove.org/motofunk/anekdotkast/feed", + "http://bitlove.org/motofunk/motofunk/feed", + "http://podcast.homerj.de/podcasts.xml", + "http://www.dradio.de/rss/podcast/sendungen/wissenschaftundbildung/", + "http://www.dradio.de/rss/podcast/sendungen/wirtschaftundverbraucher/", + "http://www.dradio.de/rss/podcast/sendungen/literatur/", + "http://www.dradio.de/rss/podcast/sendungen/sport/", + "http://www.dradio.de/rss/podcast/sendungen/wirtschaftundgesellschaft/", + "http://www.dradio.de/rss/podcast/sendungen/filmederwoche/", + "http://www.blacksweetstories.com/feed/podcast/", + "http://feeds.5by5.tv/buildanalyze", + "http://bitlove.org/ranzzeit/ranz/feed", + "http://bitlove.org/importthis/mp3/feed", + "http://bitlove.org/astro/youtube/feed", + "http://bitlove.org/channelcast/channelcast/feed", + "http://bitlove.org/cccb/chaosradio/feed", + "http://bitlove.org/astro/bitlove-show/feed", + "http://feeds.thisamericanlife.org/talpodcast", + "http://www.casasola.de/137b/1337motiv/1337motiv.xml", + "http://alternativlos.org/ogg.rss", "http://www.bitsundso.de/feed", + "http://www.gamesundso.de/feed/", + "http://feeds.feedburner.com/cre-podcast", + "http://feeds.feedburner.com/NotSafeForWorkPodcast", + "http://feeds.feedburner.com/mobile-macs-podcast", + "http://www.gamesundso.de/feed/", + "http://feeds.feedburner.com/DerLautsprecher", + "http://feeds.feedburner.com/raumzeit-podcast", + "http://feeds.feedburner.com/TheLunaticFringe", + "http://feeds.feedburner.com/Kuechenradioorg", + "http://feeds.feedburner.com/Medienradio_Podcast_RSS", + "http://feeds.feedburner.com/wrint/wrint", + "http://retrozirkel.de/episodes.mp3.rss", + "http://trackback.fritz.de/?feed=podcast", + "http://feeds.feedburner.com/linuxoutlaws-ogg", + "http://www.mevio.com/feeds/noagenda.xml", + "http://podcast.hr2.de/derTag/podcast.xml", + "http://feeds.feedburner.com/thechangelog", + "http://leoville.tv/podcasts/floss.xml", + "http://www.radiotux.de/index.php?/feeds/index.rss2", + "http://megamagis.ch/episodes.mp3.rss", + "http://www.eurogamer.net/rss/eurogamer_podcast_itunes.rss", + "http://bobsonbob.de/?feed=rss2", + "http://www.blacksweetstories.com/feed/podcast/", + "http://www.eurogamer.net/rss/eurogamer_podcast_itunes.rss", + "http://diehoppeshow.de/podcast/feed.xml", + "http://feeds.feedburner.com/ThisIsMyNextPodcast?format=xml", + "http://bitlove.org/343max/maerchenstunde/feed", + "http://bitlove.org/343max/wmr-aac/feed", + "http://bitlove.org/343max/wmr-mp3/feed", + "http://bitlove.org/343max/wmr-oga/feed", + "http://bitlove.org/adamc1999/noagenda/feed", + "http://bitlove.org/alexbrueckel/normalzeit_podcast/feed", + "http://bitlove.org/alexbrueckel/normalzeit_podcast_mp3/feed", + "http://bitlove.org/alexbrueckel/tisch3-podcast/feed", + "http://bitlove.org/alexolma/iphoneblog/feed", + "http://www.cczwei.de/rss_tvissues.php" }; +} diff --git a/src/instrumentationTest/de/test/antennapod/util/FilenameGeneratorTest.java b/src/instrumentationTest/de/test/antennapod/util/FilenameGeneratorTest.java new file mode 100644 index 000000000..552d34941 --- /dev/null +++ b/src/instrumentationTest/de/test/antennapod/util/FilenameGeneratorTest.java @@ -0,0 +1,59 @@ +package instrumentationTest.de.test.antennapod.util; + +import java.io.File; +import java.io.IOException; + +import de.danoeh.antennapod.util.FileNameGenerator; +import android.test.AndroidTestCase; + +public class FilenameGeneratorTest extends AndroidTestCase { + + private static final String VALID1 = "abc abc"; + private static final String INVALID1 = "ab/c: <abc"; + private static final String INVALID2 = "abc abc "; + + public FilenameGeneratorTest() { + super(); + } + + public void testGenerateFileName() throws IOException { + String result = FileNameGenerator.generateFileName(VALID1); + assertEquals(result, VALID1); + createFiles(result); + } + + public void testGenerateFileName1() throws IOException { + String result = FileNameGenerator.generateFileName(INVALID1); + assertEquals(result, VALID1); + createFiles(result); + } + + public void testGenerateFileName2() throws IOException { + String result = FileNameGenerator.generateFileName(INVALID2); + assertEquals(result, VALID1); + createFiles(result); + } + + /** + * Tests if files can be created. + * + * @throws IOException + */ + private void createFiles(String name) throws IOException { + File cache = getContext().getExternalCacheDir(); + File testFile = new File(cache, name); + testFile.mkdir(); + assertTrue(testFile.exists()); + testFile.delete(); + assertTrue(testFile.createNewFile()); + + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + File f = new File(getContext().getExternalCacheDir(), VALID1); + f.delete(); + } + +} |