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