diff options
Diffstat (limited to 'src/de/danoeh/antennapod/service')
14 files changed, 0 insertions, 5050 deletions
diff --git a/src/de/danoeh/antennapod/service/GpodnetSyncService.java b/src/de/danoeh/antennapod/service/GpodnetSyncService.java deleted file mode 100644 index c8c9fc31e..000000000 --- a/src/de/danoeh/antennapod/service/GpodnetSyncService.java +++ /dev/null @@ -1,245 +0,0 @@ -package de.danoeh.antennapod.service; - -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.Context; -import android.content.Intent; -import android.os.IBinder; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.gpoddernet.GpodnetService; -import de.danoeh.antennapod.gpoddernet.GpodnetServiceAuthenticationException; -import de.danoeh.antennapod.gpoddernet.GpodnetServiceException; -import de.danoeh.antennapod.gpoddernet.model.GpodnetSubscriptionChange; -import de.danoeh.antennapod.gpoddernet.model.GpodnetUploadChangesResponse; -import de.danoeh.antennapod.preferences.GpodnetPreferences; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DownloadRequestException; -import de.danoeh.antennapod.storage.DownloadRequester; -import de.danoeh.antennapod.util.NetworkUtils; - -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; - -/** - * Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument. - * This class also provides static methods for starting the GpodnetSyncService. - */ -public class GpodnetSyncService extends Service { - private static final String TAG = "GpodnetSyncService"; - - private static final long WAIT_INTERVAL = 5000L; - - public static final String ARG_ACTION = "action"; - - public static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync"; - - private GpodnetService service; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null; - if (action != null && action.equals(ACTION_SYNC)) { - Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL)); - syncWaiterThread.restart(); - } else { - Log.e(TAG, "Received invalid intent: action argument is null or invalid"); - } - return START_FLAG_REDELIVERY; - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (BuildConfig.DEBUG) Log.d(TAG, "onDestroy"); - syncWaiterThread.interrupt(); - - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - private synchronized GpodnetService tryLogin() throws GpodnetServiceException { - if (service == null) { - service = new GpodnetService(); - service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); - } - return service; - } - - private synchronized void syncChanges() { - if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(this)) { - final long timestamp = GpodnetPreferences.getLastSyncTimestamp(); - try { - final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this); - GpodnetService service = tryLogin(); - - if (timestamp == 0) { - // first sync: download all subscriptions... - GpodnetSubscriptionChange changes = - service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), 0); - if (BuildConfig.DEBUG) Log.d(TAG, "Downloaded subscription changes: " + changes); - processSubscriptionChanges(localSubscriptions, changes); - - // ... then upload all local subscriptions - if (BuildConfig.DEBUG) Log.d(TAG, "Uploading subscription list: " + localSubscriptions); - GpodnetUploadChangesResponse uploadChangesResponse = - service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), localSubscriptions, new LinkedList<String>()); - if (BuildConfig.DEBUG) Log.d(TAG, "Uploading changes response: " + uploadChangesResponse); - GpodnetPreferences.removeAddedFeeds(localSubscriptions); - GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy()); - GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp); - } else { - Set<String> added = GpodnetPreferences.getAddedFeedsCopy(); - Set<String> removed = GpodnetPreferences.getRemovedFeedsCopy(); - - // download remote changes first... - GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), timestamp); - if (BuildConfig.DEBUG) Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); - processSubscriptionChanges(localSubscriptions, subscriptionChanges); - - // ... then upload changes local changes - if (BuildConfig.DEBUG) Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s", - added.toString(), removed)); - GpodnetUploadChangesResponse uploadChangesResponse = service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), added, removed); - if (BuildConfig.DEBUG) Log.d(TAG, "Upload subscriptions response: " + uploadChangesResponse); - - GpodnetPreferences.removeAddedFeeds(added); - GpodnetPreferences.removeRemovedFeeds(removed); - GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp); - } - clearErrorNotifications(); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - updateErrorNotification(e); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } - } - stopSelf(); - } - - private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException { - for (String downloadUrl : changes.getAdded()) { - if (!localSubscriptions.contains(downloadUrl)) { - Feed feed = new Feed(downloadUrl, new Date()); - DownloadRequester.getInstance().downloadFeed(this, feed); - } - } - for (String downloadUrl : changes.getRemoved()) { - DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl); - } - } - - private void clearErrorNotifications() { - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(R.id.notification_gpodnet_sync_error); - nm.cancel(R.id.notification_gpodnet_sync_autherror); - } - - private void updateErrorNotification(GpodnetServiceException exception) { - if (BuildConfig.DEBUG) Log.d(TAG, "Posting error notification"); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(this); - final String title; - final String description; - final int id; - if (exception instanceof GpodnetServiceAuthenticationException) { - title = getString(R.string.gpodnetsync_auth_error_title); - description = getString(R.string.gpodnetsync_auth_error_descr); - id = R.id.notification_gpodnet_sync_autherror; - } else { - title = getString(R.string.gpodnetsync_error_title); - description = getString(R.string.gpodnetsync_error_descr) + exception.getMessage(); - id = R.id.notification_gpodnet_sync_error; - } - - PendingIntent activityIntent = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), PendingIntent.FLAG_UPDATE_CURRENT); - - Notification notification = builder.setContentTitle(title) - .setContentText(description) - .setContentIntent(activityIntent) - .setSmallIcon(R.drawable.stat_notify_sync_error) - .setAutoCancel(true) - .build(); - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(id, notification); - } - - private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) { - @Override - public void onWaitCompleted() { - syncChanges(); - } - }; - - private abstract class WaiterThread { - private long waitInterval; - private Thread thread; - - private WaiterThread(long waitInterval) { - this.waitInterval = waitInterval; - reinit(); - } - - public abstract void onWaitCompleted(); - - public void exec() { - if (!thread.isAlive()) { - thread.start(); - } - } - - private void reinit() { - if (thread != null && thread.isAlive()) { - Log.d(TAG, "Interrupting waiter thread"); - thread.interrupt(); - } - thread = new Thread() { - @Override - public void run() { - try { - Thread.sleep(waitInterval); - } catch (InterruptedException e) { - e.printStackTrace(); - } - if (!isInterrupted()) { - synchronized (this) { - onWaitCompleted(); - } - } - } - }; - } - - public void restart() { - reinit(); - exec(); - } - - public void interrupt() { - if (thread != null && thread.isAlive()) { - thread.interrupt(); - } - } - } - - public static void sendSyncIntent(Context context) { - if (GpodnetPreferences.loggedIn()) { - Intent intent = new Intent(context, GpodnetSyncService.class); - intent.putExtra(ARG_ACTION, ACTION_SYNC); - context.startService(intent); - } - } -} diff --git a/src/de/danoeh/antennapod/service/download/APRedirectHandler.java b/src/de/danoeh/antennapod/service/download/APRedirectHandler.java deleted file mode 100644 index ddf8d605d..000000000 --- a/src/de/danoeh/antennapod/service/download/APRedirectHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import org.apache.http.Header; -import org.apache.http.HttpResponse; -import org.apache.http.impl.client.DefaultRedirectHandler; -import org.apache.http.protocol.HttpContext; - -import java.net.URI; - -public class APRedirectHandler extends DefaultRedirectHandler { - // Identifier for logger - private static final String TAG = "APRedirectHandler"; - // Header field, which has to be potentially fixed - private static final String LOC = "Location"; - // Regular expressions for character strings, which should not appear in URLs - private static final String CHi[] = { "\\{", "\\}", "\\|", "\\\\", "\\^", "~", "\\[", "\\]", "\\`"}; - private static final String CHo[] = { "%7B", "%7D", "%7C", "%5C", "%5E", "%7E", "%5B", "%5D", "%60"}; - - /** - * Workaround for broken URLs in redirection. - * Proper solution involves LaxRedirectStrategy() which is not available in - * current API yet. - */ - @Override - public URI getLocationURI(HttpResponse response, HttpContext context) - throws org.apache.http.ProtocolException { - - Header h[] = response.getHeaders(LOC); - if (h.length>0) { - String s = h[0].getValue(); - - // Fix broken URL - for(int i=0; i<CHi.length;i++) - s = s.replaceAll(CHi[i], CHo[i]); - - // If anything had to be fixed, then replace the header - if (!s.equals(h[0].getValue())) - { - if (BuildConfig.DEBUG) - Log.d(TAG, "Original URL: " + h[0].getValue()); - - response.setHeader(LOC, s); - - if (BuildConfig.DEBUG) - Log.d(TAG, "Fixed URL: " + s); - } - } - - // call DefaultRedirectHandler with fixed URL - return super.getLocationURI(response, context); - } -} diff --git a/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java b/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java deleted file mode 100644 index be331ce9b..000000000 --- a/src/de/danoeh/antennapod/service/download/AntennapodHttpClient.java +++ /dev/null @@ -1,96 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import android.util.Log; -import de.danoeh.antennapod.AppConfig; -import de.danoeh.antennapod.BuildConfig; -import org.apache.http.client.HttpClient; -import org.apache.http.client.params.HttpClientParams; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.conn.params.ConnManagerPNames; -import org.apache.http.conn.scheme.PlainSocketFactory; -import org.apache.http.conn.scheme.Scheme; -import org.apache.http.conn.scheme.SchemeRegistry; -import org.apache.http.conn.ssl.SSLSocketFactory; -import org.apache.http.impl.client.AbstractHttpClient; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; -import org.apache.http.params.BasicHttpParams; -import org.apache.http.params.CoreProtocolPNames; -import org.apache.http.params.HttpConnectionParams; -import org.apache.http.params.HttpParams; - -import java.util.concurrent.TimeUnit; - -/** - * Provides access to a HttpClient singleton. - */ -public class AntennapodHttpClient { - private static final String TAG = "AntennapodHttpClient"; - - public static final long EXPIRED_CONN_TIMEOUT_SEC = 30; - - public static final int MAX_REDIRECTS = 5; - public static final int CONNECTION_TIMEOUT = 30000; - public static final int SOCKET_TIMEOUT = 30000; - - public static final int MAX_CONNECTIONS = 8; - - - private static volatile HttpClient httpClient = null; - - /** - * Returns the HttpClient singleton. - */ - public static synchronized HttpClient getHttpClient() { - if (httpClient == null) { - if (BuildConfig.DEBUG) Log.d(TAG, "Creating new instance of HTTP client"); - - HttpParams params = new BasicHttpParams(); - params.setParameter(CoreProtocolPNames.USER_AGENT, AppConfig.USER_AGENT); - params.setIntParameter("http.protocol.max-redirects", MAX_REDIRECTS); - params.setBooleanParameter("http.protocol.reject-relative-redirect", - false); - HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT); - HttpConnectionParams.setConnectionTimeout(params, CONNECTION_TIMEOUT); - HttpClientParams.setRedirecting(params, true); - - httpClient = new DefaultHttpClient(createClientConnectionManager(), params); - // Workaround for broken URLs in redirection - ((AbstractHttpClient) httpClient) - .setRedirectHandler(new APRedirectHandler()); - } - return httpClient; - } - - /** - * Closes expired connections. This method should be called by the using class once has finished its work with - * the HTTP client. - */ - public static synchronized void cleanup() { - if (httpClient != null) { - httpClient.getConnectionManager().closeExpiredConnections(); - httpClient.getConnectionManager().closeIdleConnections(EXPIRED_CONN_TIMEOUT_SEC, TimeUnit.SECONDS); - } - } - - - private static ClientConnectionManager createClientConnectionManager() { - HttpParams params = new BasicHttpParams(); - params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, MAX_CONNECTIONS); - return new ThreadSafeClientConnManager(params, prepareSchemeRegistry()); - } - - private static SchemeRegistry prepareSchemeRegistry() { - SchemeRegistry sr = new SchemeRegistry(); - - Scheme http = new Scheme("http", - PlainSocketFactory.getSocketFactory(), 80); - sr.register(http); - Scheme https = new Scheme("https", - SSLSocketFactory.getSocketFactory(), 443); - sr.register(https); - - return sr; - } - -} diff --git a/src/de/danoeh/antennapod/service/download/DownloadRequest.java b/src/de/danoeh/antennapod/service/download/DownloadRequest.java deleted file mode 100644 index e803d30d4..000000000 --- a/src/de/danoeh/antennapod/service/download/DownloadRequest.java +++ /dev/null @@ -1,209 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import android.os.Parcel; -import android.os.Parcelable; - -import org.apache.commons.lang3.Validate; - -public class DownloadRequest implements Parcelable { - - private final String destination; - private final String source; - private final String title; - private String username; - private String password; - private boolean deleteOnFailure; - private final long feedfileId; - private final int feedfileType; - - protected int progressPercent; - protected long soFar; - protected long size; - protected int statusMsg; - - public DownloadRequest(String destination, String source, String title, - long feedfileId, int feedfileType, String username, String password, boolean deleteOnFailure) { - Validate.notNull(destination); - Validate.notNull(source); - Validate.notNull(title); - - this.destination = destination; - this.source = source; - this.title = title; - this.feedfileId = feedfileId; - this.feedfileType = feedfileType; - this.username = username; - this.password = password; - this.deleteOnFailure = deleteOnFailure; - } - - public DownloadRequest(String destination, String source, String title, - long feedfileId, int feedfileType) { - this(destination, source, title, feedfileId, feedfileType, null, null, true); - } - - private DownloadRequest(Parcel in) { - destination = in.readString(); - source = in.readString(); - title = in.readString(); - feedfileId = in.readLong(); - feedfileType = in.readInt(); - deleteOnFailure = (in.readByte() > 0); - if (in.dataAvail() > 0) { - username = in.readString(); - } else { - username = null; - } - if (in.dataAvail() > 0) { - password = in.readString(); - } else { - password = null; - } - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public void writeToParcel(Parcel dest, int flags) { - dest.writeString(destination); - dest.writeString(source); - dest.writeString(title); - dest.writeLong(feedfileId); - dest.writeInt(feedfileType); - dest.writeByte((deleteOnFailure) ? (byte) 1 : 0); - if (username != null) { - dest.writeString(username); - } - if (password != null) { - dest.writeString(password); - } - } - - public static final Parcelable.Creator<DownloadRequest> CREATOR = new Parcelable.Creator<DownloadRequest>() { - public DownloadRequest createFromParcel(Parcel in) { - return new DownloadRequest(in); - } - - public DownloadRequest[] newArray(int size) { - return new DownloadRequest[size]; - } - }; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - DownloadRequest that = (DownloadRequest) o; - - if (deleteOnFailure != that.deleteOnFailure) return false; - if (feedfileId != that.feedfileId) return false; - if (feedfileType != that.feedfileType) return false; - if (progressPercent != that.progressPercent) return false; - if (size != that.size) return false; - if (soFar != that.soFar) return false; - if (statusMsg != that.statusMsg) return false; - if (destination != null ? !destination.equals(that.destination) : that.destination != null) - return false; - if (password != null ? !password.equals(that.password) : that.password != null) - return false; - if (source != null ? !source.equals(that.source) : that.source != null) return false; - if (title != null ? !title.equals(that.title) : that.title != null) return false; - if (username != null ? !username.equals(that.username) : that.username != null) - return false; - - return true; - } - - @Override - public int hashCode() { - int result = destination != null ? destination.hashCode() : 0; - result = 31 * result + (source != null ? source.hashCode() : 0); - result = 31 * result + (title != null ? title.hashCode() : 0); - result = 31 * result + (username != null ? username.hashCode() : 0); - result = 31 * result + (password != null ? password.hashCode() : 0); - result = 31 * result + (deleteOnFailure ? 1 : 0); - result = 31 * result + (int) (feedfileId ^ (feedfileId >>> 32)); - result = 31 * result + feedfileType; - result = 31 * result + progressPercent; - result = 31 * result + (int) (soFar ^ (soFar >>> 32)); - result = 31 * result + (int) (size ^ (size >>> 32)); - result = 31 * result + statusMsg; - return result; - } - - public String getDestination() { - return destination; - } - - public String getSource() { - return source; - } - - public String getTitle() { - return title; - } - - public long getFeedfileId() { - return feedfileId; - } - - public int getFeedfileType() { - return feedfileType; - } - - public int getProgressPercent() { - return progressPercent; - } - - public void setProgressPercent(int progressPercent) { - this.progressPercent = progressPercent; - } - - public long getSoFar() { - return soFar; - } - - public void setSoFar(long soFar) { - this.soFar = soFar; - } - - public long getSize() { - return size; - } - - public void setSize(long size) { - this.size = size; - } - - public int getStatusMsg() { - return statusMsg; - } - - public void setStatusMsg(int statusMsg) { - this.statusMsg = statusMsg; - } - - public String getUsername() { - return username; - } - - public String getPassword() { - return password; - } - - public void setUsername(String username) { - this.username = username; - } - - public void setPassword(String password) { - this.password = password; - } - - public boolean isDeleteOnFailure() { - return deleteOnFailure; - } -} diff --git a/src/de/danoeh/antennapod/service/download/DownloadService.java b/src/de/danoeh/antennapod/service/download/DownloadService.java deleted file mode 100644 index 63be91b57..000000000 --- a/src/de/danoeh/antennapod/service/download/DownloadService.java +++ /dev/null @@ -1,1230 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import android.annotation.SuppressLint; -import android.app.Notification; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.Service; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.MediaMetadataRetriever; -import android.os.Binder; -import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.webkit.URLUtil; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; -import org.apache.http.HttpStatus; -import org.xml.sax.SAXException; - -import java.io.File; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Date; -import java.util.LinkedList; -import java.util.List; -import java.util.Queue; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.Callable; -import java.util.concurrent.CompletionService; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorCompletionService; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -import javax.xml.parsers.ParserConfigurationException; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.DownloadAuthenticationActivity; -import de.danoeh.antennapod.activity.MainActivity; -import de.danoeh.antennapod.adapter.NavListAdapter; -import de.danoeh.antennapod.feed.EventDistributor; -import de.danoeh.antennapod.feed.Feed; -import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.feed.FeedPreferences; -import de.danoeh.antennapod.fragment.DownloadsFragment; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.storage.DownloadRequestException; -import de.danoeh.antennapod.storage.DownloadRequester; -import de.danoeh.antennapod.syndication.handler.FeedHandler; -import de.danoeh.antennapod.syndication.handler.UnsupportedFeedtypeException; -import de.danoeh.antennapod.util.ChapterUtils; -import de.danoeh.antennapod.util.DownloadError; -import de.danoeh.antennapod.util.InvalidFeedException; - -/** - * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent. - * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUEST field of - * the intent. - * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the - * type of the feedfile. - */ -public class DownloadService extends Service { - private static final String TAG = "DownloadService"; - - /** - * Cancels one download. The intent MUST have an EXTRA_DOWNLOAD_URL extra that contains the download URL of the - * object whose download should be cancelled. - */ - public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.service.cancelDownload"; - - /** - * Cancels all running downloads. - */ - public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.service.cancelAllDownloads"; - - /** - * Extra for ACTION_CANCEL_DOWNLOAD - */ - public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; - - /** - * Sent by the DownloadService when the content of the downloads list - * changes. - */ - public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.service.downloadsContentChanged"; - - /** - * Extra for ACTION_ENQUEUE_DOWNLOAD intent. - */ - public static final String EXTRA_REQUEST = "request"; - - /** - * Stores new media files that will be queued for auto-download if possible. - */ - private List<Long> newMediaFiles; - - /** - * Contains all completed downloads that have not been included in the report yet. - */ - private List<DownloadStatus> reportQueue; - - private ExecutorService syncExecutor; - private CompletionService<Downloader> downloadExecutor; - private FeedSyncThread feedSyncThread; - - /** - * Number of threads of downloadExecutor. - */ - private static final int NUM_PARALLEL_DOWNLOADS = 6; - - private DownloadRequester requester; - - - private NotificationCompat.Builder notificationCompatBuilder; - private Notification.BigTextStyle notificationBuilder; - private int NOTIFICATION_ID = 2; - private int REPORT_ID = 3; - - /** - * Currently running downloads. - */ - private List<Downloader> downloads; - - /** - * Number of running downloads. - */ - private AtomicInteger numberOfDownloads; - - /** - * True if service is running. - */ - public static boolean isRunning = false; - - private Handler handler; - - private NotificationUpdater notificationUpdater; - private ScheduledFuture notificationUpdaterFuture; - private static final int SCHED_EX_POOL_SIZE = 1; - private ScheduledThreadPoolExecutor schedExecutor; - - private final IBinder mBinder = new LocalBinder(); - - public class LocalBinder extends Binder { - public DownloadService getService() { - return DownloadService.this; - } - } - - private Thread downloadCompletionThread = new Thread() { - private static final String TAG = "downloadCompletionThread"; - - @Override - public void run() { - if (BuildConfig.DEBUG) Log.d(TAG, "downloadCompletionThread was started"); - while (!isInterrupted()) { - try { - Downloader downloader = downloadExecutor.take().get(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Received 'Download Complete' - message."); - removeDownload(downloader); - DownloadStatus status = downloader.getResult(); - boolean successful = status.isSuccessful(); - - final int type = status.getFeedfileType(); - if (successful) { - if (type == Feed.FEEDFILETYPE_FEED) { - handleCompletedFeedDownload(downloader - .getDownloadRequest()); - } else if (type == FeedImage.FEEDFILETYPE_FEEDIMAGE) { - handleCompletedImageDownload(status, downloader.getDownloadRequest()); - } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - handleCompletedFeedMediaDownload(status, downloader.getDownloadRequest()); - } - } else { - numberOfDownloads.decrementAndGet(); - if (!status.isCancelled()) { - if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { - postAuthenticationNotification(downloader.getDownloadRequest()); - } else if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR - && Integer.valueOf(status.getReasonDetailed()) == HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE) { - - Log.d(TAG, "Requested invalid range, restarting download from the beginning"); - FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination())); - DownloadRequester.getInstance().download(DownloadService.this, downloader.getDownloadRequest()); - } else { - Log.e(TAG, "Download failed"); - saveDownloadStatus(status); - handleFailedDownload(status, downloader.getDownloadRequest()); - } - } - sendDownloadHandledIntent(); - queryDownloadsAsync(); - } - } catch (InterruptedException e) { - if (BuildConfig.DEBUG) Log.d(TAG, "DownloadCompletionThread was interrupted"); - } catch (ExecutionException e) { - e.printStackTrace(); - numberOfDownloads.decrementAndGet(); - } - } - if (BuildConfig.DEBUG) Log.d(TAG, "End of downloadCompletionThread"); - } - }; - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (intent.getParcelableExtra(EXTRA_REQUEST) != null) { - onDownloadQueued(intent); - } else if (numberOfDownloads.get() == 0) { - stopSelf(); - } - return Service.START_NOT_STICKY; - } - - @SuppressLint("NewApi") - @Override - public void onCreate() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Service started"); - isRunning = true; - handler = new Handler(); - newMediaFiles = Collections.synchronizedList(new ArrayList<Long>()); - reportQueue = Collections.synchronizedList(new ArrayList<DownloadStatus>()); - downloads = new ArrayList<Downloader>(); - numberOfDownloads = new AtomicInteger(0); - - IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); - cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); - registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); - syncExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - downloadExecutor = new ExecutorCompletionService<Downloader>( - Executors.newFixedThreadPool(NUM_PARALLEL_DOWNLOADS, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - } - ) - ); - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }, new RejectedExecutionHandler() { - - @Override - public void rejectedExecution(Runnable r, - ThreadPoolExecutor executor) { - Log.w(TAG, "SchedEx rejected submission of new task"); - } - } - ); - downloadCompletionThread.start(); - feedSyncThread = new FeedSyncThread(); - feedSyncThread.start(); - - setupNotificationBuilders(); - requester = DownloadRequester.getInstance(); - } - - @Override - public IBinder onBind(Intent intent) { - return mBinder; - } - - @Override - public void onDestroy() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Service shutting down"); - isRunning = false; - updateReport(); - - stopForeground(true); - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(NOTIFICATION_ID); - - downloadCompletionThread.interrupt(); - syncExecutor.shutdown(); - schedExecutor.shutdown(); - feedSyncThread.shutdown(); - cancelNotificationUpdater(); - unregisterReceiver(cancelDownloadReceiver); - - if (!newMediaFiles.isEmpty()) { - DBTasks.autodownloadUndownloadedItems(getApplicationContext(), - ArrayUtils.toPrimitive(newMediaFiles.toArray(new Long[newMediaFiles.size()]))); - } - } - - @SuppressLint("NewApi") - private void setupNotificationBuilders() { - Intent intent = new Intent(this, MainActivity.class); - intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); - intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS); - Bundle args = new Bundle(); - args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_RUNNING); - intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); - - PendingIntent pIntent = PendingIntent.getActivity(this, 0, intent, - PendingIntent.FLAG_UPDATE_CURRENT - ); - - - Bitmap icon = BitmapFactory.decodeResource(getResources(), - R.drawable.stat_notify_sync); - - if (android.os.Build.VERSION.SDK_INT >= 16) { - notificationBuilder = new Notification.BigTextStyle( - new Notification.Builder(this).setOngoing(true) - .setContentIntent(pIntent).setLargeIcon(icon) - .setSmallIcon(R.drawable.stat_notify_sync) - ); - } else { - notificationCompatBuilder = new NotificationCompat.Builder(this) - .setOngoing(true).setContentIntent(pIntent) - .setLargeIcon(icon) - .setSmallIcon(R.drawable.stat_notify_sync); - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Notification set up"); - } - - /** - * Updates the contents of the service's notifications. Should be called - * before setupNotificationBuilders. - */ - @SuppressLint("NewApi") - private Notification updateNotifications() { - String contentTitle = getString(R.string.download_notification_title); - int numDownloads = requester.getNumberOfDownloads(); - String downloadsLeft; - if (numDownloads > 0) { - downloadsLeft = requester.getNumberOfDownloads() - + getString(R.string.downloads_left); - } else { - downloadsLeft = getString(R.string.downloads_processing); - } - if (android.os.Build.VERSION.SDK_INT >= 16) { - - if (notificationBuilder != null) { - - StringBuilder bigText = new StringBuilder(""); - for (int i = 0; i < downloads.size(); i++) { - Downloader downloader = downloads.get(i); - final DownloadRequest request = downloader - .getDownloadRequest(); - if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - if (request.getTitle() != null) { - if (i > 0) { - bigText.append("\n"); - } - bigText.append("\u2022 " + request.getTitle()); - } - } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - if (request.getTitle() != null) { - if (i > 0) { - bigText.append("\n"); - } - bigText.append("\u2022 " + request.getTitle() - + " (" + request.getProgressPercent() - + "%)"); - } - } - - } - notificationBuilder.setSummaryText(downloadsLeft); - notificationBuilder.setBigContentTitle(contentTitle); - if (bigText != null) { - notificationBuilder.bigText(bigText.toString()); - } - return notificationBuilder.build(); - } - } else { - if (notificationCompatBuilder != null) { - notificationCompatBuilder.setContentTitle(contentTitle); - notificationCompatBuilder.setContentText(downloadsLeft); - return notificationCompatBuilder.build(); - } - } - return null; - } - - private Downloader getDownloader(String downloadUrl) { - for (Downloader downloader : downloads) { - if (downloader.getDownloadRequest().getSource().equals(downloadUrl)) { - return downloader; - } - } - return null; - } - - private BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) { - String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); - Validate.notNull(url, "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); - - if (BuildConfig.DEBUG) - Log.d(TAG, "Cancelling download with url " + url); - Downloader d = getDownloader(url); - if (d != null) { - d.cancel(); - } else { - Log.e(TAG, "Could not cancel download with url " + url); - } - - } else if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) { - for (Downloader d : downloads) { - d.cancel(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Cancelled all downloads"); - } - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - - } - queryDownloads(); - } - - }; - - private void onDownloadQueued(Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received enqueue request"); - DownloadRequest request = intent.getParcelableExtra(EXTRA_REQUEST); - if (request == null) { - throw new IllegalArgumentException( - "ACTION_ENQUEUE_DOWNLOAD intent needs request extra"); - } - - Downloader downloader = getDownloader(request); - if (downloader != null) { - numberOfDownloads.incrementAndGet(); - downloads.add(downloader); - downloadExecutor.submit(downloader); - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - } - - queryDownloads(); - } - - private Downloader getDownloader(DownloadRequest request) { - if (URLUtil.isHttpUrl(request.getSource()) - || URLUtil.isHttpsUrl(request.getSource())) { - return new HttpDownloader(request); - } - Log.e(TAG, - "Could not find appropriate downloader for " - + request.getSource() - ); - return null; - } - - /** - * Remove download from the DownloadRequester list and from the - * DownloadService list. - */ - private void removeDownload(final Downloader d) { - handler.post(new Runnable() { - @Override - public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Removing downloader: " - + d.getDownloadRequest().getSource()); - boolean rc = downloads.remove(d); - if (BuildConfig.DEBUG) - Log.d(TAG, "Result of downloads.remove: " + rc); - DownloadRequester.getInstance().removeDownload(d.getDownloadRequest()); - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - } - }); - } - - /** - * Adds a new DownloadStatus object to the list of completed downloads and - * saves it in the database - * - * @param status the download that is going to be saved - */ - private void saveDownloadStatus(DownloadStatus status) { - reportQueue.add(status); - DBWriter.addDownloadStatus(this, status); - } - - private void sendDownloadHandledIntent() { - EventDistributor.getInstance().sendDownloadHandledBroadcast(); - } - - /** - * Creates a notification at the end of the service lifecycle to notify the - * user about the number of completed downloads. A report will only be - * created if the number of successfully downloaded feeds is bigger than 1 - * or if there is at least one failed download which is not an image or if - * there is at least one downloaded media file. - */ - private void updateReport() { - // check if report should be created - boolean createReport = false; - int successfulDownloads = 0; - int failedDownloads = 0; - - // a download report is created if at least one download has failed - // (excluding failed image downloads) - for (DownloadStatus status : reportQueue) { - if (status.isSuccessful()) { - successfulDownloads++; - } else if (!status.isCancelled()) { - if (status.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { - createReport = true; - } - failedDownloads++; - } - } - - if (createReport) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating report"); - Intent intent = new Intent(this, MainActivity.class); - intent.putExtra(MainActivity.EXTRA_NAV_TYPE, NavListAdapter.VIEW_TYPE_NAV); - intent.putExtra(MainActivity.EXTRA_NAV_INDEX, MainActivity.POS_DOWNLOADS); - Bundle args = new Bundle(); - args.putInt(DownloadsFragment.ARG_SELECTED_TAB, DownloadsFragment.POS_LOG); - intent.putExtra(MainActivity.EXTRA_FRAGMENT_ARGS, args); - - // create notification object - Notification notification = new NotificationCompat.Builder(this) - .setTicker( - getString(de.danoeh.antennapod.R.string.download_report_title)) - .setContentTitle( - getString(de.danoeh.antennapod.R.string.download_report_title)) - .setContentText( - String.format( - getString(R.string.download_report_content), - successfulDownloads, failedDownloads) - ) - .setSmallIcon(R.drawable.stat_notify_sync) - .setLargeIcon( - BitmapFactory.decodeResource(getResources(), - R.drawable.stat_notify_sync) - ) - .setContentIntent( - PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) - ) - .setAutoCancel(true).build(); - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(REPORT_ID, notification); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "No report is created"); - } - reportQueue.clear(); - } - - /** - * Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is - * used from a thread other than the main thread. - */ - void queryDownloadsAsync() { - handler.post(new Runnable() { - public void run() { - queryDownloads(); - ; - } - }); - } - - /** - * Check if there's something else to download, otherwise stop - */ - void queryDownloads() { - if (BuildConfig.DEBUG) { - Log.d(TAG, numberOfDownloads.get() + " downloads left"); - } - - if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); - stopSelf(); - } else { - setupNotificationUpdater(); - startForeground(NOTIFICATION_ID, updateNotifications()); - } - } - - private void postAuthenticationNotification(final DownloadRequest downloadRequest) { - handler.post(new Runnable() { - @Override - public void run() { - final String resourceTitle = (downloadRequest.getTitle() != null) - ? downloadRequest.getTitle() : downloadRequest.getSource(); - - final Intent activityIntent = new Intent(getApplicationContext(), DownloadAuthenticationActivity.class); - activityIntent.putExtra(DownloadAuthenticationActivity.ARG_DOWNLOAD_REQUEST, downloadRequest); - activityIntent.putExtra(DownloadAuthenticationActivity.ARG_SEND_TO_DOWNLOAD_REQUESTER_BOOL, true); - final PendingIntent contentIntent = PendingIntent.getActivity(getApplicationContext(), 0, activityIntent, PendingIntent.FLAG_ONE_SHOT); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this); - builder.setTicker(getText(R.string.authentication_notification_title)) - .setContentTitle(getText(R.string.authentication_notification_title)) - .setContentText(getText(R.string.authentication_notification_msg)) - .setStyle(new NotificationCompat.BigTextStyle().bigText(getText(R.string.authentication_notification_msg) - + ": " + resourceTitle)) - .setSmallIcon(R.drawable.ic_stat_authentication) - .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_stat_authentication)) - .setAutoCancel(true) - .setContentIntent(contentIntent); - Notification n = builder.build(); - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(downloadRequest.getSource().hashCode(), n); - } - }); - } - - /** - * Is called whenever a Feed is downloaded - */ - private void handleCompletedFeedDownload(DownloadRequest request) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling completed Feed Download"); - feedSyncThread.submitCompletedDownload(request); - - } - - /** - * Is called whenever a Feed-Image is downloaded - */ - private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling completed Image Download"); - syncExecutor.execute(new ImageHandlerThread(status, request)); - } - - /** - * Is called whenever a FeedMedia is downloaded. - */ - private void handleCompletedFeedMediaDownload(DownloadStatus status, DownloadRequest request) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling completed FeedMedia Download"); - syncExecutor.execute(new MediaHandlerThread(status, request)); - } - - private void handleFailedDownload(DownloadStatus status, DownloadRequest request) { - if (BuildConfig.DEBUG) Log.d(TAG, "Handling failed download"); - syncExecutor.execute(new FailedDownloadHandler(status, request)); - } - - /** - * Takes a single Feed, parses the corresponding file and refreshes - * information in the manager - */ - class FeedSyncThread extends Thread { - private static final String TAG = "FeedSyncThread"; - - private BlockingQueue<DownloadRequest> completedRequests = new LinkedBlockingDeque<DownloadRequest>(); - private CompletionService<Feed> parserService = new ExecutorCompletionService<Feed>(Executors.newSingleThreadExecutor()); - private ExecutorService dbService = Executors.newSingleThreadExecutor(); - private Future<?> dbUpdateFuture; - private volatile boolean isActive = true; - private volatile boolean isCollectingRequests = false; - - private final long WAIT_TIMEOUT = 3000; - - - /** - * Waits for completed requests. Once the first request has been taken, the method will wait WAIT_TIMEOUT ms longer to - * collect more completed requests. - * - * @return Collected feeds or null if the method has been interrupted during the first waiting period. - */ - private List<Feed> collectCompletedRequests() { - List<Feed> results = new LinkedList<Feed>(); - DownloadRequester requester = DownloadRequester.getInstance(); - int tasks = 0; - - try { - DownloadRequest request = completedRequests.take(); - parserService.submit(new FeedParserTask(request)); - tasks++; - } catch (InterruptedException e) { - return null; - } - - tasks += pollCompletedDownloads(); - - isCollectingRequests = true; - - if (requester.isDownloadingFeeds()) { - // wait for completion of more downloads - long startTime = System.currentTimeMillis(); - long currentTime = startTime; - while (requester.isDownloadingFeeds() && (currentTime - startTime) < WAIT_TIMEOUT) { - try { - if (BuildConfig.DEBUG) - Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms"); - sleep(startTime + WAIT_TIMEOUT - currentTime); - } catch (InterruptedException e) { - if (BuildConfig.DEBUG) - Log.d(TAG, "interrupted while waiting for more downloads"); - tasks += pollCompletedDownloads(); - } finally { - currentTime = System.currentTimeMillis(); - } - } - - tasks += pollCompletedDownloads(); - - } - - isCollectingRequests = false; - - for (int i = 0; i < tasks; i++) { - try { - Feed f = parserService.take().get(); - if (f != null) { - results.add(f); - } - } catch (InterruptedException e) { - e.printStackTrace(); - - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - return results; - } - - private int pollCompletedDownloads() { - int tasks = 0; - for (int i = 0; i < completedRequests.size(); i++) { - parserService.submit(new FeedParserTask(completedRequests.poll())); - tasks++; - } - return tasks; - } - - @Override - public void run() { - while (isActive) { - final List<Feed> feeds = collectCompletedRequests(); - - if (feeds == null) { - continue; - } - - if (BuildConfig.DEBUG) Log.d(TAG, "Bundling " + feeds.size() + " feeds"); - - for (Feed feed : feeds) { - removeDuplicateImages(feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet. - } - - // Save information of feed in DB - if (dbUpdateFuture != null) { - try { - dbUpdateFuture.get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - dbUpdateFuture = dbService.submit(new Runnable() { - @Override - public void run() { - Feed[] savedFeeds = DBTasks.updateFeed(DownloadService.this, feeds.toArray(new Feed[feeds.size()])); - - for (Feed savedFeed : savedFeeds) { - // Download Feed Image if provided and not downloaded - if (savedFeed.getImage() != null - && savedFeed.getImage().isDownloaded() == false) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Feed has image; Downloading...."); - savedFeed.getImage().setOwner(savedFeed); - final Feed savedFeedRef = savedFeed; - try { - requester.downloadImage(DownloadService.this, - savedFeedRef.getImage()); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DBWriter.addDownloadStatus( - DownloadService.this, - new DownloadStatus( - savedFeedRef.getImage(), - savedFeedRef - .getImage() - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, - false, e.getMessage() - ) - ); - } - } - - // queue new media files for automatic download - for (FeedItem item : savedFeed.getItems()) { - if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) { - newMediaFiles.add(item.getMedia().getId()); - } - } - - numberOfDownloads.decrementAndGet(); - } - - sendDownloadHandledIntent(); - - queryDownloadsAsync(); - } - }); - - } - - if (dbUpdateFuture != null) { - try { - dbUpdateFuture.get(); - } catch (InterruptedException e) { - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - - if (BuildConfig.DEBUG) Log.d(TAG, "Shutting down"); - - } - - private class FeedParserTask implements Callable<Feed> { - - private DownloadRequest request; - - private FeedParserTask(DownloadRequest request) { - this.request = request; - } - - @Override - public Feed call() throws Exception { - return parseFeed(request); - } - } - - private Feed parseFeed(DownloadRequest request) { - Feed savedFeed = null; - - Feed feed = new Feed(request.getSource(), new Date()); - feed.setFile_url(request.getDestination()); - feed.setId(request.getFeedfileId()); - feed.setDownloaded(true); - feed.setPreferences(new FeedPreferences(0, true, request.getUsername(), request.getPassword())); - - DownloadError reason = null; - String reasonDetailed = null; - boolean successful = true; - FeedHandler feedHandler = new FeedHandler(); - - try { - feed = feedHandler.parseFeed(feed).feed; - if (BuildConfig.DEBUG) - Log.d(TAG, feed.getTitle() + " parsed"); - if (checkFeedData(feed) == false) { - throw new InvalidFeedException(); - } - - } catch (SAXException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (IOException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (ParserConfigurationException e) { - successful = false; - e.printStackTrace(); - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } catch (UnsupportedFeedtypeException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_UNSUPPORTED_TYPE; - reasonDetailed = e.getMessage(); - } catch (InvalidFeedException e) { - e.printStackTrace(); - successful = false; - reason = DownloadError.ERROR_PARSER_EXCEPTION; - reasonDetailed = e.getMessage(); - } - - // cleanup(); - if (savedFeed == null) { - savedFeed = feed; - } - - - if (successful) { - return savedFeed; - } else { - numberOfDownloads.decrementAndGet(); - saveDownloadStatus(new DownloadStatus(savedFeed, - savedFeed.getHumanReadableIdentifier(), reason, successful, - reasonDetailed)); - return null; - } - } - - - /** - * Checks if the feed was parsed correctly. - */ - private boolean checkFeedData(Feed feed) { - if (feed.getTitle() == null) { - Log.e(TAG, "Feed has no title."); - return false; - } - if (!hasValidFeedItems(feed)) { - Log.e(TAG, "Feed has invalid items"); - return false; - } - return true; - } - - /** - * Checks if the FeedItems of this feed have images that point - * to the same URL. If two FeedItems have an image that points to - * the same URL, the reference of the second item is removed, so that every image - * reference is unique. - */ - private void removeDuplicateImages(Feed feed) { - for (int x = 0; x < feed.getItems().size(); x++) { - for (int y = x + 1; y < feed.getItems().size(); y++) { - FeedItem item1 = feed.getItems().get(x); - FeedItem item2 = feed.getItems().get(y); - if (item1.hasItemImage() && item2.hasItemImage()) { - if (StringUtils.equals(item1.getImage().getDownload_url(), item2.getImage().getDownload_url())) { - item2.setImage(null); - } - } - } - } - } - - private boolean hasValidFeedItems(Feed feed) { - for (FeedItem item : feed.getItems()) { - if (item.getTitle() == null) { - Log.e(TAG, "Item has no title"); - return false; - } - if (item.getPubDate() == null) { - Log.e(TAG, - "Item has no pubDate. Using current time as pubDate"); - if (item.getTitle() != null) { - Log.e(TAG, "Title of invalid item: " + item.getTitle()); - } - item.setPubDate(new Date()); - } - } - return true; - } - - /** - * Delete files that aren't needed anymore - */ - private void cleanup(Feed feed) { - if (feed.getFile_url() != null) { - if (new File(feed.getFile_url()).delete()) - if (BuildConfig.DEBUG) - Log.d(TAG, "Successfully deleted cache file."); - else - Log.e(TAG, "Failed to delete cache file."); - feed.setFile_url(null); - } else if (BuildConfig.DEBUG) { - Log.d(TAG, "Didn't delete cache file: File url is not set."); - } - } - - public void shutdown() { - isActive = false; - if (isCollectingRequests) { - interrupt(); - } - } - - public void submitCompletedDownload(DownloadRequest request) { - completedRequests.offer(request); - if (isCollectingRequests) { - interrupt(); - } - } - - } - - /** - * Handles failed downloads. - * <p/> - * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location - * of the downloaded file. - * <p/> - * Currently, this handler only handles FeedMedia objects, because Feeds and FeedImages are deleted if the download fails. - */ - class FailedDownloadHandler implements Runnable { - - private DownloadRequest request; - private DownloadStatus status; - - FailedDownloadHandler(DownloadStatus status, DownloadRequest request) { - this.request = request; - this.status = status; - } - - @Override - public void run() { - if (request.isDeleteOnFailure()) { - if (BuildConfig.DEBUG) Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); - } else { - File dest = new File(request.getDestination()); - if (dest.exists() && request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - Log.d(TAG, "File has been partially downloaded. Writing file url"); - FeedMedia media = DBReader.getFeedMedia(DownloadService.this, request.getFeedfileId()); - media.setFile_url(request.getDestination()); - try { - DBWriter.setFeedMedia(DownloadService.this, media).get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - } - } - } - - /** - * Handles a completed image download. - */ - class ImageHandlerThread implements Runnable { - - private DownloadRequest request; - private DownloadStatus status; - - public ImageHandlerThread(DownloadStatus status, DownloadRequest request) { - Validate.notNull(status); - Validate.notNull(request); - - this.status = status; - this.request = request; - } - - @Override - public void run() { - FeedImage image = DBReader.getFeedImage(DownloadService.this, request.getFeedfileId()); - if (image == null) { - throw new IllegalStateException("Could not find downloaded image in database"); - } - - image.setFile_url(request.getDestination()); - image.setDownloaded(true); - - saveDownloadStatus(status); - sendDownloadHandledIntent(); - DBWriter.setFeedImage(DownloadService.this, image); - numberOfDownloads.decrementAndGet(); - queryDownloadsAsync(); - } - } - - /** - * Handles a completed media download. - */ - class MediaHandlerThread implements Runnable { - - private DownloadRequest request; - private DownloadStatus status; - - public MediaHandlerThread(DownloadStatus status, DownloadRequest request) { - Validate.notNull(status); - Validate.notNull(request); - - this.status = status; - this.request = request; - } - - @Override - public void run() { - FeedMedia media = DBReader.getFeedMedia(DownloadService.this, - request.getFeedfileId()); - if (media == null) { - throw new IllegalStateException( - "Could not find downloaded media object in database"); - } - boolean chaptersRead = false; - media.setDownloaded(true); - media.setFile_url(request.getDestination()); - - // Get duration - MediaMetadataRetriever mmr = null; - try { - mmr = new MediaMetadataRetriever(); - mmr.setDataSource(media.getFile_url()); - String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - media.setDuration(Integer.parseInt(durationStr)); - if (BuildConfig.DEBUG) - Log.d(TAG, "Duration of file is " + media.getDuration()); - } catch (NumberFormatException e) { - e.printStackTrace(); - } catch (RuntimeException e) { - e.printStackTrace(); - } finally { - if (mmr != null) { - mmr.release(); - } - } - - if (media.getItem().getChapters() == null) { - ChapterUtils.loadChaptersFromFileUrl(media); - if (media.getItem().getChapters() != null) { - chaptersRead = true; - } - } - - try { - if (chaptersRead) { - DBWriter.setFeedItem(DownloadService.this, media.getItem()).get(); - } - DBWriter.setFeedMedia(DownloadService.this, media).get(); - if (!DBTasks.isInQueue(DownloadService.this, media.getItem().getId())) { - DBWriter.addQueueItem(DownloadService.this, media.getItem().getId()).get(); - } - } catch (ExecutionException e) { - e.printStackTrace(); - status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); - } catch (InterruptedException e) { - e.printStackTrace(); - status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); - } - - saveDownloadStatus(status); - sendDownloadHandledIntent(); - - numberOfDownloads.decrementAndGet(); - queryDownloadsAsync(); - } - } - - /** - * Schedules the notification updater task if it hasn't been scheduled yet. - */ - private void setupNotificationUpdater() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting up notification updater"); - if (notificationUpdater == null) { - notificationUpdater = new NotificationUpdater(); - notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate( - notificationUpdater, 5L, 5L, TimeUnit.SECONDS); - } - } - - private void cancelNotificationUpdater() { - boolean result = false; - if (notificationUpdaterFuture != null) { - result = notificationUpdaterFuture.cancel(true); - } - notificationUpdater = null; - notificationUpdaterFuture = null; - Log.d(TAG, "NotificationUpdater cancelled. Result: " + result); - } - - private class NotificationUpdater implements Runnable { - public void run() { - handler.post(new Runnable() { - @Override - public void run() { - Notification n = updateNotifications(); - if (n != null) { - NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - nm.notify(NOTIFICATION_ID, n); - } - } - }); - } - } - - public List<Downloader> getDownloads() { - return downloads; - } - -} diff --git a/src/de/danoeh/antennapod/service/download/DownloadStatus.java b/src/de/danoeh/antennapod/service/download/DownloadStatus.java deleted file mode 100644 index 1d76770bb..000000000 --- a/src/de/danoeh/antennapod/service/download/DownloadStatus.java +++ /dev/null @@ -1,181 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.feed.FeedFile; -import de.danoeh.antennapod.util.DownloadError; - -import java.util.Date; - -/** Contains status attributes for one download */ -public class DownloadStatus { - /** - * Downloaders should use this constant for the size attribute if necessary - * so that the listadapters etc. can react properly. - */ - public static final int SIZE_UNKNOWN = -1; - - // ----------------------------------- ATTRIBUTES STORED IN DB - /** Unique id for storing the object in database. */ - protected long id; - /** - * A human-readable string which is shown to the user so that he can - * identify the download. Should be the title of the item/feed/media or the - * URL if the download has no other title. - */ - protected String title; - protected DownloadError reason; - /** - * A message which can be presented to the user to give more information. - * Should be null if Download was successful. - */ - protected String reasonDetailed; - protected boolean successful; - protected Date completionDate; - protected long feedfileId; - /** - * Is used to determine the type of the feedfile even if the feedfile does - * not exist anymore. The value should be FEEDFILETYPE_FEED, - * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA - */ - protected int feedfileType; - - // ------------------------------------ NOT STORED IN DB - protected boolean done; - protected boolean cancelled; - - /** Constructor for restoring Download status entries from DB. */ - public DownloadStatus(long id, String title, long feedfileId, - int feedfileType, boolean successful, DownloadError reason, - Date completionDate, String reasonDetailed) { - this.id = id; - this.title = title; - this.done = true; - this.feedfileId = feedfileId; - this.reason = reason; - this.successful = successful; - this.completionDate = (Date) completionDate.clone(); - this.reasonDetailed = reasonDetailed; - this.feedfileType = feedfileType; - } - - public DownloadStatus(DownloadRequest request, DownloadError reason, - boolean successful, boolean cancelled, String reasonDetailed) { - Validate.notNull(request); - - this.title = request.getTitle(); - this.feedfileId = request.getFeedfileId(); - this.feedfileType = request.getFeedfileType(); - this.reason = reason; - this.successful = successful; - this.cancelled = cancelled; - this.reasonDetailed = reasonDetailed; - this.completionDate = new Date(); - } - - /** Constructor for creating new completed downloads. */ - public DownloadStatus(FeedFile feedfile, String title, DownloadError reason, - boolean successful, String reasonDetailed) { - Validate.notNull(feedfile); - - this.title = title; - this.done = true; - this.feedfileId = feedfile.getId(); - this.feedfileType = feedfile.getTypeAsInt(); - this.reason = reason; - this.successful = successful; - this.completionDate = new Date(); - this.reasonDetailed = reasonDetailed; - } - - /** Constructor for creating new completed downloads. */ - public DownloadStatus(long feedfileId, int feedfileType, String title, - DownloadError reason, boolean successful, String reasonDetailed) { - this.title = title; - this.done = true; - this.feedfileId = feedfileId; - this.feedfileType = feedfileType; - this.reason = reason; - this.successful = successful; - this.completionDate = new Date(); - this.reasonDetailed = reasonDetailed; - } - - @Override - public String toString() { - return "DownloadStatus [id=" + id + ", title=" + title + ", reason=" - + reason + ", reasonDetailed=" + reasonDetailed - + ", successful=" + successful + ", completionDate=" - + completionDate + ", feedfileId=" + feedfileId - + ", feedfileType=" + feedfileType + ", done=" + done - + ", cancelled=" + cancelled + "]"; - } - - public long getId() { - return id; - } - - public String getTitle() { - return title; - } - - public DownloadError getReason() { - return reason; - } - - public String getReasonDetailed() { - return reasonDetailed; - } - - public boolean isSuccessful() { - return successful; - } - - public Date getCompletionDate() { - return (Date) completionDate.clone(); - } - - public long getFeedfileId() { - return feedfileId; - } - - public int getFeedfileType() { - return feedfileType; - } - - public boolean isDone() { - return done; - } - - public boolean isCancelled() { - return cancelled; - } - - public void setSuccessful() { - this.successful = true; - this.reason = DownloadError.SUCCESS; - this.done = true; - } - - public void setFailed(DownloadError reason, String reasonDetailed) { - this.successful = false; - this.reason = reason; - this.reasonDetailed = reasonDetailed; - this.done = true; - } - - public void setCancelled() { - this.successful = false; - this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED; - this.done = true; - this.cancelled = true; - } - - public void setCompletionDate(Date completionDate) { - this.completionDate = (Date) completionDate.clone(); - } - - public void setId(long id) { - this.id = id; - } -}
\ No newline at end of file diff --git a/src/de/danoeh/antennapod/service/download/Downloader.java b/src/de/danoeh/antennapod/service/download/Downloader.java deleted file mode 100644 index 80cc5b3f8..000000000 --- a/src/de/danoeh/antennapod/service/download/Downloader.java +++ /dev/null @@ -1,69 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import android.content.Context; -import android.net.wifi.WifiManager; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.R; - -import java.util.concurrent.Callable; - -/** Downloads files */ -public abstract class Downloader implements Callable<Downloader> { - private static final String TAG = "Downloader"; - - protected volatile boolean finished; - - protected volatile boolean cancelled; - - protected DownloadRequest request; - protected DownloadStatus result; - - public Downloader(DownloadRequest request) { - super(); - this.request = request; - this.request.setStatusMsg(R.string.download_pending); - this.cancelled = false; - this.result = new DownloadStatus(request, null, false, false, null); - } - - protected abstract void download(); - - public final Downloader call() { - WifiManager wifiManager = (WifiManager) PodcastApp.getInstance().getSystemService(Context.WIFI_SERVICE); - WifiManager.WifiLock wifiLock = null; - if (wifiManager != null) { - wifiLock = wifiManager.createWifiLock(TAG); - wifiLock.acquire(); - } - - download(); - - if (wifiLock != null) { - wifiLock.release(); - } - - if (result == null) { - throw new IllegalStateException( - "Downloader hasn't created DownloadStatus object"); - } - finished = true; - return this; - } - - public DownloadRequest getDownloadRequest() { - return request; - } - - public DownloadStatus getResult() { - return result; - } - - public boolean isFinished() { - return finished; - } - - public void cancel() { - cancelled = true; - } - -}
\ No newline at end of file diff --git a/src/de/danoeh/antennapod/service/download/DownloaderCallback.java b/src/de/danoeh/antennapod/service/download/DownloaderCallback.java deleted file mode 100644 index 08420e83a..000000000 --- a/src/de/danoeh/antennapod/service/download/DownloaderCallback.java +++ /dev/null @@ -1,10 +0,0 @@ -package de.danoeh.antennapod.service.download; - -/** - * Callback used by the Downloader-classes to notify the requester that the - * download has completed. - */ -public interface DownloaderCallback { - - public void onDownloadCompleted(Downloader downloader); -} diff --git a/src/de/danoeh/antennapod/service/download/HttpDownloader.java b/src/de/danoeh/antennapod/service/download/HttpDownloader.java deleted file mode 100644 index 7ae96dc07..000000000 --- a/src/de/danoeh/antennapod/service/download/HttpDownloader.java +++ /dev/null @@ -1,246 +0,0 @@ -package de.danoeh.antennapod.service.download; - -import android.net.http.AndroidHttpClient; -import android.util.Log; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.PodcastApp; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.feed.FeedImage; -import de.danoeh.antennapod.util.DownloadError; -import de.danoeh.antennapod.util.StorageUtils; -import de.danoeh.antennapod.util.URIUtil; -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.http.Header; -import org.apache.http.HttpEntity; -import org.apache.http.HttpResponse; -import org.apache.http.HttpStatus; -import org.apache.http.auth.UsernamePasswordCredentials; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.auth.BasicScheme; -import org.apache.http.message.BasicHeader; - -import java.io.*; -import java.net.HttpURLConnection; -import java.net.SocketTimeoutException; -import java.net.UnknownHostException; - -public class HttpDownloader extends Downloader { - private static final String TAG = "HttpDownloader"; - - private static final int BUFFER_SIZE = 8 * 1024; - - public HttpDownloader(DownloadRequest request) { - super(request); - } - - @Override - protected void download() { - File destination = new File(request.getDestination()); - final boolean fileExists = destination.exists(); - - if (request.isDeleteOnFailure() && fileExists) { - Log.w(TAG, "File already exists"); - if (request.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { - onFail(DownloadError.ERROR_FILE_EXISTS, null); - return; - } else { - onSuccess(); - return; - } - } - - HttpClient httpClient = AntennapodHttpClient.getHttpClient(); - RandomAccessFile out = null; - InputStream connection = null; - try { - HttpGet httpGet = new HttpGet(URIUtil.getURIFromRequestUrl(request.getSource())); - - // add authentication information - String userInfo = httpGet.getURI().getUserInfo(); - if (userInfo != null) { - String[] parts = userInfo.split(":"); - if (parts.length == 2) { - httpGet.addHeader(BasicScheme.authenticate( - new UsernamePasswordCredentials(parts[0], parts[1]), - "UTF-8", false)); - } - } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { - httpGet.addHeader(BasicScheme.authenticate(new UsernamePasswordCredentials(request.getUsername(), - request.getPassword()), "UTF-8", false)); - } - - // add range header if necessary - if (fileExists) { - request.setSoFar(destination.length()); - httpGet.addHeader(new BasicHeader("Range", - "bytes=" + request.getSoFar() + "-")); - if (BuildConfig.DEBUG) Log.d(TAG, "Adding range header: " + request.getSoFar()); - } - - HttpResponse response = httpClient.execute(httpGet); - HttpEntity httpEntity = response.getEntity(); - int responseCode = response.getStatusLine().getStatusCode(); - Header contentEncodingHeader = response.getFirstHeader("Content-Encoding"); - - final boolean isGzip = contentEncodingHeader != null && - contentEncodingHeader.getValue().equalsIgnoreCase("gzip"); - - if (BuildConfig.DEBUG) - Log.d(TAG, "Response code is " + responseCode); - - if (responseCode / 100 != 2 || httpEntity == null) { - final DownloadError error; - final String details; - if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { - error = DownloadError.ERROR_UNAUTHORIZED; - details = String.valueOf(responseCode); - } else { - error = DownloadError.ERROR_HTTP_DATA_ERROR; - details = String.valueOf(responseCode); - } - onFail(error, details); - return; - } - - if (!StorageUtils.storageAvailable(PodcastApp.getInstance())) { - onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null); - return; - } - - connection = new BufferedInputStream(AndroidHttpClient - .getUngzippedContent(httpEntity)); - - Header[] contentRangeHeaders = (fileExists) ? response.getHeaders("Content-Range") : null; - - if (fileExists && responseCode == HttpStatus.SC_PARTIAL_CONTENT - && contentRangeHeaders != null && contentRangeHeaders.length > 0) { - String start = contentRangeHeaders[0].getValue().substring("bytes ".length(), - contentRangeHeaders[0].getValue().indexOf("-")); - request.setSoFar(Long.valueOf(start)); - Log.d(TAG, "Starting download at position " + request.getSoFar()); - - out = new RandomAccessFile(destination, "rw"); - out.seek(request.getSoFar()); - } else { - destination.delete(); - destination.createNewFile(); - out = new RandomAccessFile(destination, "rw"); - } - - - byte[] buffer = new byte[BUFFER_SIZE]; - int count = 0; - request.setStatusMsg(R.string.download_running); - if (BuildConfig.DEBUG) - Log.d(TAG, "Getting size of download"); - request.setSize(httpEntity.getContentLength() + request.getSoFar()); - if (BuildConfig.DEBUG) - Log.d(TAG, "Size is " + request.getSize()); - if (request.getSize() < 0) { - request.setSize(DownloadStatus.SIZE_UNKNOWN); - } - - long freeSpace = StorageUtils.getFreeSpaceAvailable(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Free space is " + freeSpace); - - if (request.getSize() != DownloadStatus.SIZE_UNKNOWN - && request.getSize() > freeSpace) { - onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null); - return; - } - - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting download"); - while (!cancelled - && (count = connection.read(buffer)) != -1) { - out.write(buffer, 0, count); - request.setSoFar(request.getSoFar() + count); - request.setProgressPercent((int) (((double) request - .getSoFar() / (double) request - .getSize()) * 100)); - } - if (cancelled) { - onCancelled(); - } else { - // check if size specified in the response header is the same as the size of the - // written file. This check cannot be made if compression was used - if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN && - request.getSoFar() != request.getSize()) { - onFail(DownloadError.ERROR_IO_ERROR, - "Download completed but size: " + - request.getSoFar() + - " does not equal expected size " + - request.getSize() - ); - return; - } - onSuccess(); - } - - } catch (IllegalArgumentException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage()); - } catch (SocketTimeoutException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage()); - } catch (UnknownHostException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage()); - } catch (IOException e) { - e.printStackTrace(); - onFail(DownloadError.ERROR_IO_ERROR, e.getMessage()); - } catch (NullPointerException e) { - // might be thrown by connection.getInputStream() - e.printStackTrace(); - onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); - } finally { - IOUtils.closeQuietly(out); - AntennapodHttpClient.cleanup(); - } - } - - private void onSuccess() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Download was successful"); - result.setSuccessful(); - } - - private void onFail(DownloadError reason, String reasonDetailed) { - if (BuildConfig.DEBUG) { - Log.d(TAG, "Download failed"); - } - result.setFailed(reason, reasonDetailed); - if (request.isDeleteOnFailure()) { - cleanup(); - } - } - - private void onCancelled() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Download was cancelled"); - result.setCancelled(); - cleanup(); - } - - /** - * Deletes unfinished downloads. - */ - private void cleanup() { - if (request.getDestination() != null) { - File dest = new File(request.getDestination()); - if (dest.exists()) { - boolean rc = dest.delete(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " - + rc); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "cleanup() didn't delete file: does not exist."); - } - } - } - -} diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackService.java b/src/de/danoeh/antennapod/service/playback/PlaybackService.java deleted file mode 100644 index 6c292c08a..000000000 --- a/src/de/danoeh/antennapod/service/playback/PlaybackService.java +++ /dev/null @@ -1,1132 +0,0 @@ -package de.danoeh.antennapod.service.playback; - -import android.annotation.SuppressLint; -import android.app.Notification; -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.MediaMetadataRetriever; -import android.media.MediaPlayer; -import android.media.RemoteControlClient; -import android.media.RemoteControlClient.MetadataEditor; -import android.os.AsyncTask; -import android.os.Binder; -import android.os.Build; -import android.os.IBinder; -import android.preference.PreferenceManager; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.util.Pair; -import android.view.KeyEvent; -import android.view.SurfaceHolder; -import android.widget.Toast; - -import org.apache.commons.lang3.StringUtils; - -import java.io.IOException; -import java.util.List; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.activity.AudioplayerActivity; -import de.danoeh.antennapod.activity.VideoplayerActivity; -import de.danoeh.antennapod.asynctask.PicassoProvider; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.feed.FeedMedia; -import de.danoeh.antennapod.feed.MediaType; -import de.danoeh.antennapod.preferences.PlaybackPreferences; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.receiver.PlayerWidget; -import de.danoeh.antennapod.storage.DBTasks; -import de.danoeh.antennapod.storage.DBWriter; -import de.danoeh.antennapod.util.QueueAccess; -import de.danoeh.antennapod.util.flattr.FlattrUtils; -import de.danoeh.antennapod.util.playback.Playable; - -/** - * Controls the MediaPlayer that plays a FeedMedia-file - */ -public class PlaybackService extends Service { - /** - * Logging tag - */ - private static final String TAG = "PlaybackService"; - - /** - * Parcelable of type Playable. - */ - public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; - /** - * True if media should be streamed. - */ - public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.service.shouldStream"; - /** - * True if playback should be started immediately after media has been - * prepared. - */ - public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.service.startWhenPrepared"; - - public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.service.prepareImmediately"; - - public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.service.playerStatusChanged"; - private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; - private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"; - - public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.service.playerNotification"; - public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.service.notificationCode"; - public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.service.notificationType"; - - /** - * If the PlaybackService receives this action, it will stop playback and - * try to shutdown. - */ - public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.service.actionShutdownPlaybackService"; - - /** - * If the PlaybackService receives this action, it will end playback of the - * current episode and load the next episode if there is one available. - */ - public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.service.skipCurrentEpisode"; - - /** - * Used in NOTIFICATION_TYPE_RELOAD. - */ - public static final int EXTRA_CODE_AUDIO = 1; - public static final int EXTRA_CODE_VIDEO = 2; - - public static final int NOTIFICATION_TYPE_ERROR = 0; - public static final int NOTIFICATION_TYPE_INFO = 1; - public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; - - /** - * Receivers of this intent should update their information about the curently playing media - */ - public static final int NOTIFICATION_TYPE_RELOAD = 3; - /** - * The state of the sleeptimer changed. - */ - public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; - public static final int NOTIFICATION_TYPE_BUFFER_START = 5; - public static final int NOTIFICATION_TYPE_BUFFER_END = 6; - /** - * No more episodes are going to be played. - */ - public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; - - /** - * Playback speed has changed - */ - public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; - - /** - * Returned by getPositionSafe() or getDurationSafe() if the playbackService - * is in an invalid state. - */ - public static final int INVALID_TIME = -1; - - /** - * Is true if service is running. - */ - public static boolean isRunning = false; - /** - * Is true if service has received a valid start command. - */ - public static boolean started = false; - - private static final int NOTIFICATION_ID = 1; - - private RemoteControlClient remoteControlClient; - private PlaybackServiceMediaPlayer mediaPlayer; - private PlaybackServiceTaskManager taskManager; - - private static volatile MediaType currentMediaType = MediaType.UNKNOWN; - - private final IBinder mBinder = new LocalBinder(); - - public class LocalBinder extends Binder { - public PlaybackService getService() { - return PlaybackService.this; - } - } - - @Override - public boolean onUnbind(Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received onUnbind event"); - return super.onUnbind(intent); - } - - /** - * Returns an intent which starts an audio- or videoplayer, depending on the - * type of media that is being played. If the playbackservice is not - * running, the type of the last played media will be looked up. - */ - public static Intent getPlayerActivityIntent(Context context) { - if (isRunning) { - if (currentMediaType == MediaType.VIDEO) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } else { - if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } - } - - /** - * Same as getPlayerActivityIntent(context), but here the type of activity - * depends on the FeedMedia that is provided as an argument. - */ - public static Intent getPlayerActivityIntent(Context context, Playable media) { - MediaType mt = media.getMediaType(); - if (mt == MediaType.VIDEO) { - return new Intent(context, VideoplayerActivity.class); - } else { - return new Intent(context, AudioplayerActivity.class); - } - } - - @SuppressLint("NewApi") - @Override - public void onCreate() { - super.onCreate(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service created."); - isRunning = true; - - registerReceiver(headsetDisconnected, new IntentFilter( - Intent.ACTION_HEADSET_PLUG)); - registerReceiver(shutdownReceiver, new IntentFilter( - ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - registerReceiver(audioBecomingNoisy, new IntentFilter( - AudioManager.ACTION_AUDIO_BECOMING_NOISY)); - registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( - ACTION_SKIP_CURRENT_EPISODE)); - remoteControlClient = setupRemoteControlClient(); - taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); - mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback); - - } - - @SuppressLint("NewApi") - @Override - public void onDestroy() { - super.onDestroy(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service is about to be destroyed"); - isRunning = false; - started = false; - currentMediaType = MediaType.UNKNOWN; - - unregisterReceiver(headsetDisconnected); - unregisterReceiver(shutdownReceiver); - unregisterReceiver(audioBecomingNoisy); - unregisterReceiver(skipCurrentEpisodeReceiver); - mediaPlayer.shutdown(); - taskManager.shutdown(); - } - - @Override - public IBinder onBind(Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received onBind event"); - return mBinder; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - super.onStartCommand(intent, flags, startId); - - if (BuildConfig.DEBUG) - Log.d(TAG, "OnStartCommand called"); - final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); - final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); - if (keycode == -1 && playable == null) { - Log.e(TAG, "PlaybackService was started with no arguments"); - stopSelf(); - } - - if ((flags & Service.START_FLAG_REDELIVERY) != 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); - stopForeground(true); - } else { - - if (keycode != -1) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received media button event"); - handleKeycode(keycode); - } else { - started = true; - boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, - true); - boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); - boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); - } - } - - return Service.START_REDELIVER_INTENT; - } - - /** - * Handles media button events - */ - private void handleKeycode(int keycode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Handling keycode: " + keycode); - - final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); - final PlayerStatus status = info.playerStatus; - switch (keycode) { - case KeyEvent.KEYCODE_HEADSETHOOK: - case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - if (status == PlayerStatus.PLAYING) { - if (UserPreferences.isPersistNotify()) { - mediaPlayer.pause(false, true); - } - else { - mediaPlayer.pause(true, true); - } - } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { - mediaPlayer.resume(); - } else if (status == PlayerStatus.PREPARING) { - mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared()); - } else if (status == PlayerStatus.INITIALIZED) { - mediaPlayer.setStartWhenPrepared(true); - mediaPlayer.prepare(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PLAY: - if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { - mediaPlayer.resume(); - } else if (status == PlayerStatus.INITIALIZED) { - mediaPlayer.setStartWhenPrepared(true); - mediaPlayer.prepare(); - } - break; - case KeyEvent.KEYCODE_MEDIA_PAUSE: - if (status == PlayerStatus.PLAYING) { - if (UserPreferences.isPersistNotify()) { - mediaPlayer.pause(false, true); - } - else { - mediaPlayer.pause(true, true); - } - } - break; - case KeyEvent.KEYCODE_MEDIA_NEXT: - case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: - mediaPlayer.seekDelta(UserPreferences.getSeekDeltaMs()); - break; - case KeyEvent.KEYCODE_MEDIA_PREVIOUS: - case KeyEvent.KEYCODE_MEDIA_REWIND: - mediaPlayer.seekDelta(-UserPreferences.getSeekDeltaMs()); - break; - case KeyEvent.KEYCODE_MEDIA_STOP: - if (status == PlayerStatus.PLAYING) { - mediaPlayer.pause(true, true); - } - stopForeground(true); // gets rid of persistent notification - break; - default: - if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something - String message = String.format(getResources().getString(R.string.unknown_media_key), keycode); - Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); - } - break; - } - } - - /** - * Called by a mediaplayer Activity as soon as it has prepared its - * mediaplayer. - */ - public void setVideoSurface(SurfaceHolder sh) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting display"); - mediaPlayer.setVideoSurface(sh); - } - - /** - * Called when the surface holder of the mediaplayer has to be changed. - */ - private void resetVideoSurface() { - taskManager.cancelPositionSaver(); - mediaPlayer.resetVideoSurface(); - } - - public void notifyVideoSurfaceAbandoned() { - stopForeground(true); - mediaPlayer.resetVideoSurface(); - } - - private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { - @Override - public void positionSaverTick() { - saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL); - } - - @Override - public void onSleepTimerExpired() { - mediaPlayer.pause(true, true); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - - @Override - public void onWidgetUpdaterTick() { - updateWidget(); - } - - @Override - public void onChapterLoaded(Playable media) { - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); - } - }; - - private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { - @Override - public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { - currentMediaType = mediaPlayer.getCurrentMediaType(); - switch (newInfo.playerStatus) { - case INITIALIZED: - writePlaybackPreferences(); - break; - - case PREPARED: - taskManager.startChapterLoader(newInfo.playable); - break; - - case PAUSED: - taskManager.cancelPositionSaver(); - saveCurrentPosition(false, 0); - taskManager.cancelWidgetUpdater(); - if (UserPreferences.isPersistNotify() && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - // do not remove notification on pause based on user pref and whether android version supports expanded notifications - } - else { - // remove notifcation on pause - stopForeground(true); - } - break; - - case STOPPED: - //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); - //stopSelf(); - break; - - case PLAYING: - if (BuildConfig.DEBUG) - Log.d(TAG, "Audiofocus successfully requested"); - if (BuildConfig.DEBUG) - Log.d(TAG, "Resuming/Starting playback"); - - taskManager.startPositionSaver(); - taskManager.startWidgetUpdater(); - setupNotification(newInfo); - break; - case ERROR: - writePlaybackPreferencesNoMediaPlaying(); - break; - - } - - sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); - updateWidget(); - refreshRemoteControlClientState(newInfo); - bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); - bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); - } - - @Override - public void shouldStop() { - stopSelf(); - } - - @Override - public void playbackSpeedChanged(float s) { - sendNotificationBroadcast( - NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0); - } - - @Override - public void onBufferingUpdate(int percent) { - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent); - } - - @Override - public boolean onMediaPlayerInfo(int code) { - switch (code) { - case MediaPlayer.MEDIA_INFO_BUFFERING_START: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0); - return true; - case MediaPlayer.MEDIA_INFO_BUFFERING_END: - sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0); - return true; - default: - return false; - } - } - - @Override - public boolean onMediaPlayerError(Object inObj, int what, int extra) { - final String TAG = "PlaybackService.onErrorListener"; - Log.w(TAG, "An error has occured: " + what + " " + extra); - if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) { - mediaPlayer.pause(true, false); - } - sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); - writePlaybackPreferencesNoMediaPlaying(); - stopSelf(); - return true; - } - - @Override - public boolean endPlayback(boolean playNextEpisode) { - PlaybackService.this.endPlayback(true); - return true; - } - - @Override - public RemoteControlClient getRemoteControlClient() { - return remoteControlClient; - } - }; - - private void endPlayback(boolean playNextEpisode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback ended"); - - final Playable media = mediaPlayer.getPSMPInfo().playable; - if (media == null) { - Log.e(TAG, "Cannot end playback: media was null"); - return; - } - - taskManager.cancelPositionSaver(); - - boolean isInQueue = false; - FeedItem nextItem = null; - - if (media instanceof FeedMedia) { - FeedItem item = ((FeedMedia) media).getItem(); - DBWriter.markItemRead(PlaybackService.this, item, true, true); - - try { - final List<FeedItem> queue = taskManager.getQueue(); - isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId()); - nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); - } catch (InterruptedException e) { - e.printStackTrace(); - // isInQueue remains false - } - if (isInQueue) { - DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); - } - DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); - - // auto-flattr if enabled - if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { - DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); - } - } - - // Load next episode if previous episode was in the queue and if there - // is an episode in the queue left. - // Start playback immediately if continuous playback is enabled - Playable nextMedia = null; - boolean loadNextItem = isInQueue && nextItem != null; - playNextEpisode = playNextEpisode && loadNextItem - && UserPreferences.isFollowQueue(); - if (loadNextItem) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading next item in queue"); - nextMedia = nextItem.getMedia(); - } - final boolean prepareImmediately; - final boolean startWhenPrepared; - final boolean stream; - - if (playNextEpisode) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback of next episode will start immediately."); - prepareImmediately = startWhenPrepared = true; - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "No more episodes available to play"); - - prepareImmediately = startWhenPrepared = false; - stopForeground(true); - stopWidgetUpdater(); - } - - writePlaybackPreferencesNoMediaPlaying(); - if (nextMedia != null) { - stream = !media.localFileAvailable(); - mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); - sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, - (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); - } else { - sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); - mediaPlayer.stop(); - //stopSelf(); - } - } - - public void setSleepTimer(long waitingTime) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) - + " milliseconds"); - taskManager.setSleepTimer(waitingTime); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - public void disableSleepTimer() { - taskManager.disableSleepTimer(); - sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); - } - - private void writePlaybackPreferencesNoMediaPlaying() { - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.commit(); - } - - - private void writePlaybackPreferences() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Writing playback preferences"); - - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()).edit(); - PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); - MediaType mediaType = mediaPlayer.getCurrentMediaType(); - boolean stream = mediaPlayer.isStreaming(); - - if (info.playable != null) { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - info.playable.getPlayableType()); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, - stream); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO, - mediaType == MediaType.VIDEO); - if (info.playable instanceof FeedMedia) { - FeedMedia fMedia = (FeedMedia) info.playable; - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - fMedia.getItem().getFeed().getId()); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - fMedia.getId()); - } else { - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - info.playable.writeToPreferences(editor); - } else { - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, - PlaybackPreferences.NO_MEDIA_PLAYING); - } - - editor.commit(); - } - - /** - * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. - */ - private void postStatusUpdateIntent() { - sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); - } - - 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); - } - - /** - * Used by setupNotification to load notification data in another thread. - */ - private AsyncTask<Void, Void, Void> notificationSetupTask; - - /** - * Prepares notification and starts the service in the foreground. - */ - @SuppressLint("NewApi") - private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) { - final PendingIntent pIntent = PendingIntent.getActivity(this, 0, - PlaybackService.getPlayerActivityIntent(this), - PendingIntent.FLAG_UPDATE_CURRENT); - - if (notificationSetupTask != null) { - notificationSetupTask.cancel(true); - } - notificationSetupTask = new AsyncTask<Void, Void, Void>() { - Bitmap icon = null; - - @Override - protected Void doInBackground(Void... params) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting background work"); - if (android.os.Build.VERSION.SDK_INT >= 11) { - if (info.playable != null) { - try { - int iconSize = getResources().getDimensionPixelSize( - android.R.dimen.notification_large_icon_width); - icon = PicassoProvider.getMediaMetadataPicassoInstance(PlaybackService.this) - .load(info.playable.getImageUri()) - .resize(iconSize, iconSize) - .get(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - } - if (icon == null) { - icon = BitmapFactory.decodeResource(getResources(), - R.drawable.ic_stat_antenna); - } - - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); - if (!isCancelled() && info.playerStatus == PlayerStatus.PLAYING - && info.playable != null) { - String contentText = info.playable.getFeedTitle(); - String contentTitle = info.playable.getEpisodeTitle(); - Notification notification = null; - if (android.os.Build.VERSION.SDK_INT >= 16) { - Intent pauseButtonIntent = new Intent( // pause button intent - PlaybackService.this, PlaybackService.class); - pauseButtonIntent.putExtra( - MediaButtonReceiver.EXTRA_KEYCODE, - KeyEvent.KEYCODE_MEDIA_PAUSE); - PendingIntent pauseButtonPendingIntent = PendingIntent - .getService(PlaybackService.this, 0, - pauseButtonIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - Intent playButtonIntent = new Intent( // play button intent - PlaybackService.this, PlaybackService.class); - playButtonIntent.putExtra( - MediaButtonReceiver.EXTRA_KEYCODE, - KeyEvent.KEYCODE_MEDIA_PLAY); - PendingIntent playButtonPendingIntent = PendingIntent - .getService(PlaybackService.this, 1, - playButtonIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - Intent stopButtonIntent = new Intent( // stop button intent - PlaybackService.this, PlaybackService.class); - stopButtonIntent.putExtra( - MediaButtonReceiver.EXTRA_KEYCODE, - KeyEvent.KEYCODE_MEDIA_STOP); - PendingIntent stopButtonPendingIntent = PendingIntent - .getService(PlaybackService.this, 2, - stopButtonIntent, - PendingIntent.FLAG_UPDATE_CURRENT); - Notification.Builder notificationBuilder = new Notification.Builder( - PlaybackService.this) - .setContentTitle(contentTitle) - .setContentText(contentText) - .setOngoing(true) - .setContentIntent(pIntent) - .setLargeIcon(icon) - .setSmallIcon(R.drawable.ic_stat_antenna) - .setPriority(UserPreferences.getNotifyPriority()) // set notification priority - .addAction(android.R.drawable.ic_media_play, //play action - getString(R.string.play_label), - playButtonPendingIntent) - .addAction(android.R.drawable.ic_media_pause, //pause action - getString(R.string.pause_label), - pauseButtonPendingIntent) - .addAction(android.R.drawable.ic_menu_close_clear_cancel, // stop action - getString(R.string.stop_label), - stopButtonPendingIntent); - notification = notificationBuilder.build(); - } else { - NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( - PlaybackService.this) - .setContentTitle(contentTitle) - .setContentText(contentText).setOngoing(true) - .setContentIntent(pIntent).setLargeIcon(icon) - .setSmallIcon(R.drawable.ic_stat_antenna); - notification = notificationBuilder.getNotification(); - } - startForeground(NOTIFICATION_ID, notification); - if (BuildConfig.DEBUG) - Log.d(TAG, "Notification set up"); - } - } - - }; - if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { - notificationSetupTask - .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } else { - notificationSetupTask.execute(); - } - - } - - /** - * Saves the current position of the media file to the DB - * - * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects - * @param deltaPlayedDuration value by which played_duration should be increased. - */ - private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { - int position = getCurrentPosition(); - int duration = getDuration(); - float playbackSpeed = getCurrentPlaybackSpeed(); - final Playable playable = mediaPlayer.getPSMPInfo().playable; - if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Saving current position to " + position); - if (updatePlayedDuration && playable instanceof FeedMedia) { - FeedMedia m = (FeedMedia) playable; - FeedItem item = m.getItem(); - m.setPlayedDuration(m.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); - // Auto flattr - if (isAutoFlattrable(m) && - (m.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { - - if (BuildConfig.DEBUG) - Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration()) - + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration)); - DBTasks.flattrItemIfLoggedIn(this, item); - } - } - playable.saveCurrentPosition(PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()), - position - ); - } - } - - private void stopWidgetUpdater() { - taskManager.cancelWidgetUpdater(); - sendBroadcast(new Intent(PlayerWidget.STOP_WIDGET_UPDATE)); - } - - private void updateWidget() { - PlaybackService.this.sendBroadcast(new Intent( - PlayerWidget.FORCE_WIDGET_UPDATE)); - } - - public boolean sleepTimerActive() { - return taskManager.isSleepTimerActive(); - } - - public long getSleepTimerTimeLeft() { - return taskManager.getSleepTimerTimeLeft(); - } - - @SuppressLint("NewApi") - private RemoteControlClient setupRemoteControlClient() { - if (Build.VERSION.SDK_INT < 14) { - return null; - } - - Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); - mediaButtonIntent.setComponent(new ComponentName(getPackageName(), - MediaButtonReceiver.class.getName())); - PendingIntent mediaPendingIntent = PendingIntent.getBroadcast( - getApplicationContext(), 0, mediaButtonIntent, 0); - remoteControlClient = new RemoteControlClient(mediaPendingIntent); - int controlFlags; - if (android.os.Build.VERSION.SDK_INT < 16) { - controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE - | RemoteControlClient.FLAG_KEY_MEDIA_NEXT; - } else { - controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; - } - remoteControlClient.setTransportControlFlags(controlFlags); - return remoteControlClient; - } - - /** - * Refresh player status and metadata. - */ - @SuppressLint("NewApi") - private void refreshRemoteControlClientState(PlaybackServiceMediaPlayer.PSMPInfo info) { - if (android.os.Build.VERSION.SDK_INT >= 14) { - if (remoteControlClient != null) { - switch (info.playerStatus) { - case PLAYING: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); - break; - case PAUSED: - case INITIALIZED: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); - break; - case STOPPED: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); - break; - case ERROR: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR); - break; - default: - remoteControlClient - .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING); - } - if (info.playable != null) { - MetadataEditor editor = remoteControlClient - .editMetadata(false); - editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, - info.playable.getEpisodeTitle()); - - editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, - info.playable.getFeedTitle()); - - editor.apply(); - } - if (BuildConfig.DEBUG) - Log.d(TAG, "RemoteControlClient state was refreshed"); - } - } - } - - private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) { - boolean isPlaying = false; - - if (info.playerStatus == PlayerStatus.PLAYING) { - isPlaying = true; - } - - if (info.playable != null) { - Intent i = new Intent(whatChanged); - i.putExtra("id", 1); - i.putExtra("artist", ""); - i.putExtra("album", info.playable.getFeedTitle()); - i.putExtra("track", info.playable.getEpisodeTitle()); - i.putExtra("playing", isPlaying); - final List<FeedItem> queue = taskManager.getQueueIfLoaded(); - if (queue != null) { - i.putExtra("ListSize", queue.size()); - } - i.putExtra("duration", info.playable.getDuration()); - i.putExtra("position", info.playable.getPosition()); - sendBroadcast(i); - } - } - - /** - * 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 (StringUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { - int state = intent.getIntExtra("state", -1); - if (state != -1) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Headset plug event. State is " + state); - if (state == UNPLUGGED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Headset was unplugged during playback."); - pauseIfPauseOnDisconnect(); - } - } else { - Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); - } - } - } - }; - - private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - // sound is about to change, eg. bluetooth -> speaker - if (BuildConfig.DEBUG) - Log.d(TAG, "Pausing playback because audio is becoming noisy"); - pauseIfPauseOnDisconnect(); - } - // android.media.AUDIO_BECOMING_NOISY - }; - - /** - * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. - */ - private void pauseIfPauseOnDisconnect() { - if (UserPreferences.isPauseOnHeadsetDisconnect()) { - if (UserPreferences.isPersistNotify()) { - mediaPlayer.pause(false, true); - } - else { - mediaPlayer.pause(true, true); - } - } - } - - private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { - stopSelf(); - } - } - - }; - - private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); - mediaPlayer.endPlayback(); - } - } - }; - - public static MediaType getCurrentMediaType() { - return currentMediaType; - } - - public void resume() { - mediaPlayer.resume(); - } - - public void prepare() { - mediaPlayer.prepare(); - } - - public void pause(boolean abandonAudioFocus, boolean reinit) { - mediaPlayer.pause(abandonAudioFocus, reinit); - } - - public void reinit() { - mediaPlayer.reinit(); - } - - public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() { - return mediaPlayer.getPSMPInfo(); - } - - public PlayerStatus getStatus() { - return mediaPlayer.getPSMPInfo().playerStatus; - } - - public Playable getPlayable() { - return mediaPlayer.getPSMPInfo().playable; - } - - public void setSpeed(float speed) { - mediaPlayer.setSpeed(speed); - } - - public boolean canSetSpeed() { - return mediaPlayer.canSetSpeed(); - } - - public float getCurrentPlaybackSpeed() { - return mediaPlayer.getPlaybackSpeed(); - } - - public boolean isStartWhenPrepared() { - return mediaPlayer.isStartWhenPrepared(); - } - - public void setStartWhenPrepared(boolean s) { - mediaPlayer.setStartWhenPrepared(s); - } - - - public void seekTo(final int t) { - mediaPlayer.seekTo(t); - } - - - public void seekDelta(final int d) { - mediaPlayer.seekDelta(d); - } - - /** - * @see de.danoeh.antennapod.service.playback.PlaybackServiceMediaPlayer#seekToChapter(de.danoeh.antennapod.feed.Chapter) - */ - public void seekToChapter(Chapter c) { - mediaPlayer.seekToChapter(c); - } - - /** - * call getDuration() on mediaplayer or return INVALID_TIME if player is in - * an invalid state. - */ - public int getDuration() { - return mediaPlayer.getDuration(); - } - - /** - * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player - * is in an invalid state. - */ - public int getCurrentPosition() { - return mediaPlayer.getPosition(); - } - - public boolean isStreaming() { - return mediaPlayer.isStreaming(); - } - - public Pair<Integer, Integer> getVideoSize() { - return mediaPlayer.getVideoSize(); - } - - private boolean isAutoFlattrable(Playable p) { - if (p != null && p instanceof FeedMedia) { - FeedMedia media = (FeedMedia) p; - FeedItem item = ((FeedMedia) p).getItem(); - return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred(); - } else { - return false; - } - } -} diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java deleted file mode 100644 index 9978fff3c..000000000 --- a/src/de/danoeh/antennapod/service/playback/PlaybackServiceMediaPlayer.java +++ /dev/null @@ -1,979 +0,0 @@ -package de.danoeh.antennapod.service.playback; - -import android.content.ComponentName; -import android.content.Context; -import android.media.AudioManager; -import android.media.RemoteControlClient; -import android.net.wifi.WifiManager; -import android.os.PowerManager; -import android.telephony.TelephonyManager; -import android.util.Log; -import android.util.Pair; -import android.view.SurfaceHolder; - -import org.apache.commons.lang3.Validate; - -import java.io.IOException; -import java.util.concurrent.LinkedBlockingDeque; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.locks.ReentrantLock; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.Chapter; -import de.danoeh.antennapod.feed.MediaType; -import de.danoeh.antennapod.preferences.UserPreferences; -import de.danoeh.antennapod.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.util.playback.AudioPlayer; -import de.danoeh.antennapod.util.playback.IPlayer; -import de.danoeh.antennapod.util.playback.Playable; -import de.danoeh.antennapod.util.playback.VideoPlayer; - -/** - * Manages the MediaPlayer object of the PlaybackService. - */ -public class PlaybackServiceMediaPlayer { - public static final String TAG = "PlaybackServiceMediaPlayer"; - - /** - * Return value of some PSMP methods if the method call failed. - */ - public static final int INVALID_TIME = -1; - - private final AudioManager audioManager; - - private volatile PlayerStatus playerStatus; - private volatile PlayerStatus statusBeforeSeeking; - private volatile IPlayer mediaPlayer; - private volatile Playable media; - - private volatile boolean stream; - private volatile MediaType mediaType; - private volatile AtomicBoolean startWhenPrepared; - private volatile boolean pausedBecauseOfTransientAudiofocusLoss; - private volatile Pair<Integer, Integer> videoSize; - - /** - * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads - * have to wait until these operations have finished. - */ - private final ReentrantLock playerLock; - - private final PSMPCallback callback; - private final Context context; - - private final ThreadPoolExecutor executor; - - /** - * A wifi-lock that is acquired if the media file is being streamed. - */ - private WifiManager.WifiLock wifiLock; - - public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) { - Validate.notNull(context); - Validate.notNull(callback); - - this.context = context; - this.callback = callback; - this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - this.playerLock = new ReentrantLock(); - this.startWhenPrepared = new AtomicBoolean(false); - executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<Runnable>(), - new RejectedExecutionHandler() { - @Override - public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { - if (BuildConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable"); - } - } - ); - - mediaPlayer = null; - statusBeforeSeeking = null; - pausedBecauseOfTransientAudiofocusLoss = false; - mediaType = MediaType.UNKNOWN; - playerStatus = PlayerStatus.STOPPED; - videoSize = null; - } - - /** - * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing - * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will - * not do anything. - * Whether playback starts immediately depends on the given parameters. See below for more details. - * <p/> - * States: - * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters. - * <p/> - * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If - * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state. - * <p/> - * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object - * will enter the ERROR state. - * <p/> - * This method is executed on an internal executor service. - * - * @param playable The Playable object that is supposed to be played. This parameter must not be null. - * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via - * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by - * the Android MediaPlayer via getStreamUrl. - * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the - * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared - * for playback immediately (see 'prepareImmediately' parameter for more details) - * @param prepareImmediately Set to true if the method should also prepare the episode for playback. - */ - public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - Validate.notNull(playable); - - if (BuildConfig.DEBUG) Log.d(TAG, "Play media object."); - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - try { - playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); - } catch (RuntimeException e) { - e.printStackTrace(); - throw e; - } finally { - playerLock.unlock(); - } - } - }); - } - - /** - * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if - * the given playable parameter is the same object as the currently playing media. - * <p/> - * This method requires the playerLock and is executed on the caller's thread. - * - * @see #playMediaObject(de.danoeh.antennapod.util.playback.Playable, boolean, boolean, boolean) - */ - private void playMediaObject(final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { - Validate.notNull(playable); - if (!playerLock.isHeldByCurrentThread()) - throw new IllegalStateException("method requires playerLock"); - - - if (media != null) { - if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) { - // episode is already playing -> ignore method call - if (BuildConfig.DEBUG) - Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); - return; - } else { - // stop playback of this episode - if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) { - mediaPlayer.stop(); - } - setPlayerStatus(PlayerStatus.INDETERMINATE, null); - } - } - - this.media = playable; - this.stream = stream; - this.mediaType = media.getMediaType(); - this.videoSize = null; - createMediaPlayer(); - PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared); - setPlayerStatus(PlayerStatus.INITIALIZING, media); - try { - media.loadMetadata(); - if (stream) { - mediaPlayer.setDataSource(media.getStreamUrl()); - } else { - mediaPlayer.setDataSource(media.getLocalMediaUrl()); - } - setPlayerStatus(PlayerStatus.INITIALIZED, media); - - if (mediaType == MediaType.VIDEO) { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - // vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT); - } - - if (prepareImmediately) { - setPlayerStatus(PlayerStatus.PREPARING, media); - mediaPlayer.prepare(); - onPrepared(startWhenPrepared); - } - - } catch (Playable.PlayableException e) { - e.printStackTrace(); - setPlayerStatus(PlayerStatus.ERROR, null); - } catch (IOException e) { - e.printStackTrace(); - setPlayerStatus(PlayerStatus.ERROR, null); - } catch (IllegalStateException e) { - e.printStackTrace(); - setPlayerStatus(PlayerStatus.ERROR, null); - } - } - - - /** - * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state. - * nothing will happen. - * <p/> - * This method is executed on an internal executor service. - */ - public void resume() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - resumeSync(); - playerLock.unlock(); - } - }); - } - - private void resumeSync() { - if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - int focusGained = audioManager.requestAudioFocus( - audioFocusChangeListener, AudioManager.STREAM_MUSIC, - AudioManager.AUDIOFOCUS_GAIN); - if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - acquireWifiLockIfNecessary(); - setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed())); - mediaPlayer.start(); - if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { - mediaPlayer.seekTo(media.getPosition()); - } - - setPlayerStatus(PlayerStatus.PLAYING, media); - pausedBecauseOfTransientAudiofocusLoss = false; - if (android.os.Build.VERSION.SDK_INT >= 14) { - RemoteControlClient remoteControlClient = callback.getRemoteControlClient(); - if (remoteControlClient != null) { - audioManager - .registerRemoteControlClient(remoteControlClient); - } - } - audioManager - .registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(), - MediaButtonReceiver.class.getName())); - media.onPlaybackStart(); - - } else { - if (BuildConfig.DEBUG) Log.e(TAG, "Failed to request audio focus"); - } - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); - } - } - - - /** - * Saves the current position and pauses playback. Note that, if audiofocus - * is abandoned, the lockscreen controls will also disapear. - * <p/> - * This method is executed on an internal executor service. - * - * @param abandonFocus is true if the service should release audio focus - * @param reinit is true if service should reinit after pausing if the media - * file is being streamed - */ - public void pause(final boolean abandonFocus, final boolean reinit) { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - releaseWifiLockIfNecessary(); - if (playerStatus == PlayerStatus.PLAYING) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Pausing playback."); - mediaPlayer.pause(); - setPlayerStatus(PlayerStatus.PAUSED, media); - - if (abandonFocus) { - audioManager.abandonAudioFocus(audioFocusChangeListener); - pausedBecauseOfTransientAudiofocusLoss = false; - } - if (stream && reinit) { - reinit(); - } - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); - } - - playerLock.unlock(); - } - }); - } - - /** - * Prepared media player for playback if the service is in the INITALIZED - * state. - * <p/> - * This method is executed on an internal executor service. - */ - public void prepare() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - - if (playerStatus == PlayerStatus.INITIALIZED) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Preparing media player"); - setPlayerStatus(PlayerStatus.PREPARING, media); - try { - mediaPlayer.prepare(); - onPrepared(startWhenPrepared.get()); - } catch (IOException e) { - e.printStackTrace(); - setPlayerStatus(PlayerStatus.ERROR, null); - } - } - playerLock.unlock(); - - } - }); - } - - /** - * Called after media player has been prepared. This method is executed on the caller's thread. - */ - void onPrepared(final boolean startWhenPrepared) { - playerLock.lock(); - - if (playerStatus != PlayerStatus.PREPARING) { - playerLock.unlock(); - throw new IllegalStateException("Player is not in PREPARING state"); - } - - if (BuildConfig.DEBUG) - Log.d(TAG, "Resource prepared"); - - if (mediaType == MediaType.VIDEO) { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight()); - } - - if (media.getPosition() > 0) { - mediaPlayer.seekTo(media.getPosition()); - } - - if (media.getDuration() == 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting duration of media"); - media.setDuration(mediaPlayer.getDuration()); - } - setPlayerStatus(PlayerStatus.PREPARED, media); - - if (startWhenPrepared) { - resumeSync(); - } - - playerLock.unlock(); - } - - /** - * Resets the media player and moves it into INITIALIZED state. - * <p/> - * This method is executed on an internal executor service. - */ - public void reinit() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - releaseWifiLockIfNecessary(); - if (media != null) { - playMediaObject(media, true, stream, startWhenPrepared.get(), false); - } else if (mediaPlayer != null) { - mediaPlayer.reset(); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); - } - playerLock.unlock(); - } - }); - } - - - /** - * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. - * - * @param t The position to seek to in milliseconds. t < 0 will be interpreted as t = 0 - * <p/> - * This method is executed on the caller's thread. - */ - private void seekToSync(int t) { - if (t < 0) { - t = 0; - } - playerLock.lock(); - - if (playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) { - if (stream) { - // statusBeforeSeeking = playerStatus; - // setPlayerStatus(PlayerStatus.SEEKING, media); - } - mediaPlayer.seekTo(t); - - } else if (playerStatus == PlayerStatus.INITIALIZED) { - media.setPosition(t); - startWhenPrepared.set(true); - prepare(); - } - playerLock.unlock(); - } - - /** - * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. - * Invalid time values (< 0) will be ignored. - * <p/> - * This method is executed on an internal executor service. - */ - public void seekTo(final int t) { - executor.submit(new Runnable() { - @Override - public void run() { - seekToSync(t); - } - }); - } - - /** - * Seek a specific position from the current position - * - * @param d offset from current position (positive or negative) - */ - public void seekDelta(final int d) { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - int currentPosition = getPosition(); - if (currentPosition != INVALID_TIME) { - seekToSync(currentPosition + d); - } else { - Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); - } - - playerLock.unlock(); - } - }); - } - - /** - * Seek to the start of the specified chapter. - */ - public void seekToChapter(Chapter c) { - Validate.notNull(c); - - seekTo((int) c.getStart()); - } - - /** - * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. - */ - public int getDuration() { - if (!playerLock.tryLock()) { - return INVALID_TIME; - } - - int retVal = INVALID_TIME; - if (playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) { - retVal = mediaPlayer.getDuration(); - } else if (media != null && media.getDuration() > 0) { - retVal = media.getDuration(); - } - - playerLock.unlock(); - return retVal; - } - - /** - * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. - */ - public int getPosition() { - if (!playerLock.tryLock()) { - return INVALID_TIME; - } - - int retVal = INVALID_TIME; - if (playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) { - retVal = mediaPlayer.getCurrentPosition(); - } else if (media != null && media.getPosition() > 0) { - retVal = media.getPosition(); - } - - playerLock.unlock(); - return retVal; - } - - public boolean isStartWhenPrepared() { - return startWhenPrepared.get(); - } - - public void setStartWhenPrepared(boolean startWhenPrepared) { - this.startWhenPrepared.set(startWhenPrepared); - } - - /** - * Returns true if the playback speed can be adjusted. - */ - public boolean canSetSpeed() { - boolean retVal = false; - if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) { - retVal = (mediaPlayer).canSetSpeed(); - } - return retVal; - } - - /** - * Sets the playback speed. - * This method is executed on the caller's thread. - */ - private void setSpeedSync(float speed) { - playerLock.lock(); - if (media != null && media.getMediaType() == MediaType.AUDIO) { - if (mediaPlayer.canSetSpeed()) { - mediaPlayer.setPlaybackSpeed((float) speed); - if (BuildConfig.DEBUG) - Log.d(TAG, "Playback speed was set to " + speed); - callback.playbackSpeedChanged(speed); - } - } - playerLock.unlock(); - } - - /** - * Sets the playback speed. - * This method is executed on an internal executor service. - */ - public void setSpeed(final float speed) { - executor.submit(new Runnable() { - @Override - public void run() { - setSpeedSync(speed); - } - }); - } - - /** - * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned. - */ - public float getPlaybackSpeed() { - if (!playerLock.tryLock()) { - return 1; - } - - float retVal = 1; - if ((playerStatus == PlayerStatus.PLAYING - || playerStatus == PlayerStatus.PAUSED - || playerStatus == PlayerStatus.PREPARED) && mediaPlayer.canSetSpeed()) { - retVal = mediaPlayer.getCurrentSpeedMultiplier(); - } - playerLock.unlock(); - return retVal; - } - - public MediaType getCurrentMediaType() { - return mediaType; - } - - public boolean isStreaming() { - return stream; - } - - - /** - * Releases internally used resources. This method should only be called when the object is not used anymore. - */ - public void shutdown() { - executor.shutdown(); - if (mediaPlayer != null) { - mediaPlayer.release(); - } - releaseWifiLockIfNecessary(); - } - - public void setVideoSurface(final SurfaceHolder surface) { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - if (mediaPlayer != null) { - mediaPlayer.setDisplay(surface); - } - playerLock.unlock(); - } - }); - } - - public void resetVideoSurface() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Resetting video surface"); - mediaPlayer.setDisplay(null); - reinit(); - playerLock.unlock(); - } - }); - } - - /** - * Return width and height of the currently playing video as a pair. - * - * @return Width and height as a Pair or null if the video size could not be determined. The method might still - * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return - * invalid values. - */ - public Pair<Integer, Integer> getVideoSize() { - if (!playerLock.tryLock()) { - // use cached value if lock can't be aquired - return videoSize; - } - Pair<Integer, Integer> res; - if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) { - res = null; - } else { - VideoPlayer vp = (VideoPlayer) mediaPlayer; - videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight()); - res = videoSize; - } - playerLock.unlock(); - return res; - } - - /** - * Returns a PSMInfo object that contains information about the current state of the PSMP object. - * - * @return The PSMPInfo object. - */ - public synchronized PSMPInfo getPSMPInfo() { - return new PSMPInfo(playerStatus, media); - } - - /** - * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time - * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null). - * <p/> - * This method will notify the callback about the change of the player status (even if the new status is the same - * as the old one). - * - * @param newStatus The new PlayerStatus. This must not be null. - * @param newMedia The new playable object of the PSMP object. This can be null. - */ - private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) { - Validate.notNull(newStatus); - - if (BuildConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus); - - this.playerStatus = newStatus; - this.media = newMedia; - callback.statusChanged(new PSMPInfo(playerStatus, media)); - } - - private IPlayer createMediaPlayer() { - if (mediaPlayer != null) { - mediaPlayer.release(); - } - if (media == null || media.getMediaType() == MediaType.VIDEO) { - mediaPlayer = new VideoPlayer(); - } else { - mediaPlayer = new AudioPlayer(context); - } - mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); - mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); - return setMediaPlayerListeners(mediaPlayer); - } - - private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { - - @Override - public void onAudioFocusChange(final int focusChange) { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - - // If there is an incoming call, playback should be paused permanently - TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); - final int callState = (tm != null) ? tm.getCallState() : 0; - if (BuildConfig.DEBUG) Log.d(TAG, "Call state: " + callState); - Log.i(TAG, "Call state:" + callState); - - if (focusChange == AudioManager.AUDIOFOCUS_LOSS || callState != TelephonyManager.CALL_STATE_IDLE) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus"); - pause(true, false); - callback.shouldStop(); - } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Gained audio focus"); - if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now - resume(); - } else { // we ducked => raise audio level back - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_RAISE, 0); - } - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { - if (playerStatus == PlayerStatus.PLAYING) { - if (!UserPreferences.shouldPauseForFocusLoss()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Ducking..."); - audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, - AudioManager.ADJUST_LOWER, 0); - pausedBecauseOfTransientAudiofocusLoss = false; - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); - pause(false, false); - pausedBecauseOfTransientAudiofocusLoss = true; - } - } - } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { - if (playerStatus == PlayerStatus.PLAYING) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Lost audio focus temporarily. Pausing..."); - pause(false, false); - pausedBecauseOfTransientAudiofocusLoss = true; - } - } - playerLock.unlock(); - } - }); - } - }; - - - public void endPlayback() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - releaseWifiLockIfNecessary(); - - if (playerStatus != PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.INDETERMINATE, media); - } - if (mediaPlayer != null) { - mediaPlayer.reset(); - - } - audioManager.abandonAudioFocus(audioFocusChangeListener); - callback.endPlayback(true); - - playerLock.unlock(); - } - }); - } - - /** - * Moves the PlaybackServiceMediaPlayer into STOPPED state. This call is only valid if the player is currently in - * INDETERMINATE state, for example after a call to endPlayback. - * This method will only take care of changing the PlayerStatus of this object! Other tasks like - * abandoning audio focus have to be done with other methods. - */ - public void stop() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - releaseWifiLockIfNecessary(); - - if (playerStatus == PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.STOPPED, null); - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); - } - playerLock.unlock(); - - } - }); - } - - private synchronized void acquireWifiLockIfNecessary() { - if (stream) { - if (wifiLock == null) { - wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)) - .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); - wifiLock.setReferenceCounted(false); - } - wifiLock.acquire(); - } - } - - private synchronized void releaseWifiLockIfNecessary() { - if (wifiLock != null && wifiLock.isHeld()) { - wifiLock.release(); - } - } - - /** - * Holds information about a PSMP object. - */ - public class PSMPInfo { - public PlayerStatus playerStatus; - public Playable playable; - - public PSMPInfo(PlayerStatus playerStatus, Playable playable) { - this.playerStatus = playerStatus; - this.playable = playable; - } - } - - public static interface PSMPCallback { - public void statusChanged(PSMPInfo newInfo); - - public void shouldStop(); - - public void playbackSpeedChanged(float s); - - public void onBufferingUpdate(int percent); - - public boolean onMediaPlayerInfo(int code); - - public boolean onMediaPlayerError(Object inObj, int what, int extra); - - public boolean endPlayback(boolean playNextEpisode); - - public RemoteControlClient getRemoteControlClient(); - } - - private IPlayer setMediaPlayerListeners(IPlayer mp) { - if (mp != null && media != null) { - if (media.getMediaType() == MediaType.AUDIO) { - ((AudioPlayer) mp) - .setOnCompletionListener(audioCompletionListener); - ((AudioPlayer) mp) - .setOnSeekCompleteListener(audioSeekCompleteListener); - ((AudioPlayer) mp).setOnErrorListener(audioErrorListener); - ((AudioPlayer) mp) - .setOnBufferingUpdateListener(audioBufferingUpdateListener); - ((AudioPlayer) mp).setOnInfoListener(audioInfoListener); - } else { - ((VideoPlayer) mp) - .setOnCompletionListener(videoCompletionListener); - ((VideoPlayer) mp) - .setOnSeekCompleteListener(videoSeekCompleteListener); - ((VideoPlayer) mp).setOnErrorListener(videoErrorListener); - ((VideoPlayer) mp) - .setOnBufferingUpdateListener(videoBufferingUpdateListener); - ((VideoPlayer) mp).setOnInfoListener(videoInfoListener); - } - } - return mp; - } - - private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(com.aocate.media.MediaPlayer mp) { - genericOnCompletion(); - } - }; - - private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { - @Override - public void onCompletion(android.media.MediaPlayer mp) { - genericOnCompletion(); - } - }; - - private void genericOnCompletion() { - endPlayback(); - } - - private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() { - @Override - public void onBufferingUpdate(com.aocate.media.MediaPlayer mp, - int percent) { - genericOnBufferingUpdate(percent); - } - }; - - private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { - @Override - public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { - genericOnBufferingUpdate(percent); - } - }; - - private void genericOnBufferingUpdate(int percent) { - callback.onBufferingUpdate(percent); - } - - private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() { - @Override - public boolean onInfo(com.aocate.media.MediaPlayer mp, int what, - int extra) { - return genericInfoListener(what); - } - }; - - private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() { - @Override - public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { - return genericInfoListener(what); - } - }; - - private boolean genericInfoListener(int what) { - return callback.onMediaPlayerInfo(what); - } - - private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() { - @Override - public boolean onError(com.aocate.media.MediaPlayer mp, int what, - int extra) { - return genericOnError(mp, what, extra); - } - }; - - private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() { - @Override - public boolean onError(android.media.MediaPlayer mp, int what, int extra) { - return genericOnError(mp, what, extra); - } - }; - - private boolean genericOnError(Object inObj, int what, int extra) { - return callback.onMediaPlayerError(inObj, what, extra); - } - - private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() { - @Override - public void onSeekComplete(com.aocate.media.MediaPlayer mp) { - genericSeekCompleteListener(); - } - }; - - private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { - @Override - public void onSeekComplete(android.media.MediaPlayer mp) { - genericSeekCompleteListener(); - } - }; - - private final void genericSeekCompleteListener() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - if (playerStatus == PlayerStatus.SEEKING) { - setPlayerStatus(statusBeforeSeeking, media); - } - playerLock.unlock(); - } - }); - } -} diff --git a/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java b/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java deleted file mode 100644 index 680ec2e2f..000000000 --- a/src/de/danoeh/antennapod/service/playback/PlaybackServiceTaskManager.java +++ /dev/null @@ -1,384 +0,0 @@ -package de.danoeh.antennapod.service.playback; - -import android.content.Context; -import android.util.Log; - -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.feed.EventDistributor; -import de.danoeh.antennapod.feed.FeedItem; -import de.danoeh.antennapod.storage.DBReader; -import de.danoeh.antennapod.util.playback.Playable; - -import java.util.List; -import java.util.concurrent.*; - -/** - * Manages the background tasks of PlaybackSerivce, i.e. - * the sleep timer, the position saver, the widget updater and - * the queue loader. - * <p/> - * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback) - * to notify the PlaybackService about updates from the running tasks. - */ -public class PlaybackServiceTaskManager { - private static final String TAG = "PlaybackServiceTaskManager"; - - /** - * Update interval of position saver in milliseconds. - */ - public static final int POSITION_SAVER_WAITING_INTERVAL = 5000; - /** - * Notification interval of widget updater in milliseconds. - */ - public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500; - - private static final int SCHED_EX_POOL_SIZE = 2; - private final ScheduledThreadPoolExecutor schedExecutor; - - private ScheduledFuture positionSaverFuture; - private ScheduledFuture widgetUpdaterFuture; - private ScheduledFuture sleepTimerFuture; - private volatile Future<List<FeedItem>> queueFuture; - private volatile Future chapterLoaderFuture; - - private SleepTimer sleepTimer; - - private final Context context; - private final PSTMCallback callback; - - /** - * Sets up a new PSTM. This method will also start the queue loader task. - * - * @param context - * @param callback A PSTMCallback object for notifying the user about updates. Must not be null. - */ - public PlaybackServiceTaskManager(Context context, PSTMCallback callback) { - Validate.notNull(context); - Validate.notNull(callback); - - this.context = context; - this.callback = callback; - schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() { - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } - }); - loadQueue(); - EventDistributor.getInstance().register(eventDistributorListener); - } - - private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() { - @Override - public void update(EventDistributor eventDistributor, Integer arg) { - if ((EventDistributor.QUEUE_UPDATE & arg) != 0) { - cancelQueueLoader(); - loadQueue(); - } - } - }; - - private synchronized boolean isQueueLoaderActive() { - return queueFuture != null && !queueFuture.isDone(); - } - - private synchronized void cancelQueueLoader() { - if (isQueueLoaderActive()) { - queueFuture.cancel(true); - } - } - - private synchronized void loadQueue() { - if (!isQueueLoaderActive()) { - queueFuture = schedExecutor.submit(new Callable<List<FeedItem>>() { - @Override - public List<FeedItem> call() throws Exception { - return DBReader.getQueue(context); - } - }); - } - } - - /** - * Returns the queue if it is already loaded or null if it hasn't been loaded yet. - * In order to wait until the queue has been loaded, use getQueue() - */ - public synchronized List<FeedItem> getQueueIfLoaded() { - if (queueFuture.isDone()) { - try { - return queueFuture.get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - return null; - } - - /** - * Returns the queue or waits until the PSTM has loaded the queue from the database. - */ - public synchronized List<FeedItem> getQueue() throws InterruptedException { - try { - return queueFuture.get(); - } catch (ExecutionException e) { - throw new IllegalArgumentException(e); - } - } - - /** - * Starts the position saver task. If the position saver is already active, nothing will happen. - */ - public synchronized void startPositionSaver() { - if (!isPositionSaverActive()) { - Runnable positionSaver = new Runnable() { - @Override - public void run() { - callback.positionSaverTick(); - } - }; - positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL, - POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS); - - if (BuildConfig.DEBUG) Log.d(TAG, "Started PositionSaver"); - } else { - if (BuildConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored."); - } - } - - /** - * Returns true if the position saver is currently running. - */ - public synchronized boolean isPositionSaverActive() { - return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone(); - } - - /** - * Cancels the position saver. If the position saver is not running, nothing will happen. - */ - public synchronized void cancelPositionSaver() { - if (isPositionSaverActive()) { - positionSaverFuture.cancel(false); - if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver"); - } - } - - /** - * Starts the widget updater task. If the widget updater is already active, nothing will happen. - */ - public synchronized void startWidgetUpdater() { - if (!isWidgetUpdaterActive()) { - Runnable widgetUpdater = new Runnable() { - @Override - public void run() { - callback.onWidgetUpdaterTick(); - } - }; - widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL, - WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS); - - if (BuildConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater"); - } else { - if (BuildConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored."); - } - } - - /** - * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be - * cancelled first. - * After waitingTime has elapsed, onSleepTimerExpired() will be called. - * - * @throws java.lang.IllegalArgumentException if waitingTime <= 0 - */ - public synchronized void setSleepTimer(long waitingTime) { - Validate.isTrue(waitingTime > 0, "Waiting time <= 0"); - - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) - + " milliseconds"); - if (isSleepTimerActive()) { - sleepTimerFuture.cancel(true); - } - sleepTimer = new SleepTimer(waitingTime); - sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS); - } - - /** - * Returns true if the sleep timer is currently active. - */ - public synchronized boolean isSleepTimerActive() { - return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone() && sleepTimer.isWaiting; - } - - /** - * Disables the sleep timer. If the sleep timer is not active, nothing will happen. - */ - public synchronized void disableSleepTimer() { - if (isSleepTimerActive()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Disabling sleep timer"); - sleepTimerFuture.cancel(true); - } - } - - /** - * Returns the current sleep timer time or 0 if the sleep timer is not active. - */ - public synchronized long getSleepTimerTimeLeft() { - if (isSleepTimerActive()) { - return sleepTimer.getWaitingTime(); - } else { - return 0; - } - } - - - /** - * Returns true if the widget updater is currently running. - */ - public synchronized boolean isWidgetUpdaterActive() { - return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone(); - } - - /** - * Cancels the widget updater. If the widget updater is not running, nothing will happen. - */ - public synchronized void cancelWidgetUpdater() { - if (isWidgetUpdaterActive()) { - widgetUpdaterFuture.cancel(false); - if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater"); - } - } - - private synchronized void cancelChapterLoader() { - if (isChapterLoaderActive()) { - chapterLoaderFuture.cancel(true); - } - } - - private synchronized boolean isChapterLoaderActive() { - return chapterLoaderFuture != null && !chapterLoaderFuture.isDone(); - } - - /** - * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active, - * it will be cancelled first. - * On completion, the callback's onChapterLoaded method will be called. - */ - public synchronized void startChapterLoader(final Playable media) { - Validate.notNull(media); - - if (isChapterLoaderActive()) { - cancelChapterLoader(); - } - - Runnable chapterLoader = new Runnable() { - @Override - public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Chapter loader started"); - if (media.getChapters() == null) { - media.loadChapterMarks(); - if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) { - callback.onChapterLoaded(media); - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Chapter loader stopped"); - } - }; - chapterLoaderFuture = schedExecutor.submit(chapterLoader); - } - - - /** - * Cancels all tasks. The PSTM will be in the initial state after execution of this method. - */ - public synchronized void cancelAllTasks() { - cancelPositionSaver(); - cancelWidgetUpdater(); - disableSleepTimer(); - cancelQueueLoader(); - cancelChapterLoader(); - } - - /** - * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after - * execution of this method. - */ - public synchronized void shutdown() { - EventDistributor.getInstance().unregister(eventDistributorListener); - cancelAllTasks(); - schedExecutor.shutdown(); - } - - /** - * Sleeps for a given time and then pauses playback. - */ - private class SleepTimer implements Runnable { - private static final String TAG = "SleepTimer"; - private static final long UPDATE_INTERVALL = 1000L; - private volatile long waitingTime; - private volatile boolean isWaiting; - - public SleepTimer(long waitingTime) { - super(); - this.waitingTime = waitingTime; - isWaiting = true; - } - - @Override - public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting"); - while (waitingTime > 0) { - try { - Thread.sleep(UPDATE_INTERVALL); - waitingTime -= UPDATE_INTERVALL; - - if (waitingTime <= 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Waiting completed"); - postExecute(); - if (!Thread.currentThread().isInterrupted()) { - callback.onSleepTimerExpired(); - } - - } - } catch (InterruptedException e) { - Log.d(TAG, "Thread was interrupted while waiting"); - break; - } - } - postExecute(); - } - - protected void postExecute() { - isWaiting = false; - } - - public long getWaitingTime() { - return waitingTime; - } - - public boolean isWaiting() { - return isWaiting; - } - - } - - public static interface PSTMCallback { - void positionSaverTick(); - - void onSleepTimerExpired(); - - void onWidgetUpdaterTick(); - - void onChapterLoaded(Playable media); - } -} diff --git a/src/de/danoeh/antennapod/service/playback/PlayerStatus.java b/src/de/danoeh/antennapod/service/playback/PlayerStatus.java deleted file mode 100644 index 3d2b4ad39..000000000 --- a/src/de/danoeh/antennapod/service/playback/PlayerStatus.java +++ /dev/null @@ -1,14 +0,0 @@ -package de.danoeh.antennapod.service.playback; - -public enum PlayerStatus { - INDETERMINATE, // player is currently changing its state, listeners should wait until the player has left this state. - ERROR, - PREPARING, - PAUSED, - PLAYING, - STOPPED, - PREPARED, - SEEKING, - INITIALIZING, // playback service is loading the Playable's metadata - INITIALIZED // playback service was started, data source of media player was set. -} diff --git a/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java b/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java deleted file mode 100644 index ec28724ed..000000000 --- a/src/de/danoeh/antennapod/service/playback/PlayerWidgetService.java +++ /dev/null @@ -1,201 +0,0 @@ -package de.danoeh.antennapod.service.playback; - -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.os.Build; -import android.os.IBinder; -import android.util.Log; -import android.view.KeyEvent; -import android.view.View; -import android.widget.RemoteViews; -import de.danoeh.antennapod.BuildConfig; -import de.danoeh.antennapod.R; -import de.danoeh.antennapod.receiver.MediaButtonReceiver; -import de.danoeh.antennapod.receiver.PlayerWidget; -import de.danoeh.antennapod.util.Converter; -import de.danoeh.antennapod.util.playback.Playable; - -/** Updates the state of the player widget */ -public class PlayerWidgetService extends Service { - private static final String TAG = "PlayerWidgetService"; - - private PlaybackService playbackService; - - /** Controls write access to playbackservice reference */ - private Object psLock; - - /** True while service is updating the widget */ - private volatile boolean isUpdating; - - public PlayerWidgetService() { - } - - @Override - public void onCreate() { - super.onCreate(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service created"); - isUpdating = false; - psLock = new Object(); - } - - @Override - public void onDestroy() { - super.onDestroy(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Service is about to be destroyed"); - try { - unbindService(mConnection); - } catch (IllegalArgumentException e) { - Log.w(TAG, "IllegalArgumentException when trying to unbind service"); - } - } - - @Override - public IBinder onBind(Intent intent) { - return null; - } - - @Override - public int onStartCommand(Intent intent, int flags, int startId) { - if (!isUpdating) { - if (playbackService == null && PlaybackService.isRunning) { - bindService(new Intent(this, PlaybackService.class), - mConnection, 0); - } else { - startViewUpdaterIfNotRunning(); - } - } else { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Service was called while updating. Ignoring update request"); - } - return Service.START_NOT_STICKY; - } - - private void updateViews() { - if (playbackService == null) { - return; - } - isUpdating = true; - - 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, - PlaybackService.getPlayerActivityIntent(this), 0); - - views.setOnClickPendingIntent(R.id.layout_left, startMediaplayer); - final Playable media = playbackService.getPlayable(); - if (playbackService != null && media != null) { - PlayerStatus status = playbackService.getStatus(); - - views.setTextViewText(R.id.txtvTitle, media.getEpisodeTitle()); - - if (status == PlayerStatus.PLAYING) { - String progressString = getProgressString(playbackService); - if (progressString != null) { - views.setTextViewText(R.id.txtvProgress, progressString); - } - views.setImageViewResource(R.id.butPlay, R.drawable.av_pause_dark); - if (Build.VERSION.SDK_INT >= 15) { - views.setContentDescription(R.id.butPlay, getString(R.string.pause_label)); - } - } else { - views.setImageViewResource(R.id.butPlay, R.drawable.av_play_dark); - if (Build.VERSION.SDK_INT >= 15) { - views.setContentDescription(R.id.butPlay, getString(R.string.play_label)); - } - } - views.setOnClickPendingIntent(R.id.butPlay, - createMediaButtonIntent()); - } else { - 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); - isUpdating = false; - } - - /** 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(PlaybackService ps) { - int position = ps.getCurrentPosition(); - int duration = ps.getDuration(); - if (position != PlaybackService.INVALID_TIME - && duration != PlaybackService.INVALID_TIME) { - return Converter.getDurationStringLong(position) + " / " - + Converter.getDurationStringLong(duration); - } else { - return null; - } - } - - private ServiceConnection mConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName className, IBinder service) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Connection to service established"); - synchronized (psLock) { - playbackService = ((PlaybackService.LocalBinder) service) - .getService(); - startViewUpdaterIfNotRunning(); - } - } - - @Override - public void onServiceDisconnected(ComponentName name) { - synchronized (psLock) { - playbackService = null; - if (BuildConfig.DEBUG) - Log.d(TAG, "Disconnected from service"); - } - } - - }; - - private void startViewUpdaterIfNotRunning() { - if (!isUpdating) { - ViewUpdater updateThread = new ViewUpdater(this); - updateThread.start(); - } - } - - class ViewUpdater extends Thread { - private static final String THREAD_NAME = "ViewUpdater"; - private PlayerWidgetService service; - - public ViewUpdater(PlayerWidgetService service) { - super(); - setName(THREAD_NAME); - this.service = service; - - } - - @Override - public void run() { - synchronized (psLock) { - service.updateViews(); - } - } - - } - -} |