diff options
Diffstat (limited to 'core/src/main/java')
107 files changed, 5750 insertions, 7829 deletions
diff --git a/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java deleted file mode 100644 index 7c2ea3d61..000000000 --- a/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java +++ /dev/null @@ -1,470 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.media; - -import android.content.Context; -import android.media.MediaPlayer; -import android.net.Uri; -import android.util.Log; - -import java.io.IOException; - -public class AndroidMediaPlayer extends MediaPlayerImpl { - private final static String AMP_TAG = "AocateAndroidMediaPlayer"; - - // private static final long TIMEOUT_DURATION_MS = 500; - - android.media.MediaPlayer mp = null; - - private android.media.MediaPlayer.OnBufferingUpdateListener onBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { - public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { - if (owningMediaPlayer != null) { - owningMediaPlayer.lock.lock(); - try { - if ((owningMediaPlayer.onBufferingUpdateListener != null) - && (owningMediaPlayer.mpi == AndroidMediaPlayer.this)) { - owningMediaPlayer.onBufferingUpdateListener.onBufferingUpdate(owningMediaPlayer, percent); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - } - }; - - private android.media.MediaPlayer.OnCompletionListener onCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { - public void onCompletion(android.media.MediaPlayer mp) { - Log.d(AMP_TAG, "onCompletionListener being called"); - if (owningMediaPlayer != null) { - owningMediaPlayer.lock.lock(); - try { - if (owningMediaPlayer.onCompletionListener != null) { - owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - } - }; - - private android.media.MediaPlayer.OnErrorListener onErrorListener = new android.media.MediaPlayer.OnErrorListener() { - public boolean onError(android.media.MediaPlayer mp, int what, int extra) { - // Once we're in errored state, any received messages are going to be junked - if (owningMediaPlayer != null) { - owningMediaPlayer.lock.lock(); - try { - if (owningMediaPlayer.onErrorListener != null) { - return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - return false; - } - }; - - private android.media.MediaPlayer.OnInfoListener onInfoListener = new android.media.MediaPlayer.OnInfoListener() { - public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { - if (owningMediaPlayer != null) { - owningMediaPlayer.lock.lock(); - try { - if ((owningMediaPlayer.onInfoListener != null) - && (owningMediaPlayer.mpi == AndroidMediaPlayer.this)) { - return owningMediaPlayer.onInfoListener.onInfo(owningMediaPlayer, what, extra); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - return false; - } - }; - - // We have to assign this.onPreparedListener because the - // onPreparedListener in owningMediaPlayer sets the state - // to PREPARED. Due to prepareAsync, that's the only - // reasonable place to do it - // The others it just didn't make sense to have a setOnXListener that didn't use the parameter - private android.media.MediaPlayer.OnPreparedListener onPreparedListener = new android.media.MediaPlayer.OnPreparedListener() { - public void onPrepared(android.media.MediaPlayer mp) { - Log.d(AMP_TAG, "Calling onPreparedListener.onPrepared()"); - if (AndroidMediaPlayer.this.owningMediaPlayer != null) { - AndroidMediaPlayer.this.lockMuteOnPreparedCount.lock(); - try { - if (AndroidMediaPlayer.this.muteOnPreparedCount > 0) { - AndroidMediaPlayer.this.muteOnPreparedCount--; - } - else { - AndroidMediaPlayer.this.muteOnPreparedCount = 0; - if (AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener != null) { - Log.d(AMP_TAG, "Invoking AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener.onPrepared"); - AndroidMediaPlayer.this.owningMediaPlayer.onPreparedListener.onPrepared(AndroidMediaPlayer.this.owningMediaPlayer); - } - } - } - finally { - AndroidMediaPlayer.this.lockMuteOnPreparedCount.unlock(); - } - if (owningMediaPlayer.mpi != AndroidMediaPlayer.this) { - Log.d(AMP_TAG, "owningMediaPlayer has changed implementation"); - } - } - } - }; - - private android.media.MediaPlayer.OnSeekCompleteListener onSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { - public void onSeekComplete(android.media.MediaPlayer mp) { - if (owningMediaPlayer != null) { - owningMediaPlayer.lock.lock(); - try { - lockMuteOnSeekCount.lock(); - try { - if (AndroidMediaPlayer.this.muteOnSeekCount > 0) { - AndroidMediaPlayer.this.muteOnSeekCount--; - } - else { - AndroidMediaPlayer.this.muteOnSeekCount = 0; - if (AndroidMediaPlayer.this.owningMediaPlayer.onSeekCompleteListener != null) { - owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer); - } - } - } - finally { - lockMuteOnSeekCount.unlock(); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - } - }; - - public AndroidMediaPlayer(com.aocate.media.MediaPlayer owningMediaPlayer, Context context) { - super(owningMediaPlayer, context); - - mp = new MediaPlayer(); - -// final ReentrantLock lock = new ReentrantLock(); -// Handler handler = new Handler(Looper.getMainLooper()) { -// @Override -// public void handleMessage(Message msg) { -// Log.d(AMP_TAG, "Instantiating new AndroidMediaPlayer from Handler"); -// lock.lock(); -// if (mp == null) { -// mp = new MediaPlayer(); -// } -// lock.unlock(); -// } -// }; -// -// long endTime = System.currentTimeMillis() + TIMEOUT_DURATION_MS; -// -// while (true) { -// // Retry messages until mp isn't null or it's time to give up -// handler.sendMessage(handler.obtainMessage()); -// if ((mp != null) -// || (endTime < System.currentTimeMillis())) { -// break; -// } -// try { -// Thread.sleep(50); -// } catch (InterruptedException e) { -// // TODO Auto-generated catch block -// e.printStackTrace(); -// } -// } - - if (mp == null) { - throw new IllegalStateException("Did not instantiate android.media.MediaPlayer successfully"); - } - - mp.setOnBufferingUpdateListener(this.onBufferingUpdateListener); - mp.setOnCompletionListener(this.onCompletionListener); - mp.setOnErrorListener(this.onErrorListener); - mp.setOnInfoListener(this.onInfoListener); - Log.d(AMP_TAG, " ++++++++++++++++++++++++++++++++ Setting prepared listener to this.onPreparedListener"); - mp.setOnPreparedListener(this.onPreparedListener); - mp.setOnSeekCompleteListener(this.onSeekCompleteListener); - } - - @Override - public boolean canSetPitch() { - return false; - } - - @Override - public boolean canSetSpeed() { - return false; - } - - @Override - public float getCurrentPitchStepsAdjustment() { - return 0; - } - - @Override - public int getCurrentPosition() { - owningMediaPlayer.lock.lock(); - try { - return mp.getCurrentPosition(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public float getCurrentSpeedMultiplier() { - return 1f; - } - - @Override - public int getDuration() { - owningMediaPlayer.lock.lock(); - try { - return mp.getDuration(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public float getMaxSpeedMultiplier() { - return 1f; - } - - @Override - public float getMinSpeedMultiplier() { - return 1f; - } - - @Override - public boolean isLooping() { - owningMediaPlayer.lock.lock(); - try { - return mp.isLooping(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public boolean isPlaying() { - owningMediaPlayer.lock.lock(); - try { - return mp.isPlaying(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void pause() { - owningMediaPlayer.lock.lock(); - try { - mp.pause(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void prepare() throws IllegalStateException, IOException { - owningMediaPlayer.lock.lock(); - Log.d(AMP_TAG, "prepare()"); - try { - mp.prepare(); - Log.d(AMP_TAG, "Finish prepare()"); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void prepareAsync() { - mp.prepareAsync(); - } - - @Override - public void release() { - owningMediaPlayer.lock.lock(); - try { - if (mp != null) { - Log.d(AMP_TAG, "mp.release()"); - mp.release(); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void reset() { - owningMediaPlayer.lock.lock(); - try { - mp.reset(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void seekTo(int msec) throws IllegalStateException { - owningMediaPlayer.lock.lock(); - try { - mp.setOnSeekCompleteListener(this.onSeekCompleteListener); - mp.seekTo(msec); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setAudioStreamType(int streamtype) { - owningMediaPlayer.lock.lock(); - try { - mp.setAudioStreamType(streamtype); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setDataSource(Context context, Uri uri) - throws IllegalArgumentException, IllegalStateException, IOException { - owningMediaPlayer.lock.lock(); - try { - Log.d(AMP_TAG, "setDataSource(context, " + uri.toString() + ")"); - mp.setDataSource(context, uri); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setDataSource(String path) throws IllegalArgumentException, - IllegalStateException, IOException { - owningMediaPlayer.lock.lock(); - try { - Log.d(AMP_TAG, "setDataSource(" + path + ")"); - mp.setDataSource(path); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { - // Can't! - } - - @Override - public void setLooping(boolean loop) { - owningMediaPlayer.lock.lock(); - try { - mp.setLooping(loop); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setPitchStepsAdjustment(float pitchSteps) { - // Can't! - } - - @Override - public void setPlaybackPitch(float f) { - // Can't! - } - - @Override - public void setPlaybackSpeed(float f) { - // Can't! - Log.d(AMP_TAG, "setPlaybackSpeed(" + f + ")"); - } - - @Override - public void setSpeedAdjustmentAlgorithm(int algorithm) { - // Can't! - Log.d(AMP_TAG, "setSpeedAdjustmentAlgorithm(" + algorithm + ")"); - } - - @Override - public void setVolume(float leftVolume, float rightVolume) { - owningMediaPlayer.lock.lock(); - try { - mp.setVolume(leftVolume, rightVolume); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void setWakeMode(Context context, int mode) { - owningMediaPlayer.lock.lock(); - try { - if (mode != 0) { - mp.setWakeMode(context, mode); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void start() { - owningMediaPlayer.lock.lock(); - try { - mp.start(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - @Override - public void stop() { - owningMediaPlayer.lock.lock(); - try { - mp.stop(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } -} diff --git a/core/src/main/java/com/aocate/media/MediaPlayer.java b/core/src/main/java/com/aocate/media/MediaPlayer.java deleted file mode 100644 index 79e63d03d..000000000 --- a/core/src/main/java/com/aocate/media/MediaPlayer.java +++ /dev/null @@ -1,1310 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.media; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.media.AudioManager; -import android.net.Uri; -import android.os.Handler; -import android.os.Handler.Callback; -import android.os.IBinder; -import android.os.Message; -import android.util.Log; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.locks.ReentrantLock; - -import de.danoeh.antennapod.core.BuildConfig; - -public class MediaPlayer { - public static final String TAG = "com.aocate.media.MediaPlayer"; - - public interface OnBufferingUpdateListener { - public abstract void onBufferingUpdate(MediaPlayer arg0, int percent); - } - - public interface OnCompletionListener { - public abstract void onCompletion(MediaPlayer arg0); - } - - public interface OnErrorListener { - public abstract boolean onError(MediaPlayer arg0, int what, int extra); - } - - public interface OnInfoListener { - public abstract boolean onInfo(MediaPlayer arg0, int what, int extra); - } - - public interface OnPitchAdjustmentAvailableChangedListener { - /** - * @param arg0 The owning media player - * @param pitchAdjustmentAvailable True if pitch adjustment is available, false if not - */ - public abstract void onPitchAdjustmentAvailableChanged( - MediaPlayer arg0, boolean pitchAdjustmentAvailable); - } - - public interface OnPreparedListener { - public abstract void onPrepared(MediaPlayer arg0); - } - - public interface OnSeekCompleteListener { - public abstract void onSeekComplete(MediaPlayer arg0); - } - - public interface OnSpeedAdjustmentAvailableChangedListener { - /** - * @param arg0 The owning media player - * @param speedAdjustmentAvailable True if speed adjustment is available, false if not - */ - public abstract void onSpeedAdjustmentAvailableChanged( - MediaPlayer arg0, boolean speedAdjustmentAvailable); - } - - public enum State { - IDLE, INITIALIZED, PREPARED, STARTED, PAUSED, STOPPED, PREPARING, PLAYBACK_COMPLETED, END, ERROR - } - - private static Uri SPEED_ADJUSTMENT_MARKET_URI = Uri - .parse("market://details?id=com.aocate.presto"); - - private static Intent prestoMarketIntent = null; - - public static final int MEDIA_ERROR_SERVER_DIED = android.media.MediaPlayer.MEDIA_ERROR_SERVER_DIED; - public static final int MEDIA_ERROR_UNKNOWN = android.media.MediaPlayer.MEDIA_ERROR_UNKNOWN; - public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = android.media.MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK; - - /** - * Indicates whether the specified action can be used as an intent. This - * method queries the package manager for installed packages that can - * respond to an intent with the specified action. If no suitable package is - * found, this method returns false. - * - * @param context The application's environment. - * @param action The Intent action to check for availability. - * @return True if an Intent with the specified action can be sent and - * responded to, false otherwise. - */ - public static boolean isIntentAvailable(Context context, String action) { - final PackageManager packageManager = context.getPackageManager(); - final Intent intent = new Intent(action); - List<ResolveInfo> list = packageManager.queryIntentServices(intent, - PackageManager.MATCH_DEFAULT_ONLY); - return list.size() > 0; - } - - /** - * Returns an explicit Intent for a service that accepts the given Intent - * or null if no such service was found. - * - * @param context The application's environment. - * @param action The Intent action to check for availability. - * @return The explicit service Intent or null if no service was found. - */ - public static Intent getPrestoServiceIntent(Context context, String action) { - final PackageManager packageManager = context.getPackageManager(); - final Intent actionIntent = new Intent(action); - List<ResolveInfo> list = packageManager.queryIntentServices(actionIntent, - PackageManager.MATCH_DEFAULT_ONLY); - if (list.size() > 0) { - ResolveInfo first = list.get(0); - if (first.serviceInfo != null) { - Intent intent = new Intent(); - intent.setComponent(new ComponentName(first.serviceInfo.packageName, - first.serviceInfo.name)); - Log.i(TAG, "Returning intent:" + intent.toString()); - return intent; - } else { - Log.e(TAG, "Found service that accepts " + action + ", but serviceInfo was null"); - return null; - } - } else { - return null; - } - } - - /** - * Indicates whether the Presto library is installed - * - * @param context The context to use to query the package manager. - * @return True if the Presto library is installed, false if not. - */ - public static boolean isPrestoLibraryInstalled(Context context) { - return isIntentAvailable(context, ServiceBackedMediaPlayer.INTENT_NAME); - } - - /** - * Return an Intent that opens the Android Market page for the speed - * alteration library - * - * @return The Intent for the Presto library on the Android Market - */ - public static Intent getPrestoMarketIntent() { - if (prestoMarketIntent == null) { - prestoMarketIntent = new Intent(Intent.ACTION_VIEW, - SPEED_ADJUSTMENT_MARKET_URI); - } - return prestoMarketIntent; - } - - /** - * Open the Android Market page for the Presto library - * - * @param context The context from which to open the Android Market page - */ - public static void openPrestoMarketIntent(Context context) { - context.startActivity(getPrestoMarketIntent()); - } - - private static final String MP_TAG = "AocateReplacementMediaPlayer"; - - private static final double PITCH_STEP_CONSTANT = 1.0594630943593; - - private AndroidMediaPlayer amp = null; - // This is whether speed adjustment should be enabled (by the Service) - // To avoid the Service entirely, set useService to false - protected boolean enableSpeedAdjustment = true; - private int lastKnownPosition = 0; - // In some cases, we're going to have to replace the - // android.media.MediaPlayer on the fly, and we don't want to touch the - // wrong media player, so lock it way too much. - ReentrantLock lock = new ReentrantLock(); - private int mAudioStreamType = AudioManager.STREAM_MUSIC; - private Context mContext; - private boolean mIsLooping = false; - private float mLeftVolume = 1f; - private float mPitchStepsAdjustment = 0f; - private float mRightVolume = 1f; - private float mSpeedMultiplier = 1f; - private int mWakeMode = 0; - MediaPlayerImpl mpi = null; - protected boolean pitchAdjustmentAvailable = false; - private ServiceBackedMediaPlayer sbmp = null; - protected boolean speedAdjustmentAvailable = false; - - private Handler mServiceDisconnectedHandler = null; - - // Some parts of state cannot be found by calling MediaPlayerImpl functions, - // so store our own state. This also helps copy state when changing - // implementations - State state = State.INITIALIZED; - String stringDataSource = null; - Uri uriDataSource = null; - private boolean useService = false; - - // Naming Convention for Listeners - // Most listeners can both be set by clients and called by MediaPlayImpls - // There are a few that have to do things in this class as well as calling - // the function. In all cases, onX is what is called by MediaPlayerImpl - // If there is work to be done in this class, then the listener that is - // set by setX is X (with the first letter lowercase). - OnBufferingUpdateListener onBufferingUpdateListener = null; - OnCompletionListener onCompletionListener = null; - OnErrorListener onErrorListener = null; - OnInfoListener onInfoListener = null; - - // Special case. Pitch adjustment ceases to be available when we switch - // to the android.media.MediaPlayer (though it is not guaranteed to be - // available when using the ServiceBackedMediaPlayer) - OnPitchAdjustmentAvailableChangedListener onPitchAdjustmentAvailableChangedListener = new OnPitchAdjustmentAvailableChangedListener() { - public void onPitchAdjustmentAvailableChanged(MediaPlayer arg0, - boolean pitchAdjustmentAvailable) { - lock.lock(); - try { - Log - .d( - MP_TAG, - "onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged being called"); - if (MediaPlayer.this.pitchAdjustmentAvailable != pitchAdjustmentAvailable) { - Log.d(MP_TAG, "Pitch adjustment state has changed from " - + MediaPlayer.this.pitchAdjustmentAvailable - + " to " + pitchAdjustmentAvailable); - MediaPlayer.this.pitchAdjustmentAvailable = pitchAdjustmentAvailable; - if (MediaPlayer.this.pitchAdjustmentAvailableChangedListener != null) { - MediaPlayer.this.pitchAdjustmentAvailableChangedListener - .onPitchAdjustmentAvailableChanged(arg0, - pitchAdjustmentAvailable); - } - } - } finally { - lock.unlock(); - } - } - }; - OnPitchAdjustmentAvailableChangedListener pitchAdjustmentAvailableChangedListener = null; - - MediaPlayer.OnPreparedListener onPreparedListener = new MediaPlayer.OnPreparedListener() { - public void onPrepared(MediaPlayer arg0) { - Log.d(MP_TAG, "onPreparedListener 242 setting state to PREPARED"); - MediaPlayer.this.state = State.PREPARED; - if (MediaPlayer.this.preparedListener != null) { - Log.d(MP_TAG, "Calling preparedListener"); - MediaPlayer.this.preparedListener.onPrepared(arg0); - } - Log.d(MP_TAG, "Wrap up onPreparedListener"); - } - }; - - OnPreparedListener preparedListener = null; - OnSeekCompleteListener onSeekCompleteListener = null; - - // Special case. Speed adjustment ceases to be available when we switch - // to the android.media.MediaPlayer (though it is not guaranteed to be - // available when using the ServiceBackedMediaPlayer) - OnSpeedAdjustmentAvailableChangedListener onSpeedAdjustmentAvailableChangedListener = new OnSpeedAdjustmentAvailableChangedListener() { - public void onSpeedAdjustmentAvailableChanged(MediaPlayer arg0, - boolean speedAdjustmentAvailable) { - lock.lock(); - try { - Log - .d( - MP_TAG, - "onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged being called"); - if (MediaPlayer.this.speedAdjustmentAvailable != speedAdjustmentAvailable) { - Log.d(MP_TAG, "Speed adjustment state has changed from " - + MediaPlayer.this.speedAdjustmentAvailable - + " to " + speedAdjustmentAvailable); - MediaPlayer.this.speedAdjustmentAvailable = speedAdjustmentAvailable; - if (MediaPlayer.this.speedAdjustmentAvailableChangedListener != null) { - MediaPlayer.this.speedAdjustmentAvailableChangedListener - .onSpeedAdjustmentAvailableChanged(arg0, - speedAdjustmentAvailable); - } - } - } finally { - lock.unlock(); - } - } - }; - OnSpeedAdjustmentAvailableChangedListener speedAdjustmentAvailableChangedListener = null; - - private int speedAdjustmentAlgorithm = SpeedAdjustmentAlgorithm.SONIC; - - public MediaPlayer(final Context context) { - this(context, true); - } - - public MediaPlayer(final Context context, boolean useService) { - this.mContext = context; - this.useService = useService; - - // So here's the major problem - // Sometimes the service won't exist or won't be connected, - // so start with an android.media.MediaPlayer, and when - // the service is connected, use that from then on - this.mpi = this.amp = new AndroidMediaPlayer(this, context); - - // setupMpi will go get the Service, if it can, then bring that - // implementation into sync - Log.d(MP_TAG, "setupMpi"); - setupMpi(context); - } - - private boolean invalidServiceConnectionConfiguration() { - if (!(this.mpi instanceof ServiceBackedMediaPlayer)) { - if (this.useService && isPrestoLibraryInstalled()) { - // In this case, the Presto library has been installed - // or something while playing sound - // We could be using the service, but we're not - Log.d(MP_TAG, "We could be using the service, but we're not 316"); - return true; - } - // If useService is false, then we shouldn't be using the SBMP - // If the Presto library isn't installed, ditto - Log.d(MP_TAG, "this.mpi is not a ServiceBackedMediaPlayer, but we couldn't use it anyway 321"); - return false; - } else { - if (BuildConfig.DEBUG && !(this.mpi instanceof ServiceBackedMediaPlayer)) - throw new AssertionError(); - if (this.useService && isPrestoLibraryInstalled()) { - // We should be using the service, and we are. Great! - Log.d(MP_TAG, "We could be using a ServiceBackedMediaPlayer and we are 327"); - return false; - } - // We're trying to use the service when we shouldn't, - // that's an invalid configuration - Log.d(MP_TAG, "We're trying to use a ServiceBackedMediaPlayer but we shouldn't be 332"); - return true; - } - } - - private void setupMpi(final Context context) { - lock.lock(); - try { - Log.d(MP_TAG, "setupMpi 336"); - // Check if the client wants to use the service at all, - // then if we're already using the right kind of media player - if (this.useService && isPrestoLibraryInstalled()) { - if ((this.mpi != null) - && (this.mpi instanceof ServiceBackedMediaPlayer)) { - Log.d(MP_TAG, "Already using ServiceBackedMediaPlayer"); - return; - } - if (this.sbmp == null) { - Log.d(MP_TAG, "Instantiating new ServiceBackedMediaPlayer 346"); - this.sbmp = new ServiceBackedMediaPlayer(this, context, - new ServiceConnection() { - public void onServiceConnected( - ComponentName className, - final IBinder service) { - Thread t = new Thread(new Runnable() { - public void run() { - // This lock probably isn't granular - // enough - MediaPlayer.this.lock.lock(); - Log.d(MP_TAG, - "onServiceConnected 257"); - try { - MediaPlayer.this - .switchMediaPlayerImpl( - MediaPlayer.this.amp, - MediaPlayer.this.sbmp); - Log.d(MP_TAG, "End onServiceConnected 362"); - } finally { - MediaPlayer.this.lock.unlock(); - } - } - }); - t.start(); - } - - public void onServiceDisconnected( - ComponentName className) { - MediaPlayer.this.lock.lock(); - try { - // Can't get any more useful information - // out of sbmp - if (MediaPlayer.this.sbmp != null) { - MediaPlayer.this.sbmp.release(); - } - // Unlike most other cases, sbmp gets set - // to null since there's nothing useful - // backing it now - MediaPlayer.this.sbmp = null; - - if (mServiceDisconnectedHandler == null) { - mServiceDisconnectedHandler = new Handler(new Callback() { - public boolean handleMessage(Message msg) { - // switchMediaPlayerImpl won't try to - // clone anything from null - lock.lock(); - try { - if (MediaPlayer.this.amp == null) { - // This should never be in this state - MediaPlayer.this.amp = new AndroidMediaPlayer( - MediaPlayer.this, - MediaPlayer.this.mContext); - } - // Use sbmp instead of null in case by some miracle it's - // been restored in the meantime - MediaPlayer.this.switchMediaPlayerImpl( - MediaPlayer.this.sbmp, - MediaPlayer.this.amp); - return true; - } finally { - lock.unlock(); - } - } - }); - } - - // This code needs to execute on the - // original thread to instantiate - // the new object in the right place - mServiceDisconnectedHandler - .sendMessage( - mServiceDisconnectedHandler - .obtainMessage()); - // Note that we do NOT want to set - // useService. useService is about - // what the user wants, not what they - // get - } finally { - MediaPlayer.this.lock.unlock(); - } - } - } - ); - } - switchMediaPlayerImpl(this.amp, this.sbmp); - } else { - if ((this.mpi != null) - && (this.mpi instanceof AndroidMediaPlayer)) { - Log.d(MP_TAG, "Already using AndroidMediaPlayer"); - return; - } - if (this.amp == null) { - Log.d(MP_TAG, "Instantiating new AndroidMediaPlayer (this should be impossible)"); - this.amp = new AndroidMediaPlayer(this, context); - } - switchMediaPlayerImpl(this.sbmp, this.amp); - } - } finally { - lock.unlock(); - } - } - - private void switchMediaPlayerImpl(MediaPlayerImpl from, MediaPlayerImpl to) { - lock.lock(); - try { - Log.d(MP_TAG, "switchMediaPlayerImpl"); - if ((from == to) - // Same object, nothing to synchronize - || (to == null) - // Nothing to copy to (maybe this should throw an error?) - || ((to instanceof ServiceBackedMediaPlayer) && !((ServiceBackedMediaPlayer) to).isConnected()) - // ServiceBackedMediaPlayer hasn't yet connected, onServiceConnected will take care of the transition - || (MediaPlayer.this.state == State.END)) { - // State.END is after a release(), no further functions should - // be called on this class and from is likely to have problems - // retrieving state that won't be used anyway - return; - } - // Extract all that we can from the existing implementation - // and copy it to the new implementation - - Log.d(MP_TAG, "switchMediaPlayerImpl(), current state is " - + this.state.toString()); - - to.reset(); - - // Do this first so we don't have to prepare the same - // data file twice - to.setEnableSpeedAdjustment(MediaPlayer.this.enableSpeedAdjustment); - - // This is a reasonable place to set all of these, - // none of them require prepare() or the like first - to.setAudioStreamType(this.mAudioStreamType); - to.setSpeedAdjustmentAlgorithm(this.speedAdjustmentAlgorithm); - to.setLooping(this.mIsLooping); - to.setPitchStepsAdjustment(this.mPitchStepsAdjustment); - Log.d(MP_TAG, "Setting playback speed to " + this.mSpeedMultiplier); - to.setPlaybackSpeed(this.mSpeedMultiplier); - to.setVolume(MediaPlayer.this.mLeftVolume, - MediaPlayer.this.mRightVolume); - to.setWakeMode(this.mContext, this.mWakeMode); - - Log.d(MP_TAG, "asserting at least one data source is null"); - assert ((MediaPlayer.this.stringDataSource == null) || (MediaPlayer.this.uriDataSource == null)); - - if (uriDataSource != null) { - Log.d(MP_TAG, "switchMediaPlayerImpl(): uriDataSource != null"); - try { - to.setDataSource(this.mContext, uriDataSource); - } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IllegalStateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - if (stringDataSource != null) { - Log.d(MP_TAG, - "switchMediaPlayerImpl(): stringDataSource != null"); - try { - to.setDataSource(stringDataSource); - } catch (IllegalArgumentException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IllegalStateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - if ((this.state == State.PREPARED) - || (this.state == State.PREPARING) - || (this.state == State.PAUSED) - || (this.state == State.STOPPED) - || (this.state == State.STARTED) - || (this.state == State.PLAYBACK_COMPLETED)) { - Log.d(MP_TAG, "switchMediaPlayerImpl(): prepare and seek"); - // Use prepare here instead of prepareAsync so that - // we wait for it to be ready before we try to use it - try { - to.muteNextOnPrepare(); - to.prepare(); - } catch (IllegalStateException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - int seekPos = 0; - if (from != null) { - seekPos = from.getCurrentPosition(); - } else if (this.lastKnownPosition < to.getDuration()) { - // This can happen if the Service unexpectedly - // disconnected. Because it would result in too much - // information being passed around, we don't constantly - // poll for the lastKnownPosition, but we'll save it - // when getCurrentPosition is called - seekPos = this.lastKnownPosition; - } - to.muteNextSeek(); - to.seekTo(seekPos); - } - if ((from != null) - && from.isPlaying()) { - from.pause(); - } - if ((this.state == State.STARTED) - || (this.state == State.PAUSED) - || (this.state == State.STOPPED)) { - Log.d(MP_TAG, "switchMediaPlayerImpl(): start"); - if (to != null) { - to.start(); - } - } - - if (this.state == State.PAUSED) { - Log.d(MP_TAG, "switchMediaPlayerImpl(): paused"); - if (to != null) { - to.pause(); - } - } else if (this.state == State.STOPPED) { - Log.d(MP_TAG, "switchMediaPlayerImpl(): stopped"); - if (to != null) { - to.stop(); - } - } - - this.mpi = to; - - // Cheating here by relying on the side effect in - // on(Pitch|Speed)AdjustmentAvailableChanged - if ((to.canSetPitch() != this.pitchAdjustmentAvailable) - && (this.onPitchAdjustmentAvailableChangedListener != null)) { - this.onPitchAdjustmentAvailableChangedListener - .onPitchAdjustmentAvailableChanged(this, to - .canSetPitch()); - } - if ((to.canSetSpeed() != this.speedAdjustmentAvailable) - && (this.onSpeedAdjustmentAvailableChangedListener != null)) { - this.onSpeedAdjustmentAvailableChangedListener - .onSpeedAdjustmentAvailableChanged(this, to - .canSetSpeed()); - } - Log.d(MP_TAG, "switchMediaPlayerImpl() 625 " + this.state.toString()); - } finally { - lock.unlock(); - } - } - - /** - * Returns true if pitch can be changed at this moment - * - * @return True if pitch can be changed - */ - public boolean canSetPitch() { - lock.lock(); - try { - return this.mpi.canSetPitch(); - } finally { - lock.unlock(); - } - } - - /** - * Returns true if speed can be changed at this moment - * - * @return True if speed can be changed - */ - public boolean canSetSpeed() { - lock.lock(); - try { - return this.mpi.canSetSpeed(); - } finally { - lock.unlock(); - } - } - - protected void finalize() throws Throwable { - lock.lock(); - try { - Log.d(MP_TAG, "finalize() 626"); - this.release(); - } finally { - lock.unlock(); - } - } - - /** - * Returns the number of steps (in a musical scale) by which playback is - * currently shifted. When greater than zero, pitch is shifted up. When less - * than zero, pitch is shifted down. - * - * @return The number of steps pitch is currently shifted by - */ - public float getCurrentPitchStepsAdjustment() { - lock.lock(); - try { - return this.mpi.getCurrentPitchStepsAdjustment(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.getCurrentPosition() - * Accurate only to frame size of encoded data (26 ms for MP3s) - * - * @return Current position (in milliseconds) - */ - public int getCurrentPosition() { - lock.lock(); - try { - return (this.lastKnownPosition = this.mpi.getCurrentPosition()); - } finally { - lock.unlock(); - } - } - - /** - * Returns the current speed multiplier. Defaults to 1.0 (normal speed) - * - * @return The current speed multiplier - */ - public float getCurrentSpeedMultiplier() { - lock.lock(); - try { - return this.mpi.getCurrentSpeedMultiplier(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.getDuration() - * - * @return Length of the track (in milliseconds) - */ - public int getDuration() { - lock.lock(); - try { - return this.mpi.getDuration(); - } finally { - lock.unlock(); - } - } - - /** - * Get the maximum value that can be passed to setPlaybackSpeed - * - * @return The maximum speed multiplier - */ - public float getMaxSpeedMultiplier() { - lock.lock(); - try { - return this.mpi.getMaxSpeedMultiplier(); - } finally { - lock.unlock(); - } - } - - /** - * Get the minimum value that can be passed to setPlaybackSpeed - * - * @return The minimum speed multiplier - */ - public float getMinSpeedMultiplier() { - lock.lock(); - try { - return this.mpi.getMinSpeedMultiplier(); - } finally { - lock.unlock(); - } - } - - /** - * Gets the version code of the backing service - * - * @return -1 if ServiceBackedMediaPlayer is not used, 0 if the service is not - * connected, otherwise the version code retrieved from the service - */ - public int getServiceVersionCode() { - lock.lock(); - try { - if (this.mpi instanceof ServiceBackedMediaPlayer) { - return ((ServiceBackedMediaPlayer) this.mpi).getServiceVersionCode(); - } else { - return -1; - } - } finally { - lock.unlock(); - } - } - - /** - * Gets the version name of the backing service - * - * @return null if ServiceBackedMediaPlayer is not used, empty string if - * the service is not connected, otherwise the version name retrieved from - * the service - */ - public String getServiceVersionName() { - lock.lock(); - try { - if (this.mpi instanceof ServiceBackedMediaPlayer) { - return ((ServiceBackedMediaPlayer) this.mpi).getServiceVersionName(); - } else { - return null; - } - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.isLooping() - * - * @return True if the track is looping - */ - public boolean isLooping() { - lock.lock(); - try { - return this.mpi.isLooping(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.isPlaying() - * - * @return True if the track is playing - */ - public boolean isPlaying() { - lock.lock(); - try { - return this.mpi.isPlaying(); - } finally { - lock.unlock(); - } - } - - /** - * Returns true if this MediaPlayer has access to the Presto - * library - * - * @return True if the Presto library is installed - */ - public boolean isPrestoLibraryInstalled() { - if ((this.mpi == null) || (this.mpi.mContext == null)) { - return false; - } - return isPrestoLibraryInstalled(this.mpi.mContext); - } - - /** - * Open the Android Market page in the same context as this MediaPlayer - */ - public void openPrestoMarketIntent() { - if ((this.mpi != null) && (this.mpi.mContext != null)) { - openPrestoMarketIntent(this.mpi.mContext); - } - } - - /** - * Functions identically to android.media.MediaPlayer.pause() Pauses the - * track - */ - public void pause() { - lock.lock(); - try { - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.PAUSED; - this.mpi.pause(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.prepare() Prepares the - * track. This or prepareAsync must be called before start() - */ - public void prepare() throws IllegalStateException, IOException { - lock.lock(); - try { - Log.d(MP_TAG, "prepare() 746 using " + ((this.mpi == null) ? "null (this shouldn't happen)" : this.mpi.getClass().toString()) + " state " + this.state.toString()); - Log.d(MP_TAG, "onPreparedListener is: " + ((this.onPreparedListener == null) ? "null" : "non-null")); - Log.d(MP_TAG, "preparedListener is: " + ((this.preparedListener == null) ? "null" : "non-null")); - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.mpi.prepare(); - this.state = State.PREPARED; - Log.d(MP_TAG, "prepare() finished 778"); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.prepareAsync() - * Prepares the track. This or prepare must be called before start() - */ - public void prepareAsync() { - lock.lock(); - try { - Log.d(MP_TAG, "prepareAsync() 779"); - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.PREPARING; - this.mpi.prepareAsync(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.release() Releases the - * underlying resources used by the media player. - */ - public void release() { - lock.lock(); - try { - Log.d(MP_TAG, "Releasing MediaPlayer 791"); - - this.state = State.END; - if (this.amp != null) { - this.amp.release(); - } - if (this.sbmp != null) { - this.sbmp.release(); - } - - this.onBufferingUpdateListener = null; - this.onCompletionListener = null; - this.onErrorListener = null; - this.onInfoListener = null; - this.preparedListener = null; - this.onPitchAdjustmentAvailableChangedListener = null; - this.pitchAdjustmentAvailableChangedListener = null; - Log.d(MP_TAG, "Setting onSeekCompleteListener to null 871"); - this.onSeekCompleteListener = null; - this.onSpeedAdjustmentAvailableChangedListener = null; - this.speedAdjustmentAvailableChangedListener = null; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.reset() Resets the - * track to idle state - */ - public void reset() { - lock.lock(); - try { - this.state = State.IDLE; - this.stringDataSource = null; - this.uriDataSource = null; - this.mpi.reset(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.seekTo(int msec) Seeks - * to msec in the track - */ - public void seekTo(int msec) throws IllegalStateException { - lock.lock(); - try { - this.mpi.seekTo(msec); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setAudioStreamType(int - * streamtype) Sets the audio stream type. - */ - public void setAudioStreamType(int streamtype) { - lock.lock(); - try { - this.mAudioStreamType = streamtype; - this.mpi.setAudioStreamType(streamtype); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setDataSource(Context - * context, Uri uri) Sets uri as data source in the context given - */ - public void setDataSource(Context context, Uri uri) - throws IllegalArgumentException, IllegalStateException, IOException { - lock.lock(); - try { - Log.d(MP_TAG, "In setDataSource(context, " + uri.toString() + "), using " + this.mpi.getClass().toString()); - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.INITIALIZED; - this.stringDataSource = null; - this.uriDataSource = uri; - this.mpi.setDataSource(context, uri); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setDataSource(String - * path) Sets the data source of the track to a file given. - */ - public void setDataSource(String path) throws IllegalArgumentException, - IllegalStateException, IOException { - lock.lock(); - try { - Log.d(MP_TAG, "In setDataSource(context, " + path + ")"); - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.INITIALIZED; - this.stringDataSource = path; - this.uriDataSource = null; - this.mpi.setDataSource(path); - } finally { - lock.unlock(); - } - } - - /** - * Sets whether to use speed adjustment or not. Speed adjustment on is more - * computation-intensive than with it off. - * - * @param enableSpeedAdjustment Whether speed adjustment should be supported. - */ - public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { - lock.lock(); - try { - this.enableSpeedAdjustment = enableSpeedAdjustment; - this.mpi.setEnableSpeedAdjustment(enableSpeedAdjustment); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setLooping(boolean - * loop) Sets the track to loop infinitely if loop is true, play once if - * loop is false - */ - public void setLooping(boolean loop) { - lock.lock(); - try { - this.mIsLooping = loop; - this.mpi.setLooping(loop); - } finally { - lock.unlock(); - } - } - - /** - * Sets the number of steps (in a musical scale) by which playback is - * currently shifted. When greater than zero, pitch is shifted up. When less - * than zero, pitch is shifted down. - * - * @param pitchSteps The number of steps by which to shift playback - */ - public void setPitchStepsAdjustment(float pitchSteps) { - lock.lock(); - try { - this.mPitchStepsAdjustment = pitchSteps; - this.mpi.setPitchStepsAdjustment(pitchSteps); - } finally { - lock.unlock(); - } - } - - /** - * Set the algorithm to use for changing the speed and pitch of audio - * See SpeedAdjustmentAlgorithm constants for more details - * - * @param algorithm The algorithm to use. - */ - public void setSpeedAdjustmentAlgorithm(int algorithm) { - lock.lock(); - try { - this.speedAdjustmentAlgorithm = algorithm; - if (this.mpi != null) { - this.mpi.setSpeedAdjustmentAlgorithm(algorithm); - } - } finally { - lock.unlock(); - } - } - - private static float getPitchStepsAdjustment(float pitch) { - return (float) (Math.log(pitch) / (2 * Math.log(PITCH_STEP_CONSTANT))); - } - - /** - * Sets the percentage by which pitch is currently shifted. When greater - * than zero, pitch is shifted up. When less than zero, pitch is shifted - * down - * - * @param f The percentage to shift pitch - */ - public void setPlaybackPitch(float pitch) { - lock.lock(); - try { - this.mPitchStepsAdjustment = getPitchStepsAdjustment(pitch); - this.mpi.setPlaybackPitch(pitch); - } finally { - lock.unlock(); - } - } - - /** - * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so on. - * Speed should never be set to 0 or below. - * - * @param f The speed multiplier to use for further playback - */ - public void setPlaybackSpeed(float f) { - lock.lock(); - try { - this.mSpeedMultiplier = f; - this.mpi.setPlaybackSpeed(f); - } finally { - lock.unlock(); - } - } - - /** - * Sets whether to use speed adjustment or not. Speed adjustment on is more - * computation-intensive than with it off. - * - * @param enableSpeedAdjustment Whether speed adjustment should be supported. - */ - public void setUseService(boolean useService) { - lock.lock(); - try { - this.useService = useService; - setupMpi(this.mpi.mContext); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setVolume(float - * leftVolume, float rightVolume) Sets the stereo volume - */ - public void setVolume(float leftVolume, float rightVolume) { - lock.lock(); - try { - this.mLeftVolume = leftVolume; - this.mRightVolume = rightVolume; - this.mpi.setVolume(leftVolume, rightVolume); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setWakeMode(Context - * context, int mode) Acquires a wake lock in the context given. You must - * request the appropriate permissions in your AndroidManifest.xml file. - */ - public void setWakeMode(Context context, int mode) { - lock.lock(); - try { - this.mWakeMode = mode; - this.mpi.setWakeMode(context, mode); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener - * listener) Sets a listener to be used when a track completes playing. - */ - public void setOnBufferingUpdateListener(OnBufferingUpdateListener listener) { - lock.lock(); - try { - this.onBufferingUpdateListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnCompletionListener(OnCompletionListener - * listener) Sets a listener to be used when a track completes playing. - */ - public void setOnCompletionListener(OnCompletionListener listener) { - lock.lock(); - try { - this.onCompletionListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnErrorListener(OnErrorListener listener) - * Sets a listener to be used when a track encounters an error. - */ - public void setOnErrorListener(OnErrorListener listener) { - lock.lock(); - try { - this.onErrorListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnInfoListener(OnInfoListener listener) Sets - * a listener to be used when a track has info. - */ - public void setOnInfoListener(OnInfoListener listener) { - lock.lock(); - try { - this.onInfoListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Sets a listener that will fire when pitch adjustment becomes available or - * stops being available - */ - public void setOnPitchAdjustmentAvailableChangedListener( - OnPitchAdjustmentAvailableChangedListener listener) { - lock.lock(); - try { - this.pitchAdjustmentAvailableChangedListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnPreparedListener(OnPreparedListener - * listener) Sets a listener to be used when a track finishes preparing. - */ - public void setOnPreparedListener(OnPreparedListener listener) { - lock.lock(); - Log.d(MP_TAG, " ++++++++++++++++++++++++++++++++++++++++++++ setOnPreparedListener"); - try { - this.preparedListener = listener; - // For this one, we do not explicitly set the MediaPlayer or the - // Service listener. This is because in addition to calling the - // listener provided by the client, it's necessary to change - // state to PREPARED. See prepareAsync for implementation details - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to - * android.media.MediaPlayer.setOnSeekCompleteListener - * (OnSeekCompleteListener listener) Sets a listener to be used when a track - * finishes seeking. - */ - public void setOnSeekCompleteListener(OnSeekCompleteListener listener) { - lock.lock(); - try { - this.onSeekCompleteListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Sets a listener that will fire when speed adjustment becomes available or - * stops being available - */ - public void setOnSpeedAdjustmentAvailableChangedListener( - OnSpeedAdjustmentAvailableChangedListener listener) { - lock.lock(); - try { - this.speedAdjustmentAvailableChangedListener = listener; - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.start() Starts a track - * playing - */ - public void start() { - lock.lock(); - try { - Log.d(MP_TAG, "start() 1149"); - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.STARTED; - Log.d(MP_TAG, "start() 1154"); - this.mpi.start(); - } finally { - lock.unlock(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.stop() Stops a track - * playing and resets its position to the start. - */ - public void stop() { - lock.lock(); - try { - if (invalidServiceConnectionConfiguration()) { - setupMpi(this.mpi.mContext); - } - this.state = State.STOPPED; - this.mpi.stop(); - } finally { - lock.unlock(); - } - } -}
\ No newline at end of file diff --git a/core/src/main/java/com/aocate/media/MediaPlayerImpl.java b/core/src/main/java/com/aocate/media/MediaPlayerImpl.java deleted file mode 100644 index 856ab47ce..000000000 --- a/core/src/main/java/com/aocate/media/MediaPlayerImpl.java +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.media; - -import java.io.IOException; -import java.util.concurrent.locks.ReentrantLock; - -import android.content.Context; -import android.net.Uri; -import android.util.Log; - -public abstract class MediaPlayerImpl { - private static final String MPI_TAG = "AocateMediaPlayerImpl"; - protected final MediaPlayer owningMediaPlayer; - protected final Context mContext; - protected int muteOnPreparedCount = 0; - protected int muteOnSeekCount = 0; - - public MediaPlayerImpl(MediaPlayer owningMediaPlayer, Context context) { - this.owningMediaPlayer = owningMediaPlayer; - - this.mContext = context; - } - - public abstract boolean canSetPitch(); - - public abstract boolean canSetSpeed(); - - public abstract float getCurrentPitchStepsAdjustment(); - - public abstract int getCurrentPosition(); - - public abstract float getCurrentSpeedMultiplier(); - - public abstract int getDuration(); - - public abstract float getMaxSpeedMultiplier(); - - public abstract float getMinSpeedMultiplier(); - - public abstract boolean isLooping(); - - public abstract boolean isPlaying(); - - public abstract void pause(); - - public abstract void prepare() throws IllegalStateException, IOException; - - public abstract void prepareAsync(); - - public abstract void release(); - - public abstract void reset(); - - public abstract void seekTo(int msec) throws IllegalStateException; - - public abstract void setAudioStreamType(int streamtype); - - public abstract void setDataSource(Context context, Uri uri) throws IllegalArgumentException, IllegalStateException, IOException; - - public abstract void setDataSource(String path) throws IllegalArgumentException, IllegalStateException, IOException; - - public abstract void setEnableSpeedAdjustment(boolean enableSpeedAdjustment); - - public abstract void setLooping(boolean loop); - - public abstract void setPitchStepsAdjustment(float pitchSteps); - - public abstract void setPlaybackPitch(float f); - - public abstract void setPlaybackSpeed(float f); - - public abstract void setSpeedAdjustmentAlgorithm(int algorithm); - - public abstract void setVolume(float leftVolume, float rightVolume); - - public abstract void setWakeMode(Context context, int mode); - - public abstract void start(); - - public abstract void stop(); - - protected ReentrantLock lockMuteOnPreparedCount = new ReentrantLock(); - public void muteNextOnPrepare() { - lockMuteOnPreparedCount.lock(); - Log.d(MPI_TAG, "muteNextOnPrepare()"); - try { - this.muteOnPreparedCount++; - } - finally { - lockMuteOnPreparedCount.unlock(); - } - } - - protected ReentrantLock lockMuteOnSeekCount = new ReentrantLock(); - public void muteNextSeek() { - lockMuteOnSeekCount.lock(); - Log.d(MPI_TAG, "muteNextOnSeek()"); - try { - this.muteOnSeekCount++; - } - finally { - lockMuteOnSeekCount.unlock(); - } - } -} diff --git a/core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java b/core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java deleted file mode 100644 index 0e27a8014..000000000 --- a/core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java +++ /dev/null @@ -1,1203 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// -// ----------------------------------------------------------------------- -// Compared to the original version, this class been slightly modified so -// that any acquired WakeLocks are only held while the MediaPlayer is -// playing (see the stayAwake method for more details). - - -package com.aocate.media; - -import java.io.IOException; - -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.ServiceConnection; -import android.media.AudioManager; -import android.net.Uri; -import android.os.IBinder; -import android.os.PowerManager; -import android.os.RemoteException; -import android.os.PowerManager.WakeLock; -import android.util.Log; - -import com.aocate.media.MediaPlayer.State; -import com.aocate.presto.service.IDeathCallback_0_8; -import com.aocate.presto.service.IOnBufferingUpdateListenerCallback_0_8; -import com.aocate.presto.service.IOnCompletionListenerCallback_0_8; -import com.aocate.presto.service.IOnErrorListenerCallback_0_8; -import com.aocate.presto.service.IOnInfoListenerCallback_0_8; -import com.aocate.presto.service.IOnPitchAdjustmentAvailableChangedListenerCallback_0_8; -import com.aocate.presto.service.IOnPreparedListenerCallback_0_8; -import com.aocate.presto.service.IOnSeekCompleteListenerCallback_0_8; -import com.aocate.presto.service.IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8; -import com.aocate.presto.service.IPlayMedia_0_8; - -import de.danoeh.antennapod.core.BuildConfig; - -/** - * Class for connecting to remote speed-altering, media playing Service - * Note that there is unusually high coupling between MediaPlayer and this - * class. This is an unfortunate compromise, since the alternative was to - * track state in two different places in this code (plus the internal state - * of the remote media player). - * @author aocate - * - */ -public class ServiceBackedMediaPlayer extends MediaPlayerImpl { - static final String INTENT_NAME = "com.aocate.intent.PLAY_AUDIO_ADJUST_SPEED_0_8"; - - private static final String SBMP_TAG = "AocateServiceBackedMediaPlayer"; - - private ServiceConnection mPlayMediaServiceConnection = null; - protected IPlayMedia_0_8 pmInterface = null; - private Intent playMediaServiceIntent = null; - // In some cases, we're going to have to replace the - // android.media.MediaPlayer on the fly, and we don't want to touch the - // wrong media player. - - private long sessionId = 0; - private boolean isErroring = false; - private int mAudioStreamType = AudioManager.STREAM_MUSIC; - - private WakeLock mWakeLock = null; - - // So here's the major problem - // Sometimes the service won't exist or won't be connected, - // so start with an android.media.MediaPlayer, and when - // the service is connected, use that from then on - public ServiceBackedMediaPlayer(MediaPlayer owningMediaPlayer, final Context context, final ServiceConnection serviceConnection) { - super(owningMediaPlayer, context); - Log.d(SBMP_TAG, "Instantiating ServiceBackedMediaPlayer 87"); - this.playMediaServiceIntent = - MediaPlayer.getPrestoServiceIntent(context, INTENT_NAME); - this.mPlayMediaServiceConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName name, IBinder service) { - IPlayMedia_0_8 tmpPlayMediaInterface = IPlayMedia_0_8.Stub.asInterface((IBinder) service); - - Log.d(SBMP_TAG, "Setting up pmInterface 94"); - if (ServiceBackedMediaPlayer.this.sessionId == 0) { - try { - // The IDeathCallback isn't a conventional callback. - // It exists so that if the client ceases to exist, - // the Service becomes aware of that and can shut - // down whatever it needs to shut down - ServiceBackedMediaPlayer.this.sessionId = tmpPlayMediaInterface.startSession(new IDeathCallback_0_8.Stub() { - }); - // This is really bad if this fails - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - Log.d(SBMP_TAG, "Assigning pmInterface"); - - ServiceBackedMediaPlayer.this.setOnBufferingUpdateCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnCompletionCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnErrorCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnInfoCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnPitchAdjustmentAvailableChangedListener(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnPreparedCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnSeekCompleteCallback(tmpPlayMediaInterface); - ServiceBackedMediaPlayer.this.setOnSpeedAdjustmentAvailableChangedCallback(tmpPlayMediaInterface); - - // In order to avoid race conditions from the sessionId or listener not being assigned - pmInterface = tmpPlayMediaInterface; - - Log.d(SBMP_TAG, "Invoking onServiceConnected"); - serviceConnection.onServiceConnected(name, service); - } - - public void onServiceDisconnected(ComponentName name) { - Log.d(SBMP_TAG, "onServiceDisconnected 114"); - - pmInterface = null; - - sessionId = 0; - - serviceConnection.onServiceDisconnected(name); - } - }; - - Log.d(SBMP_TAG, "Connecting PlayMediaService 124"); - if (!ConnectPlayMediaService()) { - Log.e(SBMP_TAG, "bindService failed"); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private boolean ConnectPlayMediaService() { - Log.d(SBMP_TAG, "ConnectPlayMediaService()"); - - if (MediaPlayer.isIntentAvailable(mContext, INTENT_NAME)) { - Log.d(SBMP_TAG, INTENT_NAME + " is available"); - if (pmInterface == null) { - try { - Log.d(SBMP_TAG, "Binding service"); - return mContext.bindService(playMediaServiceIntent, mPlayMediaServiceConnection, Context.BIND_AUTO_CREATE); - } catch (Exception e) { - Log.e(SBMP_TAG, "Could not bind with service", e); - return false; - } - } else { - Log.d(SBMP_TAG, "Service already bound"); - return true; - } - } - else { - Log.d(SBMP_TAG, INTENT_NAME + " is not available"); - return false; - } - } - - /** - * Returns true if pitch can be changed at this moment - * @return True if pitch can be changed - */ - @Override - public boolean canSetPitch() { - Log.d(SBMP_TAG, "canSetPitch() 155"); - - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set pitch if the service isn't connected - try { - return pmInterface.canSetPitch(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return false; - } - - /** - * Returns true if speed can be changed at this moment - * @return True if speed can be changed - */ - @Override - public boolean canSetSpeed() { - Log.d(SBMP_TAG, "canSetSpeed() 180"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the service isn't connected - try { - return pmInterface.canSetSpeed(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return false; - } - - void error(int what, int extra) { - owningMediaPlayer.lock.lock(); - Log.e(SBMP_TAG, "error(" + what + ", " + extra + ")"); - stayAwake(false); - try { - if (!this.isErroring) { - this.isErroring = true; - owningMediaPlayer.state = State.ERROR; - if (owningMediaPlayer.onErrorListener != null) { - if (owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra)) { - return; - } - } - if (owningMediaPlayer.onCompletionListener != null) { - owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); - } - } - } - finally { - this.isErroring = false; - owningMediaPlayer.lock.unlock(); - } - } - - protected void finalize() throws Throwable { - owningMediaPlayer.lock.lock(); - try { - Log.d(SBMP_TAG, "finalize() 224"); - this.release(); - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - /** - * Returns the number of steps (in a musical scale) by which playback is - * currently shifted. When greater than zero, pitch is shifted up. - * When less than zero, pitch is shifted down. - * @return The number of steps pitch is currently shifted by - */ - @Override - public float getCurrentPitchStepsAdjustment() { - Log.d(SBMP_TAG, "getCurrentPitchStepsAdjustment() 240"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set pitch if the service isn't connected - try { - return pmInterface.getCurrentPitchStepsAdjustment( - ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return 0f; - } - - /** - * Functions identically to android.media.MediaPlayer.getCurrentPosition() - * @return Current position (in milliseconds) - */ - @Override - public int getCurrentPosition() { - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - return pmInterface.getCurrentPosition( - ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - return 0; - } - - /** - * Returns the current speed multiplier. Defaults to 1.0 (normal speed) - * @return The current speed multiplier - */ - @Override - public float getCurrentSpeedMultiplier() { - Log.d(SBMP_TAG, "getCurrentSpeedMultiplier() 286"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the service isn't connected - try { - return pmInterface.getCurrentSpeedMultiplier( - ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return 1; - } - - /** - * Functions identically to android.media.MediaPlayer.getDuration() - * @return Length of the track (in milliseconds) - */ - @Override - public int getDuration() { - Log.d(SBMP_TAG, "getDuration() 311"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - return pmInterface.getDuration(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - return 0; - } - - /** - * Get the maximum value that can be passed to setPlaybackSpeed - * @return The maximum speed multiplier - */ - @Override - public float getMaxSpeedMultiplier() { - Log.d(SBMP_TAG, "getMaxSpeedMultiplier() 332"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - return pmInterface.getMaxSpeedMultiplier( - ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return 1f; - } - - /** - * Get the minimum value that can be passed to setPlaybackSpeed - * @return The minimum speed multiplier - */ - @Override - public float getMinSpeedMultiplier() { - Log.d(SBMP_TAG, "getMinSpeedMultiplier() 357"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - return pmInterface.getMinSpeedMultiplier( - ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return 1f; - } - - public int getServiceVersionCode() { - Log.d(SBMP_TAG, "getVersionCode"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - return pmInterface.getVersionCode(); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - return 0; - } - - public String getServiceVersionName() { - Log.d(SBMP_TAG, "getVersionName"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - return pmInterface.getVersionName(); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - return ""; - } - - public boolean isConnected() { - return (pmInterface != null); - } - - /** - * Functions identically to android.media.MediaPlayer.isLooping() - * @return True if the track is looping - */ - @Override - public boolean isLooping() { - Log.d(SBMP_TAG, "isLooping() 382"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - return pmInterface.isLooping(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - return false; - } - - /** - * Functions identically to android.media.MediaPlayer.isPlaying() - * @return True if the track is playing - */ - @Override - public boolean isPlaying() { - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - try { - return pmInterface.isPlaying(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - return false; - } - - /** - * Functions identically to android.media.MediaPlayer.pause() - * Pauses the track - */ - @Override - public void pause() { - Log.d(SBMP_TAG, "pause() 424"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.pause(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - stayAwake(false); - } - - /** - * Functions identically to android.media.MediaPlayer.prepare() - * Prepares the track. This or prepareAsync must be called before start() - */ - @Override - public void prepare() throws IllegalStateException, IOException { - Log.d(SBMP_TAG, "prepare() 444"); - Log.d(SBMP_TAG, "onPreparedCallback is: " + ((this.mOnPreparedCallback == null) ? "null" : "non-null")); - if (pmInterface == null) { - Log.d(SBMP_TAG, "prepare: pmInterface is null"); - if (!ConnectPlayMediaService()) { - Log.d(SBMP_TAG, "prepare: Failed to connect play media service"); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - Log.d(SBMP_TAG, "prepare: pmInterface isn't null"); - try { - Log.d(SBMP_TAG, "prepare: Remote invoke pmInterface.prepare(" + ServiceBackedMediaPlayer.this.sessionId + ")"); - pmInterface.prepare(ServiceBackedMediaPlayer.this.sessionId); - Log.d(SBMP_TAG, "prepare: prepared"); - } catch (RemoteException e) { - Log.d(SBMP_TAG, "prepare: RemoteException"); - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - Log.d(SBMP_TAG, "Done with prepare()"); - } - - /** - * Functions identically to android.media.MediaPlayer.prepareAsync() - * Prepares the track. This or prepare must be called before start() - */ - @Override - public void prepareAsync() { - Log.d(SBMP_TAG, "prepareAsync() 469"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.prepareAsync(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.release() - * Releases the underlying resources used by the media player. - */ - @Override - public void release() { - Log.d(SBMP_TAG, "release() 492"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - Log.d(SBMP_TAG, "release() 500"); - try { - pmInterface.release(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - mContext.unbindService(this.mPlayMediaServiceConnection); - // Don't try to keep awake (if we were) - this.setWakeMode(mContext, 0); - pmInterface = null; - this.sessionId = 0; - } - - if ((this.mWakeLock != null) && this.mWakeLock.isHeld()) { - Log.d(SBMP_TAG, "Releasing wakelock"); - this.mWakeLock.release(); - } - } - - /** - * Functions identically to android.media.MediaPlayer.reset() - * Resets the track to idle state - */ - @Override - public void reset() { - Log.d(SBMP_TAG, "reset() 523"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.reset(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - stayAwake(false); - } - - /** - * Functions identically to android.media.MediaPlayer.seekTo(int msec) - * Seeks to msec in the track - */ - @Override - public void seekTo(int msec) throws IllegalStateException { - Log.d(SBMP_TAG, "seekTo(" + msec + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.seekTo(ServiceBackedMediaPlayer.this.sessionId, msec); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setAudioStreamType(int streamtype) - * Sets the audio stream type. - */ - @Override - public void setAudioStreamType(int streamtype) { - Log.d(SBMP_TAG, "setAudioStreamType(" + streamtype + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.setAudioStreamType( - ServiceBackedMediaPlayer.this.sessionId, - this.mAudioStreamType); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - - /** - * Functions identically to android.media.MediaPlayer.setDataSource(Context context, Uri uri) - * Sets uri as data source in the context given - */ - @Override - public void setDataSource(Context context, Uri uri) throws IllegalArgumentException, IllegalStateException, IOException { - Log.d(SBMP_TAG, "setDataSource(context, uri)"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.setDataSourceUri( - ServiceBackedMediaPlayer.this.sessionId, - uri); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setDataSource(String path) - * Sets the data source of the track to a file given. - */ - @Override - public void setDataSource(String path) throws IllegalArgumentException, IllegalStateException, IOException { - Log.d(SBMP_TAG, "setDataSource(path)"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface == null) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - else { - try { - pmInterface.setDataSourceString( - ServiceBackedMediaPlayer.this.sessionId, - path); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - } - - /** - * Sets whether to use speed adjustment or not. Speed adjustment on is - * more computation-intensive than with it off. - * @param enableSpeedAdjustment Whether speed adjustment should be supported. - */ - @Override - public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) { - // TODO: This has no business being here, I think - owningMediaPlayer.lock.lock(); - Log.d(SBMP_TAG, "setEnableSpeedAdjustment(enableSpeedAdjustment)"); - try { - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - pmInterface.setEnableSpeedAdjustment( - ServiceBackedMediaPlayer.this.sessionId, - enableSpeedAdjustment); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - - - /** - * Functions identically to android.media.MediaPlayer.setLooping(boolean loop) - * Sets the track to loop infinitely if loop is true, play once if loop is false - */ - @Override - public void setLooping(boolean loop) { - Log.d(SBMP_TAG, "setLooping(" + loop + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.setLooping(ServiceBackedMediaPlayer.this.sessionId, loop); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Sets the number of steps (in a musical scale) by which playback is - * currently shifted. When greater than zero, pitch is shifted up. - * When less than zero, pitch is shifted down. - * - * @param pitchSteps The number of steps by which to shift playback - */ - @Override - public void setPitchStepsAdjustment(float pitchSteps) { - Log.d(SBMP_TAG, "setPitchStepsAdjustment(" + pitchSteps + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - pmInterface.setPitchStepsAdjustment( - ServiceBackedMediaPlayer.this.sessionId, - pitchSteps); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - } - - /** - * Sets the percentage by which pitch is currently shifted. When - * greater than zero, pitch is shifted up. When less than zero, pitch - * is shifted down - * @param f The percentage to shift pitch - */ - @Override - public void setPlaybackPitch(float f) { - Log.d(SBMP_TAG, "setPlaybackPitch(" + f + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - pmInterface.setPlaybackPitch( - ServiceBackedMediaPlayer.this.sessionId, - f); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - } - - /** - * Set playback speed. 1.0 is normal speed, 2.0 is double speed, and so - * on. Speed should never be set to 0 or below. - * @param f The speed multiplier to use for further playback - */ - @Override - public void setPlaybackSpeed(float f) { - Log.d(SBMP_TAG, "setPlaybackSpeed(" + f + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - if (pmInterface != null) { - // Can't set speed if the Service isn't connected - try { - pmInterface.setPlaybackSpeed( - ServiceBackedMediaPlayer.this.sessionId, - f); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - } - - @Override - public void setSpeedAdjustmentAlgorithm(int algorithm) { - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.setSpeedAdjustmentAlgorithm( - ServiceBackedMediaPlayer.this.sessionId, - algorithm); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setVolume(float leftVolume, float rightVolume) - * Sets the stereo volume - */ - @Override - public void setVolume(float leftVolume, float rightVolume) { - Log.d(SBMP_TAG, "setVolume(" + leftVolume + ", " + rightVolume + ")"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.setVolume( - ServiceBackedMediaPlayer.this.sessionId, - leftVolume, - rightVolume); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.setWakeMode(Context context, int mode) - * Acquires a wake lock in the context given. You must request the appropriate permissions - * in your AndroidManifest.xml file. - */ - @Override - // This does not just call .setWakeMode() in the Service because doing so - // would add a permission requirement to the Service. Do it here, and it's - // the client app's responsibility to request that permission - public void setWakeMode(Context context, int mode) { - Log.d(SBMP_TAG, "setWakeMode(context, " + mode + ")"); - if ((this.mWakeLock != null) - && (this.mWakeLock.isHeld())) { - this.mWakeLock.release(); - } - if (mode != 0) { - if (this.mWakeLock == null) { - PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE); - // Since mode can't be changed on the fly, we have to allocate a new one - this.mWakeLock = pm.newWakeLock(mode, this.getClass().getName()); - this.mWakeLock.setReferenceCounted(false); - } - - this.mWakeLock.acquire(); - } - } - - /** - * Changes the state of the WakeLock if it has been acquired. - * If no WakeLock has been acquired with setWakeMode, this method does nothing. - * */ - private void stayAwake(boolean awake) { - if (BuildConfig.DEBUG) Log.d(SBMP_TAG, "stayAwake(" + awake + ")"); - if (mWakeLock != null) { - if (awake && !mWakeLock.isHeld()) { - mWakeLock.acquire(); - } else if (!awake && mWakeLock.isHeld()) { - mWakeLock.release(); - } - } - } - - private IOnBufferingUpdateListenerCallback_0_8.Stub mOnBufferingUpdateCallback = null; - private void setOnBufferingUpdateCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnBufferingUpdateCallback == null) { - mOnBufferingUpdateCallback = new IOnBufferingUpdateListenerCallback_0_8.Stub() { - public void onBufferingUpdate(int percent) - throws RemoteException { - owningMediaPlayer.lock.lock(); - try { - if ((owningMediaPlayer.onBufferingUpdateListener != null) - && (owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this)) { - owningMediaPlayer.onBufferingUpdateListener.onBufferingUpdate(owningMediaPlayer, percent); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnBufferingUpdateCallback( - ServiceBackedMediaPlayer.this.sessionId, - mOnBufferingUpdateCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnCompletionListenerCallback_0_8.Stub mOnCompletionCallback = null; - private void setOnCompletionCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnCompletionCallback == null) { - this.mOnCompletionCallback = new IOnCompletionListenerCallback_0_8.Stub() { - public void onCompletion() throws RemoteException { - owningMediaPlayer.lock.lock(); - Log.d(SBMP_TAG, "onCompletionListener being called"); - stayAwake(false); - try { - if (owningMediaPlayer.onCompletionListener != null) { - owningMediaPlayer.onCompletionListener.onCompletion(owningMediaPlayer); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnCompletionCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnCompletionCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnErrorListenerCallback_0_8.Stub mOnErrorCallback = null; - private void setOnErrorCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnErrorCallback == null) { - this.mOnErrorCallback = new IOnErrorListenerCallback_0_8.Stub() { - public boolean onError(int what, int extra) throws RemoteException { - owningMediaPlayer.lock.lock(); - stayAwake(false); - try { - if (owningMediaPlayer.onErrorListener != null) { - return owningMediaPlayer.onErrorListener.onError(owningMediaPlayer, what, extra); - } - return false; - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnErrorCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnErrorCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnInfoListenerCallback_0_8.Stub mOnInfoCallback = null; - private void setOnInfoCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnInfoCallback == null) { - this.mOnInfoCallback = new IOnInfoListenerCallback_0_8.Stub() { - public boolean onInfo(int what, int extra) throws RemoteException { - owningMediaPlayer.lock.lock(); - try { - if ((owningMediaPlayer.onInfoListener != null) - && (owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this)) { - return owningMediaPlayer.onInfoListener.onInfo(owningMediaPlayer, what, extra); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - return false; - } - }; - } - iface.registerOnInfoCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnInfoCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnPitchAdjustmentAvailableChangedCallback = null; - private void setOnPitchAdjustmentAvailableChangedListener(IPlayMedia_0_8 iface) { - try { - if (this.mOnPitchAdjustmentAvailableChangedCallback == null) { - this.mOnPitchAdjustmentAvailableChangedCallback = new IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.Stub() { - public void onPitchAdjustmentAvailableChanged( - boolean pitchAdjustmentAvailable) - throws RemoteException { - owningMediaPlayer.lock.lock(); - try { - if (owningMediaPlayer.onPitchAdjustmentAvailableChangedListener != null) { - owningMediaPlayer.onPitchAdjustmentAvailableChangedListener.onPitchAdjustmentAvailableChanged(owningMediaPlayer, pitchAdjustmentAvailable); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnPitchAdjustmentAvailableChangedCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnPitchAdjustmentAvailableChangedCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnPreparedListenerCallback_0_8.Stub mOnPreparedCallback = null; - private void setOnPreparedCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnPreparedCallback == null) { - this.mOnPreparedCallback = new IOnPreparedListenerCallback_0_8.Stub() { - public void onPrepared() throws RemoteException { - owningMediaPlayer.lock.lock(); - Log.d(SBMP_TAG, "setOnPreparedCallback.mOnPreparedCallback.onPrepared 1050"); - try { - Log.d(SBMP_TAG, "owningMediaPlayer.onPreparedListener is " + ((owningMediaPlayer.onPreparedListener == null) ? "null" : "non-null")); - Log.d(SBMP_TAG, "owningMediaPlayer.mpi is " + ((owningMediaPlayer.mpi == ServiceBackedMediaPlayer.this) ? "this" : "not this")); - ServiceBackedMediaPlayer.this.lockMuteOnPreparedCount.lock(); - try { - if (ServiceBackedMediaPlayer.this.muteOnPreparedCount > 0) { - ServiceBackedMediaPlayer.this.muteOnPreparedCount--; - } - else { - ServiceBackedMediaPlayer.this.muteOnPreparedCount = 0; - if (ServiceBackedMediaPlayer.this.owningMediaPlayer.onPreparedListener != null) { - owningMediaPlayer.onPreparedListener.onPrepared(owningMediaPlayer); - } - } - } - finally { - ServiceBackedMediaPlayer.this.lockMuteOnPreparedCount.unlock(); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnPreparedCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnPreparedCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnSeekCompleteListenerCallback_0_8.Stub mOnSeekCompleteCallback = null; - private void setOnSeekCompleteCallback(IPlayMedia_0_8 iface) { - try { - if (this.mOnSeekCompleteCallback == null) { - this.mOnSeekCompleteCallback = new IOnSeekCompleteListenerCallback_0_8.Stub() { - public void onSeekComplete() throws RemoteException { - Log.d(SBMP_TAG, "onSeekComplete() 941"); - owningMediaPlayer.lock.lock(); - try { - if (ServiceBackedMediaPlayer.this.muteOnSeekCount > 0) { - Log.d(SBMP_TAG, "The next " + ServiceBackedMediaPlayer.this.muteOnSeekCount + " seek events are muted (counting this one)"); - ServiceBackedMediaPlayer.this.muteOnSeekCount--; - } - else { - ServiceBackedMediaPlayer.this.muteOnSeekCount = 0; - Log.d(SBMP_TAG, "Attempting to invoke next seek event"); - if (ServiceBackedMediaPlayer.this.owningMediaPlayer.onSeekCompleteListener != null) { - Log.d(SBMP_TAG, "Invoking onSeekComplete"); - owningMediaPlayer.onSeekCompleteListener.onSeekComplete(owningMediaPlayer); - } - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnSeekCompleteCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnSeekCompleteCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - private IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub mOnSpeedAdjustmentAvailableChangedCallback = null; - private void setOnSpeedAdjustmentAvailableChangedCallback(IPlayMedia_0_8 iface) { - try { - Log.d(SBMP_TAG, "Setting the service of on speed adjustment available changed"); - if (this.mOnSpeedAdjustmentAvailableChangedCallback == null) { - this.mOnSpeedAdjustmentAvailableChangedCallback = new IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.Stub() { - public void onSpeedAdjustmentAvailableChanged( - boolean speedAdjustmentAvailable) - throws RemoteException { - owningMediaPlayer.lock.lock(); - try { - if (owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener != null) { - owningMediaPlayer.onSpeedAdjustmentAvailableChangedListener.onSpeedAdjustmentAvailableChanged(owningMediaPlayer, speedAdjustmentAvailable); - } - } - finally { - owningMediaPlayer.lock.unlock(); - } - } - }; - } - iface.registerOnSpeedAdjustmentAvailableChangedCallback( - ServiceBackedMediaPlayer.this.sessionId, - this.mOnSpeedAdjustmentAvailableChangedCallback); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - - /** - * Functions identically to android.media.MediaPlayer.start() - * Starts a track playing - */ - @Override - public void start() { - Log.d(SBMP_TAG, "start()"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.start(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - stayAwake(true); - } - - /** - * Functions identically to android.media.MediaPlayer.stop() - * Stops a track playing and resets its position to the start. - */ - @Override - public void stop() { - Log.d(SBMP_TAG, "stop()"); - if (pmInterface == null) { - if (!ConnectPlayMediaService()) { - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - } - try { - pmInterface.stop(ServiceBackedMediaPlayer.this.sessionId); - } catch (RemoteException e) { - e.printStackTrace(); - ServiceBackedMediaPlayer.this.error(MediaPlayer.MEDIA_ERROR_UNKNOWN, 0); - } - stayAwake(false); - } -}
\ No newline at end of file diff --git a/core/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java b/core/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java deleted file mode 100644 index d337a0452..000000000 --- a/core/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2011, Aocate, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package com.aocate.media; - -public class SpeedAdjustmentAlgorithm { - /** - * Use this to use the user-specified algorithm - */ - public static int DEFAULT = 0; - - /** - * Better for voice audio - */ - public static int SONIC = 1; - /** - * Better for music audio - */ - public static int WSOLA = 2; -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java index 3bc1ce4eb..1064e98ac 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java @@ -20,5 +20,4 @@ public interface ApplicationCallbacks { */ public Intent getStorageErrorActivity(Context context); - public void setUpdateInterval(long updateInterval); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java index 1a2671555..6619e706b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java +++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java @@ -21,7 +21,5 @@ public class ClientConfig { public static FlattrCallbacks flattrCallbacks; - public static StorageCallbacks storageCallbacks; - public static DBTasksCallbacks dbTasksCallbacks; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java deleted file mode 100644 index 5d1a0fffc..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java +++ /dev/null @@ -1,27 +0,0 @@ -package de.danoeh.antennapod.core; - -import android.database.sqlite.SQLiteDatabase; - -/** - * Callbacks for the classes in the storage package of the core module. - */ -public interface StorageCallbacks { - - /** - * Returns the current version of the database. - * - * @return The non-negative version number of the database. - */ - public int getDatabaseVersion(); - - /** - * Upgrades the given database from an old version to a newer version. - * - * @param db The database that is supposed to be upgraded. - * @param oldVersion The old version of the database. - * @param newVersion The version that the database is supposed to be upgraded to. - */ - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion); - - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java deleted file mode 100644 index a13130082..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java +++ /dev/null @@ -1,177 +0,0 @@ -package de.danoeh.antennapod.core.asynctask; - -import android.app.Activity; -import android.content.*; -import android.os.Handler; -import android.os.IBinder; -import android.util.Log; - -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.service.download.DownloadService; -import de.danoeh.antennapod.core.service.download.Downloader; - -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * Provides access to the DownloadService's list of items that are currently being downloaded. - * The DownloadObserver object should be created in the activity's onCreate() method. resume() and pause() - * should be called in the activity's onResume() and onPause() methods - */ -public class DownloadObserver { - private static final String TAG = "DownloadObserver"; - - /** - * Time period between update notifications. - */ - public static final int WAITING_INTERVAL_MS = 3000; - - private volatile Activity activity; - private final Handler handler; - private final Callback callback; - - private DownloadService downloadService = null; - private AtomicBoolean mIsBound = new AtomicBoolean(false); - - private Thread refresherThread; - private AtomicBoolean refresherThreadRunning = new AtomicBoolean(false); - - - /** - * Creates a new download observer. - * - * @param activity Used for registering receivers - * @param handler All callback methods are executed on this handler. The handler MUST run on the GUI thread. - * @param callback Callback methods for posting content updates - * @throws java.lang.IllegalArgumentException if one of the arguments is null. - */ - public DownloadObserver(Activity activity, Handler handler, Callback callback) { - Validate.notNull(activity); - Validate.notNull(handler); - Validate.notNull(callback); - - this.activity = activity; - this.handler = handler; - this.callback = callback; - } - - public void onResume() { - if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver resumed"); - activity.registerReceiver(contentChangedReceiver, new IntentFilter(DownloadService.ACTION_DOWNLOADS_CONTENT_CHANGED)); - connectToDownloadService(); - } - - public void onPause() { - if (BuildConfig.DEBUG) Log.d(TAG, "DownloadObserver paused"); - try { - activity.unregisterReceiver(contentChangedReceiver); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - try { - activity.unbindService(mConnection); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - } - stopRefresher(); - } - - private BroadcastReceiver contentChangedReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - // reconnect to DownloadService if connection has been closed - if (downloadService == null) { - connectToDownloadService(); - } - callback.onContentChanged(); - startRefresher(); - } - }; - - public interface Callback { - void onContentChanged(); - - void onDownloadDataAvailable(List<Downloader> downloaderList); - } - - private void connectToDownloadService() { - activity.bindService(new Intent(activity, DownloadService.class), mConnection, 0); - } - - private ServiceConnection mConnection = new ServiceConnection() { - public void onServiceDisconnected(ComponentName className) { - downloadService = null; - mIsBound.set(false); - stopRefresher(); - Log.i(TAG, "Closed connection with DownloadService."); - } - - public void onServiceConnected(ComponentName name, IBinder service) { - downloadService = ((DownloadService.LocalBinder) service) - .getService(); - mIsBound.set(true); - if (BuildConfig.DEBUG) - Log.d(TAG, "Connection to service established"); - List<Downloader> downloaderList = downloadService.getDownloads(); - if (downloaderList != null && !downloaderList.isEmpty()) { - callback.onDownloadDataAvailable(downloaderList); - startRefresher(); - } - } - }; - - private void stopRefresher() { - if (refresherThread != null) { - refresherThread.interrupt(); - } - } - - private void startRefresher() { - if (refresherThread == null || refresherThread.isInterrupted()) { - refresherThread = new Thread(new RefresherThread()); - refresherThread.start(); - } - } - - private class RefresherThread implements Runnable { - - public void run() { - refresherThreadRunning.set(true); - while (!Thread.interrupted()) { - try { - Thread.sleep(WAITING_INTERVAL_MS); - } catch (InterruptedException e) { - Log.d(TAG, "Refresher thread was interrupted"); - } - if (mIsBound.get()) { - postUpdate(); - } - } - refresherThreadRunning.set(false); - } - - private void postUpdate() { - handler.post(new Runnable() { - @Override - public void run() { - callback.onContentChanged(); - if (downloadService != null) { - List<Downloader> downloaderList = downloadService.getDownloads(); - if (downloaderList == null || downloaderList.isEmpty()) { - Thread.currentThread().interrupt(); - } - } - } - }); - } - } - - public void setActivity(Activity activity) { - Validate.notNull(activity); - this.activity = activity; - } - -} - diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java index 255b95119..7ff622f34 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java @@ -3,20 +3,22 @@ package de.danoeh.antennapod.core.asynctask; import android.annotation.SuppressLint; import android.app.ProgressDialog; import android.content.Context; -import android.content.DialogInterface; -import android.content.DialogInterface.OnCancelListener; +import android.content.Intent; import android.os.AsyncTask; + +import java.util.concurrent.ExecutionException; + import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.storage.DBWriter; -import java.util.concurrent.ExecutionException; - /** Removes a feed in the background. */ public class FeedRemover extends AsyncTask<Void, Void, Void> { Context context; ProgressDialog dialog; Feed feed; + public boolean skipOnCompletion = false; public FeedRemover(Context context, Feed feed) { super(); @@ -35,30 +37,22 @@ public class FeedRemover extends AsyncTask<Void, Void, Void> { } return null; } - - @Override - protected void onCancelled() { - dialog.dismiss(); - } - + @Override protected void onPostExecute(Void result) { dialog.dismiss(); + if(skipOnCompletion) { + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SKIP_CURRENT_EPISODE)); + } } @Override protected void onPreExecute() { dialog = new ProgressDialog(context); dialog.setMessage(context.getString(R.string.feed_remover_msg)); - dialog.setOnCancelListener(new OnCancelListener() { - - @Override - public void onCancel(DialogInterface dialog) { - cancel(true); - - } - - }); + dialog.setIndeterminate(true); + dialog.setCancelable(false); dialog.show(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java index 5d2d5d441..ac032fcc0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java @@ -6,11 +6,11 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.content.Context; import android.os.AsyncTask; +import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import android.util.Log; import android.widget.Toast; -import org.apache.commons.lang3.Validate; import org.shredzone.flattr4j.exception.FlattrException; import java.util.LinkedList; @@ -63,8 +63,7 @@ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorke * * @param context A context for accessing the database and posting notifications. Must not be null. */ - public FlattrClickWorker(Context context) { - Validate.notNull(context); + public FlattrClickWorker(@NonNull Context context) { this.context = context.getApplicationContext(); } @@ -90,11 +89,11 @@ public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorke return ExitCode.NO_TOKEN; } - if (!NetworkUtils.networkAvailable(context)) { + if (!NetworkUtils.networkAvailable()) { return ExitCode.NO_NETWORK; } - final List<FlattrThing> flattrQueue = DBReader.getFlattrQueue(context); + final List<FlattrThing> flattrQueue = DBReader.getFlattrQueue(); if (extraFlattrThing != null) { flattrQueue.add(extraFlattrThing); } else if (flattrQueue.size() == 1) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java index c4aa76ac7..888591e89 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java @@ -32,7 +32,7 @@ public class FlattrStatusFetcher extends Thread { try { List<Flattr> flattredThings = FlattrUtils.retrieveFlattredThings(); - DBWriter.setFlattredStatus(context, flattredThings).get(); + DBWriter.setFlattredStatus(flattredThings).get(); } catch (FlattrException e) { e.printStackTrace(); Log.d(TAG, "flattrQueue exception retrieving list with flattred items " + e.getMessage()); diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java index c0d8049db..edd69f15b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/ImageResource.java @@ -6,7 +6,7 @@ import android.net.Uri; * Classes that implement this interface provide access to an image resource that can * be loaded by the Picasso library. */ -public interface PicassoImageResource { +public interface ImageResource { /** * This scheme should be used by PicassoImageResources to diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java deleted file mode 100644 index 4f2d5b204..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java +++ /dev/null @@ -1,510 +0,0 @@ -package de.danoeh.antennapod.core.asynctask; - -import android.content.ContentResolver; -import android.content.Context; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.media.MediaMetadataRetriever; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Log; - -import com.squareup.okhttp.Interceptor; -import com.squareup.okhttp.OkHttpClient; -import com.squareup.okhttp.Response; -import com.squareup.picasso.Cache; -import com.squareup.picasso.LruCache; -import com.squareup.picasso.OkHttpDownloader; -import com.squareup.picasso.Picasso; -import com.squareup.picasso.Request; -import com.squareup.picasso.RequestHandler; -import com.squareup.picasso.Transformation; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; - -import de.danoeh.antennapod.core.service.download.HttpDownloader; -import de.danoeh.antennapod.core.storage.DBReader; - -/** - * Provides access to Picasso instances. - */ -public class PicassoProvider { - - private static final String TAG = "PicassoProvider"; - - private static final boolean DEBUG = false; - - private static ExecutorService executorService; - private static Cache memoryCache; - - private static synchronized ExecutorService getExecutorService() { - if (executorService == null) { - executorService = Executors.newFixedThreadPool(3); - } - return executorService; - } - - private static synchronized Cache getMemoryCache(Context context) { - if (memoryCache == null) { - memoryCache = new LruCache(context); - } - return memoryCache; - } - - private static volatile boolean picassoSetup = false; - - public static synchronized void setupPicassoInstance(Context appContext) { - if (picassoSetup) { - return; - } - OkHttpClient client = new OkHttpClient(); - client.interceptors().add(new BasicAuthenticationInterceptor(appContext)); - Picasso picasso = new Picasso.Builder(appContext) - .indicatorsEnabled(DEBUG) - .loggingEnabled(DEBUG) - .downloader(new OkHttpDownloader(client)) - .addRequestHandler(new MediaRequestHandler(appContext)) - .executor(getExecutorService()) - .memoryCache(getMemoryCache(appContext)) - .listener(new Picasso.Listener() { - @Override - public void onImageLoadFailed(Picasso picasso, Uri uri, Exception e) { - Log.e(TAG, "Failed to load Uri:" + uri.toString()); - e.printStackTrace(); - } - }) - .build(); - Picasso.setSingletonInstance(picasso); - picassoSetup = true; - } - - private static class BasicAuthenticationInterceptor implements Interceptor { - - private final Context context; - - public BasicAuthenticationInterceptor(Context context) { - this.context = context; - } - - @Override - public Response intercept(Chain chain) throws IOException { - com.squareup.okhttp.Request request = chain.request(); - String url = request.urlString(); - String authentication = DBReader.getImageAuthentication(context, url); - - if(TextUtils.isEmpty(authentication)) { - Log.d(TAG, "no credentials for '" + url + "'"); - return chain.proceed(request); - } - - // add authentication - String[] auth = authentication.split(":"); - String credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "ISO-8859-1"); - com.squareup.okhttp.Request newRequest = request - .newBuilder() - .addHeader("Authorization", credentials) - .build(); - Log.d(TAG, "Basic authentication with ISO-8859-1 encoding"); - Response response = chain.proceed(newRequest); - if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { - credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "UTF-8"); - newRequest = request - .newBuilder() - .addHeader("Authorization", credentials) - .build(); - Log.d(TAG, "Basic authentication with UTF-8 encoding"); - return chain.proceed(newRequest); - } else { - return response; - } - } - } - - private static class MediaRequestHandler extends RequestHandler { - - final Context context; - - public MediaRequestHandler(Context context) { - super(); - this.context = context; - } - - @Override - public boolean canHandleRequest(Request data) { - return StringUtils.equals(data.uri.getScheme(), PicassoImageResource.SCHEME_MEDIA); - } - - @Override - public Result load(Request data, int networkPolicy) throws IOException { - Bitmap bitmap = null; - MediaMetadataRetriever mmr = null; - try { - mmr = new MediaMetadataRetriever(); - mmr.setDataSource(data.uri.getPath()); - byte[] image = mmr.getEmbeddedPicture(); - if (image != null) { - bitmap = decodeStreamFromByteArray(data, image); - } - } catch (RuntimeException e) { - Log.e(TAG, "Failed to decode image in media file", e); - } finally { - if (mmr != null) { - mmr.release(); - } - } - - if (bitmap == null) { - Log.wtf(TAG, "THIS SHOULD NEVER EVER HAPPEN!!"); - } - return new Result(bitmap, Picasso.LoadedFrom.DISK); - - } - - /* Copied/Adapted from Picasso RequestHandler classes */ - - private Bitmap decodeStreamFromByteArray(Request data, byte[] bytes) throws IOException { - - final BitmapFactory.Options options = createBitmapOptions(data); - final ByteArrayInputStream in = new ByteArrayInputStream(bytes); - in.mark(0); - if (requiresInSampleSize(options)) { - try { - BitmapFactory.decodeStream(in, null, options); - } finally { - in.reset(); - } - calculateInSampleSize(data.targetWidth, data.targetHeight, options, data); - } - try { - return BitmapFactory.decodeStream(in, null, options); - } finally { - IOUtils.closeQuietly(in); - } - } - - private Bitmap decodeStreamFromFile(Request data, Uri uri) throws IOException { - ContentResolver contentResolver = context.getContentResolver(); - final BitmapFactory.Options options = createBitmapOptions(data); - if (requiresInSampleSize(options)) { - InputStream is = null; - try { - is = contentResolver.openInputStream(uri); - BitmapFactory.decodeStream(is, null, options); - } finally { - IOUtils.closeQuietly(is); - } - calculateInSampleSize(data.targetWidth, data.targetHeight, options, data); - } - InputStream is = contentResolver.openInputStream(uri); - try { - return BitmapFactory.decodeStream(is, null, options); - } finally { - IOUtils.closeQuietly(is); - } - } - - private BitmapFactory.Options createBitmapOptions(Request data) { - final boolean justBounds = data.hasSize(); - final boolean hasConfig = data.config != null; - BitmapFactory.Options options = null; - if (justBounds || hasConfig) { - options = new BitmapFactory.Options(); - options.inJustDecodeBounds = justBounds; - if (hasConfig) { - options.inPreferredConfig = data.config; - } - } - return options; - } - - private static boolean requiresInSampleSize(BitmapFactory.Options options) { - return options != null && options.inJustDecodeBounds; - } - - private static void calculateInSampleSize(int reqWidth, int reqHeight, BitmapFactory.Options options, - Request request) { - calculateInSampleSize(reqWidth, reqHeight, options.outWidth, options.outHeight, options, - request); - } - - private static void calculateInSampleSize(int reqWidth, int reqHeight, int width, int height, - BitmapFactory.Options options, Request request) { - int sampleSize = 1; - if (height > reqHeight || width > reqWidth) { - final int heightRatio; - final int widthRatio; - if (reqHeight == 0) { - sampleSize = (int) Math.floor((float) width / (float) reqWidth); - } else if (reqWidth == 0) { - sampleSize = (int) Math.floor((float) height / (float) reqHeight); - } else { - heightRatio = (int) Math.floor((float) height / (float) reqHeight); - widthRatio = (int) Math.floor((float) width / (float) reqWidth); - sampleSize = request.centerInside - ? Math.max(heightRatio, widthRatio) - : Math.min(heightRatio, widthRatio); - } - } - options.inSampleSize = sampleSize; - options.inJustDecodeBounds = false; - } - } - - public static final int BLUR_RADIUS = 1; - public static final int BLUR_IMAGE_SIZE = 100; - public static final String BLUR_KEY = "blur"; - - public static final Transformation blurTransformation = new Transformation() { - @Override - public Bitmap transform(Bitmap source) { - Bitmap result = fastblur(source, BLUR_RADIUS); - source.recycle(); - return result; - } - - @Override - public String key() { - return BLUR_KEY; - } - }; - - public static Bitmap fastblur(Bitmap sentBitmap, int radius) { - - // Stack Blur v1.0 from - // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html - // - // Java Author: Mario Klingemann <mario at quasimondo.com> - // http://incubator.quasimondo.com - // created Feburary 29, 2004 - // Android port : Yahel Bouaziz <yahel at kayenko.com> - // http://www.kayenko.com - // ported april 5th, 2012 - - // This is a compromise between Gaussian Blur and Box blur - // It creates much better looking blurs than Box Blur, but is - // 7x faster than my Gaussian Blur implementation. - // - // I called it Stack Blur because this describes best how this - // filter works internally: it creates a kind of moving stack - // of colors whilst scanning through the image. Thereby it - // just has to add one new block of color to the right side - // of the stack and remove the leftmost color. The remaining - // colors on the topmost layer of the stack are either added on - // or reduced by one, depending on if they are on the right or - // on the left side of the stack. - // - // If you are using this algorithm in your code please add - // the following line: - // - // Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com> - - Bitmap bitmap = sentBitmap.copy(sentBitmap.getConfig(), true); - - if (radius < 1) { - return (null); - } - - int w = bitmap.getWidth(); - int h = bitmap.getHeight(); - - int[] pix = new int[w * h]; - Log.e("pix", w + " " + h + " " + pix.length); - bitmap.getPixels(pix, 0, w, 0, 0, w, h); - - int wm = w - 1; - int hm = h - 1; - int wh = w * h; - int div = radius + radius + 1; - - int r[] = new int[wh]; - int g[] = new int[wh]; - int b[] = new int[wh]; - int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; - int vmin[] = new int[Math.max(w, h)]; - - int divsum = (div + 1) >> 1; - divsum *= divsum; - int dv[] = new int[256 * divsum]; - for (i = 0; i < 256 * divsum; i++) { - dv[i] = (i / divsum); - } - - yw = yi = 0; - - int[][] stack = new int[div][3]; - int stackpointer; - int stackstart; - int[] sir; - int rbs; - int r1 = radius + 1; - int routsum, goutsum, boutsum; - int rinsum, ginsum, binsum; - - for (y = 0; y < h; y++) { - rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; - for (i = -radius; i <= radius; i++) { - p = pix[yi + Math.min(wm, Math.max(i, 0))]; - sir = stack[i + radius]; - sir[0] = (p & 0xff0000) >> 16; - sir[1] = (p & 0x00ff00) >> 8; - sir[2] = (p & 0x0000ff); - rbs = r1 - Math.abs(i); - rsum += sir[0] * rbs; - gsum += sir[1] * rbs; - bsum += sir[2] * rbs; - if (i > 0) { - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - } else { - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - } - } - stackpointer = radius; - - for (x = 0; x < w; x++) { - - r[yi] = dv[rsum]; - g[yi] = dv[gsum]; - b[yi] = dv[bsum]; - - rsum -= routsum; - gsum -= goutsum; - bsum -= boutsum; - - stackstart = stackpointer - radius + div; - sir = stack[stackstart % div]; - - routsum -= sir[0]; - goutsum -= sir[1]; - boutsum -= sir[2]; - - if (y == 0) { - vmin[x] = Math.min(x + radius + 1, wm); - } - p = pix[yw + vmin[x]]; - - sir[0] = (p & 0xff0000) >> 16; - sir[1] = (p & 0x00ff00) >> 8; - sir[2] = (p & 0x0000ff); - - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - - rsum += rinsum; - gsum += ginsum; - bsum += binsum; - - stackpointer = (stackpointer + 1) % div; - sir = stack[(stackpointer) % div]; - - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - - rinsum -= sir[0]; - ginsum -= sir[1]; - binsum -= sir[2]; - - yi++; - } - yw += w; - } - for (x = 0; x < w; x++) { - rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; - yp = -radius * w; - for (i = -radius; i <= radius; i++) { - yi = Math.max(0, yp) + x; - - sir = stack[i + radius]; - - sir[0] = r[yi]; - sir[1] = g[yi]; - sir[2] = b[yi]; - - rbs = r1 - Math.abs(i); - - rsum += r[yi] * rbs; - gsum += g[yi] * rbs; - bsum += b[yi] * rbs; - - if (i > 0) { - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - } else { - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - } - - if (i < hm) { - yp += w; - } - } - yi = x; - stackpointer = radius; - for (y = 0; y < h; y++) { - // Preserve alpha channel: ( 0xff000000 & pix[yi] ) - pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; - - rsum -= routsum; - gsum -= goutsum; - bsum -= boutsum; - - stackstart = stackpointer - radius + div; - sir = stack[stackstart % div]; - - routsum -= sir[0]; - goutsum -= sir[1]; - boutsum -= sir[2]; - - if (x == 0) { - vmin[y] = Math.min(y + r1, hm) * w; - } - p = x + vmin[y]; - - sir[0] = r[p]; - sir[1] = g[p]; - sir[2] = b[p]; - - rinsum += sir[0]; - ginsum += sir[1]; - binsum += sir[2]; - - rsum += rinsum; - gsum += ginsum; - bsum += binsum; - - stackpointer = (stackpointer + 1) % div; - sir = stack[stackpointer]; - - routsum += sir[0]; - goutsum += sir[1]; - boutsum += sir[2]; - - rinsum -= sir[0]; - ginsum -= sir[1]; - binsum -= sir[2]; - - yi += w; - } - } - - Log.e("pix", w + " " + h + " " + pix.length); - bitmap.setPixels(pix, 0, w, 0, 0, w, h); - - return (bitmap); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java index 1535e2e9a..5ea0ba904 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java @@ -89,7 +89,7 @@ public class OpmlBackupAgent extends BackupAgentHelper { try { // Write OPML - new OpmlWriter().writeDocument(DBReader.getFeedList(mContext), writer); + new OpmlWriter().writeDocument(DBReader.getFeedList(), writer); // Compare checksum of new and old file to see if we need to perform a backup at all if (digester != null) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java b/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java index ba1add895..abb75e5e7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java +++ b/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java @@ -1,10 +1,10 @@ package de.danoeh.antennapod.core.dialog; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import android.support.v7.app.AlertDialog; import android.util.Log; -import de.danoeh.antennapod.core.BuildConfig; + import de.danoeh.antennapod.core.R; /** @@ -12,12 +12,16 @@ import de.danoeh.antennapod.core.R; * classes can handle events like confirmation or cancellation. */ public abstract class ConfirmationDialog { - private static final String TAG = "ConfirmationDialog"; - Context context; + private static final String TAG = ConfirmationDialog.class.getSimpleName(); + + protected Context context; int titleId; int messageId; + int positiveText; + int negativeText; + public ConfirmationDialog(Context context, int titleId, int messageId) { this.context = context; this.titleId = titleId; @@ -25,18 +29,26 @@ public abstract class ConfirmationDialog { } public void onCancelButtonPressed(DialogInterface dialog) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Dialog was cancelled"); + Log.d(TAG, "Dialog was cancelled"); dialog.dismiss(); } + public void setPositiveText(int id) { + this.positiveText = id; + } + + public void setNegativeText(int id) { + this.negativeText = id; + } + + public abstract void onConfirmButtonPressed(DialogInterface dialog); public final AlertDialog createNewDialog() { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(titleId); builder.setMessage(messageId); - builder.setPositiveButton(R.string.confirm_label, + builder.setPositiveButton(positiveText != 0 ? positiveText : R.string.confirm_label, new DialogInterface.OnClickListener() { @Override @@ -44,7 +56,7 @@ public abstract class ConfirmationDialog { onConfirmButtonPressed(dialog); } }); - builder.setNegativeButton(R.string.cancel_label, + builder.setNegativeButton(negativeText != 0 ? negativeText : R.string.cancel_label, new DialogInterface.OnClickListener() { @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java b/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java index 3d174bd8e..b7e79431d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java @@ -1,8 +1,9 @@ package de.danoeh.antennapod.core.dialog; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; +import android.support.v7.app.AlertDialog; + import de.danoeh.antennapod.core.R; /** Creates Alert Dialogs if a DownloadRequestException has happened. */ diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java new file mode 100644 index 000000000..124fd3e64 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/event/DownloadEvent.java @@ -0,0 +1,28 @@ +package de.danoeh.antennapod.core.event; + +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.core.service.download.Downloader; + +public class DownloadEvent { + + public final DownloaderUpdate update; + + private DownloadEvent(DownloaderUpdate downloader) { + this.update = downloader; + } + + public static DownloadEvent refresh(List<Downloader> list) { + list = new ArrayList<>(list); + DownloaderUpdate update = new DownloaderUpdate(list); + return new DownloadEvent(update); + } + + @Override + public String toString() { + return "DownloadEvent{" + + "update=" + update + + '}'; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java b/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java new file mode 100644 index 000000000..dcb033267 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/event/DownloaderUpdate.java @@ -0,0 +1,53 @@ +package de.danoeh.antennapod.core.event; + +import java.util.Arrays; +import java.util.List; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.service.download.Downloader; +import de.danoeh.antennapod.core.util.LongList; + +public class DownloaderUpdate { + + /* Downloaders that are currently running */ + public final List<Downloader> downloaders; + + /** + * IDs of feeds that are currently being downloaded + * Often used to show some progress wheel in the action bar + */ + public final long[] feedIds; + + /** + * IDs of feed media that are currently being downloaded + * Can be used to show and update download progress bars + */ + public final long[] mediaIds; + + public DownloaderUpdate(List<Downloader> downloaders) { + this.downloaders = downloaders; + LongList feedIds1 = new LongList(), mediaIds1 = new LongList(); + for(Downloader d1 : downloaders) { + int type = d1.getDownloadRequest().getFeedfileType(); + long id = d1.getDownloadRequest().getFeedfileId(); + if(type == Feed.FEEDFILETYPE_FEED) { + feedIds1.add(id); + } else if(type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + mediaIds1.add(id); + } + } + + this.feedIds = feedIds1.toArray(); + this.mediaIds = mediaIds1.toArray(); + } + + @Override + public String toString() { + return "DownloaderUpdate{" + + "downloaders=" + downloaders + + ", feedIds=" + Arrays.toString(feedIds) + + ", mediaIds=" + Arrays.toString(mediaIds) + + '}'; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java new file mode 100644 index 000000000..d09f6802f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/event/FavoritesEvent.java @@ -0,0 +1,38 @@ +package de.danoeh.antennapod.core.event; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import de.danoeh.antennapod.core.feed.FeedItem; + +public class FavoritesEvent { + + public enum Action { + ADDED, REMOVED + } + + public final Action action; + public final FeedItem item; + + private FavoritesEvent(Action action, FeedItem item) { + this.action = action; + this.item = item; + } + + public static FavoritesEvent added(FeedItem item) { + return new FavoritesEvent(Action.ADDED, item); + } + + public static FavoritesEvent removed(FeedItem item) { + return new FavoritesEvent(Action.REMOVED, item); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("action", action) + .append("item", item) + .toString(); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java new file mode 100644 index 000000000..7ff241456 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/event/FeedItemEvent.java @@ -0,0 +1,48 @@ +package de.danoeh.antennapod.core.event; + + +import android.support.annotation.NonNull; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.Arrays; +import java.util.List; + +import de.danoeh.antennapod.core.feed.FeedItem; + +public class FeedItemEvent { + + public enum Action { + UPDATE, DELETE_MEDIA + } + + @NonNull public final Action action; + @NonNull public final List<FeedItem> items; + + private FeedItemEvent(Action action, List<FeedItem> items) { + this.action = action; + this.items = items; + } + + public static FeedItemEvent deletedMedia(List<FeedItem> items) { + return new FeedItemEvent(Action.DELETE_MEDIA, items); + } + + public static FeedItemEvent updated(List<FeedItem> items) { + return new FeedItemEvent(Action.UPDATE, items); + } + + public static FeedItemEvent updated(FeedItem... items) { + return new FeedItemEvent(Action.UPDATE, Arrays.asList(items)); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("action", action) + .append("items", items) + .toString(); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/FeedMediaEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/FeedMediaEvent.java new file mode 100644 index 000000000..864d0a405 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/event/FeedMediaEvent.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.core.event; + +import de.danoeh.antennapod.core.feed.FeedMedia; + +public class FeedMediaEvent { + + public enum Action { + UPDATE + } + + public final Action action; + public final FeedMedia media; + + private FeedMediaEvent(Action action, FeedMedia media) { + this.action = action; + this.media = media; + } + + public static FeedMediaEvent update(FeedMedia media) { + return new FeedMediaEvent(Action.UPDATE, media); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/ProgressEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/ProgressEvent.java new file mode 100644 index 000000000..3769d6bb1 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/event/ProgressEvent.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.core.event; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class ProgressEvent { + + public enum Action { + START, END + } + + public final Action action; + public final String message; + + private ProgressEvent(Action action, String message) { + this.action = action; + this.message = message; + } + + public static ProgressEvent start(String message) { + return new ProgressEvent(Action.START, message); + } + + public static ProgressEvent end() { + return new ProgressEvent(Action.END, null); + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("action", action) + .append("message", message) + .toString(); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java b/core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java new file mode 100644 index 000000000..a84e8456f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/event/QueueEvent.java @@ -0,0 +1,83 @@ +package de.danoeh.antennapod.core.event; + +import android.support.annotation.Nullable; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.List; + +import de.danoeh.antennapod.core.feed.FeedItem; + +public class QueueEvent { + + public enum Action { + ADDED, ADDED_ITEMS, SET_QUEUE, REMOVED, IRREVERSIBLE_REMOVED, CLEARED, DELETED_MEDIA, SORTED, MOVED + } + + public final Action action; + public final FeedItem item; + public final int position; + public final List<FeedItem> items; + + + private QueueEvent(Action action, + @Nullable FeedItem item, + @Nullable List<FeedItem> items, + int position) { + this.action = action; + this.item = item; + this.items = items; + this.position = position; + } + + public static QueueEvent added(FeedItem item, int position) { + return new QueueEvent(Action.ADDED, item, null, position); + } + + public static QueueEvent setQueue(List<FeedItem> queue) { + return new QueueEvent(Action.SET_QUEUE, null, queue, -1); + } + + public static QueueEvent removed(FeedItem item) { + return new QueueEvent(Action.REMOVED, item, null, -1); + } + + public static QueueEvent irreversibleRemoved(FeedItem item) { + return new QueueEvent(Action.IRREVERSIBLE_REMOVED, item, null, -1); + } + + public static QueueEvent cleared() { + return new QueueEvent(Action.CLEARED, null, null, -1); + } + + public static QueueEvent sorted(List<FeedItem> sortedQueue) { + return new QueueEvent(Action.SORTED, null, sortedQueue, -1); + } + + public static QueueEvent moved(FeedItem item, int newPosition) { + return new QueueEvent(Action.MOVED, item, null, newPosition); + } + + public boolean contains(long id) { + if(item != null) { + return item.getId() == id; + } + for(FeedItem item : items) { + if(item.getId() == id) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) + .append("action", action) + .append("item", item) + .append("items", items) + .append("position", position) + .toString(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java index ce3352ed6..bb594ff87 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java @@ -1,5 +1,9 @@ package de.danoeh.antennapod.core.feed; +import android.database.Cursor; + +import de.danoeh.antennapod.core.storage.PodDBAdapter; + public abstract class Chapter extends FeedComponent { /** Defines starting point in milliseconds. */ @@ -22,6 +26,33 @@ public abstract class Chapter extends FeedComponent { this.link = link; } + public static Chapter fromCursor(Cursor cursor, FeedItem item) { + int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE); + int indexStart = cursor.getColumnIndex(PodDBAdapter.KEY_START); + int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK); + int indexChapterType = cursor.getColumnIndex(PodDBAdapter.KEY_CHAPTER_TYPE); + + String title = cursor.getString(indexTitle); + long start = cursor.getLong(indexStart); + String link = cursor.getString(indexLink); + int chapterType = cursor.getInt(indexChapterType); + + Chapter chapter = null; + switch (chapterType) { + case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER: + chapter = new SimpleChapter(start, title, item, link); + break; + case ID3Chapter.CHAPTERTYPE_ID3CHAPTER: + chapter = new ID3Chapter(start, title, item, link); + break; + case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER: + chapter = new VorbisCommentChapter(start, title, item, link); + break; + } + return chapter; + } + + public abstract int getChapterType(); public long getStart() { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java index 20a85d43f..7ccb742fb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java @@ -3,8 +3,6 @@ package de.danoeh.antennapod.core.feed; import android.os.Handler; import android.util.Log; -import org.apache.commons.lang3.Validate; - import java.util.AbstractQueue; import java.util.Observable; import java.util.Observer; @@ -26,7 +24,6 @@ public class EventDistributor extends Observable { public static final int UNREAD_ITEMS_UPDATE = 2; public static final int DOWNLOADLOG_UPDATE = 8; public static final int PLAYBACK_HISTORY_UPDATE = 16; - public static final int DOWNLOAD_QUEUED = 32; public static final int DOWNLOAD_HANDLED = 64; public static final int PLAYER_STATUS_UPDATE = 128; @@ -85,11 +82,6 @@ public class EventDistributor extends Observable { @Override public void addObserver(Observer observer) { super.addObserver(observer); - Validate.isInstanceOf(EventListener.class, observer); - } - - public void sendDownloadQueuedBroadcast() { - addEvent(DOWNLOAD_QUEUED); } public void sendUnreadItemsUpdateBroadcast() { @@ -108,13 +100,7 @@ public class EventDistributor extends Observable { addEvent(DOWNLOADLOG_UPDATE); } - public void sendDownloadHandledBroadcast() { - addEvent(DOWNLOAD_HANDLED); - } - - public void sendPlayerStatusUpdateBroadcast() { - addEvent(PLAYER_STATUS_UPDATE); - } + public void sendPlayerStatusUpdateBroadcast() { addEvent(PLAYER_STATUS_UPDATE); } public static abstract class EventListener implements Observer { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java index 29ba721fe..4be788f33 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java @@ -1,17 +1,18 @@ package de.danoeh.antennapod.core.feed; import android.content.Context; +import android.database.Cursor; import android.net.Uri; import android.support.annotation.Nullable; - -import org.apache.commons.lang3.StringUtils; +import android.text.TextUtils; import java.util.ArrayList; import java.util.Date; import java.util.List; -import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.asynctask.ImageResource; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import de.danoeh.antennapod.core.util.flattr.FlattrThing; @@ -20,7 +21,7 @@ import de.danoeh.antennapod.core.util.flattr.FlattrThing; * * @author daniel */ -public class Feed extends FeedFile implements FlattrThing, PicassoImageResource { +public class Feed extends FeedFile implements FlattrThing, ImageResource { public static final int FEEDFILETYPE_FEED = 0; public static final String TYPE_RSS2 = "rss"; public static final String TYPE_RSS091 = "rss"; @@ -167,20 +168,80 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource */ public Feed(String url, Date lastUpdate, String title, String username, String password) { this(url, lastUpdate, title); - preferences = new FeedPreferences(0, true, username, password); + preferences = new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, username, password); + } + + public static Feed fromCursor(Cursor cursor) { + int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); + int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE); + int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE); + int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK); + int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION); + int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK); + int indexAuthor = cursor.getColumnIndex(PodDBAdapter.KEY_AUTHOR); + int indexLanguage = cursor.getColumnIndex(PodDBAdapter.KEY_LANGUAGE); + int indexType = cursor.getColumnIndex(PodDBAdapter.KEY_TYPE); + int indexFeedIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_IDENTIFIER); + int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL); + int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL); + int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED); + int indexFlattrStatus = cursor.getColumnIndex(PodDBAdapter.KEY_FLATTR_STATUS); + int indexIsPaged = cursor.getColumnIndex(PodDBAdapter.KEY_IS_PAGED); + int indexNextPageLink = cursor.getColumnIndex(PodDBAdapter.KEY_NEXT_PAGE_LINK); + int indexHide = cursor.getColumnIndex(PodDBAdapter.KEY_HIDE); + int indexLastUpdateFailed = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED); + + Date lastUpdate = new Date(cursor.getLong(indexLastUpdate)); + + Feed feed = new Feed( + cursor.getLong(indexId), + lastUpdate, + cursor.getString(indexTitle), + cursor.getString(indexLink), + cursor.getString(indexDescription), + cursor.getString(indexPaymentLink), + cursor.getString(indexAuthor), + cursor.getString(indexLanguage), + cursor.getString(indexType), + cursor.getString(indexFeedIdentifier), + null, + cursor.getString(indexFileUrl), + cursor.getString(indexDownloadUrl), + cursor.getInt(indexDownloaded) > 0, + new FlattrStatus(cursor.getLong(indexFlattrStatus)), + cursor.getInt(indexIsPaged) > 0, + cursor.getString(indexNextPageLink), + cursor.getString(indexHide), + cursor.getInt(indexLastUpdateFailed) > 0 + ); + + FeedPreferences preferences = FeedPreferences.fromCursor(cursor); + feed.setPreferences(preferences); + return feed; + } + + + /** + * Returns true if at least one item in the itemlist is unread. + * + */ + public boolean hasNewItems() { + for (FeedItem item : items) { + if (item.isNew()) { + return true; + } + } + return false; } - /** * Returns true if at least one item in the itemlist is unread. * */ - public boolean hasNewItems() { + public boolean hasUnplayedItems() { for (FeedItem item : items) { - if (item.getState() == FeedItem.State.UNREAD) { - if (item.getMedia() != null) { - return true; - } + if (false == item.isNew() && false == item.isPlayed()) { + return true; } } return false; @@ -230,7 +291,8 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource } public void updateFromOther(Feed other) { - super.updateFromOther(other); + // don't update feed's download_url, we do that manually if redirected + // see AntennapodHttpClient if (other.title != null) { title = other.title; } @@ -304,7 +366,7 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource if (other.isPaged() && !this.isPaged()) { return true; } - if (!StringUtils.equals(other.getNextPageLink(), this.getNextPageLink())) { + if (!TextUtils.equals(other.getNextPageLink(), this.getNextPageLink())) { return true; } return false; @@ -433,7 +495,7 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource } public void savePreferences(Context context) { - DBWriter.setFeedPreferences(context, preferences); + DBWriter.setFeedPreferences(preferences); } @Override @@ -482,7 +544,7 @@ public class Feed extends FeedFile implements FlattrThing, PicassoImageResource return itemfilter; } - public void setHiddenItemProperties(String[] properties) { + public void setItemFilter(String[] properties) { if (properties != null) { this.itemfilter = new FeedItemFilter(properties); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFilter.java new file mode 100644 index 000000000..35abb8de6 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFilter.java @@ -0,0 +1,111 @@ +package de.danoeh.antennapod.core.feed; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class FeedFilter { + + private static final String TAG = "FeedFilter"; + + private String includeFilter; + private String excludeFilter; + + public FeedFilter() { + this("", ""); + } + + public FeedFilter(String includeFilter, String excludeFilter) { + // We're storing the strings and not the parsed terms because + // 1. It's easier to show the user exactly what they typed in this way + // (we don't have to recreate it) + // 2. We don't know if we'll actually be asked to parse anything anyways. + this.includeFilter = includeFilter; + this.excludeFilter = excludeFilter; + } + + /** + * Parses the text in to a list of single words or quoted strings. + * Example: "One "Two Three"" returns ["One", "Two Three"] + * @param filter string to parse in to terms + * @return list of terms + */ + private List<String> parseTerms(String filter) { + // from http://stackoverflow.com/questions/7804335/split-string-on-spaces-in-java-except-if-between-quotes-i-e-treat-hello-wor + List<String> list = new ArrayList<>(); + Matcher m = Pattern.compile("([^\"]\\S*|\".+?\")\\s*").matcher(filter); + while (m.find()) + list.add(m.group(1).replace("\"", "")); + return list; + } + + /** + * @param item + * @return true if the item should be downloaded + */ + public boolean shouldAutoDownload(FeedItem item) { + + List<String> includeTerms = parseTerms(includeFilter); + List<String> excludeTerms = parseTerms(excludeFilter); + + if (includeTerms.size() == 0 && excludeTerms.size() == 0) { + // nothing has been specified, so include everything + return true; + } + + // check using lowercase so the users don't have to worry about case. + String title = item.getTitle().toLowerCase(); + + // if it's explicitly excluded, it shouldn't be autodownloaded + // even if it has include terms + for (String term : excludeTerms) { + if (title.contains(term.trim().toLowerCase())) { + return false; + } + } + + for (String term : includeTerms) { + if (title.contains(term.trim().toLowerCase())) { + return true; + } + } + + // now's the tricky bit + // if they haven't set an include filter, but they have set an exclude filter + // default to including, but if they've set both, then exclude + if (!hasIncludeFilter() && hasExcludeFilter()) { + return true; + } + + return false; + } + + public String getIncludeFilter() { + return includeFilter; + } + + public String getExcludeFilter() { return excludeFilter; } + + /** + * @return true if only include is set + */ + public boolean includeOnly() { + return hasIncludeFilter() && !hasExcludeFilter(); + } + + /** + * @return true if only exclude is set + */ + public boolean excludeOnly() { + return hasExcludeFilter() && !hasIncludeFilter(); + } + + public boolean hasIncludeFilter() { + return includeFilter.length() > 0; + } + + public boolean hasExcludeFilter() { + return excludeFilter.length() > 0; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java index c6f24367e..bd7ceb54f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java @@ -1,13 +1,15 @@ package de.danoeh.antennapod.core.feed; +import android.database.Cursor; import android.net.Uri; import java.io.File; -import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.asynctask.ImageResource; +import de.danoeh.antennapod.core.storage.PodDBAdapter; -public class FeedImage extends FeedFile implements PicassoImageResource { +public class FeedImage extends FeedFile implements ImageResource { public static final int FEEDFILETYPE_FEEDIMAGE = 1; protected String title; @@ -31,6 +33,23 @@ public class FeedImage extends FeedFile implements PicassoImageResource { super(); } + public static FeedImage fromCursor(Cursor cursor) { + int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); + int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE); + int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL); + int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL); + int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED); + + return new FeedImage( + cursor.getLong(indexId), + cursor.getString(indexTitle), + cursor.getString(indexFileUrl), + cursor.getString(indexDownloadUrl), + cursor.getInt(indexDownloaded) > 0 + ); + } + + @Override public String getHumanReadableIdentifier() { if (owner != null && owner.getHumanReadableIdentifier() != null) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java index 11348953e..d8c32f55e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java @@ -1,17 +1,21 @@ package de.danoeh.antennapod.core.feed; +import android.database.Cursor; import android.net.Uri; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import java.util.Date; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.asynctask.ImageResource; import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.ShownotesProvider; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import de.danoeh.antennapod.core.util.flattr.FlattrThing; @@ -21,7 +25,12 @@ import de.danoeh.antennapod.core.util.flattr.FlattrThing; * * @author daniel */ -public class FeedItem extends FeedComponent implements ShownotesProvider, FlattrThing, PicassoImageResource { +public class FeedItem extends FeedComponent implements ShownotesProvider, FlattrThing, ImageResource { + + /** tag that indicates this item is in the queue */ + public static final String TAG_QUEUE = "Queue"; + /** tag that indicates this item is in favorites */ + public static final String TAG_FAVORITE = "Favorite"; /** * The id/guid that can be found in the rss/atom feed. Might not be set. @@ -44,7 +53,11 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr private Feed feed; private long feedId; - private boolean read; + private int state; + public final static int NEW = -1; + public final static int UNPLAYED = 0; + public final static int PLAYED = 1; + private String paymentLink; private FlattrStatus flattrStatus; @@ -63,10 +76,21 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr private List<Chapter> chapters; private FeedImage image; - private boolean autoDownload = true; + /* + * 0: auto download disabled + * 1: auto download enabled (default) + * > 1: auto download enabled, (approx.) timestamp of the last failed attempt + * where last digit denotes the number of failed attempts + */ + private long autoDownload = 1; + + /** + * Any tags assigned to this item + */ + private Set<String> tags = new HashSet<>(); public FeedItem() { - this.read = true; + this.state = UNPLAYED; this.flattrStatus = new FlattrStatus(); this.hasChapters = false; } @@ -75,8 +99,8 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr * This constructor is used by DBReader. * */ public FeedItem(long id, String title, String link, Date pubDate, String paymentLink, long feedId, - FlattrStatus flattrStatus, boolean hasChapters, FeedImage image, boolean read, - String itemIdentifier, boolean autoDownload) { + FlattrStatus flattrStatus, boolean hasChapters, FeedImage image, int state, + String itemIdentifier, long autoDownload) { this.id = id; this.title = title; this.link = link; @@ -86,7 +110,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr this.flattrStatus = flattrStatus; this.hasChapters = hasChapters; this.image = image; - this.read = read; + this.state = state; this.itemIdentifier = itemIdentifier; this.autoDownload = autoDownload; } @@ -94,13 +118,13 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr /** * This constructor should be used for creating test objects. */ - public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, boolean read, Feed feed) { + public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, int state, Feed feed) { this.id = id; this.title = title; this.itemIdentifier = itemIdentifier; this.link = link; this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null; - this.read = read; + this.state = state; this.feed = feed; this.flattrStatus = new FlattrStatus(); this.hasChapters = false; @@ -109,18 +133,49 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr /** * This constructor should be used for creating test objects involving chapter marks. */ - public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, boolean read, Feed feed, boolean hasChapters) { + public FeedItem(long id, String title, String itemIdentifier, String link, Date pubDate, int state, Feed feed, boolean hasChapters) { this.id = id; this.title = title; this.itemIdentifier = itemIdentifier; this.link = link; this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null; - this.read = read; + this.state = state; this.feed = feed; this.flattrStatus = new FlattrStatus(); this.hasChapters = hasChapters; } + public static FeedItem fromCursor(Cursor cursor) { + int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); + int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE); + int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK); + int indexPubDate = cursor.getColumnIndex(PodDBAdapter.KEY_PUBDATE); + int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK); + int indexFeedId = cursor.getColumnIndex(PodDBAdapter.KEY_FEED); + int indexFlattrStatus = cursor.getColumnIndex(PodDBAdapter.KEY_FLATTR_STATUS); + int indexHasChapters = cursor.getColumnIndex(PodDBAdapter.KEY_HAS_CHAPTERS); + int indexRead = cursor.getColumnIndex(PodDBAdapter.KEY_READ); + int indexItemIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_ITEM_IDENTIFIER); + int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD); + + long id = cursor.getInt(indexId); + assert(id > 0); + String title = cursor.getString(indexTitle); + String link = cursor.getString(indexLink); + Date pubDate = new Date(cursor.getLong(indexPubDate)); + String paymentLink = cursor.getString(indexPaymentLink); + long feedId = cursor.getLong(indexFeedId); + boolean hasChapters = cursor.getInt(indexHasChapters) > 0; + FlattrStatus flattrStatus = new FlattrStatus(cursor.getLong(indexFlattrStatus)); + int state = cursor.getInt(indexRead); + String itemIdentifier = cursor.getString(indexItemIdentifier); + long autoDownload = cursor.getLong(indexAutoDownload); + + FeedItem item = new FeedItem(id, title, link, pubDate, paymentLink, feedId, flattrStatus, + hasChapters, null, state, itemIdentifier, autoDownload); + return item; + } + public void updateFromOther(FeedItem other) { super.updateFromOther(other); if (other.title != null) { @@ -238,12 +293,25 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr this.feed = feed; } - public boolean isRead() { - return read; + public boolean isNew() { + return state == NEW; } - public void setRead(boolean read) { - this.read = read; + + public void setNew() { + state = NEW; + } + + public boolean isPlayed() { + return state == PLAYED; + } + + public void setPlayed(boolean played) { + if(played) { + state = PLAYED; + } else { + state = UNPLAYED; + } } private boolean isInProgress() { @@ -257,11 +325,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr public void setContentEncoded(String contentEncoded) { this.contentEncoded = contentEncoded; } - - public void setFlattrStatus(FlattrStatus status) { - this.flattrStatus = status; - } - + public FlattrStatus getFlattrStatus() { return flattrStatus; } @@ -308,7 +372,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr public String call() throws Exception { if (contentEncoded == null || description == null) { - DBReader.loadExtraInformationOfFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), FeedItem.this); + DBReader.loadExtraInformationOfFeedItem(FeedItem.this); } return (contentEncoded != null) ? contentEncoded : description; @@ -320,7 +384,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr public Uri getImageUri() { if(media != null && media.hasEmbeddedPicture()) { return media.getImageUri(); - } else if (hasItemImageDownloaded()) { + } else if (image != null) { return image.getImageUri(); } else if (feed != null) { return feed.getImageUri(); @@ -342,7 +406,7 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr return State.IN_PROGRESS; } } - return (isRead() ? State.READ : State.UNREAD); + return (isPlayed() ? State.READ : State.UNREAD); } public long getFeedId() { @@ -392,21 +456,54 @@ public class FeedItem extends FeedComponent implements ShownotesProvider, Flattr } public void setAutoDownload(boolean autoDownload) { - this.autoDownload = autoDownload; + this.autoDownload = autoDownload ? 1 : 0; } public boolean getAutoDownload() { - return this.autoDownload; + return this.autoDownload > 0; + } + + public int getFailedAutoDownloadAttempts() { + if (autoDownload <= 1) { + return 0; + } + int failedAttempts = (int)(autoDownload % 10); + if (failedAttempts == 0) { + failedAttempts = 10; + } + return failedAttempts; } public boolean isAutoDownloadable() { - return this.hasMedia() && - false == this.getMedia().isPlaying() && - false == this.getMedia().isDownloaded() && - false == this.isRead() && - this.getAutoDownload(); + if (media == null || media.isPlaying() || media.isDownloaded() || autoDownload == 0) { + return false; + } + if (autoDownload == 1) { + return true; + } + int failedAttempts = getFailedAutoDownloadAttempts(); + double magicValue = 1.767; // 1.767^(10[=#maxNumAttempts]-1) = 168 hours / 7 days + int millisecondsInHour = 3600000; + long waitingTime = (long) (Math.pow(magicValue, failedAttempts - 1) * millisecondsInHour); + long grace = TimeUnit.MINUTES.toMillis(5); + return System.currentTimeMillis() > (autoDownload + waitingTime - grace); } + /** + * @return true if the item has this tag + */ + public boolean isTagged(String tag) { return tags.contains(tag); } + + /** + * @param tag adds this tag to the item. NOTE: does NOT persist to the database + */ + public void addTag(String tag) { tags.add(tag); } + + /** + * @param tag the to remove + */ + public void removeTag(String tag) { tags.remove(tag); } + @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java index 4ad084b39..fdde4b34c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java @@ -1,8 +1,6 @@ package de.danoeh.antennapod.core.feed; -import android.content.Context; - -import org.apache.commons.lang3.StringUtils; +import android.text.TextUtils; import java.util.ArrayList; import java.util.List; @@ -10,73 +8,87 @@ import java.util.List; import de.danoeh.antennapod.core.storage.DBReader; public class FeedItemFilter { + private final String[] mProperties; - private final String[] properties; - - private boolean hideUnplayed = false; - private boolean hidePaused = false; - private boolean hidePlayed = false; - private boolean hideQueued = false; - private boolean hideNotQueued = false; - private boolean hideDownloaded = false; - private boolean hideNotDownloaded = false; + private boolean showPlayed = false; + private boolean showUnplayed = false; + private boolean showPaused = false; + private boolean showQueued = false; + private boolean showNotQueued = false; + private boolean showDownloaded = false; + private boolean showNotDownloaded = false; public FeedItemFilter(String properties) { - this(StringUtils.split(properties, ',')); + this(TextUtils.split(properties, ",")); } public FeedItemFilter(String[] properties) { - this.properties = properties; + this.mProperties = properties; for(String property : properties) { // see R.arrays.feed_filter_values switch(property) { case "unplayed": - hideUnplayed = true; + showUnplayed = true; break; case "paused": - hidePaused = true; + showPaused = true; break; case "played": - hidePlayed = true; + showPlayed = true; break; case "queued": - hideQueued = true; + showQueued = true; break; case "not_queued": - hideNotQueued = true; + showNotQueued = true; break; case "downloaded": - hideDownloaded = true; + showDownloaded = true; break; case "not_downloaded": - hideNotDownloaded = true; + showNotDownloaded = true; break; } } } - public List<FeedItem> filter(Context context, List<FeedItem> items) { - if(properties.length == 0) { - return items; - } - List<FeedItem> result = new ArrayList<FeedItem>(); + /** + * Run a list of feed items through the filter. + */ + public List<FeedItem> filter(List<FeedItem> items) { + if(mProperties.length == 0) return items; + + List<FeedItem> result = new ArrayList<>(); + + // Check for filter combinations that will always return an empty list + // (e.g. requiring played and unplayed at the same time) + if (showPlayed && showUnplayed) return result; + if (showQueued && showNotQueued) return result; + if (showDownloaded && showNotDownloaded) return result; + for(FeedItem item : items) { - if(hideUnplayed && false == item.isRead()) continue; - if(hidePaused && item.getState() == FeedItem.State.IN_PROGRESS) continue; - if(hidePlayed && item.isRead()) continue; - boolean isQueued = DBReader.getQueueIDList(context).contains(item.getId()); - if(hideQueued && isQueued) continue; - if(hideNotQueued && false == isQueued) continue; - boolean isDownloaded = item.getMedia() != null && item.getMedia().isDownloaded(); - if(hideDownloaded && isDownloaded) continue; - if(hideNotDownloaded && false == isDownloaded) continue; + // If the item does not meet a requirement, skip it. + if (showPlayed && !item.isPlayed()) continue; + if (showUnplayed && item.isPlayed()) continue; + if (showPaused && item.getState() != FeedItem.State.IN_PROGRESS) continue; + + boolean queued = DBReader.getQueueIDList().contains(item.getId()); + if (showQueued && !queued) continue; + if (showNotQueued && queued) continue; + + boolean downloaded = item.getMedia() != null && item.getMedia().isDownloaded(); + if (showDownloaded && !downloaded) continue; + if (showNotDownloaded && downloaded) continue; + + // If the item reaches here, it meets all criteria result.add(item); } + return result; } public String[] getValues() { - return properties.clone(); + return mProperties.clone(); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java index f875eb812..56b996d1c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -2,20 +2,23 @@ package de.danoeh.antennapod.core.feed; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; +import android.database.Cursor; import android.media.MediaMetadataRetriever; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.text.TextUtils; import java.util.Date; import java.util.List; import java.util.concurrent.Callable; -import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.playback.Playable; @@ -28,14 +31,26 @@ public class FeedMedia extends FeedFile implements Playable { public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId"; public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId"; + /** + * Indicates we've checked on the size of the item via the network + * and got an invalid response. Using Integer.MIN_VALUE because + * 1) we'll still check on it in case it gets downloaded (it's <= 0) + * 2) By default all FeedMedia have a size of 0 if we don't know it, + * so this won't conflict with existing practice. + */ + private static final int CHECKED_ON_SIZE_BUT_UNKNOWN = Integer.MIN_VALUE; + private int duration; private int position; // Current position in file + private long lastPlayedTime; // Last time this media was played (in ms) private int played_duration; // How many ms of this file have been played (for autoflattring) private long size; // File size in Byte private String mime_type; - private volatile FeedItem item; + @Nullable private volatile FeedItem item; private Date playbackCompletionDate; - private boolean hasEmbeddedPicture; + + // if null: unknown, will be checked + private Boolean hasEmbeddedPicture; /* Used for loading item when restoring from parcel. */ private long itemID; @@ -50,9 +65,9 @@ public class FeedMedia extends FeedFile implements Playable { public FeedMedia(long id, FeedItem item, int duration, int position, long size, String mime_type, String file_url, String download_url, - boolean downloaded, Date playbackCompletionDate, int played_duration) { + boolean downloaded, Date playbackCompletionDate, int played_duration, + long lastPlayedTime) { super(file_url, download_url, downloaded); - checkEmbeddedPicture(); this.id = id; this.item = item; this.duration = duration; @@ -62,8 +77,69 @@ public class FeedMedia extends FeedFile implements Playable { this.mime_type = mime_type; this.playbackCompletionDate = playbackCompletionDate == null ? null : (Date) playbackCompletionDate.clone(); + this.lastPlayedTime = lastPlayedTime; } + public FeedMedia(long id, FeedItem item, int duration, int position, + long size, String mime_type, String file_url, String download_url, + boolean downloaded, Date playbackCompletionDate, int played_duration, + Boolean hasEmbeddedPicture, long lastPlayedTime) { + this(id, item, duration, position, size, mime_type, file_url, download_url, downloaded, + playbackCompletionDate, played_duration, lastPlayedTime); + this.hasEmbeddedPicture = hasEmbeddedPicture; + } + + public static FeedMedia fromCursor(Cursor cursor) { + int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); + int indexPlaybackCompletionDate = cursor.getColumnIndex(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE); + int indexDuration = cursor.getColumnIndex(PodDBAdapter.KEY_DURATION); + int indexPosition = cursor.getColumnIndex(PodDBAdapter.KEY_POSITION); + int indexSize = cursor.getColumnIndex(PodDBAdapter.KEY_SIZE); + int indexMimeType = cursor.getColumnIndex(PodDBAdapter.KEY_MIME_TYPE); + int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL); + int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL); + int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED); + int indexPlayedDuration = cursor.getColumnIndex(PodDBAdapter.KEY_PLAYED_DURATION); + int indexLastPlayedTime = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_PLAYED_TIME); + + long mediaId = cursor.getLong(indexId); + Date playbackCompletionDate = null; + long playbackCompletionTime = cursor.getLong(indexPlaybackCompletionDate); + if (playbackCompletionTime > 0) { + playbackCompletionDate = new Date(playbackCompletionTime); + } + + Boolean hasEmbeddedPicture; + switch(cursor.getInt(cursor.getColumnIndex(PodDBAdapter.KEY_HAS_EMBEDDED_PICTURE))) { + case 1: + hasEmbeddedPicture = Boolean.TRUE; + break; + case 0: + hasEmbeddedPicture = Boolean.FALSE; + break; + default: + hasEmbeddedPicture = null; + break; + } + + return new FeedMedia( + mediaId, + null, + cursor.getInt(indexDuration), + cursor.getInt(indexPosition), + cursor.getLong(indexSize), + cursor.getString(indexMimeType), + cursor.getString(indexFileUrl), + cursor.getString(indexDownloadUrl), + cursor.getInt(indexDownloaded) > 0, + playbackCompletionDate, + cursor.getInt(indexPlayedDuration), + hasEmbeddedPicture, + cursor.getLong(indexLastPlayedTime) + ); + } + + @Override public String getHumanReadableIdentifier() { if (item != null && item.getTitle() != null) { @@ -92,6 +168,10 @@ public class FeedMedia extends FeedFile implements Playable { } public void updateFromOther(FeedMedia other) { + // reset to new if feed item did link to a file before + if(TextUtils.isEmpty(download_url) && !TextUtils.isEmpty(other.download_url)) { + item.setNew(); + } super.updateFromOther(other); if (other.size > 0) { size = other.size; @@ -162,6 +242,11 @@ public class FeedMedia extends FeedFile implements Playable { this.duration = duration; } + @Override + public void setLastPlayedTime(long lastPlayedTime) { + this.lastPlayedTime = lastPlayedTime; + } + public int getPlayedDuration() { return played_duration; } @@ -174,8 +259,16 @@ public class FeedMedia extends FeedFile implements Playable { return position; } + @Override + public long getLastPlayedTime() { + return lastPlayedTime; + } + public void setPosition(int position) { this.position = position; + if(position > 0 && item != null && item.isNew()) { + this.item.setPlayed(false); + } } public long getSize() { @@ -186,6 +279,18 @@ public class FeedMedia extends FeedFile implements Playable { this.size = size; } + /** + * Indicates we asked the service what the size was, but didn't + * get a valid answer and we shoudln't check using the network again. + */ + public void setCheckedOnSizeButUnknown() { + this.size = CHECKED_ON_SIZE_BUT_UNKNOWN; + } + + public boolean checkedOnSizeButUnknown() { + return (CHECKED_ON_SIZE_BUT_UNKNOWN == this.size); + } + public String getMime_type() { return mime_type; } @@ -194,6 +299,7 @@ public class FeedMedia extends FeedFile implements Playable { this.mime_type = mime_type; } + @Nullable public FeedItem getItem() { return item; } @@ -230,13 +336,18 @@ public class FeedMedia extends FeedFile implements Playable { } public boolean hasEmbeddedPicture() { - return this.hasEmbeddedPicture; + return false; + // TODO: reenable! + //if(hasEmbeddedPicture == null) { + // checkEmbeddedPicture(); + //} + //return hasEmbeddedPicture; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeLong(id); - dest.writeLong(item.getId()); + dest.writeLong(item != null ? item.getId() : 0L); dest.writeInt(duration); dest.writeInt(position); @@ -247,33 +358,38 @@ public class FeedMedia extends FeedFile implements Playable { dest.writeByte((byte) ((downloaded) ? 1 : 0)); dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0); dest.writeInt(played_duration); + dest.writeLong(lastPlayedTime); } @Override public void writeToPreferences(Editor prefEditor) { - prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId()); + if(item != null && item.getFeed() != null) { + prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId()); + } else { + prefEditor.putLong(PREF_FEED_ID, 0L); + } prefEditor.putLong(PREF_MEDIA_ID, id); } @Override public void loadMetadata() throws PlayableException { if (item == null && itemID != 0) { - item = DBReader.getFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), itemID); + item = DBReader.getFeedItem(itemID); } } @Override public void loadChapterMarks() { if (item == null && itemID != 0) { - item = DBReader.getFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), itemID); + item = DBReader.getFeedItem(itemID); } // check if chapters are stored in db and not loaded yet. if (item != null && item.hasChapters() && item.getChapters() == null) { - DBReader.loadChaptersOfFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), item); + DBReader.loadChaptersOfFeedItem(item); } else if (item != null && item.getChapters() == null && !localFileAvailable()) { ChapterUtils.loadChaptersFromStreamUrl(this); if (getChapters() != null && item != null) { - DBWriter.setFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), + DBWriter.setFeedItem( item); } } @@ -349,15 +465,18 @@ public class FeedMedia extends FeedFile implements Playable { } @Override - public void saveCurrentPosition(SharedPreferences pref, int newPosition) { + public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timeStamp) { + if(item != null && item.isNew()) { + DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); + } setPosition(newPosition); - DBWriter.setFeedMediaPlaybackInformation(ClientConfig.applicationCallbacks.getApplicationInstance(), this); + setLastPlayedTime(timeStamp); + DBWriter.setFeedMediaPlaybackInformation(this); } @Override public void onPlaybackStart() { } - @Override public void onPlaybackCompleted() { @@ -380,11 +499,11 @@ public class FeedMedia extends FeedFile implements Playable { public String call() throws Exception { if (item == null) { item = DBReader.getFeedItem( - ClientConfig.applicationCallbacks.getApplicationInstance(), itemID); + itemID); } if (item.getContentEncoded() == null || item.getDescription() == null) { DBReader.loadExtraInformationOfFeedItem( - ClientConfig.applicationCallbacks.getApplicationInstance(), item); + item); } return (item.getContentEncoded() != null) ? item.getContentEncoded() : item.getDescription(); @@ -397,7 +516,7 @@ public class FeedMedia extends FeedFile implements Playable { final long id = in.readLong(); final long itemID = in.readLong(); FeedMedia result = new FeedMedia(id, null, in.readInt(), in.readInt(), in.readLong(), in.readString(), in.readString(), - in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt()); + in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt(), in.readLong()); result.itemID = itemID; return result; } @@ -409,30 +528,44 @@ public class FeedMedia extends FeedFile implements Playable { @Override public Uri getImageUri() { - if (hasEmbeddedPicture) { + if (hasEmbeddedPicture()) { Uri.Builder builder = new Uri.Builder(); builder.scheme(SCHEME_MEDIA).encodedPath(getLocalMediaUrl()); + + if (item != null && item.getFeed() != null) { + final Uri feedImgUri = item.getFeed().getImageUri(); + if (feedImgUri != null) { + builder.appendQueryParameter(PARAM_FALLBACK, feedImgUri.toString()); + } + } return builder.build(); - } else { + } else if(item != null) { return item.getImageUri(); + } else { + return null; } } + public void setHasEmbeddedPicture(Boolean hasEmbeddedPicture) { + this.hasEmbeddedPicture = hasEmbeddedPicture; + } + @Override public void setDownloaded(boolean downloaded) { super.setDownloaded(downloaded); - checkEmbeddedPicture(); + if(item != null && downloaded) { + item.setPlayed(false); + } } @Override public void setFile_url(String file_url) { super.setFile_url(file_url); - checkEmbeddedPicture(); } private void checkEmbeddedPicture() { if (!localFileAvailable()) { - hasEmbeddedPicture = false; + hasEmbeddedPicture = Boolean.FALSE; return; } MediaMetadataRetriever mmr = new MediaMetadataRetriever(); @@ -440,14 +573,13 @@ public class FeedMedia extends FeedFile implements Playable { mmr.setDataSource(getLocalMediaUrl()); byte[] image = mmr.getEmbeddedPicture(); if(image != null) { - hasEmbeddedPicture = true; - } - else { - hasEmbeddedPicture = false; + hasEmbeddedPicture = Boolean.TRUE; + } else { + hasEmbeddedPicture = Boolean.FALSE; } } catch (Exception e) { e.printStackTrace(); - hasEmbeddedPicture = false; + hasEmbeddedPicture = Boolean.FALSE; } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java index 2f0304182..faf23a37a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java @@ -1,29 +1,95 @@ package de.danoeh.antennapod.core.feed; import android.content.Context; +import android.database.Cursor; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBWriter; -import org.apache.commons.lang3.StringUtils; +import de.danoeh.antennapod.core.storage.PodDBAdapter; /** * Contains preferences for a single feed. */ public class FeedPreferences { + @NonNull + private FeedFilter filter; private long feedID; private boolean autoDownload; + private boolean keepUpdated; + + public enum AutoDeleteAction { + GLOBAL, + YES, + NO + } + private AutoDeleteAction auto_delete_action; private String username; private String password; - public FeedPreferences(long feedID, boolean autoDownload, String username, String password) { + public FeedPreferences(long feedID, boolean autoDownload, AutoDeleteAction auto_delete_action, String username, String password) { + this(feedID, autoDownload, true, auto_delete_action, username, password, new FeedFilter()); + } + + public FeedPreferences(long feedID, boolean autoDownload, boolean keepUpdated, AutoDeleteAction auto_delete_action, String username, String password, @NonNull FeedFilter filter) { this.feedID = feedID; this.autoDownload = autoDownload; + this.keepUpdated = keepUpdated; + this.auto_delete_action = auto_delete_action; this.username = username; this.password = password; + this.filter = filter; + } + + public static FeedPreferences fromCursor(Cursor cursor) { + int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); + int indexAutoDownload = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD); + int indexAutoRefresh = cursor.getColumnIndex(PodDBAdapter.KEY_KEEP_UPDATED); + int indexAutoDeleteAction = cursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DELETE_ACTION); + int indexUsername = cursor.getColumnIndex(PodDBAdapter.KEY_USERNAME); + int indexPassword = cursor.getColumnIndex(PodDBAdapter.KEY_PASSWORD); + int indexIncludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_INCLUDE_FILTER); + int indexExcludeFilter = cursor.getColumnIndex(PodDBAdapter.KEY_EXCLUDE_FILTER); + + long feedId = cursor.getLong(indexId); + boolean autoDownload = cursor.getInt(indexAutoDownload) > 0; + boolean autoRefresh = cursor.getInt(indexAutoRefresh) > 0; + int autoDeleteActionIndex = cursor.getInt(indexAutoDeleteAction); + AutoDeleteAction autoDeleteAction = AutoDeleteAction.values()[autoDeleteActionIndex]; + String username = cursor.getString(indexUsername); + String password = cursor.getString(indexPassword); + String includeFilter = cursor.getString(indexIncludeFilter); + String excludeFilter = cursor.getString(indexExcludeFilter); + return new FeedPreferences(feedId, autoDownload, autoRefresh, autoDeleteAction, username, password, new FeedFilter(includeFilter, excludeFilter)); } + /** + * @return the filter for this feed + */ + public FeedFilter getFilter() { + return filter; + } + + public void setFilter(@NonNull FeedFilter filter) { + this.filter = filter; + } /** - * Compare another FeedPreferences with this one. The feedID and autoDownload attribute are excluded from the + * @return true if this feed should be refreshed when everything else is being refreshed + * if false the feed should only be refreshed if requested directly. + */ + public boolean getKeepUpdated() { + return keepUpdated; + } + + public void setKeepUpdated(boolean keepUpdated) { + this.keepUpdated = keepUpdated; + } + + /** + * Compare another FeedPreferences with this one. The feedID, autoDownload and AutoDeleteAction attribute are excluded from the * comparison. * * @return True if the two objects are different. @@ -31,17 +97,17 @@ public class FeedPreferences { public boolean compareWithOther(FeedPreferences other) { if (other == null) return true; - if (!StringUtils.equals(username, other.username)) { + if (!TextUtils.equals(username, other.username)) { return true; } - if (!StringUtils.equals(password, other.password)) { + if (!TextUtils.equals(password, other.password)) { return true; } return false; } /** - * Update this FeedPreferences object from another one. The feedID and autoDownload attributes are excluded + * Update this FeedPreferences object from another one. The feedID, autoDownload and AutoDeleteAction attributes are excluded * from the update. */ public void updateFromOther(FeedPreferences other) { @@ -67,8 +133,30 @@ public class FeedPreferences { this.autoDownload = autoDownload; } + public AutoDeleteAction getAutoDeleteAction() { + return auto_delete_action; + } + + public void setAutoDeleteAction(AutoDeleteAction auto_delete_action) { + this.auto_delete_action = auto_delete_action; + } + + public boolean getCurrentAutoDelete() { + switch (auto_delete_action) { + case GLOBAL: + return UserPreferences.isAutoDelete(); + + case YES: + return true; + + case NO: + return false; + } + return false; // TODO - add exceptions here + } + public void save(Context context) { - DBWriter.setFeedPreferences(context, this); + DBWriter.setFeedPreferences(this); } public String getUsername() { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java deleted file mode 100644 index c8497f509..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/QueueEvent.java +++ /dev/null @@ -1,51 +0,0 @@ -package de.danoeh.antennapod.core.feed; - -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; - -import java.util.List; - -public class QueueEvent { - - public enum Action { - ADDED, ADDED_ITEMS, REMOVED, CLEARED, DELETED_MEDIA, SORTED, MOVED - } - - public final Action action; - public final FeedItem item; - public final int position; - public final List<FeedItem> items; - - public QueueEvent(Action action) { - this(action, null, null, -1); - } - - public QueueEvent(Action action, FeedItem item) { - this(action, item, null, -1); - } - - public QueueEvent(Action action, FeedItem item, int position) { - this(action, item, null, position); - } - - public QueueEvent(Action action, List<FeedItem> items) { - this(action, null, items, -1); - } - - private QueueEvent(Action action, FeedItem item, List<FeedItem> items, int position) { - this.action = action; - this.item = item; - this.items = items; - this.position = position; - } - - @Override - public String toString() { - return new ToStringBuilder(this, ToStringStyle.SHORT_PREFIX_STYLE) - .append("action", action) - .append("item", item) - .append("items", items) - .append("position", position) - .toString(); - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java new file mode 100644 index 000000000..0baff9723 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java @@ -0,0 +1,33 @@ +package de.danoeh.antennapod.core.glide; + +import android.content.Context; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.GlideBuilder; +import com.bumptech.glide.load.DecodeFormat; +import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.module.GlideModule; + +import java.io.InputStream; + +import de.danoeh.antennapod.core.preferences.UserPreferences; + +/** + * {@see com.bumptech.glide.integration.okhttp.OkHttpGlideModule} + */ +public class ApGlideModule implements GlideModule { + + @Override + public void applyOptions(Context context, GlideBuilder builder) { + builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888); + builder.setDiskCache(new InternalCacheDiskCacheFactory(context, + UserPreferences.getImageCacheSize())); + } + + @Override + public void registerComponents(Context context, Glide glide) { + glide.register(GlideUrl.class, InputStream.class, new ApOkHttpUrlLoader.Factory()); + } + +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideSettings.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideSettings.java new file mode 100644 index 000000000..fc1acd0e1 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideSettings.java @@ -0,0 +1,11 @@ +package de.danoeh.antennapod.core.glide; + +import com.bumptech.glide.load.engine.DiskCacheStrategy; + +/** + * The settings that AntennaPod will use for various Glide options + */ +public class ApGlideSettings { + + public static final DiskCacheStrategy AP_DISK_CACHE_STRATEGY = DiskCacheStrategy.ALL; +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java new file mode 100644 index 000000000..86baa459c --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApOkHttpUrlLoader.java @@ -0,0 +1,141 @@ +package de.danoeh.antennapod.core.glide; + +import android.content.Context; +import android.text.TextUtils; +import android.util.Log; + +import com.bumptech.glide.integration.okhttp.OkHttpStreamFetcher; +import com.bumptech.glide.load.data.DataFetcher; +import com.bumptech.glide.load.model.GenericLoaderFactory; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.ModelLoader; +import com.bumptech.glide.load.model.ModelLoaderFactory; +import com.squareup.okhttp.Interceptor; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Response; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; + +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.danoeh.antennapod.core.service.download.HttpDownloader; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.NetworkUtils; + +/** + * @see com.bumptech.glide.integration.okhttp.OkHttpUrlLoader + */ +public class ApOkHttpUrlLoader implements ModelLoader<GlideUrl, InputStream> { + + private static final String TAG = ApOkHttpUrlLoader.class.getSimpleName(); + + /** + * The default factory for {@link ApOkHttpUrlLoader}s. + */ + public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> { + + private static volatile OkHttpClient internalClient; + private OkHttpClient client; + + private static OkHttpClient getInternalClient() { + if (internalClient == null) { + synchronized (Factory.class) { + if (internalClient == null) { + internalClient = AntennapodHttpClient.newHttpClient(); + internalClient.interceptors().add(new NetworkAllowanceInterceptor()); + internalClient.interceptors().add(new BasicAuthenticationInterceptor()); + } + } + } + return internalClient; + } + + /** + * Constructor for a new Factory that runs requests using a static singleton client. + */ + public Factory() { + this(getInternalClient()); + } + + /** + * Constructor for a new Factory that runs requests using given client. + */ + public Factory(OkHttpClient client) { + this.client = client; + } + + @Override + public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) { + return new ApOkHttpUrlLoader(client); + } + + @Override + public void teardown() { + // Do nothing, this instance doesn't own the client. + } + } + + private final OkHttpClient client; + + public ApOkHttpUrlLoader(OkHttpClient client) { + this.client = client; + } + + @Override + public DataFetcher<InputStream> getResourceFetcher(GlideUrl model, int width, int height) { + return new OkHttpStreamFetcher(client, model); + } + + private static class NetworkAllowanceInterceptor implements Interceptor { + + @Override + public Response intercept(Chain chain) throws IOException { + if (NetworkUtils.isDownloadAllowed()) { + return chain.proceed(chain.request()); + } else { + return null; + } + } + + } + + private static class BasicAuthenticationInterceptor implements Interceptor { + + @Override + public Response intercept(Chain chain) throws IOException { + com.squareup.okhttp.Request request = chain.request(); + String url = request.urlString(); + Context context = ClientConfig.applicationCallbacks.getApplicationInstance(); + String authentication = DBReader.getImageAuthentication(url); + + if(TextUtils.isEmpty(authentication)) { + Log.d(TAG, "no credentials for '" + url + "'"); + return chain.proceed(request); + } + + // add authentication + String[] auth = authentication.split(":"); + String credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "ISO-8859-1"); + com.squareup.okhttp.Request newRequest = request + .newBuilder() + .addHeader("Authorization", credentials) + .build(); + Log.d(TAG, "Basic authentication with ISO-8859-1 encoding"); + Response response = chain.proceed(newRequest); + if (!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { + credentials = HttpDownloader.encodeCredentials(auth[0], auth[1], "UTF-8"); + newRequest = request + .newBuilder() + .addHeader("Authorization", credentials) + .build(); + Log.d(TAG, "Basic authentication with UTF-8 encoding"); + return chain.proceed(newRequest); + } else { + return response; + } + } + } + +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java b/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java new file mode 100644 index 000000000..ee58c2f39 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/FastBlurTransformation.java @@ -0,0 +1,267 @@ +package de.danoeh.antennapod.core.glide; + +import android.content.Context; +import android.graphics.Bitmap; +import android.media.ThumbnailUtils; +import android.util.Log; + +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation; + +public class FastBlurTransformation extends BitmapTransformation { + + private static final String TAG = FastBlurTransformation.class.getSimpleName(); + + private static final int STACK_BLUR_RADIUS = 1; + private static final int BLUR_IMAGE_WIDTH = 150; + + public FastBlurTransformation(Context context) { + super(context); + } + + @Override + protected Bitmap transform(BitmapPool pool, Bitmap source, + int outWidth, int outHeight) { + int targetWidth = BLUR_IMAGE_WIDTH; + int targetHeight = (int) (1.0 * outHeight * targetWidth / outWidth); + Bitmap resized = ThumbnailUtils.extractThumbnail(source, targetWidth, targetHeight); + Bitmap result = fastBlur(resized, STACK_BLUR_RADIUS); + if (result == null) { + Log.w(TAG, "result was null"); + return source; + } + return result; + } + + @Override + public String getId() { + return "FastBlurTransformation[width=" + BLUR_IMAGE_WIDTH + "px,radius=" + STACK_BLUR_RADIUS +"]"; + } + + private static Bitmap fastBlur(Bitmap bitmap, int radius) { + + // Stack Blur v1.0 from + // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html + // + // Java Author: Mario Klingemann <mario at quasimondo.com> + // http://incubator.quasimondo.com + // created Feburary 29, 2004 + // Android port : Yahel Bouaziz <yahel at kayenko.com> + // http://www.kayenko.com + // ported april 5th, 2012 + + // This is a compromise between Gaussian Blur and Box blur + // It creates much better looking blurs than Box Blur, but is + // 7x faster than my Gaussian Blur implementation. + // + // I called it Stack Blur because this describes best how this + // filter works internally: it creates a kind of moving stack + // of colors whilst scanning through the image. Thereby it + // just has to add one new block of color to the right side + // of the stack and remove the leftmost color. The remaining + // colors on the topmost layer of the stack are either added on + // or reduced by one, depending on if they are on the right or + // on the left side of the stack. + // + // If you are using this algorithm in your code please add + // the following line: + // + // Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com> + + if (radius < 1) { + return null; + } + + int w = bitmap.getWidth(); + int h = bitmap.getHeight(); + + int[] pix = new int[w * h]; + bitmap.getPixels(pix, 0, w, 0, 0, w, h); + + int wm = w - 1; + int hm = h - 1; + int wh = w * h; + int div = radius + radius + 1; + + int r[] = new int[wh]; + int g[] = new int[wh]; + int b[] = new int[wh]; + int rsum, gsum, bsum, x, y, i, p, yp, yi, yw; + int vmin[] = new int[Math.max(w, h)]; + + int divsum = (div + 1) >> 1; + divsum *= divsum; + int dv[] = new int[256 * divsum]; + for (i = 0; i < 256 * divsum; i++) { + dv[i] = (i / divsum); + } + + yw = yi = 0; + + int[][] stack = new int[div][3]; + int stackpointer; + int stackstart; + int[] sir; + int rbs; + int r1 = radius + 1; + int routsum, goutsum, boutsum; + int rinsum, ginsum, binsum; + + for (y = 0; y < h; y++) { + rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; + for (i = -radius; i <= radius; i++) { + p = pix[yi + Math.min(wm, Math.max(i, 0))]; + sir = stack[i + radius]; + sir[0] = (p & 0xff0000) >> 16; + sir[1] = (p & 0x00ff00) >> 8; + sir[2] = (p & 0x0000ff); + rbs = r1 - Math.abs(i); + rsum += sir[0] * rbs; + gsum += sir[1] * rbs; + bsum += sir[2] * rbs; + if (i > 0) { + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + } else { + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + } + } + stackpointer = radius; + + for (x = 0; x < w; x++) { + + r[yi] = dv[rsum]; + g[yi] = dv[gsum]; + b[yi] = dv[bsum]; + + rsum -= routsum; + gsum -= goutsum; + bsum -= boutsum; + + stackstart = stackpointer - radius + div; + sir = stack[stackstart % div]; + + routsum -= sir[0]; + goutsum -= sir[1]; + boutsum -= sir[2]; + + if (y == 0) { + vmin[x] = Math.min(x + radius + 1, wm); + } + p = pix[yw + vmin[x]]; + + sir[0] = (p & 0xff0000) >> 16; + sir[1] = (p & 0x00ff00) >> 8; + sir[2] = (p & 0x0000ff); + + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + + rsum += rinsum; + gsum += ginsum; + bsum += binsum; + + stackpointer = (stackpointer + 1) % div; + sir = stack[(stackpointer) % div]; + + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + + rinsum -= sir[0]; + ginsum -= sir[1]; + binsum -= sir[2]; + + yi++; + } + yw += w; + } + for (x = 0; x < w; x++) { + rinsum = ginsum = binsum = routsum = goutsum = boutsum = rsum = gsum = bsum = 0; + yp = -radius * w; + for (i = -radius; i <= radius; i++) { + yi = Math.max(0, yp) + x; + + sir = stack[i + radius]; + + sir[0] = r[yi]; + sir[1] = g[yi]; + sir[2] = b[yi]; + + rbs = r1 - Math.abs(i); + + rsum += r[yi] * rbs; + gsum += g[yi] * rbs; + bsum += b[yi] * rbs; + + if (i > 0) { + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + } else { + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + } + + if (i < hm) { + yp += w; + } + } + yi = x; + stackpointer = radius; + for (y = 0; y < h; y++) { + // Preserve alpha channel: ( 0xff000000 & pix[yi] ) + pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum]; + + rsum -= routsum; + gsum -= goutsum; + bsum -= boutsum; + + stackstart = stackpointer - radius + div; + sir = stack[stackstart % div]; + + routsum -= sir[0]; + goutsum -= sir[1]; + boutsum -= sir[2]; + + if (x == 0) { + vmin[y] = Math.min(y + r1, hm) * w; + } + p = x + vmin[y]; + + sir[0] = r[p]; + sir[1] = g[p]; + sir[2] = b[p]; + + rinsum += sir[0]; + ginsum += sir[1]; + binsum += sir[2]; + + rsum += rinsum; + gsum += ginsum; + bsum += binsum; + + stackpointer = (stackpointer + 1) % div; + sir = stack[stackpointer]; + + routsum += sir[0]; + goutsum += sir[1]; + boutsum += sir[2]; + + rinsum -= sir[0]; + ginsum -= sir[1]; + binsum -= sir[2]; + + yi += w; + } + } + bitmap.setPixels(pix, 0, w, 0, 0, w, h); + return bitmap; + } + +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java index 1a40120e2..a24e3a485 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.core.gpoddernet; -import android.os.Build; -import android.util.Log; +import android.support.annotation.NonNull; import com.squareup.okhttp.Credentials; import com.squareup.okhttp.MediaType; @@ -11,9 +10,6 @@ import com.squareup.okhttp.RequestBody; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; -import org.apache.commons.lang3.Validate; -import org.apache.http.HttpStatus; -import org.apache.http.client.ClientProtocolException; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -21,27 +17,15 @@ import org.json.JSONObject; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; -import java.security.KeyStore; -import java.security.Principal; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; import java.util.LinkedList; import java.util.List; -import java.util.Map; - -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLSocketFactory; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; -import javax.security.auth.x500.X500Principal; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; @@ -117,10 +101,9 @@ public class GpodnetService { * * @throws IllegalArgumentException if tag is null */ - public List<GpodnetPodcast> getPodcastsForTag(GpodnetTag tag, int count) + public List<GpodnetPodcast> getPodcastsForTag(@NonNull GpodnetTag tag, + int count) throws GpodnetServiceException { - Validate.notNull(tag); - try { URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( "/api/2/tag/%s/%d.json", tag.getTag(), count), null).toURL(); @@ -144,7 +127,9 @@ public class GpodnetService { */ public List<GpodnetPodcast> getPodcastToplist(int count) throws GpodnetServiceException { - Validate.isTrue(count >= 1 && count <= 100, "Count must be in range 1..100"); + if(count < 1 || count > 100) { + throw new IllegalArgumentException("Count must be in range 1..100"); + } try { URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( @@ -174,7 +159,9 @@ public class GpodnetService { * @throws GpodnetServiceAuthenticationException If there is an authentication error. */ public List<GpodnetPodcast> getSuggestions(int count) throws GpodnetServiceException { - Validate.isTrue(count >= 1 && count <= 100, "Count must be in range 1..100"); + if(count < 1 || count > 100) { + throw new IllegalArgumentException("Count must be in range 1..100"); + } try { URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( @@ -231,10 +218,8 @@ public class GpodnetService { * @throws IllegalArgumentException If username is null. * @throws GpodnetServiceAuthenticationException If there is an authentication error. */ - public List<GpodnetDevice> getDevices(String username) + public List<GpodnetDevice> getDevices(@NonNull String username) throws GpodnetServiceException { - Validate.notNull(username); - try { URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( "/api/2/devices/%s.json", username), null).toURL(); @@ -259,12 +244,11 @@ public class GpodnetService { * @throws IllegalArgumentException If username or deviceId is null. * @throws GpodnetServiceAuthenticationException If there is an authentication error. */ - public void configureDevice(String username, String deviceId, - String caption, GpodnetDevice.DeviceType type) + public void configureDevice(@NonNull String username, + @NonNull String deviceId, + String caption, + GpodnetDevice.DeviceType type) throws GpodnetServiceException { - Validate.notNull(username); - Validate.notNull(deviceId); - try { URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( "/api/2/devices/%s/%s.json", username, deviceId), null).toURL(); @@ -302,11 +286,9 @@ public class GpodnetService { * @throws IllegalArgumentException If username or deviceId is null. * @throws GpodnetServiceAuthenticationException If there is an authentication error. */ - public String getSubscriptionsOfDevice(String username, String deviceId) + public String getSubscriptionsOfDevice(@NonNull String username, + @NonNull String deviceId) throws GpodnetServiceException { - Validate.notNull(username); - Validate.notNull(deviceId); - try { URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( "/subscriptions/%s/%s.opml", username, deviceId), null).toURL(); @@ -329,9 +311,8 @@ public class GpodnetService { * @throws IllegalArgumentException If username is null. * @throws GpodnetServiceAuthenticationException If there is an authentication error. */ - public String getSubscriptionsOfUser(String username) + public String getSubscriptionsOfUser(@NonNull String username) throws GpodnetServiceException { - Validate.notNull(username); try { URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( @@ -358,12 +339,11 @@ public class GpodnetService { * @throws IllegalArgumentException If username, deviceId or subscriptions is null. * @throws GpodnetServiceAuthenticationException If there is an authentication error. */ - public void uploadSubscriptions(String username, String deviceId, - List<String> subscriptions) throws GpodnetServiceException { - if (username == null || deviceId == null || subscriptions == null) { - throw new IllegalArgumentException( - "Username, device ID and subscriptions must not be null"); - } + public void uploadSubscriptions(@NonNull String username, + @NonNull String deviceId, + @NonNull List<String> subscriptions) + throws GpodnetServiceException { + try { URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( "/subscriptions/%s/%s.txt", username, deviceId), null).toURL(); @@ -379,6 +359,7 @@ public class GpodnetService { e.printStackTrace(); throw new GpodnetServiceException(e); } + } /** @@ -397,12 +378,11 @@ public class GpodnetService { * @throws de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there * is an authentication error. */ - public GpodnetUploadChangesResponse uploadChanges(String username, String deviceId, Collection<String> added, - Collection<String> removed) throws GpodnetServiceException { - Validate.notNull(username); - Validate.notNull(deviceId); - Validate.notNull(added); - Validate.notNull(removed); + public GpodnetUploadChangesResponse uploadChanges(@NonNull String username, + @NonNull String deviceId, + @NonNull Collection<String> added, + @NonNull Collection<String> removed) + throws GpodnetServiceException { try { URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( @@ -438,10 +418,9 @@ public class GpodnetService { * @throws IllegalArgumentException If username or deviceId is null. * @throws GpodnetServiceAuthenticationException If there is an authentication error. */ - public GpodnetSubscriptionChange getSubscriptionChanges(String username, - String deviceId, long timestamp) throws GpodnetServiceException { - Validate.notNull(username); - Validate.notNull(deviceId); + public GpodnetSubscriptionChange getSubscriptionChanges(@NonNull String username, + @NonNull String deviceId, + long timestamp) throws GpodnetServiceException { String params = String.format("since=%d", timestamp); String path = String.format("/api/2/subscriptions/%s/%s.json", @@ -476,11 +455,9 @@ public class GpodnetService { * @throws de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException if added or removed contain duplicates or if there * is an authentication error. */ - public GpodnetEpisodeActionPostResponse uploadEpisodeActions(Collection<GpodnetEpisodeAction> episodeActions) + public GpodnetEpisodeActionPostResponse uploadEpisodeActions(@NonNull Collection<GpodnetEpisodeAction> episodeActions) throws GpodnetServiceException { - Validate.notNull(episodeActions); - String username = GpodnetPreferences.getUsername(); try { @@ -549,11 +526,9 @@ public class GpodnetService { * * @throws IllegalArgumentException If username or password is null. */ - public void authenticate(String username, String password) + public void authenticate(@NonNull String username, + @NonNull String password) throws GpodnetServiceException { - Validate.notNull(username); - Validate.notNull(password); - URL url; try { url = new URI(BASE_SCHEME, BASE_HOST, String.format( @@ -562,7 +537,8 @@ public class GpodnetService { e.printStackTrace(); throw new GpodnetServiceException(e); } - Request.Builder request = new Request.Builder().url(url).post(null); + RequestBody body = RequestBody.create(TEXT, ""); + Request.Builder request = new Request.Builder().url(url).post(body); executeRequestWithAuthentication(request, username, password); } @@ -579,10 +555,8 @@ public class GpodnetService { }.start(); } - private String executeRequest(Request.Builder requestB) + private String executeRequest(@NonNull Request.Builder requestB) throws GpodnetServiceException { - Validate.notNull(requestB); - Request request = requestB.header("User-Agent", ClientConfig.USER_AGENT).build(); String responseString = null; Response response = null; @@ -627,10 +601,7 @@ public class GpodnetService { checkStatusCode(response); body = response.body(); result = getStringFromResponseBody(body); - } catch (ClientProtocolException e) { - e.printStackTrace(); - throw new GpodnetServiceException(e); - } catch (IOException e) { + } catch (Exception e) { e.printStackTrace(); throw new GpodnetServiceException(e); } finally { @@ -646,10 +617,8 @@ public class GpodnetService { return result; } - private String getStringFromResponseBody(ResponseBody body) + private String getStringFromResponseBody(@NonNull ResponseBody body) throws GpodnetServiceException { - Validate.notNull(body); - ByteArrayOutputStream outputStream; int contentLength = 0; try { @@ -676,31 +645,27 @@ public class GpodnetService { return outputStream.toString(); } - private void checkStatusCode(Response response) + private void checkStatusCode(@NonNull Response response) throws GpodnetServiceException { - Validate.notNull(response); int responseCode = response.code(); - if (responseCode != HttpStatus.SC_OK) { - if (responseCode == HttpStatus.SC_UNAUTHORIZED) { + if (responseCode != HttpURLConnection.HTTP_OK) { + if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { throw new GpodnetServiceAuthenticationException("Wrong username or password"); } else { - throw new GpodnetServiceBadStatusCodeException( - "Bad response code: " + responseCode, responseCode); + throw new GpodnetServiceBadStatusCodeException("Bad response code: " + + responseCode, responseCode); } } } - private List<GpodnetPodcast> readPodcastListFromJSONArray(JSONArray array) + private List<GpodnetPodcast> readPodcastListFromJSONArray(@NonNull JSONArray array) throws JSONException { - Validate.notNull(array); - List<GpodnetPodcast> result = new ArrayList<GpodnetPodcast>( array.length()); for (int i = 0; i < array.length(); i++) { result.add(readPodcastFromJSONObject(array.getJSONObject(i))); } return result; - } private GpodnetPodcast readPodcastFromJSONObject(JSONObject object) @@ -745,10 +710,8 @@ public class GpodnetService { logoUrl, website, mygpoLink); } - private List<GpodnetDevice> readDeviceListFromJSONArray(JSONArray array) + private List<GpodnetDevice> readDeviceListFromJSONArray(@NonNull JSONArray array) throws JSONException { - Validate.notNull(array); - List<GpodnetDevice> result = new ArrayList<GpodnetDevice>( array.length()); for (int i = 0; i < array.length(); i++) { @@ -767,8 +730,7 @@ public class GpodnetService { } private GpodnetSubscriptionChange readSubscriptionChangesFromJSONObject( - JSONObject object) throws JSONException { - Validate.notNull(object); + @NonNull JSONObject object) throws JSONException { List<String> added = new LinkedList<String>(); JSONArray jsonAdded = object.getJSONArray("add"); @@ -787,8 +749,7 @@ public class GpodnetService { } private GpodnetEpisodeActionGetResponse readEpisodeActionsFromJSONObject( - JSONObject object) throws JSONException { - Validate.notNull(object); + @NonNull JSONObject object) throws JSONException { List<GpodnetEpisodeAction> episodeActions = new ArrayList<GpodnetEpisodeAction>(); @@ -804,5 +765,4 @@ public class GpodnetService { return new GpodnetEpisodeActionGetResponse(episodeActions, timestamp); } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java index 4885a243a..2d49c170a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java @@ -1,6 +1,6 @@ package de.danoeh.antennapod.core.gpoddernet.model; -import org.apache.commons.lang3.Validate; +import android.support.annotation.NonNull; public class GpodnetDevice { @@ -9,10 +9,10 @@ public class GpodnetDevice { private DeviceType type; private int subscriptions; - public GpodnetDevice(String id, String caption, String type, + public GpodnetDevice(@NonNull String id, + String caption, + String type, int subscriptions) { - Validate.notNull(id); - this.id = id; this.caption = caption; this.type = DeviceType.fromString(type); diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java index bd6210d13..2d174a6bc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeAction.java @@ -1,13 +1,9 @@ package de.danoeh.antennapod.core.gpoddernet.model; +import android.text.TextUtils; import android.util.Log; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; import org.json.JSONException; import org.json.JSONObject; @@ -90,15 +86,20 @@ public class GpodnetEpisodeAction { String podcast = object.optString("podcast", null); String episode = object.optString("episode", null); String actionString = object.optString("action", null); - if(StringUtils.isEmpty(podcast) || StringUtils.isEmpty(episode) || StringUtils.isEmpty(actionString)) { + if(TextUtils.isEmpty(podcast) || TextUtils.isEmpty(episode) || TextUtils.isEmpty(actionString)) { + return null; + } + GpodnetEpisodeAction.Action action; + try { + action = GpodnetEpisodeAction.Action.valueOf(actionString.toUpperCase()); + } catch (IllegalArgumentException e) { return null; } - GpodnetEpisodeAction.Action action = GpodnetEpisodeAction.Action.valueOf(actionString.toUpperCase()); String deviceId = object.optString("device", ""); GpodnetEpisodeAction.Builder builder = new GpodnetEpisodeAction.Builder(podcast, episode, action) .deviceId(deviceId); String utcTimestamp = object.optString("timestamp", null); - if(StringUtils.isNotEmpty(utcTimestamp)) { + if(!TextUtils.isEmpty(utcTimestamp)) { builder.timestamp(DateUtils.parse(utcTimestamp)); } if(action == GpodnetEpisodeAction.Action.PLAY) { @@ -168,34 +169,34 @@ public class GpodnetEpisodeAction { @Override public boolean equals(Object o) { - if(o == null) return false; - if(this == o) return true; - if(this.getClass() != o.getClass()) return false; - GpodnetEpisodeAction that = (GpodnetEpisodeAction)o; - return new EqualsBuilder() - .append(this.podcast, that.podcast) - .append(this.episode, that.episode) - .append(this.deviceId, that.deviceId) - .append(this.action, that.action) - .append(this.timestamp, that.timestamp) - .append(this.started, that.started) - .append(this.position, that.position) - .append(this.total, that.total) - .isEquals(); + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + GpodnetEpisodeAction that = (GpodnetEpisodeAction) o; + + if (started != that.started) return false; + if (position != that.position) return false; + if (total != that.total) return false; + if (podcast != null ? !podcast.equals(that.podcast) : that.podcast != null) return false; + if (episode != null ? !episode.equals(that.episode) : that.episode != null) return false; + if (deviceId != null ? !deviceId.equals(that.deviceId) : that.deviceId != null) + return false; + if (action != that.action) return false; + return !(timestamp != null ? !timestamp.equals(that.timestamp) : that.timestamp != null); + } @Override public int hashCode() { - return new HashCodeBuilder() - .append(this.podcast) - .append(this.episode) - .append(this.deviceId) - .append(this.action) - .append(this.timestamp) - .append(this.started) - .append(this.position) - .append(this.total) - .toHashCode(); + int result = podcast != null ? podcast.hashCode() : 0; + result = 31 * result + (episode != null ? episode.hashCode() : 0); + result = 31 * result + (deviceId != null ? deviceId.hashCode() : 0); + result = 31 * result + (action != null ? action.hashCode() : 0); + result = 31 * result + (timestamp != null ? timestamp.hashCode() : 0); + result = 31 * result + started; + result = 31 * result + position; + result = 31 * result + total; + return result; } public String writeToString() { @@ -240,7 +241,16 @@ public class GpodnetEpisodeAction { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + return "GpodnetEpisodeAction{" + + "podcast='" + podcast + '\'' + + ", episode='" + episode + '\'' + + ", deviceId='" + deviceId + '\'' + + ", action=" + action + + ", timestamp=" + timestamp + + ", started=" + started + + ", position=" + position + + ", total=" + total + + '}'; } public static class Builder { diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java index 50420f0a3..1e21efcda 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionGetResponse.java @@ -1,9 +1,7 @@ package de.danoeh.antennapod.core.gpoddernet.model; -import org.apache.commons.lang3.Validate; -import org.apache.commons.lang3.builder.ToStringBuilder; -import org.apache.commons.lang3.builder.ToStringStyle; +import android.support.annotation.NonNull; import java.util.List; @@ -12,8 +10,8 @@ public class GpodnetEpisodeActionGetResponse { private final List<GpodnetEpisodeAction> episodeActions; private final long timestamp; - public GpodnetEpisodeActionGetResponse(List<GpodnetEpisodeAction> episodeActions, long timestamp) { - Validate.notNull(episodeActions); + public GpodnetEpisodeActionGetResponse(@NonNull List<GpodnetEpisodeAction> episodeActions, + long timestamp) { this.episodeActions = episodeActions; this.timestamp = timestamp; } @@ -28,7 +26,9 @@ public class GpodnetEpisodeActionGetResponse { @Override public String toString() { - return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); + return "GpodnetEpisodeActionGetResponse{" + + "episodeActions=" + episodeActions + + ", timestamp=" + timestamp + + '}'; } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java index e06a88d5c..5f096db14 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetEpisodeActionPostResponse.java @@ -1,12 +1,13 @@ package de.danoeh.antennapod.core.gpoddernet.model; +import android.support.v4.util.ArrayMap; + import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import java.util.HashMap; import java.util.Map; public class GpodnetEpisodeActionPostResponse { @@ -36,8 +37,8 @@ public class GpodnetEpisodeActionPostResponse { public static GpodnetEpisodeActionPostResponse fromJSONObject(String objectString) throws JSONException { final JSONObject object = new JSONObject(objectString); final long timestamp = object.getLong("timestamp"); - Map<String, String> updatedUrls = new HashMap<String, String>(); JSONArray urls = object.getJSONArray("update_urls"); + Map<String, String> updatedUrls = new ArrayMap<String, String>(urls.length()); for (int i = 0; i < urls.length(); i++) { JSONArray urlPair = urls.getJSONArray(i); updatedUrls.put(urlPair.getString(0), urlPair.getString(1)); diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java index afebf66ac..191c0fa39 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java @@ -1,6 +1,6 @@ package de.danoeh.antennapod.core.gpoddernet.model; -import org.apache.commons.lang3.Validate; +import android.support.annotation.NonNull; public class GpodnetPodcast { private String url; @@ -11,12 +11,13 @@ public class GpodnetPodcast { private String website; private String mygpoLink; - public GpodnetPodcast(String url, String title, String description, - int subscribers, String logoUrl, String website, String mygpoLink) { - Validate.notNull(url); - Validate.notNull(title); - Validate.notNull(description); - + public GpodnetPodcast(@NonNull String url, + @NonNull String title, + @NonNull String description, + int subscribers, + String logoUrl, + String website, + String mygpoLink) { this.url = url; this.title = title; this.description = description; diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java index a5cb8c0f0..6cc9b79a3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java @@ -1,6 +1,6 @@ package de.danoeh.antennapod.core.gpoddernet.model; -import org.apache.commons.lang3.Validate; +import android.support.annotation.NonNull; import java.util.List; @@ -9,11 +9,9 @@ public class GpodnetSubscriptionChange { private List<String> removed; private long timestamp; - public GpodnetSubscriptionChange(List<String> added, List<String> removed, + public GpodnetSubscriptionChange(@NonNull List<String> added, + @NonNull List<String> removed, long timestamp) { - Validate.notNull(added); - Validate.notNull(removed); - this.added = added; this.removed = removed; this.timestamp = timestamp; diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java index cd865731b..42a31afc5 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java @@ -2,8 +2,7 @@ package de.danoeh.antennapod.core.gpoddernet.model; import android.os.Parcel; import android.os.Parcelable; - -import org.apache.commons.lang3.Validate; +import android.support.annotation.NonNull; public class GpodnetTag implements Parcelable { @@ -11,20 +10,16 @@ public class GpodnetTag implements Parcelable { private final String tag; private final int usage; - public GpodnetTag(String title, String tag, int usage) { - Validate.notNull(title); - Validate.notNull(tag); - + public GpodnetTag(@NonNull String title, @NonNull String tag, int usage) { this.title = title; this.tag = tag; this.usage = usage; } - public static GpodnetTag createFromParcel(Parcel in) { - final String title = in.readString(); - final String tag = in.readString(); - final int usage = in.readInt(); - return new GpodnetTag(title, tag, usage); + protected GpodnetTag(Parcel in) { + title = in.readString(); + tag = in.readString(); + usage = in.readInt(); } @Override @@ -56,5 +51,16 @@ public class GpodnetTag implements Parcelable { dest.writeInt(usage); } + public static final Creator<GpodnetTag> CREATOR = new Creator<GpodnetTag>() { + @Override + public GpodnetTag createFromParcel(Parcel in) { + return new GpodnetTag(in); + } + + @Override + public GpodnetTag[] newArray(int size) { + return new GpodnetTag[size]; + } + }; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java index 5a37efa5e..9bd1881e4 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java @@ -1,10 +1,11 @@ package de.danoeh.antennapod.core.gpoddernet.model; +import android.support.v4.util.ArrayMap; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import java.util.HashMap; import java.util.Map; /** @@ -37,7 +38,7 @@ public class GpodnetUploadChangesResponse { public static GpodnetUploadChangesResponse fromJSONObject(String objectString) throws JSONException { final JSONObject object = new JSONObject(objectString); final long timestamp = object.getLong("timestamp"); - Map<String, String> updatedUrls = new HashMap<String, String>(); + Map<String, String> updatedUrls = new ArrayMap<>(); JSONArray urls = object.getJSONArray("update_urls"); for (int i = 0; i < urls.length(); i++) { JSONArray urlPair = urls.getJSONArray(i); diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java index 2b831ca2a..c973713cb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java +++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java @@ -13,6 +13,7 @@ public final class OpmlSymbols { public static final String VERSION = "version"; public static final String HEAD = "head"; public static final String TITLE = "title"; + public static final String DATE_CREATED = "dateCreated"; private OpmlSymbols() { diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java index 641190f62..673c602df 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java @@ -2,14 +2,17 @@ package de.danoeh.antennapod.core.opml; import android.util.Log; import android.util.Xml; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.feed.Feed; + import org.xmlpull.v1.XmlSerializer; import java.io.IOException; import java.io.Writer; +import java.util.Date; import java.util.List; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.util.DateUtils; + /** Writes OPML documents. */ public class OpmlWriter { private static final String TAG = "OpmlWriter"; @@ -27,23 +30,38 @@ public class OpmlWriter { */ public void writeDocument(List<Feed> feeds, Writer writer) throws IllegalArgumentException, IllegalStateException, IOException { - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting to write document"); + Log.d(TAG, "Starting to write document"); XmlSerializer xs = Xml.newSerializer(); xs.setOutput(writer); xs.startDocument(ENCODING, false); + xs.text("\n"); xs.startTag(null, OpmlSymbols.OPML); xs.attribute(null, OpmlSymbols.VERSION, OPML_VERSION); + xs.text("\n"); + xs.text(" "); xs.startTag(null, OpmlSymbols.HEAD); + xs.text("\n"); + xs.text(" "); xs.startTag(null, OpmlSymbols.TITLE); xs.text(OPML_TITLE); xs.endTag(null, OpmlSymbols.TITLE); + xs.text("\n"); + xs.text(" "); + xs.startTag(null, OpmlSymbols.DATE_CREATED); + xs.text(DateUtils.formatRFC822Date(new Date())); + xs.endTag(null, OpmlSymbols.DATE_CREATED); + xs.text("\n"); + xs.text(" "); xs.endTag(null, OpmlSymbols.HEAD); + xs.text("\n"); + xs.text(" "); xs.startTag(null, OpmlSymbols.BODY); + xs.text("\n"); for (Feed feed : feeds) { + xs.text(" "); xs.startTag(null, OpmlSymbols.OUTLINE); xs.attribute(null, OpmlSymbols.TEXT, feed.getTitle()); xs.attribute(null, OpmlSymbols.TITLE, feed.getTitle()); @@ -55,11 +73,14 @@ public class OpmlWriter { xs.attribute(null, OpmlSymbols.HTMLURL, feed.getLink()); } xs.endTag(null, OpmlSymbols.OUTLINE); + xs.text("\n"); } + xs.text(" "); xs.endTag(null, OpmlSymbols.BODY); + xs.text("\n"); xs.endTag(null, OpmlSymbols.OPML); + xs.text("\n"); xs.endDocument(); - if (BuildConfig.DEBUG) - Log.d(TAG, "Finished writing document"); + Log.d(TAG, "Finished writing document"); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java index c3c6ce8c5..edd7b807a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java @@ -2,13 +2,11 @@ package de.danoeh.antennapod.core.preferences; import android.content.Context; import android.content.SharedPreferences; +import android.text.TextUtils; import android.util.Log; -import org.apache.commons.lang3.StringUtils; - import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -217,26 +215,36 @@ public class GpodnetPreferences { public static void removeRemovedFeeds(Collection<String> removed) { ensurePreferencesLoaded(); + feedListLock.lock(); removedFeeds.removeAll(removed); writePreference(PREF_SYNC_REMOVED, removedFeeds); + feedListLock.unlock(); } - public static synchronized void enqueueEpisodeAction(GpodnetEpisodeAction action) { + public static void enqueueEpisodeAction(GpodnetEpisodeAction action) { ensurePreferencesLoaded(); + feedListLock.lock(); queuedEpisodeActions.add(action); writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); + feedListLock.unlock(); GpodnetSyncService.sendSyncActionsIntent(ClientConfig.applicationCallbacks.getApplicationInstance()); } public static List<GpodnetEpisodeAction> getQueuedEpisodeActions() { ensurePreferencesLoaded(); - return Collections.unmodifiableList(queuedEpisodeActions); + List<GpodnetEpisodeAction> copy = new ArrayList(); + feedListLock.lock(); + copy.addAll(queuedEpisodeActions); + feedListLock.unlock(); + return copy; } - public static synchronized void removeQueuedEpisodeActions(Collection<GpodnetEpisodeAction> queued) { + public static void removeQueuedEpisodeActions(Collection<GpodnetEpisodeAction> queued) { ensurePreferencesLoaded(); + feedListLock.lock(); queuedEpisodeActions.removeAll(queued); writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); + feedListLock.unlock(); } /** @@ -252,12 +260,14 @@ public class GpodnetPreferences { setUsername(null); setPassword(null); setDeviceID(null); + feedListLock.lock(); addedFeeds.clear(); writePreference(PREF_SYNC_ADDED, addedFeeds); removedFeeds.clear(); writePreference(PREF_SYNC_REMOVED, removedFeeds); queuedEpisodeActions.clear(); writePreference(PREF_SYNC_EPISODE_ACTIONS, writeEpisodeActionsToString(queuedEpisodeActions)); + feedListLock.unlock(); setLastSubscriptionSyncTimestamp(0); } @@ -282,7 +292,7 @@ public class GpodnetPreferences { String[] lines = s.split("\n"); List<GpodnetEpisodeAction> result = new ArrayList<GpodnetEpisodeAction>(lines.length); for(String line : lines) { - if(StringUtils.isNotBlank(line)) { + if(TextUtils.isEmpty(line)) { GpodnetEpisodeAction action = GpodnetEpisodeAction.readFromString(line); if(action != null) { result.add(GpodnetEpisodeAction.readFromString(line)); diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java index 714f1b051..dfe056f14 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java @@ -3,11 +3,7 @@ package de.danoeh.antennapod.core.preferences; import android.content.Context; import android.content.SharedPreferences; import android.preference.PreferenceManager; -import android.util.Log; -import org.apache.commons.lang3.Validate; - -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.EventDistributor; /** @@ -15,159 +11,104 @@ import de.danoeh.antennapod.core.feed.EventDistributor; * instance of this class must first be instantiated via createInstance() or * otherwise every public method will throw an Exception when called. */ -public class PlaybackPreferences implements - SharedPreferences.OnSharedPreferenceChangeListener { - private static final String TAG = "PlaybackPreferences"; - - /** - * Contains the feed id of the currently playing item if it is a FeedMedia - * object. - */ - public static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId"; - - /** - * Contains the id of the currently playing FeedMedia object or - * NO_MEDIA_PLAYING if the currently playing media is no FeedMedia object. - */ - public static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId"; - - /** - * Type of the media object that is currently being played. This preference - * is set to NO_MEDIA_PLAYING after playback has been completed and is set - * as soon as the 'play' button is pressed. - */ - public static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia"; - - /** True if last played media was streamed. */ - public static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream"; - - /** True if last played media was a video. */ - public static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo"; - - /** The current player status as int. */ +public class PlaybackPreferences implements SharedPreferences.OnSharedPreferenceChangeListener { + + private static final String TAG = "PlaybackPreferences"; + + /** + * Contains the feed id of the currently playing item if it is a FeedMedia + * object. + */ + public static final String PREF_CURRENTLY_PLAYING_FEED_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedId"; + + /** + * Contains the id of the currently playing FeedMedia object or + * NO_MEDIA_PLAYING if the currently playing media is no FeedMedia object. + */ + public static final String PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID = "de.danoeh.antennapod.preferences.lastPlayedFeedMediaId"; + + /** + * Type of the media object that is currently being played. This preference + * is set to NO_MEDIA_PLAYING after playback has been completed and is set + * as soon as the 'play' button is pressed. + */ + public static final String PREF_CURRENTLY_PLAYING_MEDIA = "de.danoeh.antennapod.preferences.currentlyPlayingMedia"; + + /** + * True if last played media was streamed. + */ + public static final String PREF_CURRENT_EPISODE_IS_STREAM = "de.danoeh.antennapod.preferences.lastIsStream"; + + /** + * True if last played media was a video. + */ + public static final String PREF_CURRENT_EPISODE_IS_VIDEO = "de.danoeh.antennapod.preferences.lastIsVideo"; + + /** + * The current player status as int. + */ public static final String PREF_CURRENT_PLAYER_STATUS = "de.danoeh.antennapod.preferences.currentPlayerStatus"; - /** Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing. */ - public static final long NO_MEDIA_PLAYING = -1; + /** + * Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing. + */ + public static final long NO_MEDIA_PLAYING = -1; - /** Value of PREF_CURRENT_PLAYER_STATUS if media player status is playing. */ + /** + * Value of PREF_CURRENT_PLAYER_STATUS if media player status is playing. + */ public static final int PLAYER_STATUS_PLAYING = 1; - /** Value of PREF_CURRENT_PLAYER_STATUS if media player status is paused. */ + /** + * Value of PREF_CURRENT_PLAYER_STATUS if media player status is paused. + */ public static final int PLAYER_STATUS_PAUSED = 2; - /** Value of PREF_CURRENT_PLAYER_STATUS if media player status is neither playing nor paused. */ + /** + * Value of PREF_CURRENT_PLAYER_STATUS if media player status is neither playing nor paused. + */ public static final int PLAYER_STATUS_OTHER = 3; - private long currentlyPlayingFeedId; - private long currentlyPlayingFeedMediaId; - private long currentlyPlayingMedia; - private boolean currentEpisodeIsStream; - private boolean currentEpisodeIsVideo; - private int currentPlayerStatus; - - private static PlaybackPreferences instance; - private Context context; - - private PlaybackPreferences(Context context) { - this.context = context; - loadPreferences(); - } + private static PlaybackPreferences instance; + private static SharedPreferences prefs; - /** - * Sets up the UserPreferences class. - * - * @throws IllegalArgumentException - * if context is null - * */ - public static void createInstance(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Creating new instance of UserPreferences"); - Validate.notNull(context); - - instance = new PlaybackPreferences(context); - - PreferenceManager.getDefaultSharedPreferences(context) - .registerOnSharedPreferenceChangeListener(instance); - } + private PlaybackPreferences() { + } - private void loadPreferences() { - SharedPreferences sp = PreferenceManager - .getDefaultSharedPreferences(context); - currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, -1); - currentlyPlayingFeedMediaId = sp.getLong( - PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING); - currentlyPlayingMedia = sp.getLong(PREF_CURRENTLY_PLAYING_MEDIA, - NO_MEDIA_PLAYING); - currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true); - currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false); - currentPlayerStatus = sp.getInt(PREF_CURRENT_PLAYER_STATUS, - PLAYER_STATUS_OTHER); - } + public static void init(Context context) { + instance = new PlaybackPreferences(); + prefs = PreferenceManager.getDefaultSharedPreferences(context); + prefs.registerOnSharedPreferenceChangeListener(instance); + } - @Override - public void onSharedPreferenceChanged(SharedPreferences sp, String key) { - if (key.equals(PREF_CURRENTLY_PLAYING_FEED_ID)) { - currentlyPlayingFeedId = sp.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, - -1); - - } else if (key.equals(PREF_CURRENTLY_PLAYING_MEDIA)) { - currentlyPlayingMedia = sp - .getLong(PREF_CURRENTLY_PLAYING_MEDIA, -1); - - } else if (key.equals(PREF_CURRENT_EPISODE_IS_STREAM)) { - currentEpisodeIsStream = sp.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true); - - } else if (key.equals(PREF_CURRENT_EPISODE_IS_VIDEO)) { - currentEpisodeIsVideo = sp.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false); - - } else if (key.equals(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID)) { - currentlyPlayingFeedMediaId = sp.getLong( - PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING); - } - else if (key.equals(PREF_CURRENT_PLAYER_STATUS)) { - currentPlayerStatus = sp.getInt(PREF_CURRENT_PLAYER_STATUS, - PLAYER_STATUS_OTHER); + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if (key.equals(PREF_CURRENT_PLAYER_STATUS)) { EventDistributor.getInstance().sendPlayerStatusUpdateBroadcast(); } - } - - private static void instanceAvailable() { - if (instance == null) { - throw new IllegalStateException( - "UserPreferences was used before being set up"); - } - } - + } public static long getLastPlayedFeedId() { - instanceAvailable(); - return instance.currentlyPlayingFeedId; + return prefs.getLong(PREF_CURRENTLY_PLAYING_FEED_ID, -1); } public static long getCurrentlyPlayingMedia() { - instanceAvailable(); - return instance.currentlyPlayingMedia; + return prefs.getLong(PREF_CURRENTLY_PLAYING_MEDIA, NO_MEDIA_PLAYING); } public static long getCurrentlyPlayingFeedMediaId() { - return instance.currentlyPlayingFeedMediaId; + return prefs.getLong(PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, NO_MEDIA_PLAYING); } public static boolean getCurrentEpisodeIsStream() { - instanceAvailable(); - return instance.currentEpisodeIsStream; + return prefs.getBoolean(PREF_CURRENT_EPISODE_IS_STREAM, true); } public static boolean getCurrentEpisodeIsVideo() { - instanceAvailable(); - return instance.currentEpisodeIsVideo; + return prefs.getBoolean(PREF_CURRENT_EPISODE_IS_VIDEO, false); } public static int getCurrentPlayerStatus() { - instanceAvailable(); - return instance.currentPlayerStatus; + return prefs.getInt(PREF_CURRENT_PLAYER_STATUS, PLAYER_STATUS_OTHER); } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index 594241573..6c0aff15e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -5,12 +5,13 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; +import android.os.SystemClock; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; +import android.text.TextUtils; import android.util.Log; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; import org.json.JSONArray; import org.json.JSONException; @@ -18,32 +19,38 @@ import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; -import java.util.LinkedList; +import java.util.Calendar; import java.util.List; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver; +import de.danoeh.antennapod.core.storage.APCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.APNullCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.APQueueCleanupAlgorithm; +import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; /** * Provides access to preferences set by the user in the settings screen. A * private instance of this class must first be instantiated via - * createInstance() or otherwise every public method will throw an Exception + * init() or otherwise every public method will throw an Exception * when called. */ -public class UserPreferences implements - SharedPreferences.OnSharedPreferenceChangeListener { +public class UserPreferences { public static final String IMPORT_DIR = "import/"; private static final String TAG = "UserPreferences"; - // User Infercasce + // User Interface public static final String PREF_THEME = "prefTheme"; public static final String PREF_HIDDEN_DRAWER_ITEMS = "prefHiddenDrawerItems"; + public static final String PREF_DRAWER_FEED_ORDER = "prefDrawerFeedOrder"; + public static final String PREF_DRAWER_FEED_COUNTER = "prefDrawerFeedIndicator"; public static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; public static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; + public static final String PREF_LOCKSCREEN_BACKGROUND = "prefLockscreenBackground"; + public static final String PREF_SHOW_DOWNLOAD_REPORT = "prefShowDownloadReport"; // Queue public static final String PREF_QUEUE_ADD_TO_FRONT = "prefQueueAddToFront"; @@ -51,16 +58,20 @@ public class UserPreferences implements // Playback public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect"; public static final String PREF_UNPAUSE_ON_HEADSET_RECONNECT = "prefUnpauseOnHeadsetReconnect"; + public static final String PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT = "prefUnpauseOnBluetoothReconnect"; + public static final String PREF_HARDWARE_FOWARD_BUTTON_SKIPS = "prefHardwareForwardButtonSkips"; public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue"; + public static final String PREF_SKIP_KEEPS_EPISODE = "prefSkipKeepsEpisode"; public static final String PREF_AUTO_DELETE = "prefAutoDelete"; public static final String PREF_SMART_MARK_AS_PLAYED_SECS = "prefSmartMarkAsPlayedSecs"; - private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; + public static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; public static final String PREF_RESUME_AFTER_CALL = "prefResumeAfterCall"; // Network public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; + public static final String PREF_EPISODE_CLEANUP = "prefEpisodeCleanup"; public static final String PREF_PARALLEL_DOWNLOADS = "prefParallelDownloads"; public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize"; public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; @@ -74,193 +85,52 @@ public class UserPreferences implements // Other public static final String PREF_DATA_FOLDER = "prefDataFolder"; + public static final String PREF_IMAGE_CACHE_SIZE = "prefImageCacheSize"; // Mediaplayer public static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"; private static final String PREF_FAST_FORWARD_SECS = "prefFastForwardSecs"; private static final String PREF_REWIND_SECS = "prefRewindSecs"; public static final String PREF_QUEUE_LOCKED = "prefQueueLocked"; - - // TODO: Make this value configurable - private static final float PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT = 0.8f; - + public static final String IMAGE_CACHE_DEFAULT_VALUE = "100"; + public static final int IMAGE_CACHE_SIZE_MINIMUM = 20; + public static final String PREF_LEFT_VOLUME = "prefLeftVolume"; + public static final String PREF_RIGHT_VOLUME = "prefRightVolume"; + + // Experimental + public static final String PREF_SONIC = "prefSonic"; + public static final String PREF_STEREO_TO_MONO = "PrefStereoToMono"; + public static final String PREF_NORMALIZER = "prefNormalizer"; + public static final int EPISODE_CLEANUP_QUEUE = -1; + public static final int EPISODE_CLEANUP_NULL = -2; + public static final int EPISODE_CLEANUP_DEFAULT = 0; + + // Constants private static int EPISODE_CACHE_SIZE_UNLIMITED = -1; + public static int FEED_ORDER_COUNTER = 0; + public static int FEED_ORDER_ALPHABETICAL = 1; + public static int FEED_ORDER_LAST_UPDATE = 2; + public static int FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM = 0; + public static int FEED_COUNTER_SHOW_NEW = 1; + public static int FEED_COUNTER_SHOW_UNPLAYED = 2; + public static int FEED_COUNTER_SHOW_NONE = 3; - private static UserPreferences instance; - private final Context context; - - // User Interface - private int theme; - private List<String> hiddenDrawerItems; - private int notifyPriority; - private boolean persistNotify; - - // Queue - private boolean enqueueAtFront; - - // Playback - private boolean pauseOnHeadsetDisconnect; - private boolean unpauseOnHeadsetReconnect; - private boolean followQueue; - private boolean autoDelete; - private int smartMarkAsPlayedSecs; - private String[] playbackSpeedArray; - private boolean pauseForFocusLoss; - private boolean resumeAfterCall; - - // Network - private long updateInterval; - private boolean allowMobileUpdate; - private int parallelDownloads; - private int episodeCacheSize; - private boolean enableAutodownload; - private boolean enableAutodownloadOnBattery; - private boolean enableAutodownloadWifiFilter; - private String[] autodownloadSelectedNetworks; - - // Services - private boolean autoFlattr; - private float autoFlattrPlayedDurationThreshold; - - // Settings somewhere in the GUI - private String playbackSpeed; - private int fastForwardSecs; - private int rewindSecs; - private boolean queueLocked; - - - private UserPreferences(Context context) { - this.context = context; - loadPreferences(); - } + private static Context context; + private static SharedPreferences prefs; /** * Sets up the UserPreferences class. * * @throws IllegalArgumentException if context is null */ - public static void createInstance(Context context) { + public static void init(@NonNull Context context) { Log.d(TAG, "Creating new instance of UserPreferences"); - Validate.notNull(context); - instance = new UserPreferences(context); + UserPreferences.context = context.getApplicationContext(); + UserPreferences.prefs = PreferenceManager.getDefaultSharedPreferences(context); createImportDirectory(); createNoMediaFile(); - PreferenceManager.getDefaultSharedPreferences(context) - .registerOnSharedPreferenceChangeListener(instance); - - } - - private void loadPreferences() { - SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); - - // User Interface - theme = readThemeValue(sp.getString(PREF_THEME, "0")); - if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) { - notifyPriority = NotificationCompat.PRIORITY_MAX; - } else { - notifyPriority = NotificationCompat.PRIORITY_DEFAULT; - } - hiddenDrawerItems = Arrays.asList(StringUtils.split(sp.getString(PREF_HIDDEN_DRAWER_ITEMS, ""), ',')); - persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false); - - // Queue - enqueueAtFront = sp.getBoolean(PREF_QUEUE_ADD_TO_FRONT, false); - - // Playback - pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true); - unpauseOnHeadsetReconnect = sp.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true); - followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false); - autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); - smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")); - playbackSpeedArray = readPlaybackSpeedArray(sp.getString( - PREF_PLAYBACK_SPEED_ARRAY, null)); - pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); - - // Network - updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL, "0")); - allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); - parallelDownloads = Integer.valueOf(sp.getString(PREF_PARALLEL_DOWNLOADS, "6")); - EPISODE_CACHE_SIZE_UNLIMITED = context.getResources().getInteger( - R.integer.episode_cache_size_unlimited); - episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString(PREF_EPISODE_CACHE_SIZE, "20")); - enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false); - enableAutodownloadOnBattery = sp.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true); - enableAutodownloadWifiFilter = sp.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false); - autodownloadSelectedNetworks = StringUtils.split( - sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); - - // Services - autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); - autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, - PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT); - - // MediaPlayer - playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0"); - fastForwardSecs = sp.getInt(PREF_FAST_FORWARD_SECS, 30); - rewindSecs = sp.getInt(PREF_REWIND_SECS, 30); - queueLocked = sp.getBoolean(PREF_QUEUE_LOCKED, false); - } - - private int readThemeValue(String valueFromPrefs) { - switch (Integer.parseInt(valueFromPrefs)) { - case 0: - return R.style.Theme_AntennaPod_Light; - case 1: - return R.style.Theme_AntennaPod_Dark; - default: - return R.style.Theme_AntennaPod_Light; - } - } - - private long readUpdateInterval(String valueFromPrefs) { - int hours = Integer.parseInt(valueFromPrefs); - return TimeUnit.HOURS.toMillis(hours); - } - - private int readEpisodeCacheSizeInternal(String valueFromPrefs) { - if (valueFromPrefs.equals(context - .getString(R.string.pref_episode_cache_unlimited))) { - return EPISODE_CACHE_SIZE_UNLIMITED; - } else { - return Integer.valueOf(valueFromPrefs); - } - } - - private String[] readPlaybackSpeedArray(String valueFromPrefs) { - String[] selectedSpeeds = null; - // If this preference hasn't been set yet, return the default options - if (valueFromPrefs == null) { - String[] allSpeeds = context.getResources().getStringArray( - R.array.playback_speed_values); - List<String> speedList = new LinkedList<String>(); - for (String speedStr : allSpeeds) { - float speed = Float.parseFloat(speedStr); - if (speed < 2.0001 && speed * 10 % 1 == 0) { - speedList.add(speedStr); - } - } - selectedSpeeds = speedList.toArray(new String[speedList.size()]); - } else { - try { - JSONArray jsonArray = new JSONArray(valueFromPrefs); - selectedSpeeds = new String[jsonArray.length()]; - for (int i = 0; i < jsonArray.length(); i++) { - selectedSpeeds[i] = jsonArray.getString(i); - } - } catch (JSONException e) { - Log.e(TAG, "Got JSON error when trying to get speeds from JSONArray"); - e.printStackTrace(); - } - } - return selectedSpeeds; - } - - private static void instanceAvailable() { - if (instance == null) { - throw new IllegalStateException("UserPreferences was used before being set up"); - } } /** @@ -269,8 +139,7 @@ public class UserPreferences implements * @return R.style.Theme_AntennaPod_Light or R.style.Theme_AntennaPod_Dark */ public static int getTheme() { - instanceAvailable(); - return instance.theme; + return readThemeValue(prefs.getString(PREF_THEME, "0")); } public static int getNoTitleTheme() { @@ -283,8 +152,18 @@ public class UserPreferences implements } public static List<String> getHiddenDrawerItems() { - instanceAvailable(); - return new ArrayList<String>(instance.hiddenDrawerItems); + String hiddenItems = prefs.getString(PREF_HIDDEN_DRAWER_ITEMS, ""); + return new ArrayList<>(Arrays.asList(TextUtils.split(hiddenItems, ","))); + } + + public static int getFeedOrder() { + String value = prefs.getString(PREF_DRAWER_FEED_ORDER, "0"); + return Integer.valueOf(value); + } + + public static int getFeedCounterSetting() { + String value = prefs.getString(PREF_DRAWER_FEED_COUNTER, "0"); + return Integer.valueOf(value); } /** @@ -293,8 +172,11 @@ public class UserPreferences implements * @return NotificationCompat.PRIORITY_MAX or NotificationCompat.PRIORITY_DEFAULT */ public static int getNotifyPriority() { - instanceAvailable(); - return instance.notifyPriority; + if (prefs.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) { + return NotificationCompat.PRIORITY_MAX; + } else { + return NotificationCompat.PRIORITY_DEFAULT; + } } /** @@ -303,8 +185,25 @@ public class UserPreferences implements * @return {@code true} if notifications are persistent, {@code false} otherwise */ public static boolean isPersistNotify() { - instanceAvailable(); - return instance.persistNotify; + return prefs.getBoolean(PREF_PERSISTENT_NOTIFICATION, true); + } + + /** + * Returns true if notifications are persistent + * + * @return {@code true} if notifications are persistent, {@code false} otherwise + */ + public static boolean setLockscreenBackground() { + return prefs.getBoolean(PREF_LOCKSCREEN_BACKGROUND, true); + } + + /** + * Returns true if download reports are shown + * + * @return {@code true} if download reports are shown, {@code false} otherwise + */ + public static boolean showDownloadReport() { + return prefs.getBoolean(PREF_SHOW_DOWNLOAD_REPORT, true); } /** @@ -313,73 +212,107 @@ public class UserPreferences implements * @return {@code true} if new queue elements are added to the front; {@code false} otherwise */ public static boolean enqueueAtFront() { - instanceAvailable(); - return instance.enqueueAtFront; + return prefs.getBoolean(PREF_QUEUE_ADD_TO_FRONT, false); } public static boolean isPauseOnHeadsetDisconnect() { - instanceAvailable(); - return instance.pauseOnHeadsetDisconnect; + return prefs.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true); } public static boolean isUnpauseOnHeadsetReconnect() { - instanceAvailable(); - return instance.unpauseOnHeadsetReconnect; + return prefs.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true); + } + + public static boolean isUnpauseOnBluetoothReconnect() { + return prefs.getBoolean(PREF_UNPAUSE_ON_BLUETOOTH_RECONNECT, false); + } + + public static boolean shouldHardwareButtonSkip() { + return prefs.getBoolean(PREF_HARDWARE_FOWARD_BUTTON_SKIPS, false); } public static boolean isFollowQueue() { - instanceAvailable(); - return instance.followQueue; + return prefs.getBoolean(PREF_FOLLOW_QUEUE, true); } + public static boolean shouldSkipKeepEpisode() { return prefs.getBoolean(PREF_SKIP_KEEPS_EPISODE, true); } + public static boolean isAutoDelete() { - instanceAvailable(); - return instance.autoDelete; + return prefs.getBoolean(PREF_AUTO_DELETE, false); } public static int getSmartMarkAsPlayedSecs() { - instanceAvailable(); - return instance.smartMarkAsPlayedSecs; + return Integer.valueOf(prefs.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")); } public static boolean isAutoFlattr() { - instanceAvailable(); - return instance.autoFlattr; + return prefs.getBoolean(PREF_AUTO_FLATTR, false); } public static String getPlaybackSpeed() { - instanceAvailable(); - return instance.playbackSpeed; + return prefs.getString(PREF_PLAYBACK_SPEED, "1.00"); } public static String[] getPlaybackSpeedArray() { - instanceAvailable(); - return instance.playbackSpeedArray; + return readPlaybackSpeedArray(prefs.getString(PREF_PLAYBACK_SPEED_ARRAY, null)); + } + + public static float getLeftVolume() { + int volume = prefs.getInt(PREF_LEFT_VOLUME, 100); + if(volume == 100) { + return 1.0f; + } else { + return (float) (1 - (Math.log(100 - volume) / Math.log(100))); + } + } + + public static float getRightVolume() { + int volume = prefs.getInt(PREF_RIGHT_VOLUME, 100); + if(volume == 100) { + return 1.0f; + } else { + return (float) (1 - (Math.log(100 - volume) / Math.log(100))); + } } public static boolean shouldPauseForFocusLoss() { - instanceAvailable(); - return instance.pauseForFocusLoss; + return prefs.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); } + + public static long getUpdateInterval() { - instanceAvailable(); - return instance.updateInterval; + String updateInterval = prefs.getString(PREF_UPDATE_INTERVAL, "0"); + if(false == updateInterval.contains(":")) { + return readUpdateInterval(updateInterval); + } else { + return 0; + } + } + + public static int[] getUpdateTimeOfDay() { + String datetime = prefs.getString(PREF_UPDATE_INTERVAL, ""); + if(datetime.length() >= 3 && datetime.contains(":")) { + String[] parts = datetime.split(":"); + int hourOfDay = Integer.valueOf(parts[0]); + int minute = Integer.valueOf(parts[1]); + return new int[] { hourOfDay, minute }; + } else { + return new int[0]; + } } public static boolean isAllowMobileUpdate() { - instanceAvailable(); - return instance.allowMobileUpdate; + return prefs.getBoolean(PREF_MOBILE_UPDATE, false); } public static int getParallelDownloads() { - instanceAvailable(); - return instance.parallelDownloads; + return Integer.valueOf(prefs.getString(PREF_PARALLEL_DOWNLOADS, "4")); } public static int getEpisodeCacheSizeUnlimited() { - return EPISODE_CACHE_SIZE_UNLIMITED; + return context.getResources().getInteger(R.integer.episode_cache_size_unlimited); } /** @@ -388,33 +321,40 @@ public class UserPreferences implements * 'unlimited'. */ public static int getEpisodeCacheSize() { - instanceAvailable(); - return instance.episodeCacheSize; + return readEpisodeCacheSizeInternal(prefs.getString(PREF_EPISODE_CACHE_SIZE, "20")); } public static boolean isEnableAutodownload() { - instanceAvailable(); - return instance.enableAutodownload; + return prefs.getBoolean(PREF_ENABLE_AUTODL, false); } public static boolean isEnableAutodownloadOnBattery() { - instanceAvailable(); - return instance.enableAutodownloadOnBattery; + return prefs.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true); } public static boolean isEnableAutodownloadWifiFilter() { - instanceAvailable(); - return instance.enableAutodownloadWifiFilter; + return prefs.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false); + } + + public static int getImageCacheSize() { + String cacheSizeString = prefs.getString(PREF_IMAGE_CACHE_SIZE, IMAGE_CACHE_DEFAULT_VALUE); + int cacheSizeInt = Integer.valueOf(cacheSizeString); + // if the cache size is too small the user won't get any images at all + // that's bad, force it back to the default. + if (cacheSizeInt < IMAGE_CACHE_SIZE_MINIMUM) { + prefs.edit().putString(PREF_IMAGE_CACHE_SIZE, IMAGE_CACHE_DEFAULT_VALUE).apply(); + cacheSizeInt = Integer.valueOf(IMAGE_CACHE_DEFAULT_VALUE); + } + int cacheSizeMB = cacheSizeInt * 1024 * 1024; + return cacheSizeMB; } public static int getFastFowardSecs() { - instanceAvailable(); - return instance.fastForwardSecs; + return prefs.getInt(PREF_FAST_FORWARD_SECS, 30); } public static int getRewindSecs() { - instanceAvailable(); - return instance.rewindSecs; + return prefs.getInt(PREF_REWIND_SECS, 30); } @@ -423,145 +363,38 @@ public class UserPreferences implements * duration. */ public static float getAutoFlattrPlayedDurationThreshold() { - instanceAvailable(); - return instance.autoFlattrPlayedDurationThreshold; + return prefs.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, 0.8f); } public static String[] getAutodownloadSelectedNetworks() { - instanceAvailable(); - return instance.autodownloadSelectedNetworks; + String selectedNetWorks = prefs.getString(PREF_AUTODL_SELECTED_NETWORKS, ""); + return TextUtils.split(selectedNetWorks, ","); } public static boolean shouldResumeAfterCall() { - instanceAvailable(); - return instance.resumeAfterCall; + return prefs.getBoolean(PREF_RESUME_AFTER_CALL, true); } public static boolean isQueueLocked() { - instanceAvailable(); - return instance.queueLocked; - } - - @Override - public void onSharedPreferenceChanged(SharedPreferences sp, String key) { - Log.d(TAG, "Registered change of user preferences. Key: " + key); - switch(key) { - // User Interface - case PREF_THEME: - theme = readThemeValue(sp.getString(PREF_THEME, "")); - break; - case PREF_HIDDEN_DRAWER_ITEMS: - hiddenDrawerItems = Arrays.asList(StringUtils.split(sp.getString(PREF_HIDDEN_DRAWER_ITEMS, ""), ',')); - break; - case PREF_EXPANDED_NOTIFICATION: - if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) { - notifyPriority = NotificationCompat.PRIORITY_MAX; - } else { - notifyPriority = NotificationCompat.PRIORITY_DEFAULT; - } - break; - case PREF_PERSISTENT_NOTIFICATION: - persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false); - break; - // Queue - case PREF_QUEUE_ADD_TO_FRONT: - enqueueAtFront = sp.getBoolean(PREF_QUEUE_ADD_TO_FRONT, false); - break; - // Playback - case PREF_PAUSE_ON_HEADSET_DISCONNECT: - pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true); - break; - case PREF_UNPAUSE_ON_HEADSET_RECONNECT: - unpauseOnHeadsetReconnect = sp.getBoolean(PREF_UNPAUSE_ON_HEADSET_RECONNECT, true); - break; - case PREF_FOLLOW_QUEUE: - followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false); - break; - case PREF_AUTO_DELETE: - autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); - break; - case PREF_SMART_MARK_AS_PLAYED_SECS: - smartMarkAsPlayedSecs = Integer.valueOf(sp.getString(PREF_SMART_MARK_AS_PLAYED_SECS, "30")); - break; - case PREF_PLAYBACK_SPEED_ARRAY: - playbackSpeedArray = readPlaybackSpeedArray(sp.getString(PREF_PLAYBACK_SPEED_ARRAY, null)); - break; - case PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS: - pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); - break; - case PREF_RESUME_AFTER_CALL: - resumeAfterCall = sp.getBoolean(PREF_RESUME_AFTER_CALL, true); - break; - // Network - case PREF_UPDATE_INTERVAL: - updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL, "0")); - ClientConfig.applicationCallbacks.setUpdateInterval(updateInterval); - break; - case PREF_MOBILE_UPDATE: - allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); - break; - case PREF_PARALLEL_DOWNLOADS: - parallelDownloads = Integer.valueOf(sp.getString(PREF_PARALLEL_DOWNLOADS, "6")); - break; - case PREF_EPISODE_CACHE_SIZE: - episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString(PREF_EPISODE_CACHE_SIZE, "20")); - break; - case PREF_ENABLE_AUTODL: - enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false); - break; - case PREF_ENABLE_AUTODL_ON_BATTERY: - enableAutodownloadOnBattery = sp.getBoolean(PREF_ENABLE_AUTODL_ON_BATTERY, true); - break; - case PREF_ENABLE_AUTODL_WIFI_FILTER: - enableAutodownloadWifiFilter = sp.getBoolean(PREF_ENABLE_AUTODL_WIFI_FILTER, false); - break; - case PREF_AUTODL_SELECTED_NETWORKS: - autodownloadSelectedNetworks = StringUtils.split( - sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); - break; - // Services - case PREF_AUTO_FLATTR: - autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); - break; - case PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD: - autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, - PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT); - break; - // Mediaplayer - case PREF_PLAYBACK_SPEED: - playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0"); - break; - case PREF_FAST_FORWARD_SECS: - fastForwardSecs = sp.getInt(PREF_FAST_FORWARD_SECS, 30); - break; - case PREF_REWIND_SECS: - rewindSecs = sp.getInt(PREF_REWIND_SECS, 30); - break; - case PREF_QUEUE_LOCKED: - queueLocked = sp.getBoolean(PREF_QUEUE_LOCKED, false); - break; - default: - Log.w(TAG, "Unhandled key: " + key); - } + return prefs.getBoolean(PREF_QUEUE_LOCKED, false); } public static void setPrefFastForwardSecs(int secs) { - Log.d(TAG, "setPrefFastForwardSecs(" + secs +")"); - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit(); - editor.putInt(PREF_FAST_FORWARD_SECS, secs); - editor.commit(); + prefs.edit() + .putInt(PREF_FAST_FORWARD_SECS, secs) + .apply(); } public static void setPrefRewindSecs(int secs) { - Log.d(TAG, "setPrefRewindSecs(" + secs +")"); - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(instance.context).edit(); - editor.putInt(PREF_REWIND_SECS, secs); - editor.commit(); + prefs.edit() + .putInt(PREF_REWIND_SECS, secs) + .apply(); } public static void setPlaybackSpeed(String speed) { - PreferenceManager.getDefaultSharedPreferences(instance.context).edit() - .putString(PREF_PLAYBACK_SPEED, speed).apply(); + prefs.edit() + .putString(PREF_PLAYBACK_SPEED, speed) + .apply(); } public static void setPlaybackSpeedArray(String[] speeds) { @@ -569,74 +402,161 @@ public class UserPreferences implements for (String speed : speeds) { jsonArray.put(speed); } - PreferenceManager.getDefaultSharedPreferences(instance.context).edit() - .putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString()) - .apply(); + prefs.edit() + .putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString()) + .apply(); + } + + public static void setVolume(int leftVolume, int rightVolume) { + assert(0 <= leftVolume && leftVolume <= 100); + assert(0 <= rightVolume && rightVolume <= 100); + prefs.edit() + .putInt(PREF_LEFT_VOLUME, leftVolume) + .putInt(PREF_RIGHT_VOLUME, rightVolume) + .apply(); + } + + public static void setAutodownloadSelectedNetworks(String[] value) { + prefs.edit() + .putString(PREF_AUTODL_SELECTED_NETWORKS, TextUtils.join(",", value)) + .apply(); } - public static void setAutodownloadSelectedNetworks(Context context, - String[] value) { - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(context.getApplicationContext()) - .edit(); - editor.putString(PREF_AUTODL_SELECTED_NETWORKS, - StringUtils.join(value, ',')); - editor.commit(); + /** + * Sets the update interval value. + */ + public static void setUpdateInterval(long hours) { + prefs.edit() + .putString(PREF_UPDATE_INTERVAL, String.valueOf(hours)) + .apply(); + // when updating with an interval, we assume the user wants + // to update *now* and then every 'hours' interval thereafter. + restartUpdateAlarm(true); } /** - * Sets the update interval value. Should only be used for testing purposes! + * Sets the update interval value. */ - public static void setUpdateInterval(Context context, long newValue) { - instanceAvailable(); - SharedPreferences.Editor editor = PreferenceManager - .getDefaultSharedPreferences(context.getApplicationContext()) - .edit(); - editor.putString(PREF_UPDATE_INTERVAL, - String.valueOf(newValue)); - editor.commit(); - instance.updateInterval = newValue; + public static void setUpdateTimeOfDay(int hourOfDay, int minute) { + prefs.edit() + .putString(PREF_UPDATE_INTERVAL, hourOfDay + ":" + minute) + .apply(); + restartUpdateAlarm(false); } /** * Change the auto-flattr settings * - * @param context For accessing the shared preferences * @param enabled Whether automatic flattring should be enabled at all * @param autoFlattrThreshold The percentage of playback time after which an episode should be * flattrd. Must be a value between 0 and 1 (inclusive) * */ - public static void setAutoFlattrSettings(Context context, boolean enabled, float autoFlattrThreshold) { - instanceAvailable(); - Validate.inclusiveBetween(0.0, 1.0, autoFlattrThreshold); - PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()) - .edit() - .putBoolean(PREF_AUTO_FLATTR, enabled) - .putFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, autoFlattrThreshold) - .commit(); - instance.autoFlattr = enabled; - instance.autoFlattrPlayedDurationThreshold = autoFlattrThreshold; - } - - public static void setHiddenDrawerItems(Context context, List<String> items) { - instanceAvailable(); - instance.hiddenDrawerItems = items; - String str = StringUtils.join(items, ','); - PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()) - .edit() - .putString(PREF_HIDDEN_DRAWER_ITEMS, str) - .commit(); + public static void setAutoFlattrSettings( boolean enabled, float autoFlattrThreshold) { + if(autoFlattrThreshold < 0.0 || autoFlattrThreshold > 1.0) { + throw new IllegalArgumentException("Flattr threshold must be in range [0.0, 1.0]"); + } + prefs.edit() + .putBoolean(PREF_AUTO_FLATTR, enabled) + .putFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, autoFlattrThreshold) + .apply(); + } + + public static void setHiddenDrawerItems(List<String> items) { + String str = TextUtils.join(",", items); + prefs.edit() + .putString(PREF_HIDDEN_DRAWER_ITEMS, str) + .apply(); } public static void setQueueLocked(boolean locked) { - instanceAvailable(); - instance.queueLocked = locked; - PreferenceManager.getDefaultSharedPreferences(instance.context) - .edit() - .putBoolean(PREF_QUEUE_LOCKED, locked) - .commit(); + prefs.edit() + .putBoolean(PREF_QUEUE_LOCKED, locked) + .apply(); + } + + private static int readThemeValue(String valueFromPrefs) { + switch (Integer.parseInt(valueFromPrefs)) { + case 0: + return R.style.Theme_AntennaPod_Light; + case 1: + return R.style.Theme_AntennaPod_Dark; + default: + return R.style.Theme_AntennaPod_Light; + } } + private static long readUpdateInterval(String valueFromPrefs) { + int hours = Integer.parseInt(valueFromPrefs); + return TimeUnit.HOURS.toMillis(hours); + } + + private static int readEpisodeCacheSizeInternal(String valueFromPrefs) { + if (valueFromPrefs.equals(context.getString(R.string.pref_episode_cache_unlimited))) { + return EPISODE_CACHE_SIZE_UNLIMITED; + } else { + return Integer.valueOf(valueFromPrefs); + } + } + + private static String[] readPlaybackSpeedArray(String valueFromPrefs) { + String[] selectedSpeeds = null; + // If this preference hasn't been set yet, return the default options + if (valueFromPrefs == null) { + String[] allSpeeds = context.getResources().getStringArray(R.array.playback_speed_values); + List<String> speedList = new ArrayList<>(); + for (String speedStr : allSpeeds) { + float speed = Float.parseFloat(speedStr); + if (speed < 2.0001 && speed * 10 % 1 == 0) { + speedList.add(speedStr); + } + } + selectedSpeeds = speedList.toArray(new String[speedList.size()]); + } else { + try { + JSONArray jsonArray = new JSONArray(valueFromPrefs); + selectedSpeeds = new String[jsonArray.length()]; + for (int i = 0; i < jsonArray.length(); i++) { + selectedSpeeds[i] = jsonArray.getString(i); + } + } catch (JSONException e) { + Log.e(TAG, "Got JSON error when trying to get speeds from JSONArray"); + e.printStackTrace(); + } + } + return selectedSpeeds; + } + + public static boolean useSonic() { + return prefs.getBoolean(PREF_SONIC, false); + } + + public static void enableSonic(boolean enable) { + prefs.edit() + .putBoolean(PREF_SONIC, enable) + .apply(); + } + + public static boolean stereoToMono() { + return prefs.getBoolean(PREF_STEREO_TO_MONO, false); + } + + public static void stereoToMono(boolean enable) { + prefs.edit() + .putBoolean(PREF_STEREO_TO_MONO, enable) + .apply(); + } + + + public static EpisodeCleanupAlgorithm getEpisodeCleanupAlgorithm() { + int cleanupValue = Integer.valueOf(prefs.getString(PREF_EPISODE_CLEANUP, "-1")); + if (cleanupValue == EPISODE_CLEANUP_QUEUE) { + return new APQueueCleanupAlgorithm(); + } else if (cleanupValue == EPISODE_CLEANUP_NULL) { + return new APNullCleanupAlgorithm(); + } else { + return new APCleanupAlgorithm(cleanupValue); + } + } /** * Return the folder where the app stores all of its data. This method will @@ -647,10 +567,7 @@ public class UserPreferences implements * @return The data folder that has been requested or null if the folder * could not be created. */ - public static File getDataFolder(Context context, String type) { - instanceAvailable(); - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context.getApplicationContext()); + public static File getDataFolder(String type) { String strDir = prefs.getString(PREF_DATA_FOLDER, null); if (strDir == null) { Log.d(TAG, "Using default data folder"); @@ -672,7 +589,7 @@ public class UserPreferences implements for (int i = 0; i < dirs.length; i++) { if (dirs.length > 0) { if (i < dirs.length - 1) { - dataDir = getDataFolder(context, dirs[i]); + dataDir = getDataFolder(dirs[i]); if (dataDir == null) { return null; } @@ -695,13 +612,10 @@ public class UserPreferences implements } public static void setDataFolder(String dir) { - Log.d(TAG, "Result from DirectoryChooser: " + dir); - instanceAvailable(); - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(instance.context); - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(PREF_DATA_FOLDER, dir); - editor.commit(); + Log.d(TAG, "setDataFolder(dir: " + dir + ")"); + prefs.edit() + .putString(PREF_DATA_FOLDER, dir) + .apply(); createImportDirectory(); } @@ -709,8 +623,7 @@ public class UserPreferences implements * Create a .nomedia file to prevent scanning by the media scanner. */ private static void createNoMediaFile() { - File f = new File(instance.context.getExternalFilesDir(null), - ".nomedia"); + File f = new File(context.getExternalFilesDir(null), ".nomedia"); if (!f.exists()) { try { f.createNewFile(); @@ -727,8 +640,7 @@ public class UserPreferences implements * available */ private static void createImportDirectory() { - File importDir = getDataFolder(instance.context, - IMPORT_DIR); + File importDir = getDataFolder(IMPORT_DIR); if (importDir != null) { if (importDir.exists()) { Log.d(TAG, "Import directory already exists"); @@ -741,32 +653,68 @@ public class UserPreferences implements } } + public static void restartUpdateAlarm(boolean now) { + int[] timeOfDay = getUpdateTimeOfDay(); + Log.d(TAG, "timeOfDay: " + Arrays.toString(timeOfDay)); + if (timeOfDay.length == 2) { + restartUpdateTimeOfDayAlarm(timeOfDay[0], timeOfDay[1]); + } else { + long hours = getUpdateInterval(); + long startTrigger = hours; + if (now) { + startTrigger = TimeUnit.SECONDS.toMillis(10); + } + restartUpdateIntervalAlarm(startTrigger, hours); + } + } + /** - * Updates alarm registered with the AlarmManager service or deactivates it. + * Sets the interval in which the feeds are refreshed automatically */ - public static void restartUpdateAlarm(long triggerAtMillis, long intervalMillis) { - instanceAvailable(); + public static void restartUpdateIntervalAlarm(long triggerAtMillis, long intervalMillis) { Log.d(TAG, "Restarting update alarm."); - AlarmManager alarmManager = (AlarmManager) instance.context - .getSystemService(Context.ALARM_SERVICE); - PendingIntent updateIntent = PendingIntent.getBroadcast( - instance.context, 0, new Intent(ClientConfig.applicationCallbacks.getApplicationInstance(), FeedUpdateReceiver.class), 0); + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent intent = new Intent(context, FeedUpdateReceiver.class); + PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0, intent, 0); alarmManager.cancel(updateIntent); - if (intervalMillis != 0) { - alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, triggerAtMillis, intervalMillis, + if (intervalMillis > 0) { + alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + triggerAtMillis, updateIntent); - Log.d(TAG, "Changed alarm to new interval"); + Log.d(TAG, "Changed alarm to new interval " + TimeUnit.MILLISECONDS.toHours(intervalMillis) + " h"); } else { Log.d(TAG, "Automatic update was deactivated"); } } + /** + * Sets time of day the feeds are refreshed automatically + */ + public static void restartUpdateTimeOfDayAlarm(int hoursOfDay, int minute) { + Log.d(TAG, "Restarting update alarm."); + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + PendingIntent updateIntent = PendingIntent.getBroadcast(context, 0, + new Intent(context, FeedUpdateReceiver.class), 0); + alarmManager.cancel(updateIntent); + + Calendar now = Calendar.getInstance(); + Calendar alarm = (Calendar)now.clone(); + alarm.set(Calendar.HOUR_OF_DAY, hoursOfDay); + alarm.set(Calendar.MINUTE, minute); + if (alarm.before(now) || alarm.equals(now)) { + alarm.add(Calendar.DATE, 1); + } + Log.d(TAG, "Alarm set for: " + alarm.toString() + " : " + alarm.getTimeInMillis()); + alarmManager.set(AlarmManager.RTC_WAKEUP, + alarm.getTimeInMillis(), + updateIntent); + Log.d(TAG, "Changed alarm to new time of day " + hoursOfDay + ":" + minute); + } /** * Reads episode cache size as it is saved in the episode_cache_size_values array. */ public static int readEpisodeCacheSize(String valueFromPrefs) { - instanceAvailable(); - return instance.readEpisodeCacheSizeInternal(valueFromPrefs); + return readEpisodeCacheSizeInternal(valueFromPrefs); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java index 84277b6d5..ce5004a98 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java @@ -3,32 +3,28 @@ package de.danoeh.antennapod.core.receiver; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.text.TextUtils; import android.util.Log; -import org.apache.commons.lang3.StringUtils; - -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; /** Listens for events that make it necessary to reset the update alarm. */ public class AlarmUpdateReceiver extends BroadcastReceiver { + private static final String TAG = "AlarmUpdateReceiver"; @Override public void onReceive(Context context, Intent intent) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Received intent"); - if (StringUtils.equals(intent.getAction(), Intent.ACTION_BOOT_COMPLETED)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Resetting update alarm after reboot"); - } else if (StringUtils.equals(intent.getAction(), Intent.ACTION_PACKAGE_REPLACED)) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Resetting update alarm after app upgrade"); + Log.d(TAG, "Received intent"); + if (TextUtils.equals(intent.getAction(), Intent.ACTION_BOOT_COMPLETED)) { + Log.d(TAG, "Resetting update alarm after reboot"); + } else if (TextUtils.equals(intent.getAction(), Intent.ACTION_PACKAGE_REPLACED)) { + Log.d(TAG, "Resetting update alarm after app upgrade"); } - - ClientConfig.applicationCallbacks.setUpdateInterval(UserPreferences.getUpdateInterval()); - + PlaybackPreferences.init(context); + UserPreferences.init(context); + UserPreferences.restartUpdateAlarm(false); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java index d37f97a5f..b959c7301 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import android.util.Log; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.util.NetworkUtils; @@ -18,11 +19,12 @@ public class FeedUpdateReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "Received intent"); - if (NetworkUtils.isDownloadAllowed(context)) { - DBTasks.refreshExpiredFeeds(context); + if (NetworkUtils.isDownloadAllowed()) { + DBTasks.refreshAllFeeds(context, null); } else { Log.d(TAG, "Blocking automatic update: no wifi available / no mobile updates allowed"); } + UserPreferences.restartUpdateAlarm(false); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java index 3f2222f42..0b90cef6c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java @@ -8,12 +8,12 @@ import android.content.Context; import android.content.Intent; import android.os.IBinder; import android.support.v4.app.NotificationCompat; +import android.support.v4.util.ArrayMap; import android.util.Log; import android.util.Pair; import java.util.Collection; import java.util.Date; -import java.util.HashMap; import java.util.List; import java.util.Map; @@ -108,7 +108,7 @@ public class GpodnetSyncService extends Service { private synchronized void sync() { - if (GpodnetPreferences.loggedIn() == false || NetworkUtils.networkAvailable(this) == false) { + if (GpodnetPreferences.loggedIn() == false || NetworkUtils.networkAvailable() == false) { stopSelf(); return; } @@ -126,7 +126,7 @@ public class GpodnetSyncService extends Service { private synchronized void syncSubscriptionChanges() { final long timestamp = GpodnetPreferences.getLastSubscriptionSyncTimestamp(); try { - final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this); + final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(); Collection<String> localAdded = GpodnetPreferences.getAddedFeedsCopy(); Collection<String> localRemoved = GpodnetPreferences.getRemovedFeedsCopy(); GpodnetService service = tryLogin(); @@ -226,11 +226,11 @@ public class GpodnetSyncService extends Service { if(remoteActions.size() == 0) { return; } - Map<Pair<String, String>, GpodnetEpisodeAction> localMostRecentPlayAction = new HashMap<Pair<String, String>, GpodnetEpisodeAction>(); + Map<Pair<String, String>, GpodnetEpisodeAction> localMostRecentPlayAction = new ArrayMap<>(); for(GpodnetEpisodeAction action : localActions) { Pair key = new Pair(action.getPodcast(), action.getEpisode()); GpodnetEpisodeAction mostRecent = localMostRecentPlayAction.get(key); - if (mostRecent == null) { + if (mostRecent == null || mostRecent.getTimestamp() == null) { localMostRecentPlayAction.put(key, action); } else if (mostRecent.getTimestamp().before(action.getTimestamp())) { localMostRecentPlayAction.put(key, action); @@ -238,13 +238,13 @@ public class GpodnetSyncService extends Service { } // make sure more recent local actions are not overwritten by older remote actions - Map<Pair<String, String>, GpodnetEpisodeAction> mostRecentPlayAction = new HashMap<Pair<String, String>, GpodnetEpisodeAction>(); + Map<Pair<String, String>, GpodnetEpisodeAction> mostRecentPlayAction = new ArrayMap<>(); for (GpodnetEpisodeAction action : remoteActions) { switch (action.getAction()) { case NEW: - FeedItem newItem = DBReader.getFeedItem(this, action.getPodcast(), action.getEpisode()); + FeedItem newItem = DBReader.getFeedItem(action.getPodcast(), action.getEpisode()); if(newItem != null) { - DBWriter.markItemRead(this, newItem, false, true); + DBWriter.markItemPlayed(newItem, FeedItem.UNPLAYED, true); } else { Log.i(TAG, "Unknown feed item: " + action); } @@ -255,12 +255,15 @@ public class GpodnetSyncService extends Service { Pair key = new Pair(action.getPodcast(), action.getEpisode()); GpodnetEpisodeAction localMostRecent = localMostRecentPlayAction.get(key); if(localMostRecent == null || + localMostRecent.getTimestamp() == null || localMostRecent.getTimestamp().before(action.getTimestamp())) { GpodnetEpisodeAction mostRecent = mostRecentPlayAction.get(key); - if (mostRecent == null) { + if (mostRecent == null || mostRecent.getTimestamp() == null) { mostRecentPlayAction.put(key, action); - } else if (mostRecent.getTimestamp().before(action.getTimestamp())) { + } else if (action.getTimestamp() != null && mostRecent.getTimestamp().before(action.getTimestamp())) { mostRecentPlayAction.put(key, action); + } else { + Log.d(TAG, "No date information in action, skipping it"); } } break; @@ -270,14 +273,14 @@ public class GpodnetSyncService extends Service { } } for (GpodnetEpisodeAction action : mostRecentPlayAction.values()) { - FeedItem playItem = DBReader.getFeedItem(this, action.getPodcast(), action.getEpisode()); + FeedItem playItem = DBReader.getFeedItem(action.getPodcast(), action.getEpisode()); if (playItem != null) { FeedMedia media = playItem.getMedia(); media.setPosition(action.getPosition() * 1000); - DBWriter.setFeedMedia(this, media); + DBWriter.setFeedMedia(media); if(playItem.getMedia().hasAlmostEnded()) { - DBWriter.markItemRead(this, playItem, true, true); - DBWriter.addItemToPlaybackHistory(this, playItem.getMedia()); + DBWriter.markItemPlayed(playItem, FeedItem.PLAYED, true); + DBWriter.addItemToPlaybackHistory(playItem.getMedia()); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java deleted file mode 100644 index 3efcf4da8..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java +++ /dev/null @@ -1,54 +0,0 @@ -package de.danoeh.antennapod.core.service.download; - -import android.util.Log; -import de.danoeh.antennapod.core.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/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java index ec3d3e2fe..b23819ef7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java @@ -1,14 +1,29 @@ package de.danoeh.antennapod.core.service.download; +import android.os.Build; +import android.support.annotation.NonNull; import android.util.Log; import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import com.squareup.okhttp.internal.http.StatusLine; +import java.io.IOException; import java.net.CookieManager; import java.net.CookiePolicy; +import java.net.HttpURLConnection; +import java.net.InetAddress; +import java.net.Socket; +import java.net.URL; +import java.security.GeneralSecurityException; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.BuildConfig; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import de.danoeh.antennapod.core.storage.DBWriter; /** * Provides access to a HttpClient singleton. @@ -30,32 +45,74 @@ public class AntennapodHttpClient { public static synchronized OkHttpClient getHttpClient() { if (httpClient == null) { - if (BuildConfig.DEBUG) Log.d(TAG, "Creating new instance of HTTP client"); - - System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS)); - - OkHttpClient client = new OkHttpClient(); - - // set cookie handler - CookieManager cm = new CookieManager(); - cm.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); - client.setCookieHandler(cm); - - // set timeouts - client.setConnectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS); - client.setReadTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS); - client.setWriteTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS); - - // configure redirects - client.setFollowRedirects(true); - client.setFollowSslRedirects(true); - - httpClient = client; + httpClient = newHttpClient(); } return httpClient; } /** + * Creates a new HTTP client. Most users should just use + * getHttpClient() to get the standard AntennaPod client, + * but sometimes it's necessary for others to have their own + * copy so that the clients don't share state. + * @return http client + */ + @NonNull + public static OkHttpClient newHttpClient() { + Log.d(TAG, "Creating new instance of HTTP client"); + + System.setProperty("http.maxConnections", String.valueOf(MAX_CONNECTIONS)); + + OkHttpClient client = new OkHttpClient(); + + // detect 301 Moved permanently and 308 Permanent Redirect + client.networkInterceptors().add(chain -> { + Request request = chain.request(); + Response response = chain.proceed(request); + if(response.code() == HttpURLConnection.HTTP_MOVED_PERM || + response.code() == StatusLine.HTTP_PERM_REDIRECT) { + String location = response.header("Location"); + if(location.startsWith("/")) { // URL is not absolute, but relative + URL url = request.url(); + location = url.getProtocol() + "://" + url.getHost() + location; + } else if(!location.toLowerCase().startsWith("http://") && + !location.toLowerCase().startsWith("https://")) { + // Reference is relative to current path + URL url = request.url(); + String path = url.getPath(); + String newPath = path.substring(0, path.lastIndexOf("/") + 1) + location; + location = url.getProtocol() + "://" + url.getHost() + newPath; + } + try { + DBWriter.updateFeedDownloadURL(request.urlString(), location).get(); + } catch (Exception e) { + Log.e(TAG, Log.getStackTraceString(e)); + } + } + return response; + }); + + // set cookie handler + CookieManager cm = new CookieManager(); + cm.setCookiePolicy(CookiePolicy.ACCEPT_ORIGINAL_SERVER); + client.setCookieHandler(cm); + + // set timeouts + client.setConnectTimeout(CONNECTION_TIMEOUT, TimeUnit.MILLISECONDS); + client.setReadTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS); + client.setWriteTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS); + + // configure redirects + client.setFollowRedirects(true); + client.setFollowSslRedirects(true); + + if(16 <= Build.VERSION.SDK_INT && Build.VERSION.SDK_INT < 21) { + client.setSslSocketFactory(new CustomSslSocketFactory()); + } + return client; + } + + /** * Closes expired connections. This method should be called by the using class once has finished its work with * the HTTP client. */ @@ -64,4 +121,71 @@ public class AntennapodHttpClient { // does nothing at the moment } } + + private static class CustomSslSocketFactory extends SSLSocketFactory { + + private SSLSocketFactory factory; + + public CustomSslSocketFactory() { + try { + SSLContext sslContext = SSLContext.getInstance("TLSv1.2"); + sslContext.init(null, null, null); + factory= sslContext.getSocketFactory(); + } catch(GeneralSecurityException e) { + e.printStackTrace(); + } + } + + @Override + public String[] getDefaultCipherSuites() { + return factory.getDefaultCipherSuites(); + } + + @Override + public String[] getSupportedCipherSuites() { + return factory.getSupportedCipherSuites(); + } + + public Socket createSocket() throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(); + configureSocket(result); + return result; + } + + public Socket createSocket(String var1, int var2) throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(var1, var2); + configureSocket(result); + return result; + } + + public Socket createSocket(Socket var1, String var2, int var3, boolean var4) throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4); + configureSocket(result); + return result; + } + + public Socket createSocket(InetAddress var1, int var2) throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(var1, var2); + configureSocket(result); + return result; + } + + public Socket createSocket(String var1, int var2, InetAddress var3, int var4) throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4); + configureSocket(result); + return result; + } + + public Socket createSocket(InetAddress var1, int var2, InetAddress var3, int var4) throws IOException { + SSLSocket result = (SSLSocket) factory.createSocket(var1, var2, var3, var4); + configureSocket(result); + return result; + } + + private void configureSocket(SSLSocket s) { + s.setEnabledProtocols(new String[] { "TLSv1.2", "TLSv1.1", "TLSv1" } ); + } + + } + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java index 41bbd5ba6..bc3006eea 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java @@ -3,8 +3,7 @@ package de.danoeh.antennapod.core.service.download; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; - -import org.apache.commons.lang3.Validate; +import android.support.annotation.NonNull; import de.danoeh.antennapod.core.feed.FeedFile; import de.danoeh.antennapod.core.util.URLChecker; @@ -27,11 +26,15 @@ public class DownloadRequest implements Parcelable { protected long size; protected int statusMsg; - public DownloadRequest(String destination, String source, String title, - long feedfileId, int feedfileType, String username, String password, boolean deleteOnFailure, Bundle arguments) { - Validate.notNull(destination); - Validate.notNull(source); - Validate.notNull(title); + public DownloadRequest(@NonNull String destination, + @NonNull String source, + @NonNull String title, + long feedfileId, + int feedfileType, + String username, + String password, + boolean deleteOnFailure, + Bundle arguments) { this.destination = destination; this.source = source; @@ -260,7 +263,7 @@ public class DownloadRequest implements Parcelable { private int feedfileType; private Bundle arguments; - public Builder(String destination, FeedFile item) { + public Builder(@NonNull String destination, @NonNull FeedFile item) { this.destination = destination; this.source = URLChecker.prepareURL(item.getDownload_url()); this.title = item.getHumanReadableIdentifier(); @@ -285,9 +288,6 @@ public class DownloadRequest implements Parcelable { } public DownloadRequest build() { - Validate.notNull(destination); - Validate.notNull(source); - Validate.notNull(title); return new DownloadRequest(this); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java index e7b226eca..d69228ceb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -14,20 +14,19 @@ import android.media.MediaMetadataRetriever; import android.os.Binder; import android.os.Handler; import android.os.IBinder; +import android.support.annotation.NonNull; import android.support.v4.app.NotificationCompat; import android.support.v4.util.Pair; +import android.text.TextUtils; 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.net.HttpURLConnection; import java.util.ArrayList; import java.util.Collections; import java.util.Date; @@ -54,7 +53,8 @@ import javax.xml.parsers.ParserConfigurationException; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.event.DownloadEvent; +import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; @@ -72,9 +72,9 @@ import de.danoeh.antennapod.core.storage.DownloadRequester; import de.danoeh.antennapod.core.syndication.handler.FeedHandler; import de.danoeh.antennapod.core.syndication.handler.FeedHandlerResult; import de.danoeh.antennapod.core.syndication.handler.UnsupportedFeedtypeException; -import de.danoeh.antennapod.core.util.ChapterUtils; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.InvalidFeedException; +import de.greenrobot.event.EventBus; /** * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent. @@ -103,22 +103,11 @@ public class DownloadService extends Service { 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.core.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; @@ -136,7 +125,6 @@ public class DownloadService extends Service { private NotificationCompat.Builder notificationCompatBuilder; - private Notification.BigTextStyle notificationBuilder; private int NOTIFICATION_ID = 2; private int REPORT_ID = 3; @@ -162,6 +150,8 @@ public class DownloadService extends Service { private static final int SCHED_EX_POOL_SIZE = 1; private ScheduledThreadPoolExecutor schedExecutor; + private Handler postHandler = new Handler(); + private final IBinder mBinder = new LocalBinder(); public class LocalBinder extends Binder { @@ -171,7 +161,7 @@ public class DownloadService extends Service { } private Thread downloadCompletionThread = new Thread() { - private static final String TAG = "downloadCompletionThread"; + private static final String TAG = "downloadCompletionThd"; @Override public void run() { @@ -187,10 +177,7 @@ public class DownloadService extends Service { 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()); + handleCompletedFeedDownload(downloader.getDownloadRequest()); } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { handleCompletedFeedMediaDownload(status, downloader.getDownloadRequest()); } @@ -200,7 +187,7 @@ public class DownloadService extends Service { 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) { + && Integer.valueOf(status.getReasonDetailed()) == 416) { Log.d(TAG, "Requested invalid range, restarting download from the beginning"); FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination())); @@ -209,9 +196,32 @@ public class DownloadService extends Service { Log.e(TAG, "Download failed"); saveDownloadStatus(status); handleFailedDownload(status, downloader.getDownloadRequest()); + + if(type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + long id = status.getFeedfileId(); + FeedMedia media = DBReader.getFeedMedia(id); + if(media == null || media.getItem() == null) { + return; + } + FeedItem item = media.getItem(); + boolean httpNotFound = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR + && String.valueOf(HttpURLConnection.HTTP_NOT_FOUND).equals(status.getReasonDetailed()); + boolean notEnoughSpace = status.getReason() == DownloadError.ERROR_NOT_ENOUGH_SPACE; + if (httpNotFound || notEnoughSpace) { + DBWriter.saveFeedItemAutoDownloadFailed(item).get(); + } + // to make lists reload the failed item, we fake an item update + EventBus.getDefault().post(FeedItemEvent.updated(item)); + } + } + } else { + // if FeedMedia download has been canceled, fake FeedItem update + // so that lists reload that it + if(status.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + FeedMedia media = DBReader.getFeedMedia(status.getFeedfileId()); + EventBus.getDefault().post(FeedItemEvent.updated(media.getItem())); } } - sendDownloadHandledIntent(); queryDownloadsAsync(); } } catch (InterruptedException e) { @@ -241,9 +251,8 @@ public class DownloadService extends Service { 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>(); + downloads = Collections.synchronizedList(new ArrayList<Downloader>()); numberOfDownloads = new AtomicInteger(0); IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); @@ -309,10 +318,14 @@ public class DownloadService extends Service { Log.d(TAG, "Service shutting down"); isRunning = false; - if (ClientConfig.downloadServiceCallbacks.shouldCreateReport()) { + if (ClientConfig.downloadServiceCallbacks.shouldCreateReport() && + UserPreferences.showDownloadReport()) { updateReport(); } + postHandler.removeCallbacks(postDownloaderTask); + EventBus.getDefault().postSticky(DownloadEvent.refresh(Collections.emptyList())); + stopForeground(true); NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); nm.cancel(NOTIFICATION_ID); @@ -324,29 +337,20 @@ public class DownloadService extends Service { cancelNotificationUpdater(); unregisterReceiver(cancelDownloadReceiver); - if (!newMediaFiles.isEmpty()) { - DBTasks.autodownloadUndownloadedItems(getApplicationContext(), - ArrayUtils.toPrimitive(newMediaFiles.toArray(new Long[newMediaFiles.size()]))); - } + // start auto download in case anything new has shown up + DBTasks.autodownloadUndownloadedItems(getApplicationContext()); } - @SuppressLint("NewApi") private void setupNotificationBuilders() { 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(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this)).setLargeIcon(icon) - .setSmallIcon(R.drawable.stat_notify_sync) - ); - } else { notificationCompatBuilder = new NotificationCompat.Builder(this) - .setOngoing(true).setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this)) + .setOngoing(true) + .setContentIntent(ClientConfig.downloadServiceCallbacks.getNotificationContentIntent(this)) .setLargeIcon(icon) .setSmallIcon(R.drawable.stat_notify_sync); - } + Log.d(TAG, "Notification set up"); } @@ -354,58 +358,48 @@ public class DownloadService extends Service { * 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); + downloadsLeft = getResources() + .getQuantityString(R.plurals.downloads_left, numDownloads, numDownloads); } 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()); + if (notificationCompatBuilder != 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"); } - } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { - if (request.getTitle() != null) { - if (i > 0) { - bigText.append("\n"); - } - bigText.append("\u2022 " + request.getTitle() - + " (" + request.getProgressPercent() - + "%)"); + 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(); + notificationCompatBuilder.setContentTitle(contentTitle); + notificationCompatBuilder.setContentText(downloadsLeft); + if (bigText != null) { + notificationCompatBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(bigText.toString())); } + return notificationCompatBuilder.build(); } return null; } @@ -423,9 +417,11 @@ public class DownloadService extends Service { @Override public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) { + if (TextUtils.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(url == null) { + throw new IllegalArgumentException("ACTION_CANCEL_DOWNLOAD intent needs download url extra"); + } Log.d(TAG, "Cancelling download with url " + url); Downloader d = getDownloader(url); @@ -434,14 +430,14 @@ public class DownloadService extends Service { } else { Log.e(TAG, "Could not cancel download with url " + url); } + postDownloaders(); - } else if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) { + } else if (TextUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) { for (Downloader d : downloads) { d.cancel(); Log.d(TAG, "Cancelled all downloads"); } - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); - + postDownloaders(); } queryDownloads(); } @@ -460,13 +456,14 @@ public class DownloadService extends Service { if (downloader != null) { numberOfDownloads.incrementAndGet(); // smaller rss feeds before bigger media files - if(request.getFeedfileId() == Feed.FEEDFILETYPE_FEED) { + if(request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { downloads.add(0, downloader); } else { downloads.add(downloader); } downloadExecutor.submit(downloader); - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + + postDownloaders(); } queryDownloads(); @@ -497,7 +494,7 @@ public class DownloadService extends Service { boolean rc = downloads.remove(d); Log.d(TAG, "Result of downloads.remove: " + rc); DownloadRequester.getInstance().removeDownload(d.getDownloadRequest()); - sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + postDownloaders(); } }); } @@ -510,11 +507,7 @@ public class DownloadService extends Service { */ private void saveDownloadStatus(DownloadStatus status) { reportQueue.add(status); - DBWriter.addDownloadStatus(this, status); - } - - private void sendDownloadHandledIntent() { - EventDistributor.getInstance().sendDownloadHandledBroadcast(); + DBWriter.addDownloadStatus(status); } /** @@ -633,14 +626,6 @@ public class DownloadService extends Service { } /** - * Is called whenever a Feed-Image is downloaded - */ - private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) { - 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) { @@ -774,52 +759,8 @@ public class DownloadService extends Service { for (int i = 0; i < savedFeeds.length; i++) { Feed savedFeed = savedFeeds[i]; - // Download Feed Image if provided and not downloaded - if (savedFeed.getImage() != null - && savedFeed.getImage().isDownloaded() == false) { - 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.getPubDate() == null) { - Log.d(TAG, item.toString()); - } - if(item.getImage() != null && item.getImage().isDownloaded() == false) { - item.getImage().setOwner(item); - try { - requester.downloadImage(DownloadService.this, - item.getImage()); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } - } - if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) { - newMediaFiles.add(item.getMedia().getId()); - } - } // If loadAllPages=true, check if another page is available and queue it for download - final boolean loadAllPages = results.get(i).first.getArguments().getBoolean(DownloadRequester.REQUEST_ARG_LOAD_ALL_PAGES); final Feed feed = results.get(i).second.feed; if (loadAllPages && feed.getNextPageLink() != null) { @@ -837,8 +778,6 @@ public class DownloadService extends Service { numberOfDownloads.decrementAndGet(); } - sendDownloadHandledIntent(); - queryDownloadsAsync(); } }); @@ -889,7 +828,7 @@ public class DownloadService extends Service { feed.setFile_url(request.getDestination()); feed.setId(request.getFeedfileId()); feed.setDownloaded(true); - feed.setPreferences(new FeedPreferences(0, true, + feed.setPreferences(new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, request.getUsername(), request.getPassword())); feed.setPageNr(request.getArguments().getInt(DownloadRequester.REQUEST_ARG_PAGE_NR, 0)); @@ -938,7 +877,7 @@ public class DownloadService extends Service { if (successful) { // we create a 'successful' download log if the feed's last refresh failed - List<DownloadStatus> log = DBReader.getFeedDownloadLog(DownloadService.this, feed); + List<DownloadStatus> log = DBReader.getFeedDownloadLog(feed); if(log.size() > 0 && log.get(0).isSuccessful() == false) { saveDownloadStatus(new DownloadStatus(feed, feed.getHumanReadableIdentifier(), DownloadError.SUCCESS, successful, @@ -982,7 +921,7 @@ public class DownloadService extends Service { 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())) { + if (TextUtils.equals(item1.getImage().getDownload_url(), item2.getImage().getDownload_url())) { item2.setImage(null); } } @@ -1061,17 +1000,17 @@ public class DownloadService extends Service { @Override public void run() { if(request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { - DBWriter.setFeedLastUpdateFailed(DownloadService.this, request.getFeedfileId(), true); + DBWriter.setFeedLastUpdateFailed(request.getFeedfileId(), true); } else if (request.isDeleteOnFailure()) { 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()); + FeedMedia media = DBReader.getFeedMedia(request.getFeedfileId()); media.setFile_url(request.getDestination()); try { - DBWriter.setFeedMedia(DownloadService.this, media).get(); + DBWriter.setFeedMedia(media).get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { @@ -1083,40 +1022,6 @@ public class DownloadService extends Service { } /** - * 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 { @@ -1124,25 +1029,22 @@ public class DownloadService extends Service { private DownloadRequest request; private DownloadStatus status; - public MediaHandlerThread(DownloadStatus status, DownloadRequest request) { - Validate.notNull(status); - Validate.notNull(request); - + public MediaHandlerThread(@NonNull DownloadStatus status, + @NonNull DownloadRequest request) { this.status = status; this.request = request; } @Override public void run() { - FeedMedia media = DBReader.getFeedMedia(DownloadService.this, - request.getFeedfileId()); + FeedMedia media = DBReader.getFeedMedia(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()); + media.setHasEmbeddedPicture(null); // Get duration MediaMetadataRetriever mmr = null; @@ -1162,20 +1064,19 @@ public class DownloadService extends Service { } } - if (media.getItem().getChapters() == null) { - ChapterUtils.loadChaptersFromFileUrl(media); - if (media.getItem().getChapters() != null) { - chaptersRead = true; - } - } + final FeedItem item = media.getItem(); try { - if (chaptersRead) { - DBWriter.setFeedItem(DownloadService.this, media.getItem()).get(); + // we've received the media, we don't want to autodownload it again + if(item != null) { + item.setAutoDownload(false); + DBWriter.setFeedItem(item).get(); } - DBWriter.setFeedMedia(DownloadService.this, media).get(); - if (!DBTasks.isInQueue(DownloadService.this, media.getItem().getId())) { - DBWriter.addQueueItem(DownloadService.this, media.getItem().getId()).get(); + + DBWriter.setFeedMedia(media).get(); + + if (item != null && !DBTasks.isInQueue(DownloadService.this, item.getId())) { + DBWriter.addQueueItem(DownloadService.this, item).get(); } } catch (ExecutionException e) { e.printStackTrace(); @@ -1186,15 +1087,13 @@ public class DownloadService extends Service { } saveDownloadStatus(status); - sendDownloadHandledIntent(); - - if(GpodnetPreferences.loggedIn()) { - FeedItem item = media.getItem(); - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.DOWNLOAD) - .currentDeviceId() - .currentTimestamp() - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); + + if(GpodnetPreferences.loggedIn() && item != null) { + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.DOWNLOAD) + .currentDeviceId() + .currentTimestamp() + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); } numberOfDownloads.decrementAndGet(); @@ -1239,8 +1138,25 @@ public class DownloadService extends Service { } } - public List<Downloader> getDownloads() { - return downloads; + + private long lastPost = 0; + + final Runnable postDownloaderTask = new Runnable() { + @Override + public void run() { + List<Downloader> list = Collections.unmodifiableList(downloads); + EventBus.getDefault().postSticky(DownloadEvent.refresh(list)); + postHandler.postDelayed(postDownloaderTask, 1500); + } + }; + + private void postDownloaders() { + long now = System.currentTimeMillis(); + if(now - lastPost >= 250) { + postHandler.removeCallbacks(postDownloaderTask); + postDownloaderTask.run(); + lastPost = now; + } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java index d05650d10..ed2b00dfe 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java @@ -1,12 +1,14 @@ package de.danoeh.antennapod.core.service.download; -import org.apache.commons.lang3.Validate; +import android.database.Cursor; +import android.support.annotation.NonNull; + +import java.util.Date; import de.danoeh.antennapod.core.feed.FeedFile; +import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.DownloadError; -import java.util.Date; - /** Contains status attributes for one download */ public class DownloadStatus { /** @@ -59,10 +61,8 @@ public class DownloadStatus { this.feedfileType = feedfileType; } - public DownloadStatus(DownloadRequest request, DownloadError reason, + public DownloadStatus(@NonNull 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(); @@ -74,10 +74,8 @@ public class DownloadStatus { } /** Constructor for creating new completed downloads. */ - public DownloadStatus(FeedFile feedfile, String title, DownloadError reason, - boolean successful, String reasonDetailed) { - Validate.notNull(feedfile); - + public DownloadStatus(@NonNull FeedFile feedfile, String title, DownloadError reason, + boolean successful, String reasonDetailed) { this.title = title; this.done = true; this.feedfileId = feedfile.getId(); @@ -101,6 +99,30 @@ public class DownloadStatus { this.reasonDetailed = reasonDetailed; } + public static DownloadStatus fromCursor(Cursor cursor) { + int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); + int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE); + int indexFeedFile = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILE); + int indexFileFileType = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDFILETYPE); + int indexSuccessful = cursor.getColumnIndex(PodDBAdapter.KEY_SUCCESSFUL); + int indexReason = cursor.getColumnIndex(PodDBAdapter.KEY_REASON); + int indexCompletionDate = cursor.getColumnIndex(PodDBAdapter.KEY_COMPLETION_DATE); + int indexReasonDetailed = cursor.getColumnIndex(PodDBAdapter.KEY_REASON_DETAILED); + + long id = cursor.getLong(indexId); + String title = cursor.getString(indexTitle); + long feedfileId = cursor.getLong(indexFeedFile); + int feedfileType = cursor.getInt(indexFileFileType); + boolean successful = cursor.getInt(indexSuccessful) > 0; + int reason = cursor.getInt(indexReason); + Date completionDate = new Date(cursor.getLong(indexCompletionDate)); + String reasonDetailed = cursor.getString(indexReasonDetailed); + + return new DownloadStatus(id, title, feedfileId, + feedfileType, successful, DownloadError.fromCode(reason), completionDate, + reasonDetailed); + } + @Override public String toString() { return "DownloadStatus [id=" + id + ", title=" + title + ", reason=" diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index ac0fe8036..0b9fba6f7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -1,16 +1,16 @@ package de.danoeh.antennapod.core.service.download; +import android.text.TextUtils; import android.util.Log; import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Protocol; import com.squareup.okhttp.Request; import com.squareup.okhttp.Response; import com.squareup.okhttp.ResponseBody; import com.squareup.okhttp.internal.http.HttpDate; import org.apache.commons.io.IOUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.http.HttpStatus; import java.io.BufferedInputStream; import java.io.File; @@ -22,6 +22,7 @@ import java.net.HttpURLConnection; import java.net.SocketTimeoutException; import java.net.URI; import java.net.UnknownHostException; +import java.util.Arrays; import java.util.Date; import de.danoeh.antennapod.core.ClientConfig; @@ -84,7 +85,7 @@ public class HttpDownloader extends Downloader { String credentials = encodeCredentials(parts[0], parts[1], "ISO-8859-1"); httpReq.header("Authorization", credentials); } - } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { + } else if (!TextUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { String credentials = encodeCredentials(request.getUsername(), request.getPassword(), "ISO-8859-1"); httpReq.header("Authorization", credentials); @@ -93,15 +94,29 @@ public class HttpDownloader extends Downloader { // add range header if necessary if (fileExists) { request.setSoFar(destination.length()); - httpReq.addHeader("Range", - "bytes=" + request.getSoFar() + "-"); + httpReq.addHeader("Range", "bytes=" + request.getSoFar() + "-"); Log.d(TAG, "Adding range header: " + request.getSoFar()); } - Response response = httpClient.newCall(httpReq.build()).execute(); + Response response = null; + try { + response = httpClient.newCall(httpReq.build()).execute(); + } catch(IOException e) { + Log.e(TAG, e.toString()); + if(e.getMessage().contains("PROTOCOL_ERROR")) { + httpClient.setProtocols(Arrays.asList(Protocol.HTTP_1_1)); + response = httpClient.newCall(httpReq.build()).execute(); + } + else { + throw e; + } + } responseBody = response.body(); String contentEncodingHeader = response.header("Content-Encoding"); - boolean isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip"); + boolean isGzip = false; + if(!TextUtils.isEmpty(contentEncodingHeader)) { + isGzip = TextUtils.equals(contentEncodingHeader.toLowerCase(), "gzip"); + } Log.d(TAG, "Response code is " + response.code()); @@ -113,7 +128,7 @@ public class HttpDownloader extends Downloader { String credentials = encodeCredentials(parts[0], parts[1], "UTF-8"); httpReq.header("Authorization", credentials); } - } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { + } else if (!TextUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { String credentials = encodeCredentials(request.getUsername(), request.getPassword(), "UTF-8"); httpReq.header("Authorization", credentials); @@ -121,7 +136,9 @@ public class HttpDownloader extends Downloader { response = httpClient.newCall(httpReq.build()).execute(); responseBody = response.body(); contentEncodingHeader = response.header("Content-Encoding"); - isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip"); + if(!TextUtils.isEmpty(contentEncodingHeader)) { + isGzip = TextUtils.equals(contentEncodingHeader.toLowerCase(), "gzip"); + } } if(!response.isSuccessful() && response.code() == HttpURLConnection.HTTP_NOT_MODIFIED) { @@ -144,7 +161,7 @@ public class HttpDownloader extends Downloader { return; } - if (!StorageUtils.storageAvailable(ClientConfig.applicationCallbacks.getApplicationInstance())) { + if (!StorageUtils.storageAvailable()) { onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null); return; } @@ -153,8 +170,8 @@ public class HttpDownloader extends Downloader { String contentRangeHeader = (fileExists) ? response.header("Content-Range") : null; - if (fileExists && response.code() == HttpStatus.SC_PARTIAL_CONTENT - && !StringUtils.isEmpty(contentRangeHeader)) { + if (fileExists && response.code() == HttpURLConnection.HTTP_PARTIAL + && !TextUtils.isEmpty(contentRangeHeader)) { String start = contentRangeHeader.substring("bytes ".length(), contentRangeHeader.indexOf("-")); request.setSoFar(Long.valueOf(start)); @@ -188,13 +205,17 @@ public class HttpDownloader extends Downloader { } 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)); + try { + 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)); + } + } catch(IOException e) { + Log.e(TAG, Log.getStackTraceString(e)); } if (cancelled) { onCancelled(); @@ -210,6 +231,9 @@ public class HttpDownloader extends Downloader { request.getSize() ); return; + } else if(request.getSize() > 0 && request.getSoFar() == 0){ + onFail(DownloadError.ERROR_IO_ERROR, "Download completed, but nothing was read"); + return; } onSuccess(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/MediaButtonIntentReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/MediaButtonIntentReceiver.java new file mode 100644 index 000000000..7d06390f2 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/MediaButtonIntentReceiver.java @@ -0,0 +1,26 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; + +public class MediaButtonIntentReceiver extends BroadcastReceiver { + + private static final String TAG = "MediaButtonIntentRcver"; + + private static PlaybackServiceMediaPlayer mMediaPlayer; + + public static void setMediaPlayer(PlaybackServiceMediaPlayer mediaPlayer) { + mMediaPlayer = mediaPlayer; + } + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "onReceive(Context, " + intent.toString() +")"); + if (mMediaPlayer != null && Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) { + mMediaPlayer.handleMediaKey(intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT)); + } + } + +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index 3f6769ee4..2be075a92 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -1,11 +1,11 @@ package de.danoeh.antennapod.core.service.playback; -import android.annotation.SuppressLint; import android.app.Notification; +import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.bluetooth.BluetoothA2dp; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -13,27 +13,22 @@ 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.os.Vibrator; import android.preference.PreferenceManager; -import android.support.v4.app.NotificationCompat; +import android.support.v7.app.NotificationCompat; +import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.KeyEvent; import android.view.SurfaceHolder; import android.widget.Toast; -import com.squareup.picasso.Picasso; +import com.bumptech.glide.Glide; -import org.apache.commons.lang3.StringUtils; - -import java.io.IOException; import java.util.List; import de.danoeh.antennapod.core.ClientConfig; @@ -42,6 +37,7 @@ import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; @@ -50,6 +46,7 @@ import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.IntList; import de.danoeh.antennapod.core.util.QueueAccess; import de.danoeh.antennapod.core.util.flattr.FlattrUtils; import de.danoeh.antennapod.core.util.playback.Playable; @@ -165,7 +162,6 @@ public class PlaybackService extends Service { private static final int NOTIFICATION_ID = 1; - private RemoteControlClient remoteControlClient; private PlaybackServiceMediaPlayer mediaPlayer; private PlaybackServiceTaskManager taskManager; @@ -213,7 +209,6 @@ public class PlaybackService extends Service { return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt); } - @SuppressLint("NewApi") @Override public void onCreate() { super.onCreate(); @@ -224,8 +219,10 @@ public class PlaybackService extends Service { Intent.ACTION_HEADSET_PLUG)); registerReceiver(shutdownReceiver, new IntentFilter( ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - registerReceiver(bluetoothStateUpdated, new IntentFilter( - AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + registerReceiver(bluetoothStateUpdated, new IntentFilter( + BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)); + } registerReceiver(audioBecomingNoisy, new IntentFilter( AudioManager.ACTION_AUDIO_BECOMING_NOISY)); registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( @@ -234,13 +231,11 @@ public class PlaybackService extends Service { ACTION_PAUSE_PLAY_CURRENT_EPISODE)); registerReceiver(pauseResumeCurrentEpisodeReceiver, new IntentFilter( ACTION_RESUME_PLAY_CURRENT_EPISODE)); - remoteControlClient = setupRemoteControlClient(); taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback); } - @SuppressLint("NewApi") @Override public void onDestroy() { super.onDestroy(); @@ -251,7 +246,9 @@ public class PlaybackService extends Service { unregisterReceiver(headsetDisconnected); unregisterReceiver(shutdownReceiver); - unregisterReceiver(bluetoothStateUpdated); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + unregisterReceiver(bluetoothStateUpdated); + } unregisterReceiver(audioBecomingNoisy); unregisterReceiver(skipCurrentEpisodeReceiver); unregisterReceiver(pausePlayCurrentEpisodeReceiver); @@ -345,6 +342,8 @@ public class PlaybackService extends Service { break; case KeyEvent.KEYCODE_MEDIA_NEXT: + mediaPlayer.endPlayback(true); + break; case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: mediaPlayer.seekDelta(UserPreferences.getFastFowardSecs() * 1000); break; @@ -398,11 +397,27 @@ public class PlaybackService extends Service { } @Override + public void onSleepTimerAlmostExpired() { + float leftVolume = 0.1f * UserPreferences.getLeftVolume(); + float rightVolume = 0.1f * UserPreferences.getRightVolume(); + mediaPlayer.setVolume(leftVolume, rightVolume); + } + + @Override public void onSleepTimerExpired() { mediaPlayer.pause(true, true); + float leftVolume = UserPreferences.getLeftVolume(); + float rightVolume = UserPreferences.getRightVolume(); + mediaPlayer.setVolume(leftVolume, rightVolume); sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); } + @Override + public void onSleepTimerReset() { + float leftVolume = UserPreferences.getLeftVolume(); + float rightVolume = UserPreferences.getRightVolume(); + mediaPlayer.setVolume(leftVolume, rightVolume); + } @Override public void onWidgetUpdaterTick() { @@ -442,7 +457,7 @@ public class PlaybackService extends Service { } writePlayerStatusPlaybackPreferences(); - final Playable playable = mediaPlayer.getPSMPInfo().playable; + final Playable playable = newInfo.playable; // Gpodder: send play action if(GpodnetPreferences.loggedIn() && playable instanceof FeedMedia) { @@ -486,7 +501,6 @@ public class PlaybackService extends Service { // statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal()); sendBroadcast(statusUpdate); updateWidget(); - refreshRemoteControlClientState(newInfo); bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); } @@ -523,9 +537,9 @@ public class PlaybackService extends Service { @Override public boolean onMediaPlayerError(Object inObj, int what, int extra) { - final String TAG = "PlaybackService.onErrorListener"; + final String TAG = "PlaybackSvc.onErrorLtsn"; Log.w(TAG, "An error has occured: " + what + " " + extra); - if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) { + if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) { mediaPlayer.pause(true, false); } sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); @@ -535,21 +549,16 @@ public class PlaybackService extends Service { } @Override - public boolean endPlayback(boolean playNextEpisode) { - PlaybackService.this.endPlayback(true); + public boolean endPlayback(boolean playNextEpisode, boolean wasSkipped) { + PlaybackService.this.endPlayback(playNextEpisode, wasSkipped); return true; } - - @Override - public RemoteControlClient getRemoteControlClient() { - return remoteControlClient; - } }; - private void endPlayback(boolean playNextEpisode) { + private void endPlayback(boolean playNextEpisode, boolean wasSkipped) { Log.d(TAG, "Playback ended"); - final Playable playable = mediaPlayer.getPSMPInfo().playable; + final Playable playable = mediaPlayer.getPlayable(); if (playable == null) { Log.e(TAG, "Cannot end playback: media was null"); return; @@ -563,20 +572,28 @@ public class PlaybackService extends Service { if (playable instanceof FeedMedia) { FeedMedia media = (FeedMedia) playable; FeedItem item = media.getItem(); - DBWriter.markItemRead(PlaybackService.this, item, true, true); try { final List<FeedItem> queue = taskManager.getQueue(); isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId()); - nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); + nextItem = DBTasks.getQueueSuccessorOfItem(item.getId(), queue); } catch (InterruptedException e) { e.printStackTrace(); // isInQueue remains false } - if (isInQueue) { - DBWriter.removeQueueItem(PlaybackService.this, item, true); + + boolean shouldKeep = wasSkipped && UserPreferences.shouldSkipKeepEpisode(); + + if (!shouldKeep) { + // only mark the item as played if we're not keeping it anyways + DBWriter.markItemPlayed(item, FeedItem.PLAYED, true); + + if (isInQueue) { + DBWriter.removeQueueItem(PlaybackService.this, item, true); + } } - DBWriter.addItemToPlaybackHistory(PlaybackService.this, media); + + DBWriter.addItemToPlaybackHistory(media); // auto-flattr if enabled if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { @@ -584,7 +601,7 @@ public class PlaybackService extends Service { } // Delete episode if enabled - if(UserPreferences.isAutoDelete()) { + if(item.getFeed().getPreferences().getCurrentAutoDelete() && !shouldKeep ) { DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId()); Log.d(TAG, "Episode Deleted"); } @@ -634,7 +651,7 @@ public class PlaybackService extends Service { writePlaybackPreferencesNoMediaPlaying(); if (nextMedia != null) { - stream = !playable.localFileAvailable(); + stream = !nextMedia.localFileAvailable(); mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); @@ -645,10 +662,9 @@ public class PlaybackService extends Service { } } - public void setSleepTimer(long waitingTime) { - Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) - + " milliseconds"); - taskManager.setSleepTimer(waitingTime); + public void setSleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) { + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds"); + taskManager.setSleepTimer(waitingTime, shakeToReset, vibrate); sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); } @@ -744,8 +760,7 @@ public class PlaybackService extends Service { SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()).edit(); - PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); - int playerStatus = getCurrentPlayerStatusAsInt(info.playerStatus); + int playerStatus = getCurrentPlayerStatusAsInt(mediaPlayer.getPlayerStatus()); editor.putInt( PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus); @@ -770,151 +785,158 @@ public class PlaybackService extends Service { /** * Used by setupNotification to load notification data in another thread. */ - private AsyncTask<Void, Void, Void> notificationSetupTask; + private Thread notificationSetupThread; /** * 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); + if (notificationSetupThread != null) { + notificationSetupThread.interrupt(); } - notificationSetupTask = new AsyncTask<Void, Void, Void>() { + Runnable notificationSetupTask = new Runnable() { Bitmap icon = null; @Override - protected Void doInBackground(Void... params) { + public void run() { Log.d(TAG, "Starting background work"); if (android.os.Build.VERSION.SDK_INT >= 11) { if (info.playable != null) { + int iconSize = getResources().getDimensionPixelSize( + android.R.dimen.notification_large_icon_width); try { - int iconSize = getResources().getDimensionPixelSize( - android.R.dimen.notification_large_icon_width); - icon = Picasso.with(PlaybackService.this) + icon = Glide.with(PlaybackService.this) .load(info.playable.getImageUri()) - .resize(iconSize, iconSize) + .asBitmap() + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .centerCrop() + .into(iconSize, iconSize) .get(); - } catch (IOException e) { - e.printStackTrace(); + } catch(Throwable tr) { + Log.e(TAG, Log.getStackTraceString(tr)); } } - } if (icon == null) { icon = BitmapFactory.decodeResource(getApplicationContext().getResources(), ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext())); } - return null; - } - - @Override - protected void onPostExecute(Void result) { - super.onPostExecute(result); if (mediaPlayer == null) { return; } - PlaybackServiceMediaPlayer.PSMPInfo newInfo = mediaPlayer.getPSMPInfo(); + PlayerStatus playerStatus = mediaPlayer.getPlayerStatus(); final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext()); - if (!isCancelled() && - started && - info.playable != null) { - String contentText = info.playable.getFeedTitle(); - String contentTitle = info.playable.getEpisodeTitle(); + if (!Thread.currentThread().isInterrupted() && started && info.playable != null) { + String contentText = info.playable.getEpisodeTitle(); + String contentTitle = info.playable.getFeedTitle(); 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(smallIcon) - .setPriority(UserPreferences.getNotifyPriority()); // set notification priority - if (newInfo.playerStatus == PlayerStatus.PLAYING) { - notificationBuilder.addAction(android.R.drawable.ic_media_pause, //pause action - getString(R.string.pause_label), - pauseButtonPendingIntent); - } else { - notificationBuilder.addAction(android.R.drawable.ic_media_play, //play action - getString(R.string.play_label), - playButtonPendingIntent); - } - if (UserPreferences.isPersistNotify()) { - notificationBuilder.addAction(android.R.drawable.ic_menu_close_clear_cancel, // stop action - getString(R.string.stop_label), - stopButtonPendingIntent); - } - if (Build.VERSION.SDK_INT >= 21) { - notificationBuilder.setStyle(new Notification.MediaStyle() - .setMediaSession((android.media.session.MediaSession.Token) mediaPlayer.getSessionToken().getToken()) - .setShowActionsInCompactView(0)) - .setVisibility(Notification.VISIBILITY_PUBLIC) - .setColor(Notification.COLOR_DEFAULT); - } + // Builder is v7, even if some not overwritten methods return its parent's v4 interface + NotificationCompat.Builder notificationBuilder = (NotificationCompat.Builder) new NotificationCompat.Builder( + PlaybackService.this) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setOngoing(false) + .setContentIntent(pIntent) + .setLargeIcon(icon) + .setSmallIcon(smallIcon) + .setWhen(0) // we don't need the time + .setPriority(UserPreferences.getNotifyPriority()); // set notification priority + IntList compactActionList = new IntList(); + + + int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction + + // always let them rewind + PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction( + KeyEvent.KEYCODE_MEDIA_REWIND, numActions); + notificationBuilder.addAction(android.R.drawable.ic_media_rew, + getString(R.string.rewind_label), + rewindButtonPendingIntent); + numActions++; + + if (playerStatus == PlayerStatus.PLAYING) { + PendingIntent pauseButtonPendingIntent = getPendingIntentForMediaAction( + KeyEvent.KEYCODE_MEDIA_PAUSE, numActions); + notificationBuilder.addAction(android.R.drawable.ic_media_pause, //pause action + getString(R.string.pause_label), + pauseButtonPendingIntent); + compactActionList.add(numActions++); + } else { + PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction( + KeyEvent.KEYCODE_MEDIA_PLAY, numActions); + notificationBuilder.addAction(android.R.drawable.ic_media_play, //play action + getString(R.string.play_label), + playButtonPendingIntent); + compactActionList.add(numActions++); + } - notification = notificationBuilder.build(); + // ff follows play, then we have skip (if it's present) + PendingIntent ffButtonPendingIntent = getPendingIntentForMediaAction( + KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions); + notificationBuilder.addAction(android.R.drawable.ic_media_ff, + getString(R.string.fast_forward_label), + ffButtonPendingIntent); + numActions++; + + if (UserPreferences.isFollowQueue()) { + PendingIntent skipButtonPendingIntent = getPendingIntentForMediaAction( + KeyEvent.KEYCODE_MEDIA_NEXT, numActions); + notificationBuilder.addAction(android.R.drawable.ic_media_next, + getString(R.string.skip_episode_label), + skipButtonPendingIntent); + compactActionList.add(numActions++); + } + + PendingIntent stopButtonPendingIntent = getPendingIntentForMediaAction( + KeyEvent.KEYCODE_MEDIA_STOP, numActions); + notificationBuilder.setStyle(new android.support.v7.app.NotificationCompat.MediaStyle() + .setMediaSession(mediaPlayer.getSessionToken()) + .setShowActionsInCompactView(compactActionList.toArray()) + .setShowCancelButton(true) + .setCancelButtonIntent(stopButtonPendingIntent)) + .setVisibility(Notification.VISIBILITY_PUBLIC) + .setColor(Notification.COLOR_DEFAULT); + + notification = notificationBuilder.build(); + + if (playerStatus == PlayerStatus.PLAYING || + playerStatus == PlayerStatus.PREPARING || + playerStatus == PlayerStatus.SEEKING) { + startForeground(NOTIFICATION_ID, notification); } else { - NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( - PlaybackService.this) - .setContentTitle(contentTitle) - .setContentText(contentText).setOngoing(true) - .setContentIntent(pIntent).setLargeIcon(icon) - .setSmallIcon(smallIcon); - notification = notificationBuilder.build(); + stopForeground(false); + NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + mNotificationManager.notify(NOTIFICATION_ID, notification); } - startForeground(NOTIFICATION_ID, notification); 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(); - } + notificationSetupThread = new Thread(notificationSetupTask); + notificationSetupThread.start(); + } + private PendingIntent getPendingIntentForMediaAction(int keycodeValue, int requestCode) { + Intent intent = new Intent( + PlaybackService.this, PlaybackService.class); + intent.putExtra( + MediaButtonReceiver.EXTRA_KEYCODE, + keycodeValue); + return PendingIntent + .getService(PlaybackService.this, requestCode, + intent, + PendingIntent.FLAG_UPDATE_CURRENT); } /** - * Saves the current position of the media file to the DB + * Persists the current position and last played time of the media file. * * @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. @@ -923,7 +945,7 @@ public class PlaybackService extends Service { int position = getCurrentPosition(); int duration = getDuration(); float playbackSpeed = getCurrentPlaybackSpeed(); - final Playable playable = mediaPlayer.getPSMPInfo().playable; + final Playable playable = mediaPlayer.getPlayable(); if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { Log.d(TAG, "Saving current position to " + position); if (updatePlayedDuration && playable instanceof FeedMedia) { @@ -939,8 +961,9 @@ public class PlaybackService extends Service { } } playable.saveCurrentPosition(PreferenceManager - .getDefaultSharedPreferences(getApplicationContext()), - position + .getDefaultSharedPreferences(getApplicationContext()), + position, + System.currentTimeMillis() ); } } @@ -963,74 +986,6 @@ public class PlaybackService extends Service { 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(); - } - Log.d(TAG, "RemoteControlClient state was refreshed"); - } - } - } - private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) { boolean isPlaying = false; @@ -1059,14 +1014,14 @@ public class PlaybackService extends Service { * Pauses playback when the headset is disconnected and the preference is * set */ - private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { + private final BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { private static final String TAG = "headsetDisconnected"; private static final int UNPLUGGED = 0; private static final int PLUGGED = 1; @Override public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { + if (TextUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { int state = intent.getIntExtra("state", -1); if (state != -1) { Log.d(TAG, "Headset plug event. State is " + state); @@ -1075,7 +1030,7 @@ public class PlaybackService extends Service { pauseIfPauseOnDisconnect(); } else if (state == PLUGGED) { Log.d(TAG, "Headset was plugged in during playback."); - unpauseIfPauseOnDisconnect(); + unpauseIfPauseOnDisconnect(false); } } else { Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); @@ -1084,21 +1039,22 @@ public class PlaybackService extends Service { } }; - private BroadcastReceiver bluetoothStateUpdated = new BroadcastReceiver() { + private final BroadcastReceiver bluetoothStateUpdated = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), AudioManager.ACTION_SCO_AUDIO_STATE_UPDATED)) { - int state = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_STATE, -1); - int prevState = intent.getIntExtra(AudioManager.EXTRA_SCO_AUDIO_PREVIOUS_STATE, -1); - if (state == AudioManager.SCO_AUDIO_STATE_CONNECTED) { - Log.d(TAG, "Received bluetooth connection intent"); - unpauseIfPauseOnDisconnect(); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { + if (TextUtils.equals(intent.getAction(), BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { + int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1); + if (state == BluetoothA2dp.STATE_CONNECTED) { + Log.d(TAG, "Received bluetooth connection intent"); + unpauseIfPauseOnDisconnect(true); + } } } } }; - private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { + private final BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -1125,50 +1081,60 @@ public class PlaybackService extends Service { } } - private void unpauseIfPauseOnDisconnect() { + /** + * @param bluetooth true if the event for unpausing came from bluetooth + */ + private void unpauseIfPauseOnDisconnect(boolean bluetooth) { if (transientPause) { transientPause = false; - if (UserPreferences.isPauseOnHeadsetDisconnect() && UserPreferences.isUnpauseOnHeadsetReconnect()) { + if (!bluetooth && UserPreferences.isUnpauseOnHeadsetReconnect()) { + mediaPlayer.resume(); + } else if (bluetooth && UserPreferences.isUnpauseOnBluetoothReconnect()){ + // let the user know we've started playback again... + Vibrator v = (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE); + if(v != null) { + v.vibrate(500); + } mediaPlayer.resume(); } } } - private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { + private final BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { + if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { stopSelf(); } } }; - private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { + private final BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { + if (TextUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); - mediaPlayer.endPlayback(); + mediaPlayer.endPlayback(true); } } }; - private BroadcastReceiver pauseResumeCurrentEpisodeReceiver = new BroadcastReceiver() { + private final BroadcastReceiver pauseResumeCurrentEpisodeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ACTION_RESUME_PLAY_CURRENT_EPISODE)) { + if (TextUtils.equals(intent.getAction(), ACTION_RESUME_PLAY_CURRENT_EPISODE)) { Log.d(TAG, "Received RESUME_PLAY_CURRENT_EPISODE intent"); mediaPlayer.resume(); } } }; - private BroadcastReceiver pausePlayCurrentEpisodeReceiver = new BroadcastReceiver() { + private final BroadcastReceiver pausePlayCurrentEpisodeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - if (StringUtils.equals(intent.getAction(), ACTION_PAUSE_PLAY_CURRENT_EPISODE)) { + if (TextUtils.equals(intent.getAction(), ACTION_PAUSE_PLAY_CURRENT_EPISODE)) { Log.d(TAG, "Received PAUSE_PLAY_CURRENT_EPISODE intent"); mediaPlayer.pause(false, false); } @@ -1200,25 +1166,35 @@ public class PlaybackService extends Service { } public PlayerStatus getStatus() { - return mediaPlayer.getPSMPInfo().playerStatus; + return mediaPlayer.getPlayerStatus(); } - public Playable getPlayable() { - return mediaPlayer.getPSMPInfo().playable; + public Playable getPlayable() { return mediaPlayer.getPlayable(); } + + public boolean canSetSpeed() { + return mediaPlayer.canSetSpeed(); } public void setSpeed(float speed) { mediaPlayer.setSpeed(speed); } - public boolean canSetSpeed() { - return mediaPlayer.canSetSpeed(); + public void setVolume(float leftVolume, float rightVolume) { + mediaPlayer.setVolume(leftVolume, rightVolume); } public float getCurrentPlaybackSpeed() { return mediaPlayer.getPlaybackSpeed(); } + public boolean canDownmix() { + return mediaPlayer.canDownmix(); + } + + public void setDownmix(boolean enable) { + mediaPlayer.setDownmix(enable); + } + public boolean isStartWhenPrepared() { return mediaPlayer.isStartWhenPrepared(); } @@ -1231,7 +1207,7 @@ public class PlaybackService extends Service { public void seekTo(final int t) { if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING && GpodnetPreferences.loggedIn()) { - final Playable playable = mediaPlayer.getPSMPInfo().playable; + final Playable playable = mediaPlayer.getPlayable(); if (playable instanceof FeedMedia) { FeedMedia media = (FeedMedia) playable; FeedItem item = media.getItem(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java index 243ee78e4..a82e82506 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java @@ -1,22 +1,32 @@ package de.danoeh.antennapod.core.service.playback; +import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.graphics.Bitmap; import android.media.AudioManager; -import android.media.RemoteControlClient; import android.net.wifi.WifiManager; import android.os.PowerManager; +import android.preference.PreferenceManager; +import android.support.annotation.NonNull; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.telephony.TelephonyManager; import android.util.Log; import android.util.Pair; +import android.view.Display; +import android.view.InputDevice; +import android.view.KeyEvent; import android.view.SurfaceHolder; +import android.view.WindowManager; -import org.apache.commons.lang3.Validate; +import com.bumptech.glide.Glide; import java.io.IOException; +import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.RejectedExecutionHandler; import java.util.concurrent.ThreadPoolExecutor; @@ -28,9 +38,10 @@ import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.RewindAfterPauseUtils; import de.danoeh.antennapod.core.util.playback.AudioPlayer; import de.danoeh.antennapod.core.util.playback.IPlayer; import de.danoeh.antennapod.core.util.playback.Playable; @@ -39,7 +50,7 @@ import de.danoeh.antennapod.core.util.playback.VideoPlayer; /** * Manages the MediaPlayer object of the PlaybackService. */ -public class PlaybackServiceMediaPlayer { +public class PlaybackServiceMediaPlayer implements SharedPreferences.OnSharedPreferenceChangeListener { public static final String TAG = "PlaybackSvcMediaPlayer"; /** @@ -69,6 +80,7 @@ public class PlaybackServiceMediaPlayer { * have to wait until these operations have finished. */ private final ReentrantLock playerLock; + private CountDownLatch seekLatch; private final PSMPCallback callback; private final Context context; @@ -80,10 +92,8 @@ public class PlaybackServiceMediaPlayer { */ private WifiManager.WifiLock wifiLock; - public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) { - Validate.notNull(context); - Validate.notNull(callback); - + public PlaybackServiceMediaPlayer(@NonNull Context context, + @NonNull PSMPCallback callback) { this.context = context; this.callback = callback; this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); @@ -98,9 +108,26 @@ public class PlaybackServiceMediaPlayer { } ); - mediaSession = new MediaSessionCompat(context, TAG); - mediaSession.setCallback(sessionCallback); - mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); + MediaButtonIntentReceiver.setMediaPlayer(this); + ComponentName eventReceiver = new ComponentName(context.getPackageName(), MediaButtonIntentReceiver.class.getName()); + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(eventReceiver); + PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(context, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT); + + mediaSession = new MediaSessionCompat(context, TAG, eventReceiver, buttonReceiverIntent); + + try { + mediaSession.setCallback(sessionCallback); + mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); + mediaSession.setActive(true); + } catch (NullPointerException npe) { + // on some devices (Huawei) setting active can cause a NullPointerException + // even with correct use of the api. + // See http://stackoverflow.com/questions/31556679/android-huawei-mediassessioncompat + // and https://plus.google.com/+IanLake/posts/YgdTkKFxz7d + Log.e(TAG, "NullPointerException while setting up MediaSession"); + npe.printStackTrace(); + } mediaPlayer = null; statusBeforeSeeking = null; @@ -108,6 +135,16 @@ public class PlaybackServiceMediaPlayer { mediaType = MediaType.UNKNOWN; playerStatus = PlayerStatus.STOPPED; videoSize = null; + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + prefs.registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { + if(key.equals(UserPreferences.PREF_LOCKSCREEN_BACKGROUND)) { + updateMediaSessionMetadata(); + } } /** @@ -136,9 +173,7 @@ public class PlaybackServiceMediaPlayer { * 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); - + public void playMediaObject(@NonNull final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { Log.d(TAG, "playMediaObject(...)"); executor.submit(new Runnable() { @Override @@ -164,8 +199,7 @@ public class PlaybackServiceMediaPlayer { * * @see #playMediaObject(de.danoeh.antennapod.core.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); + private void playMediaObject(@NonNull final Playable playable, final boolean forceReset, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { if (!playerLock.isHeldByCurrentThread()) { throw new IllegalStateException("method requires playerLock"); } @@ -193,10 +227,10 @@ public class PlaybackServiceMediaPlayer { if(oldMedia.hasAlmostEnded()) { Log.d(TAG, "smart mark as read"); FeedItem item = oldMedia.getItem(); - DBWriter.markItemRead(context, item, true, false); + DBWriter.markItemPlayed(item, FeedItem.PLAYED, false); DBWriter.removeQueueItem(context, item, false); - DBWriter.addItemToPlaybackHistory(context, oldMedia); - if (UserPreferences.isAutoDelete()) { + DBWriter.addItemToPlaybackHistory(oldMedia); + if (item.getFeed().getPreferences().getCurrentAutoDelete()) { Log.d(TAG, "Delete " + oldMedia.toString()); DBWriter.deleteFeedMediaOfItem(context, oldMedia.getId()); } @@ -216,7 +250,7 @@ public class PlaybackServiceMediaPlayer { setPlayerStatus(PlayerStatus.INITIALIZING, media); try { media.loadMetadata(); - mediaSession.setMetadata(getMediaSessionMetadata(media)); + updateMediaSessionMetadata(); if (stream) { mediaPlayer.setDataSource(media.getStreamUrl()); } else { @@ -247,11 +281,38 @@ public class PlaybackServiceMediaPlayer { } } - private MediaMetadataCompat getMediaSessionMetadata(Playable p) { - MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); - builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle()); - builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle()); - return builder.build(); + private void updateMediaSessionMetadata() { + executor.execute(() -> { + final Playable p = this.media; + if (p == null) { + return; + } + MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); + builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle()); + builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle()); + builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration()); + builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle()); + builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle()); + + if (p.getImageUri() != null && UserPreferences.setLockscreenBackground()) { + builder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, p.getImageUri().toString()); + try { + WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + Display display = wm.getDefaultDisplay(); + Bitmap art = Glide.with(context) + .load(p.getImageUri()) + .asBitmap() + .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) + .centerCrop() + .into(display.getWidth(), display.getHeight()) + .get(); + builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art); + } catch (Throwable tr) { + Log.e(TAG, Log.getStackTraceString(tr)); + } + } + mediaSession.setMetadata(builder.build()); + }); } @@ -262,13 +323,10 @@ public class PlaybackServiceMediaPlayer { * 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(); - } + executor.submit(() -> { + playerLock.lock(); + resumeSync(); + playerLock.unlock(); }); } @@ -279,24 +337,26 @@ public class PlaybackServiceMediaPlayer { AudioManager.AUDIOFOCUS_GAIN); if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { acquireWifiLockIfNecessary(); - setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed())); - mediaPlayer.start(); + float speed = 1.0f; + try { + speed = Float.parseFloat(UserPreferences.getPlaybackSpeed()); + } catch(NumberFormatException e) { + Log.e(TAG, Log.getStackTraceString(e)); + UserPreferences.setPlaybackSpeed(String.valueOf(speed)); + } + setSpeed(speed); + setVolume(UserPreferences.getLeftVolume(), UserPreferences.getRightVolume()); + if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { - mediaPlayer.seekTo(media.getPosition()); + int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind( + media.getPosition(), + media.getLastPlayedTime()); + seekToSync(newPosition); } + mediaPlayer.start(); 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 { @@ -393,7 +453,7 @@ public class PlaybackServiceMediaPlayer { } if (media.getPosition() > 0) { - mediaPlayer.seekTo(media.getPosition()); + seekToSync(media.getPosition()); } if (media.getDuration() == 0) { @@ -453,8 +513,20 @@ public class PlaybackServiceMediaPlayer { statusBeforeSeeking = playerStatus; setPlayerStatus(PlayerStatus.SEEKING, media); } + if(seekLatch != null && seekLatch.getCount() > 0) { + try { + seekLatch.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Log.e(TAG, Log.getStackTraceString(e)); + } + } + seekLatch = new CountDownLatch(1); mediaPlayer.seekTo(t); - + try { + seekLatch.await(3, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Log.e(TAG, Log.getStackTraceString(e)); + } } else if (playerStatus == PlayerStatus.INITIALIZED) { media.setPosition(t); startWhenPrepared.set(false); @@ -503,9 +575,7 @@ public class PlaybackServiceMediaPlayer { /** * Seek to the start of the specified chapter. */ - public void seekToChapter(Chapter c) { - Validate.notNull(c); - + public void seekToChapter(@NonNull Chapter c) { seekTo((int) c.getStart()); } @@ -534,7 +604,9 @@ public class PlaybackServiceMediaPlayer { * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. */ public int getPosition() { - playerLock.lock(); + if (!playerLock.tryLock()) { + return INVALID_TIME; + } int retVal = INVALID_TIME; if (playerStatus == PlayerStatus.PLAYING @@ -542,6 +614,9 @@ public class PlaybackServiceMediaPlayer { || playerStatus == PlayerStatus.PREPARED || playerStatus == PlayerStatus.SEEKING) { retVal = mediaPlayer.getCurrentPosition(); + if(retVal <= 0 && media != null && media.getPosition() > 0) { + retVal = media.getPosition(); + } } else if (media != null && media.getPosition() > 0) { retVal = media.getPosition(); } @@ -617,12 +692,49 @@ public class PlaybackServiceMediaPlayer { return retVal; } - public MediaType getCurrentMediaType() { - return mediaType; + /** + * Sets the playback speed. + * This method is executed on an internal executor service. + */ + public void setVolume(final float volumeLeft, float volumeRight) { + executor.submit(() -> setVolumeSync(volumeLeft, volumeRight)); } - public PlayerStatus getPlayerStatus() { - return playerStatus; + /** + * Sets the playback speed. + * This method is executed on the caller's thread. + */ + private void setVolumeSync(float volumeLeft, float volumeRight) { + playerLock.lock(); + if (media != null && media.getMediaType() == MediaType.AUDIO) { + mediaPlayer.setVolume(volumeLeft, volumeRight); + Log.d(TAG, "Media player volume was set to " + volumeLeft + " " + volumeRight); + } + playerLock.unlock(); + } + + /** + * Returns true if the mediaplayer can mix stereo down to mono + */ + public boolean canDownmix() { + boolean retVal = false; + if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) { + retVal = mediaPlayer.canDownmix(); + } + return retVal; + } + + public void setDownmix(boolean enable) { + playerLock.lock(); + if (media != null && media.getMediaType() == MediaType.AUDIO) { + mediaPlayer.setDownmix(enable); + Log.d(TAG, "Media player downmix was set to " + enable); + } + playerLock.unlock(); + } + + public MediaType getCurrentMediaType() { + return mediaType; } public boolean isStreaming() { @@ -704,6 +816,26 @@ public class PlaybackServiceMediaPlayer { } /** + * Returns the current status, if you need the media and the player status together, you should + * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition + * could result in nonsensical results (like a status of PLAYING, but a null playable) + * @return the current player status + */ + public PlayerStatus getPlayerStatus() { + return playerStatus; + } + + /** + * Returns the current media, if you need the media and the player status together, you should + * use getPSMPInfo() to make sure they're properly synchronized. Otherwise a race condition + * could result in nonsensical results (like a status of PLAYING, but a null playable) + * @return the current media. May be null + */ + public Playable getPlayable() { + return media; + } + + /** * Returns a token to this object's MediaSession. The MediaSession should only be used for notifications * at the moment. * @@ -723,9 +855,7 @@ public class PlaybackServiceMediaPlayer { * @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); - + private synchronized void setPlayerStatus(@NonNull PlayerStatus newStatus, Playable newMedia) { Log.d(TAG, "Setting player status to " + newStatus); this.playerStatus = newStatus; @@ -768,7 +898,12 @@ public class PlaybackServiceMediaPlayer { } else { state = PlaybackStateCompat.STATE_NONE; } - sessionState.setState(state, PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, getPlaybackSpeed()); + sessionState.setState(state, getPosition(), getPlaybackSpeed()); + sessionState.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE + | PlaybackStateCompat.ACTION_REWIND + | PlaybackStateCompat.ACTION_FAST_FORWARD + | PlaybackStateCompat.ACTION_SKIP_TO_NEXT); + mediaSession.setPlaybackState(sessionState.build()); callback.statusChanged(new PSMPInfo(playerStatus, media)); } @@ -841,25 +976,24 @@ public class PlaybackServiceMediaPlayer { }; - public void endPlayback() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - releaseWifiLockIfNecessary(); + public void endPlayback(final boolean wasSkipped) { + executor.submit(() -> { + playerLock.lock(); + releaseWifiLockIfNecessary(); - if (playerStatus != PlayerStatus.INDETERMINATE) { - setPlayerStatus(PlayerStatus.INDETERMINATE, media); - } - if (mediaPlayer != null) { - mediaPlayer.reset(); + boolean isPlaying = playerStatus == PlayerStatus.PLAYING; - } - audioManager.abandonAudioFocus(audioFocusChangeListener); - callback.endPlayback(true); + if (playerStatus != PlayerStatus.INDETERMINATE) { + setPlayerStatus(PlayerStatus.INDETERMINATE, media); + } + if (mediaPlayer != null) { + mediaPlayer.reset(); - playerLock.unlock(); } + audioManager.abandonAudioFocus(audioFocusChangeListener); + callback.endPlayback(isPlaying, wasSkipped); + + playerLock.unlock(); }); } @@ -917,22 +1051,20 @@ public class PlaybackServiceMediaPlayer { } } - public static interface PSMPCallback { - public void statusChanged(PSMPInfo newInfo); + public interface PSMPCallback { + void statusChanged(PSMPInfo newInfo); - public void shouldStop(); + void shouldStop(); - public void playbackSpeedChanged(float s); + void playbackSpeedChanged(float s); - public void onBufferingUpdate(int percent); + void onBufferingUpdate(int percent); - public boolean onMediaPlayerInfo(int code); + boolean onMediaPlayerInfo(int code); - public boolean onMediaPlayerError(Object inObj, int what, int extra); + boolean onMediaPlayerError(Object inObj, int what, int extra); - public boolean endPlayback(boolean playNextEpisode); - - public RemoteControlClient getRemoteControlClient(); + boolean endPlayback(boolean playNextEpisode, boolean wasSkipped); } private IPlayer setMediaPlayerListeners(IPlayer mp) { @@ -960,9 +1092,9 @@ public class PlaybackServiceMediaPlayer { return mp; } - private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() { + private final org.antennapod.audio.MediaPlayer.OnCompletionListener audioCompletionListener = new org.antennapod.audio.MediaPlayer.OnCompletionListener() { @Override - public void onCompletion(com.aocate.media.MediaPlayer mp) { + public void onCompletion(org.antennapod.audio.MediaPlayer mp) { genericOnCompletion(); } }; @@ -975,12 +1107,12 @@ public class PlaybackServiceMediaPlayer { }; private void genericOnCompletion() { - endPlayback(); + endPlayback(false); } - private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() { + private final org.antennapod.audio.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new org.antennapod.audio.MediaPlayer.OnBufferingUpdateListener() { @Override - public void onBufferingUpdate(com.aocate.media.MediaPlayer mp, + public void onBufferingUpdate(org.antennapod.audio.MediaPlayer mp, int percent) { genericOnBufferingUpdate(percent); } @@ -997,9 +1129,9 @@ public class PlaybackServiceMediaPlayer { callback.onBufferingUpdate(percent); } - private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() { + private final org.antennapod.audio.MediaPlayer.OnInfoListener audioInfoListener = new org.antennapod.audio.MediaPlayer.OnInfoListener() { @Override - public boolean onInfo(com.aocate.media.MediaPlayer mp, int what, + public boolean onInfo(org.antennapod.audio.MediaPlayer mp, int what, int extra) { return genericInfoListener(what); } @@ -1016,11 +1148,15 @@ public class PlaybackServiceMediaPlayer { return callback.onMediaPlayerInfo(what); } - private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() { + private final org.antennapod.audio.MediaPlayer.OnErrorListener audioErrorListener = new org.antennapod.audio.MediaPlayer.OnErrorListener() { @Override - public boolean onError(com.aocate.media.MediaPlayer mp, int what, - int extra) { - return genericOnError(mp, what, extra); + public boolean onError(org.antennapod.audio.MediaPlayer mp, int what, int extra) { + if(mp.canFallback()) { + mp.fallback(); + return true; + } else { + return genericOnError(mp, what, extra); + } } }; @@ -1035,9 +1171,9 @@ public class PlaybackServiceMediaPlayer { return callback.onMediaPlayerError(inObj, what, extra); } - private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() { + private final org.antennapod.audio.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new org.antennapod.audio.MediaPlayer.OnSeekCompleteListener() { @Override - public void onSeekComplete(com.aocate.media.MediaPlayer mp) { + public void onSeekComplete(org.antennapod.audio.MediaPlayer mp) { genericSeekCompleteListener(); } }; @@ -1050,65 +1186,116 @@ public class PlaybackServiceMediaPlayer { }; private final void genericSeekCompleteListener() { - executor.submit(new Runnable() { - @Override - public void run() { - playerLock.lock(); - if (playerStatus == PlayerStatus.SEEKING) { - setPlayerStatus(statusBeforeSeeking, media); - } - playerLock.unlock(); + Thread t = new Thread(() -> { + Log.d(TAG, "genericSeekCompleteListener"); + if(seekLatch != null) { + seekLatch.countDown(); + } + playerLock.lock(); + if (playerStatus == PlayerStatus.SEEKING) { + setPlayerStatus(statusBeforeSeeking, media); } + playerLock.unlock(); }); + t.start(); } private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() { - @Override - public void onPlay() { - if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { - resume(); - } else if (playerStatus == PlayerStatus.INITIALIZED) { - setStartWhenPrepared(true); - prepare(); - } - } + private static final String TAG = "MediaSessionCompat"; @Override - public void onPause() { - super.onPause(); - if (playerStatus == PlayerStatus.PLAYING) { - pause(false, true); + public boolean onMediaButtonEvent(final Intent mediaButton) { + Log.d(TAG, "onMediaButtonEvent(" + mediaButton + ")"); + if (mediaButton != null) { + KeyEvent keyEvent = (KeyEvent) mediaButton.getExtras().get(Intent.EXTRA_KEY_EVENT); + handleMediaKey(keyEvent); } - if (UserPreferences.isPersistNotify()) { - pause(false, true); - } else { - pause(true, true); - } - } - - @Override - public void onSkipToNext() { - super.onSkipToNext(); - endPlayback(); - } - - @Override - public void onFastForward() { - super.onFastForward(); - seekDelta(UserPreferences.getFastFowardSecs() * 1000); - } - - @Override - public void onRewind() { - super.onRewind(); - seekDelta(-UserPreferences.getRewindSecs() * 1000); + return false; } + }; - @Override - public void onSeekTo(long pos) { - super.onSeekTo(pos); - seekTo((int) pos); + public boolean handleMediaKey(KeyEvent event) { + Log.d(TAG, "handleMediaKey(" + event +")"); + if (event != null + && event.getAction() == KeyEvent.ACTION_DOWN + && event.getRepeatCount() == 0) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_HEADSETHOOK: { + Log.d(TAG, "Received Play/Pause event from RemoteControlClient"); + if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { + resume(); + } else if (playerStatus == PlayerStatus.INITIALIZED) { + setStartWhenPrepared(true); + prepare(); + } else if (playerStatus == PlayerStatus.PLAYING) { + pause(false, true); + if (UserPreferences.isPersistNotify()) { + pause(false, true); + } else { + pause(true, true); + } + } + return true; + } + case KeyEvent.KEYCODE_MEDIA_PLAY: { + Log.d(TAG, "Received Play event from RemoteControlClient"); + if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { + resume(); + } else if (playerStatus == PlayerStatus.INITIALIZED) { + setStartWhenPrepared(true); + prepare(); + } + return true; + } + case KeyEvent.KEYCODE_MEDIA_PAUSE: { + Log.d(TAG, "Received Pause event from RemoteControlClient"); + if (playerStatus == PlayerStatus.PLAYING) { + pause(false, true); + } + if (UserPreferences.isPersistNotify()) { + pause(false, true); + } else { + pause(true, true); + } + return true; + } + case KeyEvent.KEYCODE_MEDIA_STOP: { + Log.d(TAG, "Received Stop event from RemoteControlClient"); + stop(); + return true; + } + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: { + seekDelta(-UserPreferences.getRewindSecs() * 1000); + return true; + } + case KeyEvent.KEYCODE_MEDIA_REWIND: { + seekDelta(-UserPreferences.getRewindSecs() * 1000); + return true; + } + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: { + seekDelta(UserPreferences.getFastFowardSecs() * 1000); + return true; + } + case KeyEvent.KEYCODE_MEDIA_NEXT: { + if(event.getSource() == InputDevice.SOURCE_CLASS_NONE || + UserPreferences.shouldHardwareButtonSkip()) { + // assume the skip command comes from a notification or the lockscreen + // a >| skip button should actually skip + endPlayback(true); + } else { + // assume skip command comes from a (bluetooth) media button + // user actually wants to fast-forward + seekDelta(UserPreferences.getFastFowardSecs() * 1000); + } + return true; + } + default: + Log.d(TAG, "Unhandled key code: " + event.getKeyCode()); + break; + } } - }; + return false; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java index fc73c9446..680fb8777 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -1,10 +1,10 @@ package de.danoeh.antennapod.core.service.playback; import android.content.Context; +import android.os.Vibrator; +import android.support.annotation.NonNull; import android.util.Log; -import org.apache.commons.lang3.Validate; - import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; @@ -14,12 +14,10 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.feed.QueueEvent; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.util.playback.Playable; - import de.greenrobot.event.EventBus; @@ -32,7 +30,7 @@ import de.greenrobot.event.EventBus; * to notify the PlaybackService about updates from the running tasks. */ public class PlaybackServiceTaskManager { - private static final String TAG = "PlaybackServiceTaskManager"; + private static final String TAG = "PlaybackServiceTaskMgr"; /** * Update interval of position saver in milliseconds. @@ -41,7 +39,7 @@ public class PlaybackServiceTaskManager { /** * Notification interval of widget updater in milliseconds. */ - public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500; + public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1000; private static final int SCHED_EX_POOL_SIZE = 2; private final ScheduledThreadPoolExecutor schedExecutor; @@ -63,10 +61,8 @@ public class PlaybackServiceTaskManager { * @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); - + public PlaybackServiceTaskManager(@NonNull Context context, + @NonNull PSTMCallback callback) { this.context = context; this.callback = callback; schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() { @@ -82,6 +78,7 @@ public class PlaybackServiceTaskManager { } public void onEvent(QueueEvent event) { + Log.d(TAG, "onEvent(QueueEvent " + event +")"); cancelQueueLoader(); loadQueue(); } @@ -101,7 +98,7 @@ public class PlaybackServiceTaskManager { queueFuture = schedExecutor.submit(new Callable<List<FeedItem>>() { @Override public List<FeedItem> call() throws Exception { - return DBReader.getQueue(context); + return DBReader.getQueue(); } }); } @@ -168,7 +165,7 @@ public class PlaybackServiceTaskManager { public synchronized void cancelPositionSaver() { if (isPositionSaverActive()) { positionSaverFuture.cancel(false); - if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver"); + Log.d(TAG, "Cancelled PositionSaver"); } } @@ -186,9 +183,9 @@ public class PlaybackServiceTaskManager { widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL, WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS); - if (BuildConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater"); + Log.d(TAG, "Started WidgetUpdater"); } else { - if (BuildConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored."); + Log.d(TAG, "Call to startWidgetUpdater was ignored."); } } @@ -199,16 +196,16 @@ public class PlaybackServiceTaskManager { * * @throws java.lang.IllegalArgumentException if waitingTime <= 0 */ - public synchronized void setSleepTimer(long waitingTime) { - Validate.isTrue(waitingTime > 0, "Waiting time <= 0"); + public synchronized void setSleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) { + if(waitingTime <= 0) { + throw new IllegalArgumentException("Waiting time <= 0"); + } - if (BuildConfig.DEBUG) - Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) - + " milliseconds"); + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds"); if (isSleepTimerActive()) { sleepTimerFuture.cancel(true); } - sleepTimer = new SleepTimer(waitingTime); + sleepTimer = new SleepTimer(waitingTime, shakeToReset, vibrate); sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS); } @@ -216,7 +213,11 @@ public class PlaybackServiceTaskManager { * Returns true if the sleep timer is currently active. */ public synchronized boolean isSleepTimerActive() { - return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone() && sleepTimer.isWaiting; + return sleepTimer != null + && sleepTimerFuture != null + && !sleepTimerFuture.isCancelled() + && !sleepTimerFuture.isDone() + && sleepTimer.getWaitingTime() > 0; } /** @@ -224,8 +225,7 @@ public class PlaybackServiceTaskManager { */ public synchronized void disableSleepTimer() { if (isSleepTimerActive()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Disabling sleep timer"); + Log.d(TAG, "Disabling sleep timer"); sleepTimerFuture.cancel(true); } } @@ -255,7 +255,7 @@ public class PlaybackServiceTaskManager { public synchronized void cancelWidgetUpdater() { if (isWidgetUpdaterActive()) { widgetUpdaterFuture.cancel(false); - if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater"); + Log.d(TAG, "Cancelled WidgetUpdater"); } } @@ -274,9 +274,7 @@ public class PlaybackServiceTaskManager { * 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); - + public synchronized void startChapterLoader(@NonNull final Playable media) { if (isChapterLoaderActive()) { cancelChapterLoader(); } @@ -284,16 +282,14 @@ public class PlaybackServiceTaskManager { Runnable chapterLoader = new Runnable() { @Override public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Chapter loader started"); + 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"); + Log.d(TAG, "Chapter loader stopped"); } }; chapterLoaderFuture = schedExecutor.submit(chapterLoader); @@ -324,63 +320,90 @@ public class PlaybackServiceTaskManager { /** * Sleeps for a given time and then pauses playback. */ - private class SleepTimer implements Runnable { + protected 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) { + private static final long UPDATE_INTERVAL = 1000L; + private static final long NOTIFICATION_THRESHOLD = 10000; + private long waitingTime; + private final boolean shakeToReset; + private final boolean vibrate; + private ShakeListener shakeListener; + + public SleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) { super(); this.waitingTime = waitingTime; - isWaiting = true; + this.shakeToReset = shakeToReset; + this.vibrate = vibrate; } @Override public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Starting"); + Log.d(TAG, "Starting"); + boolean notifiedAlmostExpired = false; + long lastTick = System.currentTimeMillis(); while (waitingTime > 0) { try { - Thread.sleep(UPDATE_INTERVALL); - waitingTime -= UPDATE_INTERVALL; - + Thread.sleep(UPDATE_INTERVAL); + long now = System.currentTimeMillis(); + waitingTime -= now - lastTick; + lastTick = now; + + if(waitingTime < NOTIFICATION_THRESHOLD && !notifiedAlmostExpired) { + Log.d(TAG, "Sleep timer is about to expire"); + if(vibrate) { + Vibrator v = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + if(v != null) { + v.vibrate(500); + } + } + if(shakeListener == null && shakeToReset) { + shakeListener = new ShakeListener(context, this); + } + callback.onSleepTimerAlmostExpired(); + notifiedAlmostExpired = true; + } if (waitingTime <= 0) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Waiting completed"); - postExecute(); + Log.d(TAG, "Sleep timer expired"); + if(shakeListener != null) { + shakeListener.pause(); + shakeListener = null; + } if (!Thread.currentThread().isInterrupted()) { callback.onSleepTimerExpired(); + } else { + Log.d(TAG, "Sleep timer interrupted"); } - } } catch (InterruptedException e) { Log.d(TAG, "Thread was interrupted while waiting"); + e.printStackTrace(); break; } } - postExecute(); - } - - protected void postExecute() { - isWaiting = false; } public long getWaitingTime() { return waitingTime; } - public boolean isWaiting() { - return isWaiting; + public void onShake() { + setSleepTimer(15 * 60 * 1000, shakeToReset, vibrate); + callback.onSleepTimerReset(); + shakeListener.pause(); + shakeListener = null; } } - public static interface PSTMCallback { + public interface PSTMCallback { void positionSaverTick(); + void onSleepTimerAlmostExpired(); + void onSleepTimerExpired(); + void onSleepTimerReset(); + void onWidgetUpdaterTick(); void onChapterLoaded(Playable media); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java new file mode 100644 index 000000000..fcd96826b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ShakeListener.java @@ -0,0 +1,64 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; +import android.util.Log; + +public class ShakeListener implements SensorEventListener +{ + private static final String TAG = ShakeListener.class.getSimpleName(); + + private Sensor mAccelerometer; + private SensorManager mSensorMgr; + private PlaybackServiceTaskManager.SleepTimer mSleepTimer; + private Context mContext; + + public ShakeListener(Context context, PlaybackServiceTaskManager.SleepTimer sleepTimer) { + mContext = context; + mSleepTimer = sleepTimer; + resume(); + } + + public void resume() { + // only a precaution, the user should actually not be able to activate shake to reset + // when the accelerometer is not available + mSensorMgr = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE); + if (mSensorMgr == null) { + throw new UnsupportedOperationException("Sensors not supported"); + } + mAccelerometer = mSensorMgr.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + if (!mSensorMgr.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_UI)) { // if not supported + mSensorMgr.unregisterListener(this); + throw new UnsupportedOperationException("Accelerometer not supported"); + } + } + + public void pause() { + if (mSensorMgr != null) { + mSensorMgr.unregisterListener(this); + mSensorMgr = null; + } + } + + @Override + public void onSensorChanged(SensorEvent event) { + float gX = event.values[0] / SensorManager.GRAVITY_EARTH; + float gY = event.values[1] / SensorManager.GRAVITY_EARTH; + float gZ = event.values[2] / SensorManager.GRAVITY_EARTH; + + double gForce = Math.sqrt(gX*gX + gY*gY + gZ*gZ); + if (gForce > 2.25) { + Log.d(TAG, "Detected shake " + gForce); + mSleepTimer.onShake(); + } + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + return; + } + +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java index f647fd537..0dc54fb6e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APCleanupAlgorithm.java @@ -4,54 +4,70 @@ import android.content.Context; import android.util.Log; import java.util.ArrayList; +import java.util.Calendar; import java.util.Collections; -import java.util.Comparator; import java.util.Date; import java.util.List; import java.util.concurrent.ExecutionException; import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.util.LongList; /** * Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPod. */ -public class APCleanupAlgorithm implements EpisodeCleanupAlgorithm<Integer> { +public class APCleanupAlgorithm extends EpisodeCleanupAlgorithm { + private static final String TAG = "APCleanupAlgorithm"; + /** the number of days after playback to wait before an item is eligible to be cleaned up */ + private final int numberOfDaysAfterPlayback; + + public APCleanupAlgorithm(int numberOfDaysAfterPlayback) { + this.numberOfDaysAfterPlayback = numberOfDaysAfterPlayback; + } @Override - public int performCleanup(Context context, Integer episodeNumber) { - List<FeedItem> candidates = new ArrayList<FeedItem>(); - List<FeedItem> downloadedItems = DBReader.getDownloadedItems(context); - LongList queue = DBReader.getQueueIDList(context); + public int performCleanup(Context context, int numberOfEpisodesToDelete) { + List<FeedItem> candidates = new ArrayList<>(); + List<FeedItem> downloadedItems = DBReader.getDownloadedItems(); List<FeedItem> delete; + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.DAY_OF_MONTH, -1 * numberOfDaysAfterPlayback); + Date mostRecentDateForDeletion = cal.getTime(); for (FeedItem item : downloadedItems) { - if (item.hasMedia() && item.getMedia().isDownloaded() - && !queue.contains(item.getId()) && item.isRead()) { - candidates.add(item); + if (item.hasMedia() + && item.getMedia().isDownloaded() + && !item.isTagged(FeedItem.TAG_QUEUE) + && item.isPlayed() + && !item.isTagged(FeedItem.TAG_FAVORITE)) { + FeedMedia media = item.getMedia(); + // make sure this candidate was played at least the proper amount of days prior + // to now + if (media != null + && media.getPlaybackCompletionDate() != null + && media.getPlaybackCompletionDate().before(mostRecentDateForDeletion)) { + candidates.add(item); + } } - } - Collections.sort(candidates, new Comparator<FeedItem>() { - @Override - public int compare(FeedItem lhs, FeedItem rhs) { - Date l = lhs.getMedia().getPlaybackCompletionDate(); - Date r = rhs.getMedia().getPlaybackCompletionDate(); + Collections.sort(candidates, (lhs, rhs) -> { + Date l = lhs.getMedia().getPlaybackCompletionDate(); + Date r = rhs.getMedia().getPlaybackCompletionDate(); - if (l == null) { - l = new Date(); - } - if (r == null) { - r = new Date(); - } - return l.compareTo(r); + if (l == null) { + l = new Date(); } + if (r == null) { + r = new Date(); + } + return l.compareTo(r); }); - if (candidates.size() > episodeNumber) { - delete = candidates.subList(0, episodeNumber); + if (candidates.size() > numberOfEpisodesToDelete) { + delete = candidates.subList(0, numberOfEpisodesToDelete); } else { delete = candidates; } @@ -69,35 +85,15 @@ public class APCleanupAlgorithm implements EpisodeCleanupAlgorithm<Integer> { Log.i(TAG, String.format( "Auto-delete deleted %d episodes (%d requested)", counter, - episodeNumber)); + numberOfEpisodesToDelete)); return counter; } @Override - public Integer getDefaultCleanupParameter(Context context) { - return getPerformAutoCleanupArgs(context, 0); - } - - @Override - public Integer getPerformCleanupParameter(Context context, List<FeedItem> items) { - return getPerformAutoCleanupArgs(context, items.size()); + public int getDefaultCleanupParameter() { + return getNumEpisodesToCleanup(0); } - static int getPerformAutoCleanupArgs(Context context, - final int episodeNumber) { - if (episodeNumber >= 0 - && UserPreferences.getEpisodeCacheSize() != UserPreferences - .getEpisodeCacheSizeUnlimited()) { - int downloadedEpisodes = DBReader - .getNumberOfDownloadedEpisodes(context); - if (downloadedEpisodes + episodeNumber >= UserPreferences - .getEpisodeCacheSize()) { - - return downloadedEpisodes + episodeNumber - - UserPreferences.getEpisodeCacheSize(); - } - } - return 0; - } + public int getNumberOfDaysAfterPlayback() { return numberOfDaysAfterPlayback; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java index 92de1eee7..26dc027bf 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java @@ -7,7 +7,9 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import de.danoeh.antennapod.core.feed.FeedFilter; import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.PowerUtils; @@ -19,28 +21,24 @@ import de.danoeh.antennapod.core.util.PowerUtils; public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm { private static final String TAG = "APDownloadAlgorithm"; - private final APCleanupAlgorithm cleanupAlgorithm = new APCleanupAlgorithm(); - /** - * Looks for undownloaded episodes in the queue or list of unread items and request a download if + * Looks for undownloaded episodes in the queue or list of new items and request a download if * 1. Network is available * 2. The device is charging or the user allows auto download on battery * 3. There is free space in the episode cache * This method is executed on an internal single thread executor. * * @param context Used for accessing the DB. - * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if - * its media ID is in the mediaIds list. * @return A Runnable that will be submitted to an ExecutorService. */ @Override - public Runnable autoDownloadUndownloadedItems(final Context context, final long... mediaIds) { + public Runnable autoDownloadUndownloadedItems(final Context context) { return new Runnable() { @Override public void run() { // true if we should auto download based on network status - boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable(context) + boolean networkShouldAutoDl = NetworkUtils.autodownloadNetworkAvailable() && UserPreferences.isEnableAutodownload(); // true if we should auto download based on power status @@ -53,17 +51,15 @@ public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm { Log.d(TAG, "Performing auto-dl of undownloaded episodes"); List<FeedItem> candidates; - if(mediaIds.length > 0) { - candidates = DBReader.getFeedItems(context, mediaIds); - } else { - final List<FeedItem> queue = DBReader.getQueue(context); - final List<FeedItem> unreadItems = DBReader.getUnreadItemsList(context); - candidates = new ArrayList<FeedItem>(queue.size() + unreadItems.size()); - candidates.addAll(queue); - for(FeedItem unreadItem : unreadItems) { - if(candidates.contains(unreadItem) == false) { - candidates.add(unreadItem); - } + final List<FeedItem> queue = DBReader.getQueue(); + final List<FeedItem> newItems = DBReader.getNewItemsList(); + candidates = new ArrayList<FeedItem>(queue.size() + newItems.size()); + candidates.addAll(queue); + for(FeedItem newItem : newItems) { + FeedPreferences feedPrefs = newItem.getFeed().getPreferences(); + FeedFilter feedFilter = feedPrefs.getFilter(); + if(candidates.contains(newItem) == false && feedFilter.shouldAutoDownload(newItem)) { + candidates.add(newItem); } } @@ -77,9 +73,9 @@ public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm { } int autoDownloadableEpisodes = candidates.size(); - int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes(context); - int deletedEpisodes = cleanupAlgorithm.performCleanup(context, - APCleanupAlgorithm.getPerformAutoCleanupArgs(context, autoDownloadableEpisodes)); + int downloadedEpisodes = DBReader.getNumberOfDownloadedEpisodes(); + int deletedEpisodes = UserPreferences.getEpisodeCleanupAlgorithm() + .makeRoomForEpisodes(context, autoDownloadableEpisodes); boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences .getEpisodeCacheSizeUnlimited(); int episodeCacheSize = UserPreferences.getEpisodeCacheSize(); @@ -107,5 +103,4 @@ public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm { } }; } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APNullCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APNullCleanupAlgorithm.java new file mode 100644 index 000000000..132b61853 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APNullCleanupAlgorithm.java @@ -0,0 +1,24 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.util.Log; + +/** + * A cleanup algorithm that never removes anything + */ +public class APNullCleanupAlgorithm extends EpisodeCleanupAlgorithm { + + private static final String TAG = "APNullCleanupAlgorithm"; + + @Override + public int performCleanup(Context context, int parameter) { + // never clean anything up + Log.i(TAG, "performCleanup: Not removing anything"); + return 0; + } + + @Override + public int getDefaultCleanupParameter() { + return 0; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java new file mode 100644 index 000000000..234d6162c --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APQueueCleanupAlgorithm.java @@ -0,0 +1,81 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.concurrent.ExecutionException; + +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.util.LongList; + +/** + * A cleanup algorithm that removes any item that isn't in the queue and isn't a favorite + * but only if space is needed. + */ +public class APQueueCleanupAlgorithm extends EpisodeCleanupAlgorithm { + + private static final String TAG = "APQueueCleanupAlgorithm"; + + @Override + public int performCleanup(Context context, int numberOfEpisodesToDelete) { + List<FeedItem> candidates = new ArrayList<>(); + List<FeedItem> downloadedItems = DBReader.getDownloadedItems(); + List<FeedItem> delete; + for (FeedItem item : downloadedItems) { + if (item.hasMedia() + && item.getMedia().isDownloaded() + && !item.isTagged(FeedItem.TAG_QUEUE) + && !item.isTagged(FeedItem.TAG_FAVORITE)) { + candidates.add(item); + } + } + + // in the absence of better data, we'll sort by item publication date + Collections.sort(candidates, (lhs, rhs) -> { + Date l = lhs.getPubDate(); + Date r = rhs.getPubDate(); + + if (l == null) { + l = new Date(); + } + if (r == null) { + r = new Date(); + } + return l.compareTo(r); + }); + + if (candidates.size() > numberOfEpisodesToDelete) { + delete = candidates.subList(0, numberOfEpisodesToDelete); + } else { + delete = candidates; + } + + for (FeedItem item : delete) { + try { + DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + int counter = delete.size(); + + + Log.i(TAG, String.format( + "Auto-delete deleted %d episodes (%d requested)", counter, + numberOfEpisodesToDelete)); + + return counter; + } + + @Override + public int getDefaultCleanupParameter() { + return getNumEpisodesToCleanup(0); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java deleted file mode 100644 index 420bbc09d..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APSPCleanupAlgorithm.java +++ /dev/null @@ -1,139 +0,0 @@ -package de.danoeh.antennapod.core.storage; - -import android.content.Context; -import android.util.Log; - -import org.apache.commons.io.FileUtils; - -import java.io.File; -import java.util.ArrayList; -import java.util.Collections; -import java.util.Comparator; -import java.util.Iterator; -import java.util.List; -import java.util.concurrent.ExecutionException; - -import de.danoeh.antennapod.core.feed.FeedItem; - -/** - * Implementation of the EpisodeCleanupAlgorithm interface used by AntennaPodSP apps. - */ -public class APSPCleanupAlgorithm implements EpisodeCleanupAlgorithm<Integer> { - private static final String TAG = "APSPCleanupAlgorithm"; - - final int numberOfNewAutomaticallyDownloadedEpisodes; - - public APSPCleanupAlgorithm(int numberOfNewAutomaticallyDownloadedEpisodes) { - this.numberOfNewAutomaticallyDownloadedEpisodes = numberOfNewAutomaticallyDownloadedEpisodes; - } - - /** - * Performs an automatic cleanup. Episodes that have been downloaded first will also be deleted first. - * The episode that is currently playing as well as the n most recent episodes (the exact value is determined - * by AppPreferences.numberOfNewAutomaticallyDownloadedEpisodes) will never be deleted. - * - * @param context - * @param episodeSize The maximum amount of space that should be freed by this method - * @return The number of episodes that have been deleted - */ - @Override - public int performCleanup(Context context, Integer episodeSize) { - Log.i(TAG, String.format("performAutoCleanup(%d)", episodeSize)); - if (episodeSize <= 0) { - return 0; - } - - List<FeedItem> candidates = getAutoCleanupCandidates(context); - List<FeedItem> deleteList = new ArrayList<FeedItem>(); - long deletedEpisodesSize = 0; - Collections.sort(candidates, new Comparator<FeedItem>() { - @Override - public int compare(FeedItem lhs, FeedItem rhs) { - File lFile = new File(lhs.getMedia().getFile_url()); - File rFile = new File(rhs.getMedia().getFile_url()); - if (!lFile.exists() || !rFile.exists()) { - return 0; - } - if (FileUtils.isFileOlder(lFile, rFile)) { - return -1; - } else { - return 1; - } - } - }); - // listened episodes will be deleted first - Iterator<FeedItem> it = candidates.iterator(); - if (it.hasNext()) { - for (FeedItem i = it.next(); it.hasNext() && deletedEpisodesSize <= episodeSize; i = it.next()) { - if (!i.getMedia().isPlaying() && i.getMedia().getPlaybackCompletionDate() != null) { - it.remove(); - deleteList.add(i); - deletedEpisodesSize += i.getMedia().getSize(); - } - } - } - - // delete unlistened old episodes if necessary - it = candidates.iterator(); - if (it.hasNext()) { - for (FeedItem i = it.next(); it.hasNext() && deletedEpisodesSize <= episodeSize; i = it.next()) { - if (!i.getMedia().isPlaying()) { - it.remove(); - deleteList.add(i); - deletedEpisodesSize += i.getMedia().getSize(); - } - } - } - for (FeedItem item : deleteList) { - try { - DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - } - Log.i(TAG, String.format("performAutoCleanup(%d) deleted %d episodes and freed %d bytes of memory", - episodeSize, deleteList.size(), deletedEpisodesSize)); - return deleteList.size(); - } - - @Override - public Integer getDefaultCleanupParameter(Context context) { - return 0; - } - - @Override - public Integer getPerformCleanupParameter(Context context, List<FeedItem> items) { - int episodeSize = 0; - for (FeedItem item : items) { - if (item.hasMedia() && !item.getMedia().isDownloaded()) { - episodeSize += item.getMedia().getSize(); - } - } - return episodeSize; - } - - /** - * Returns list of FeedItems that have been downloaded, but are not one of the - * [numberOfNewAutomaticallyDownloadedEpisodes] most recent items. - */ - private List<FeedItem> getAutoCleanupCandidates(Context context) { - List<FeedItem> downloaded = new ArrayList<FeedItem>(DBReader.getDownloadedItems(context)); - List<FeedItem> recent = new ArrayList<FeedItem>(DBReader.getRecentlyPublishedEpisodes(context, - numberOfNewAutomaticallyDownloadedEpisodes)); - for (FeedItem r : recent) { - if (r.hasMedia() && r.getMedia().isDownloaded()) { - for (int i = 0; i < downloaded.size(); i++) { - if (downloaded.get(i).getId() == r.getId()) { - downloaded.remove(i); - break; - } - } - } - } - - return downloaded; - - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APSPDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APSPDownloadAlgorithm.java deleted file mode 100644 index f760ec0ce..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APSPDownloadAlgorithm.java +++ /dev/null @@ -1,72 +0,0 @@ -package de.danoeh.antennapod.core.storage; - -import android.content.Context; -import android.util.Log; - -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; - -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.feed.FeedItem; -import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.util.NetworkUtils; - -/** - * Implements the automatic download algorithm used by AntennaPodSP apps. - */ -public class APSPDownloadAlgorithm implements AutomaticDownloadAlgorithm { - private static final String TAG = "APSPDownloadAlgorithm"; - - private final int numberOfNewAutomaticallyDownloadedEpisodes; - - public APSPDownloadAlgorithm(int numberOfNewAutomaticallyDownloadedEpisodes) { - this.numberOfNewAutomaticallyDownloadedEpisodes = numberOfNewAutomaticallyDownloadedEpisodes; - } - - /** - * Downloads the most recent episodes automatically. The exact number of - * episodes that will be downloaded can be set in the AppPreferences. - * - * @param context Used for accessing the DB. - * @return A Runnable that will be submitted to an ExecutorService. - */ - @Override - public Runnable autoDownloadUndownloadedItems(final Context context, final long... mediaIds) { - return new Runnable() { - @Override - public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Performing auto-dl of undownloaded episodes"); - if (NetworkUtils.autodownloadNetworkAvailable(context) - && UserPreferences.isEnableAutodownload()) { - - Arrays.sort(mediaIds); - List<FeedItem> itemsToDownload = DBReader.getRecentlyPublishedEpisodes(context, - numberOfNewAutomaticallyDownloadedEpisodes); - Iterator<FeedItem> it = itemsToDownload.iterator(); - - for (FeedItem item = it.next(); it.hasNext(); item = it.next()) { - if (!item.hasMedia() - || item.getMedia().isDownloaded() - || Arrays.binarySearch(mediaIds, item.getMedia().getId()) < 0) { - it.remove(); - } - } - if (BuildConfig.DEBUG) - Log.d(TAG, "Enqueueing " + itemsToDownload.size() - + " items for automatic download"); - if (!itemsToDownload.isEmpty()) { - try { - DBTasks.downloadFeedItems(false, context, - itemsToDownload.toArray(new FeedItem[itemsToDownload - .size()])); - } catch (DownloadRequestException e) { - e.printStackTrace(); - } - } - } - } - }; - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java index 9ca9620a7..72c68ddb6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/AutomaticDownloadAlgorithm.java @@ -12,9 +12,7 @@ public interface AutomaticDownloadAlgorithm { * This method is executed on an internal single thread executor. * * @param context Used for accessing the DB. - * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if - * its media ID is in the mediaIds list. * @return A Runnable that will be submitted to an ExecutorService. */ - public Runnable autoDownloadUndownloadedItems(Context context, long... mediaIds); + public Runnable autoDownloadUndownloadedItems(Context context); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index cc20b3d37..0563f878f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -1,18 +1,16 @@ package de.danoeh.antennapod.core.storage; -import android.content.Context; import android.database.Cursor; +import android.support.v4.util.ArrayMap; import android.util.Log; -import org.apache.commons.lang3.StringUtils; - import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.List; +import java.util.Map; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedImage; @@ -22,14 +20,13 @@ import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.feed.ID3Chapter; import de.danoeh.antennapod.core.feed.SimpleChapter; import de.danoeh.antennapod.core.feed.VorbisCommentChapter; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; -import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.LongIntMap; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; import de.danoeh.antennapod.core.util.comparator.PlaybackCompletionDateComparator; -import de.danoeh.antennapod.core.util.flattr.FlattrStatus; import de.danoeh.antennapod.core.util.flattr.FlattrThing; /** @@ -39,15 +36,16 @@ import de.danoeh.antennapod.core.util.flattr.FlattrThing; * This class will use the {@link de.danoeh.antennapod.core.feed.EventDistributor} to notify listeners about changes in the database. */ public final class DBReader { + private static final String TAG = "DBReader"; /** - * Maximum size of the list returned by {@link #getPlaybackHistory(android.content.Context)}. + * Maximum size of the list returned by {@link #getPlaybackHistory()}. */ public static final int PLAYBACK_HISTORY_SIZE = 50; /** - * Maximum size of the list returned by {@link #getDownloadLog(android.content.Context)}. + * Maximum size of the list returned by {@link #getDownloadLog()}. */ public static final int DOWNLOAD_LOG_SIZE = 200; @@ -58,16 +56,14 @@ public final class DBReader { /** * Returns a list of Feeds, sorted alphabetically by their title. * - * @param context A context that is used for opening a database connection. * @return A list of Feeds, sorted alphabetically by their title. A Feed-object * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list - * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)}. + * can be loaded separately with {@link #getFeedItemList(Feed)}. */ - public static List<Feed> getFeedList(final Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting Feedlist"); + public static List<Feed> getFeedList() { + Log.d(TAG, "Extracting Feedlist"); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); List<Feed> result = getFeedList(adapter); adapter.close(); @@ -75,11 +71,8 @@ public final class DBReader { } private static List<Feed> getFeedList(PodDBAdapter adapter) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting Feedlist"); - Cursor feedlistCursor = adapter.getAllFeedsCursor(); - List<Feed> feeds = new ArrayList<Feed>(feedlistCursor.getCount()); + List<Feed> feeds = new ArrayList<>(feedlistCursor.getCount()); if (feedlistCursor.moveToFirst()) { do { @@ -94,12 +87,11 @@ public final class DBReader { /** * Returns a list with the download URLs of all feeds. * - * @param context A context that is used for opening the database connection. * @return A list of Strings with the download URLs of all feeds. */ - public static List<String> getFeedListDownloadUrls(final Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - List<String> result = new ArrayList<String>(); + public static List<String> getFeedListDownloadUrls() { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + List<String> result = new ArrayList<>(); adapter.open(); Cursor feeds = adapter.getFeedCursorDownloadUrls(); if (feeds.moveToFirst()) { @@ -113,34 +105,28 @@ public final class DBReader { return result; } + /** - * Returns a list of 'expired Feeds', i.e. Feeds that have not been updated for a certain amount of time. - * - * @param context A context that is used for opening a database connection. - * @param expirationTime Time that is used for determining whether a feed is outdated or not. - * A Feed is considered expired if 'lastUpdate < (currentTime - expirationTime)' evaluates to true. - * @return A list of Feeds, sorted alphabetically by their title. A Feed-object - * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list - * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)}. + * Loads additional data in to the feed items from other database queries + * @param items the FeedItems who should have other data loaded */ - public static List<Feed> getExpiredFeedsList(final Context context, final long expirationTime) { - if (BuildConfig.DEBUG) - Log.d(TAG, String.format("getExpiredFeedsList(%d)", expirationTime)); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); + public static void loadAdditionalFeedItemListData(List<FeedItem> items) { + loadTagsOfFeedItemList(items); + loadFeedDataOfFeedItemList(items); + } - Cursor feedlistCursor = adapter.getExpiredFeedsCursor(expirationTime); - List<Feed> feeds = new ArrayList<Feed>(feedlistCursor.getCount()); + public static void loadTagsOfFeedItemList(List<FeedItem> items) { + LongList favoriteIds = getFavoriteIDList(); + LongList queueIds = getQueueIDList(); - if (feedlistCursor.moveToFirst()) { - do { - Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor); - feeds.add(feed); - } while (feedlistCursor.moveToNext()); + for (FeedItem item : items) { + if (favoriteIds.contains(item.getId())) { + item.addTag(FeedItem.TAG_FAVORITE); + } + if (queueIds.contains(item.getId())) { + item.addTag(FeedItem.TAG_QUEUE); + } } - feedlistCursor.close(); - return feeds; } /** @@ -148,12 +134,10 @@ public final class DBReader { * The feedID-attribute of a FeedItem must be set to the ID of its feed or the method will * not find the correct feed of an item. * - * @param context A context that is used for opening a database connection. * @param items The FeedItems whose Feed-objects should be loaded. */ - public static void loadFeedDataOfFeedItemlist(Context context, - List<FeedItem> items) { - List<Feed> feeds = getFeedList(context); + public static void loadFeedDataOfFeedItemList(List<FeedItem> items) { + List<Feed> feeds = getFeedList(); for (FeedItem item : items) { for (Feed feed : feeds) { if (feed.getId() == item.getFeedId()) { @@ -169,29 +153,26 @@ public final class DBReader { /** * Loads the list of FeedItems for a certain Feed-object. This method should NOT be used if the FeedItems are not - * used. In order to get information ABOUT the list of FeedItems, consider using {@link #getFeedStatisticsList(android.content.Context)} instead. + * used. In order to get information ABOUT the list of FeedItems, consider using {@link #getFeedStatisticsList()} instead. * - * @param context A context that is used for opening a database connection. * @param feed The Feed whose items should be loaded * @return A list with the FeedItems of the Feed. The Feed-attribute of the FeedItems will already be set correctly. * The method does NOT change the items-attribute of the feed. */ - public static List<FeedItem> getFeedItemList(Context context, - final Feed feed) { - Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle()); + public static List<FeedItem> getFeedItemList(final Feed feed) { + Log.d(TAG, "getFeedItemList() called with: " + "feed = [" + feed + "]"); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor itemlistCursor = adapter.getAllItemsOfFeedCursor(feed); List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor); itemlistCursor.close(); + adapter.close(); Collections.sort(items, new FeedItemPubdateComparator()); - adapter.close(); - for (FeedItem item : items) { item.setFeed(feed); } @@ -199,200 +180,121 @@ public final class DBReader { return items; } - static List<FeedItem> extractItemlistFromCursor(Context context, Cursor itemlistCursor) { - PodDBAdapter adapter = new PodDBAdapter(context); + public static List<FeedItem> extractItemlistFromCursor(Cursor itemlistCursor) { + Log.d(TAG, "extractItemlistFromCursor() called with: " + "itemlistCursor = [" + itemlistCursor + "]"); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); List<FeedItem> result = extractItemlistFromCursor(adapter, itemlistCursor); adapter.close(); return result; } - private static List<FeedItem> extractItemlistFromCursor( - PodDBAdapter adapter, Cursor itemlistCursor) { - ArrayList<String> itemIds = new ArrayList<String>(); - List<FeedItem> items = new ArrayList<FeedItem>( - itemlistCursor.getCount()); + private static List<FeedItem> extractItemlistFromCursor(PodDBAdapter adapter, + Cursor cursor) { + List<FeedItem> result = new ArrayList<>(cursor.getCount()); - if (itemlistCursor.moveToFirst()) { + LongList imageIds = new LongList(cursor.getCount()); + LongList itemIds = new LongList(cursor.getCount()); + if (cursor.moveToFirst()) { do { - long imageIndex = itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_IMAGE); - FeedImage image = null; - if (imageIndex != 0) { - image = getFeedImage(adapter, imageIndex); - } + int indexImage = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE); + long imageId = cursor.getLong(indexImage); + imageIds.add(imageId); - FeedItem item = new FeedItem(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_ID), - itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_TITLE), - itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_LINK), - new Date(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_PUBDATE)), - itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_PAYMENT_LINK), - itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_FEED), - new FlattrStatus(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS)), - itemlistCursor.getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0, - image, - (itemlistCursor.getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0), - itemlistCursor.getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER), - itemlistCursor.getInt(itemlistCursor.getColumnIndex(PodDBAdapter.KEY_AUTO_DOWNLOAD)) > 0 - ); - - itemIds.add(String.valueOf(item.getId())); - - items.add(item); - } while (itemlistCursor.moveToNext()); + FeedItem item = FeedItem.fromCursor(cursor); + result.add(item); + itemIds.add(item.getId()); + } while (cursor.moveToNext()); + Map<Long,FeedImage> images = getFeedImages(adapter, imageIds.toArray()); + Map<Long,FeedMedia> medias = getFeedMedia(adapter, itemIds.toArray()); + for(int i=0; i < result.size(); i++) { + FeedItem item = result.get(i); + long imageId = imageIds.get(i); + FeedImage image = images.get(imageId); + item.setImage(image); + FeedMedia media = medias.get(item.getId()); + item.setMedia(media); + if(media != null) { + media.setItem(item); + } + } } - - extractMediafromItemlist(adapter, items, itemIds); - return items; + return result; } - private static void extractMediafromItemlist(PodDBAdapter adapter, - List<FeedItem> items, ArrayList<String> itemIds) { + private static Map<Long,FeedMedia> getFeedMedia(PodDBAdapter adapter, + long... itemIds) { - List<FeedItem> itemsCopy = new ArrayList<FeedItem>(items); - Cursor cursor = adapter.getFeedMediaCursorByItemID(itemIds - .toArray(new String[itemIds.size()])); - if (cursor.moveToFirst()) { - do { - long itemId = cursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); - // find matching feed item - FeedItem item = getMatchingItemForMedia(itemId, itemsCopy); - if (item != null) { - item.setMedia(extractFeedMediaFromCursorRow(cursor)); - item.getMedia().setItem(item); - } - } while (cursor.moveToNext()); - cursor.close(); + String[] ids = new String[itemIds.length]; + for(int i=0, len=itemIds.length; i < len; i++) { + ids[i] = String.valueOf(itemIds[i]); } - } - - private static FeedMedia extractFeedMediaFromCursorRow(final Cursor cursor) { - long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); - Date playbackCompletionDate = null; - long playbackCompletionTime = cursor - .getLong(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE_INDEX); - if (playbackCompletionTime > 0) { - playbackCompletionDate = new Date( - playbackCompletionTime); + Map<Long,FeedMedia> result = new ArrayMap<>(itemIds.length); + Cursor cursor = adapter.getFeedMediaCursor(ids); + try { + if (cursor.moveToFirst()) { + do { + int index = cursor.getColumnIndex(PodDBAdapter.KEY_FEEDITEM); + long itemId = cursor.getLong(index); + FeedMedia media = FeedMedia.fromCursor(cursor); + result.put(itemId, media); + } while (cursor.moveToNext()); + } + } finally { + cursor.close(); } - - return new FeedMedia( - mediaId, - null, - cursor.getInt(PodDBAdapter.KEY_DURATION_INDEX), - cursor.getInt(PodDBAdapter.KEY_POSITION_INDEX), - cursor.getLong(PodDBAdapter.KEY_SIZE_INDEX), - cursor.getString(PodDBAdapter.KEY_MIME_TYPE_INDEX), - cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX), - cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX), - cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0, - playbackCompletionDate, - cursor.getInt(PodDBAdapter.KEY_PLAYED_DURATION_INDEX)); + return result; } private static Feed extractFeedFromCursorRow(PodDBAdapter adapter, Cursor cursor) { - Date lastUpdate = new Date( - cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_LASTUPDATE)); - final FeedImage image; - long imageIndex = cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_IMAGE); - if (imageIndex != 0) { - image = getFeedImage(adapter, imageIndex); + int indexImage = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE); + long imageId = cursor.getLong(indexImage); + if (imageId != 0) { + image = getFeedImage(adapter, imageId); } else { image = null; } - Feed feed = new Feed(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID), - lastUpdate, - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TITLE), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LINK), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DESCRIPTION), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_PAYMENT_LINK), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_AUTHOR), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_LANGUAGE), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_TYPE), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FEED_IDENTIFIER), - image, - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_FILE_URL), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOAD_URL), - cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_DOWNLOADED) > 0, - new FlattrStatus(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_FLATTR_STATUS)), - cursor.getInt(PodDBAdapter.IDX_FEED_SEL_STD_IS_PAGED) > 0, - cursor.getString(PodDBAdapter.IDX_FEED_SEL_STD_NEXT_PAGE_LINK), - cursor.getString(cursor.getColumnIndex(PodDBAdapter.KEY_HIDE)), - cursor.getInt(cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED)) > 0 - ); + Feed feed = Feed.fromCursor(cursor); if (image != null) { + feed.setImage(image); image.setOwner(feed); } - FeedPreferences preferences = new FeedPreferences(cursor.getLong(PodDBAdapter.IDX_FEED_SEL_STD_ID), - cursor.getInt(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD) > 0, - cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_USERNAME), - cursor.getString(PodDBAdapter.IDX_FEED_SEL_PREFERENCES_PASSWORD)); - + FeedPreferences preferences = FeedPreferences.fromCursor(cursor); feed.setPreferences(preferences); - return feed; - } - - private static DownloadStatus extractDownloadStatusFromCursorRow(final Cursor cursor) { - long id = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); - long feedfileId = cursor.getLong(PodDBAdapter.KEY_FEEDFILE_INDEX); - int feedfileType = cursor.getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX); - boolean successful = cursor.getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0; - int reason = cursor.getInt(PodDBAdapter.KEY_REASON_INDEX); - String reasonDetailed = cursor.getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX); - String title = cursor.getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX); - Date completionDate = new Date(cursor.getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX)); - - return new DownloadStatus(id, title, feedfileId, - feedfileType, successful, DownloadError.fromCode(reason), completionDate, - reasonDetailed); - } - - private static FeedItem getMatchingItemForMedia(long itemId, - List<FeedItem> items) { - for (FeedItem item : items) { - if (item.getId() == itemId) { - return item; - } - } - return null; + return feed; } - static List<FeedItem> getQueue(Context context, PodDBAdapter adapter) { + static List<FeedItem> getQueue(PodDBAdapter adapter) { Log.d(TAG, "getQueue()"); - Cursor itemlistCursor = adapter.getQueueCursor(); - List<FeedItem> items = extractItemlistFromCursor(adapter, - itemlistCursor); + List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor); itemlistCursor.close(); - loadFeedDataOfFeedItemlist(context, items); - + loadAdditionalFeedItemListData(items); return items; } /** * Loads the IDs of the FeedItems in the queue. This method should be preferred over - * {@link #getQueue(android.content.Context)} if the FeedItems of the queue are not needed. + * {@link #getQueue()} if the FeedItems of the queue are not needed. * - * @param context A context that is used for opening a database connection. * @return A list of IDs sorted by the same order as the queue. The caller can wrap the returned * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties. */ - public static LongList getQueueIDList(Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - + public static LongList getQueueIDList() { + Log.d(TAG, "getQueueIDList() called"); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); LongList result = getQueueIDList(adapter); adapter.close(); - return result; } static LongList getQueueIDList(PodDBAdapter adapter) { - adapter.open(); Cursor queueCursor = adapter.getQueueIDCursor(); LongList queueIds = new LongList(queueCursor.getCount()); @@ -401,40 +303,23 @@ public final class DBReader { queueIds.add(queueCursor.getLong(0)); } while (queueCursor.moveToNext()); } + queueCursor.close(); return queueIds; } - - /** - * Return the size of the queue. - * - * @param context A context that is used for opening a database connection. - * @return Size of the queue. - */ - public static int getQueueSize(Context context) { - Log.d(TAG, "getQueueSize()"); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - int size = adapter.getQueueSize(); - adapter.close(); - return size; - } - /** * Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using - * {@link #getQueueIDList(android.content.Context)} instead. + * {@link #getQueueIDList()} instead. * - * @param context A context that is used for opening a database connection. * @return A list of FeedItems sorted by the same order as the queue. The caller can wrap the returned * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties. */ - public static List<FeedItem> getQueue(Context context) { - Log.d(TAG, "getQueue()"); + public static List<FeedItem> getQueue() { + Log.d(TAG, "getQueue() called with: " + ""); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - List<FeedItem> items = getQueue(context, adapter); + List<FeedItem> items = getQueue(adapter); adapter.close(); return items; } @@ -442,24 +327,23 @@ public final class DBReader { /** * Loads a list of FeedItems whose episode has been downloaded. * - * @param context A context that is used for opening a database connection. * @return A list of FeedItems whose episdoe has been downloaded. */ - public static List<FeedItem> getDownloadedItems(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting downloaded items"); + public static List<FeedItem> getDownloadedItems() { + Log.d(TAG, "getDownloadedItems() called with: " + ""); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor itemlistCursor = adapter.getDownloadedItemsCursor(); List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor); itemlistCursor.close(); - loadFeedDataOfFeedItemlist(context, items); + loadAdditionalFeedItemListData(items); + adapter.close(); + Collections.sort(items, new FeedItemPubdateComparator()); - adapter.close(); return items; } @@ -467,23 +351,18 @@ public final class DBReader { /** * Loads a list of FeedItems whose 'read'-attribute is set to false. * - * @param context A context that is used for opening a database connection. - * @return A list of FeedItems whose 'read'-attribute it set to false. If the FeedItems in the list are not used, - * consider using {@link #getUnreadItemIds(android.content.Context)} instead. + * @return A list of FeedItems whose 'read'-attribute it set to false. */ - public static List<FeedItem> getUnreadItemsList(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting unread items list"); + public static List<FeedItem> getUnreadItemsList() { + Log.d(TAG, "getUnreadItemsList() called"); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor itemlistCursor = adapter.getUnreadItemsCursor(); - List<FeedItem> items = extractItemlistFromCursor(adapter, - itemlistCursor); + List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor); itemlistCursor.close(); - loadFeedDataOfFeedItemlist(context, items); + loadAdditionalFeedItemListData(items); adapter.close(); @@ -492,70 +371,76 @@ public final class DBReader { /** * Loads a list of FeedItems that are considered new. - * - * @param context A context that is used for opening a database connection. + * Excludes items from feeds that do not have keep updated enabled. * @return A list of FeedItems that are considered new. */ - public static List<FeedItem> getNewItemsList(Context context) { - Log.d(TAG, "getNewItemsList()"); + public static List<FeedItem> getNewItemsList() { + Log.d(TAG, "getNewItemsList() called"); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor itemlistCursor = adapter.getNewItemsCursor(); List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor); itemlistCursor.close(); - loadFeedDataOfFeedItemlist(context, items); + loadAdditionalFeedItemListData(items); adapter.close(); return items; } - /** - * Loads the IDs of the FeedItems whose 'read'-attribute is set to false. - * - * @param context A context that is used for opening a database connection. - * @return A list of IDs of the FeedItems whose 'read'-attribute is set to false. This method should be preferred - * over {@link #getUnreadItemsList(android.content.Context)} if the FeedItems in the UnreadItems list are not used. - */ - public static LongList getNewItemIds(Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); + public static List<FeedItem> getFavoriteItemsList() { + Log.d(TAG, "getFavoriteItemsList() called"); + + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Cursor cursor = adapter.getNewItemIdsCursor(); - LongList itemIds = new LongList(cursor.getCount()); - int i = 0; - if (cursor.moveToFirst()) { + + Cursor itemlistCursor = adapter.getFavoritesCursor(); + List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor); + itemlistCursor.close(); + + loadAdditionalFeedItemListData(items); + + adapter.close(); + + return items; + } + + public static LongList getFavoriteIDList() { + Log.d(TAG, "getFavoriteIDList() called"); + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + Cursor favoritesCursor = adapter.getFavoritesCursor(); + + LongList favoriteIDs = new LongList(favoritesCursor.getCount()); + if (favoritesCursor.moveToFirst()) { do { - long id = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); - itemIds.add(id); - i++; - } while (cursor.moveToNext()); + favoriteIDs.add(favoritesCursor.getLong(0)); + } while (favoritesCursor.moveToNext()); } - return itemIds; + favoritesCursor.close(); + adapter.close(); + return favoriteIDs; } - /** * Loads a list of FeedItems sorted by pubDate in descending order. * - * @param context A context that is used for opening a database connection. * @param limit The maximum number of episodes that should be loaded. */ - public static List<FeedItem> getRecentlyPublishedEpisodes(Context context, int limit) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting recently published items list"); + public static List<FeedItem> getRecentlyPublishedEpisodes(int limit) { + Log.d(TAG, "getRecentlyPublishedEpisodes() called with: " + "limit = [" + limit + "]"); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor itemlistCursor = adapter.getRecentlyPublishedItemsCursor(limit); - List<FeedItem> items = extractItemlistFromCursor(adapter, - itemlistCursor); + List<FeedItem> items = extractItemlistFromCursor(adapter, itemlistCursor); itemlistCursor.close(); - loadFeedDataOfFeedItemlist(context, items); + loadAdditionalFeedItemListData(items); adapter.close(); @@ -566,26 +451,25 @@ public final class DBReader { * Loads the playback history from the database. A FeedItem is in the playback history if playback of the correpsonding episode * has been completed at least once. * - * @param context A context that is used for opening a database connection. * @return The playback history. The FeedItems are sorted by their media's playbackCompletionDate in descending order. * The size of the returned list is limited by {@link #PLAYBACK_HISTORY_SIZE}. */ - public static List<FeedItem> getPlaybackHistory(final Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading playback history"); + public static List<FeedItem> getPlaybackHistory() { + Log.d(TAG, "getPlaybackHistory() called"); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor mediaCursor = adapter.getCompletedMediaCursor(PLAYBACK_HISTORY_SIZE); String[] itemIds = new String[mediaCursor.getCount()]; for (int i = 0; i < itemIds.length && mediaCursor.moveToPosition(i); i++) { - itemIds[i] = Long.toString(mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX)); + int index = mediaCursor.getColumnIndex(PodDBAdapter.KEY_FEEDITEM); + itemIds[i] = Long.toString(mediaCursor.getLong(index)); } mediaCursor.close(); Cursor itemCursor = adapter.getFeedItemCursor(itemIds); List<FeedItem> items = extractItemlistFromCursor(adapter, itemCursor); - loadFeedDataOfFeedItemlist(context, items); + loadAdditionalFeedItemListData(items); itemCursor.close(); adapter.close(); @@ -596,26 +480,25 @@ public final class DBReader { /** * Loads the download log from the database. * - * @param context A context that is used for opening a database connection. * @return A list with DownloadStatus objects that represent the download log. * The size of the returned list is limited by {@link #DOWNLOAD_LOG_SIZE}. */ - public static List<DownloadStatus> getDownloadLog(Context context) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Extracting DownloadLog"); + public static List<DownloadStatus> getDownloadLog() { + Log.d(TAG, "getDownloadLog() called"); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor logCursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE); - List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>( - logCursor.getCount()); + List<DownloadStatus> downloadLog = new ArrayList<>(logCursor.getCount()); if (logCursor.moveToFirst()) { do { - downloadLog.add(extractDownloadStatusFromCursorRow(logCursor)); + DownloadStatus status = DownloadStatus.fromCursor(logCursor); + downloadLog.add(status); } while (logCursor.moveToNext()); } logCursor.close(); + adapter.close(); Collections.sort(downloadLog, new DownloadStatusComparator()); return downloadLog; } @@ -623,77 +506,47 @@ public final class DBReader { /** * Loads the download log for a particular feed from the database. * - * @param context A context that is used for opening a database connection. * @param feed Feed for which the download log is loaded * @return A list with DownloadStatus objects that represent the feed's download log, * newest events first. */ - public static List<DownloadStatus> getFeedDownloadLog(Context context, Feed feed) { - Log.d(TAG, "getFeedDownloadLog(CONTEXT, " + feed.toString() + ")"); + public static List<DownloadStatus> getFeedDownloadLog(Feed feed) { + Log.d(TAG, "getFeedDownloadLog() called with: " + "feed = [" + feed + "]"); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor cursor = adapter.getDownloadLog(Feed.FEEDFILETYPE_FEED, feed.getId()); - List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>( - cursor.getCount()); - - if (cursor.moveToFirst()) { - do { - downloadLog.add(extractDownloadStatusFromCursorRow(cursor)); - } while (cursor.moveToNext()); - } - cursor.close(); - Collections.sort(downloadLog, new DownloadStatusComparator()); - return downloadLog; - } - - /** - * Loads the download log for a particular feed media from the database. - * - * @param context A context that is used for opening a database connection. - * @param media Feed media for which the download log is loaded - * @return A list with DownloadStatus objects that represent the feed media's download log, - * newest events first. - */ - public static List<DownloadStatus> getFeedMediaDownloadLog(Context context, FeedMedia media) { - Log.d(TAG, "getFeedDownloadLog(CONTEXT, " + media.toString() + ")"); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - Cursor cursor = adapter.getDownloadLog(FeedMedia.FEEDFILETYPE_FEEDMEDIA, media.getId()); - List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>( - cursor.getCount()); + List<DownloadStatus> downloadLog = new ArrayList<>(cursor.getCount()); if (cursor.moveToFirst()) { do { - downloadLog.add(extractDownloadStatusFromCursorRow(cursor)); + DownloadStatus status = DownloadStatus.fromCursor(cursor); + downloadLog.add(status); } while (cursor.moveToNext()); } cursor.close(); + adapter.close(); Collections.sort(downloadLog, new DownloadStatusComparator()); return downloadLog; } /** * Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over - * {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)} if only metadata about + * {@link #getFeedItemList(Feed)} if only metadata about * the FeedItems is needed. * - * @param context A context that is used for opening a database connection. * @return A list of FeedItemStatistics objects sorted alphabetically by their Feed's title. */ - public static List<FeedItemStatistics> getFeedStatisticsList(final Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); + public static List<FeedItemStatistics> getFeedStatisticsList() { + Log.d(TAG, "getFeedStatisticsList() called"); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - List<FeedItemStatistics> result = new ArrayList<FeedItemStatistics>(); + List<FeedItemStatistics> result = new ArrayList<>(); Cursor cursor = adapter.getFeedStatisticsCursor(); if (cursor.moveToFirst()) { do { - result.add(new FeedItemStatistics(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_FEED), - cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NUM_ITEMS), - cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NEW_ITEMS), - cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES), - new Date(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_LATEST_EPISODE)))); + FeedItemStatistics fis = FeedItemStatistics.fromCursor(cursor); + result.add(fis); } while (cursor.moveToNext()); } @@ -705,28 +558,26 @@ public final class DBReader { /** * Loads a specific Feed from the database. * - * @param context A context that is used for opening a database connection. * @param feedId The ID of the Feed * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the * database and the items-attribute will be set correctly. */ - public static Feed getFeed(final Context context, final long feedId) { - PodDBAdapter adapter = new PodDBAdapter(context); + public static Feed getFeed(final long feedId) { + Log.d(TAG, "getFeed() called with: " + "feedId = [" + feedId + "]"); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - Feed result = getFeed(context, feedId, adapter); + Feed result = getFeed(feedId, adapter); adapter.close(); return result; } - static Feed getFeed(final Context context, final long feedId, PodDBAdapter adapter) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading feed with id " + feedId); + static Feed getFeed(final long feedId, PodDBAdapter adapter) { Feed feed = null; Cursor feedCursor = adapter.getFeedCursor(feedId); if (feedCursor.moveToFirst()) { feed = extractFeedFromCursorRow(adapter, feedCursor); - feed.setItems(getFeedItemList(context, feed)); + feed.setItems(getFeedItemList(feed)); } else { Log.e(TAG, "getFeed could not find feed with id " + feedId); } @@ -734,9 +585,8 @@ public final class DBReader { return feed; } - static FeedItem getFeedItem(final Context context, final long itemId, PodDBAdapter adapter) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading feeditem with id " + itemId); + static FeedItem getFeedItem(final long itemId, PodDBAdapter adapter) { + Log.d(TAG, "Loading feeditem with id " + itemId); FeedItem item = null; Cursor itemCursor = adapter.getFeedItemCursor(Long.toString(itemId)); @@ -744,16 +594,17 @@ public final class DBReader { List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor); if (list.size() > 0) { item = list.get(0); - loadFeedDataOfFeedItemlist(context, list); + loadAdditionalFeedItemListData(list); if (item.hasChapters()) { loadChaptersOfFeedItem(adapter, item); } } } + itemCursor.close(); return item; } - static List<FeedItem> getFeedItems(final Context context, PodDBAdapter adapter, final long... itemIds) { + static List<FeedItem> getFeedItems(PodDBAdapter adapter, final long... itemIds) { String[] ids = new String[itemIds.length]; for(int i = 0; i < itemIds.length; i++) { @@ -766,7 +617,7 @@ public final class DBReader { Cursor itemCursor = adapter.getFeedItemCursor(ids); if (itemCursor.moveToFirst()) { result = extractItemlistFromCursor(adapter, itemCursor); - loadFeedDataOfFeedItemlist(context, result); + loadAdditionalFeedItemListData(result); for(FeedItem item : result) { if (item.hasChapters()) { loadChaptersOfFeedItem(adapter, item); @@ -775,6 +626,7 @@ public final class DBReader { } else { result = Collections.emptyList(); } + itemCursor.close(); return result; } @@ -783,23 +635,21 @@ public final class DBReader { * Loads a specific FeedItem from the database. This method should not be used for loading more * than one FeedItem because this method might query the database several times for each item. * - * @param context A context that is used for opening a database connection. * @param itemId The ID of the FeedItem * @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes * as well as chapter marks of the FeedItem will also be loaded from the database. */ - public static FeedItem getFeedItem(final Context context, final long itemId) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Loading feeditem with id " + itemId); + public static FeedItem getFeedItem(final long itemId) { + Log.d(TAG, "getFeedItem() called with: " + "itemId = [" + itemId + "]"); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - FeedItem item = getFeedItem(context, itemId, adapter); + FeedItem item = getFeedItem(itemId, adapter); adapter.close(); return item; } - static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) { + static FeedItem getFeedItem(final String podcastUrl, final String episodeUrl, PodDBAdapter adapter) { Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl); FeedItem item = null; Cursor itemCursor = adapter.getFeedItemCursor(podcastUrl, episodeUrl); @@ -807,12 +657,13 @@ public final class DBReader { List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor); if (list.size() > 0) { item = list.get(0); - loadFeedDataOfFeedItemlist(context, list); + loadAdditionalFeedItemListData(list); if (item.hasChapters()) { loadChaptersOfFeedItem(adapter, item); } } } + itemCursor.close(); return item; } @@ -820,17 +671,15 @@ public final class DBReader { * Loads specific FeedItems from the database. This method canbe used for loading more * than one FeedItem * - * @param context A context that is used for opening a database connection. * @param itemIds The IDs of the FeedItems * @return The FeedItems or an empty list if none of the FeedItems could be found. All FeedComponent-attributes * as well as chapter marks of the FeedItems will also be loaded from the database. */ - public static List<FeedItem> getFeedItems(final Context context, final long... itemIds) { - Log.d(TAG, "Loading feeditem with ids: " + StringUtils.join(itemIds, ",")); - - PodDBAdapter adapter = new PodDBAdapter(context); + public static List<FeedItem> getFeedItems(final long... itemIds) { + Log.d(TAG, "getFeedItems() called with: " + "itemIds = [" + itemIds + "]"); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - List<FeedItem> items = getFeedItems(context, adapter, itemIds); + List<FeedItem> items = getFeedItems(adapter, itemIds); adapter.close(); return items; } @@ -839,47 +688,55 @@ public final class DBReader { /** * Returns credentials based on image URL * - * @param context A context that is used for opening a database connection. * @param imageUrl The URL of the image * @return Credentials in format "<Username>:<Password>", empty String if no authorization given */ - public static String getImageAuthentication(final Context context, final String imageUrl) { - Log.d(TAG, "Loading credentials for image with URL " + imageUrl); + public static String getImageAuthentication(final String imageUrl) { + Log.d(TAG, "getImageAuthentication() called with: " + "imageUrl = [" + imageUrl + "]"); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - String credentials = getImageAuthentication(context, imageUrl, adapter); + String credentials = getImageAuthentication(imageUrl, adapter); adapter.close(); return credentials; } - static String getImageAuthentication(final Context context, final String imageUrl, PodDBAdapter adapter) { + static String getImageAuthentication(final String imageUrl, PodDBAdapter adapter) { String credentials = null; Cursor cursor = adapter.getImageAuthenticationCursor(imageUrl); - if (cursor.moveToFirst()) { - String username = cursor.getString(0); - String password = cursor.getString(1); - return username + ":" + password; + try { + if (cursor.moveToFirst()) { + String username = cursor.getString(0); + String password = cursor.getString(1); + if(username != null && password != null) { + credentials = username + ":" + password; + } else { + credentials = ""; + } + } else { + credentials = ""; + } + } finally { + cursor.close(); } - return ""; + return credentials; } /** * Loads a specific FeedItem from the database. * - * @param context A context that is used for opening a database connection. * @param podcastUrl the corresponding feed's url * @param episodeUrl the feed item's url * @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes * as well as chapter marks of the FeedItem will also be loaded from the database. */ - public static FeedItem getFeedItem(final Context context, final String podcastUrl, final String episodeUrl) { - Log.d(TAG, "Loading feeditem with podcast url " + podcastUrl + " and episode url " + episodeUrl); + public static FeedItem getFeedItem(final String podcastUrl, final String episodeUrl) { + Log.d(TAG, "getFeedItem() called with: " + "podcastUrl = [" + podcastUrl + "], episodeUrl = [" + episodeUrl + "]"); - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - FeedItem item = getFeedItem(context, podcastUrl, episodeUrl, adapter); + FeedItem item = getFeedItem(podcastUrl, episodeUrl, adapter); adapter.close(); return item; } @@ -887,21 +744,22 @@ public final class DBReader { /** * Loads additional information about a FeedItem, e.g. shownotes * - * @param context A context that is used for opening a database connection. * @param item The FeedItem */ - public static void loadExtraInformationOfFeedItem(final Context context, final FeedItem item) { - PodDBAdapter adapter = new PodDBAdapter(context); + public static void loadExtraInformationOfFeedItem(final FeedItem item) { + Log.d(TAG, "loadExtraInformationOfFeedItem() called with: " + "item = [" + item + "]"); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor extraCursor = adapter.getExtraInformationOfItem(item); if (extraCursor.moveToFirst()) { - String description = extraCursor - .getString(PodDBAdapter.IDX_FI_EXTRA_DESCRIPTION); - String contentEncoded = extraCursor - .getString(PodDBAdapter.IDX_FI_EXTRA_CONTENT_ENCODED); + int indexDescription = extraCursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION); + String description = extraCursor.getString(indexDescription); + int indexContentEncoded = extraCursor.getColumnIndex(PodDBAdapter.KEY_CONTENT_ENCODED); + String contentEncoded = extraCursor.getString(indexContentEncoded); item.setDescription(description); item.setContentEncoded(contentEncoded); } + extraCursor.close(); adapter.close(); } @@ -910,31 +768,31 @@ public final class DBReader { * any chapters that this FeedItem has. If no chapters were found in the database, the chapters * reference of the FeedItem will be set to null. * - * @param context A context that is used for opening a database connection. * @param item The FeedItem */ - public static void loadChaptersOfFeedItem(final Context context, final FeedItem item) { - PodDBAdapter adapter = new PodDBAdapter(context); + public static void loadChaptersOfFeedItem(final FeedItem item) { + Log.d(TAG, "loadChaptersOfFeedItem() called with: " + "item = [" + item + "]"); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); loadChaptersOfFeedItem(adapter, item); adapter.close(); } static void loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) { - Cursor chapterCursor = adapter - .getSimpleChaptersOfFeedItemCursor(item); + Cursor chapterCursor = adapter.getSimpleChaptersOfFeedItemCursor(item); if (chapterCursor.moveToFirst()) { - item.setChapters(new ArrayList<Chapter>()); + item.setChapters(new ArrayList<>()); do { - int chapterType = chapterCursor - .getInt(PodDBAdapter.KEY_CHAPTER_TYPE_INDEX); + int indexType = chapterCursor.getColumnIndex(PodDBAdapter.KEY_CHAPTER_TYPE); + int indexStart = chapterCursor.getColumnIndex(PodDBAdapter.KEY_START); + int indexTitle = chapterCursor.getColumnIndex(PodDBAdapter.KEY_TITLE); + int indexLink = chapterCursor.getColumnIndex(PodDBAdapter.KEY_LINK); + + int chapterType = chapterCursor.getInt(indexType); Chapter chapter = null; - long start = chapterCursor - .getLong(PodDBAdapter.KEY_CHAPTER_START_INDEX); - String title = chapterCursor - .getString(PodDBAdapter.KEY_TITLE_INDEX); - String link = chapterCursor - .getString(PodDBAdapter.KEY_CHAPTER_LINK_INDEX); + long start = chapterCursor.getLong(indexStart); + String title = chapterCursor.getString(indexTitle); + String link = chapterCursor.getString(indexLink); switch (chapterType) { case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER: @@ -951,8 +809,8 @@ public final class DBReader { break; } if (chapter != null) { - chapter.setId(chapterCursor - .getLong(PodDBAdapter.KEY_ID_INDEX)); + int indexId = chapterCursor.getColumnIndex(PodDBAdapter.KEY_ID); + chapter.setId(chapterCursor.getLong(indexId)); item.getChapters().add(chapter); } } while (chapterCursor.moveToNext()); @@ -965,11 +823,11 @@ public final class DBReader { /** * Returns the number of downloaded episodes. * - * @param context A context that is used for opening a database connection. * @return The number of downloaded episodes. */ - public static int getNumberOfDownloadedEpisodes(final Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); + public static int getNumberOfDownloadedEpisodes() { + Log.d(TAG, "getNumberOfDownloadedEpisodes() called with: " + ""); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); final int result = adapter.getNumberOfDownloadedEpisodes(); adapter.close(); @@ -977,29 +835,16 @@ public final class DBReader { } /** - * Returns the number of unread items. - * - * @param context A context that is used for opening a database connection. - * @return The number of unread items. - */ - public static int getNumberOfNewItems(final Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - final int result = adapter.getNumberOfNewItems(); - adapter.close(); - return result; - } - - /** - * Returns a map containing the number of unread items per feed + * Searches the DB for a FeedImage of the given id. * - * @param context A context that is used for opening a database connection. - * @return The number of unread items per feed. + * @param imageId The id of the object + * @return The found object */ - public static LongIntMap getNumberOfUnreadFeedItems(final Context context, long... feedIds) { - PodDBAdapter adapter = new PodDBAdapter(context); + public static FeedImage getFeedImage(final long imageId) { + Log.d(TAG, "getFeedImage() called with: " + "imageId = [" + imageId + "]"); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - final LongIntMap result = adapter.getNumberOfUnreadFeedItems(feedIds); + FeedImage result = getFeedImage(adapter, imageId); adapter.close(); return result; } @@ -1007,60 +852,58 @@ public final class DBReader { /** * Searches the DB for a FeedImage of the given id. * - * @param context A context that is used for opening a database connection. * @param imageId The id of the object * @return The found object */ - public static FeedImage getFeedImage(final Context context, final long imageId) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - FeedImage result = getFeedImage(adapter, imageId); - adapter.close(); - return result; + private static FeedImage getFeedImage(PodDBAdapter adapter, final long imageId) { + return getFeedImages(adapter, imageId).get(imageId); } /** * Searches the DB for a FeedImage of the given id. * - * @param id The id of the object - * @return The found object + * @param imageIds The ids of the images + * @return Map that associates the id of an image with the image itself */ - static FeedImage getFeedImage(PodDBAdapter adapter, final long id) { - Cursor cursor = adapter.getImageCursor(id); - if ((cursor.getCount() == 0) || !cursor.moveToFirst()) { - return null; + private static Map<Long,FeedImage> getFeedImages(PodDBAdapter adapter, final long... imageIds) { + String[] ids = new String[imageIds.length]; + for(int i=0, len=imageIds.length; i < len; i++) { + ids[i] = String.valueOf(imageIds[i]); } - FeedImage image = new FeedImage(id, cursor.getString(cursor - .getColumnIndex(PodDBAdapter.KEY_TITLE)), - cursor.getString(cursor - .getColumnIndex(PodDBAdapter.KEY_FILE_URL)), - cursor.getString(cursor - .getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL)), - cursor.getInt(cursor - .getColumnIndex(PodDBAdapter.KEY_DOWNLOADED)) > 0 - ); - cursor.close(); - return image; + Cursor cursor = adapter.getImageCursor(ids); + Map<Long, FeedImage> result = new ArrayMap<>(cursor.getCount()); + try { + if ((cursor.getCount() == 0) || !cursor.moveToFirst()) { + return Collections.emptyMap(); + } + do { + FeedImage image = FeedImage.fromCursor(cursor); + result.put(image.getId(), image); + } while(cursor.moveToNext()); + } finally { + cursor.close(); + } + return result; } /** * Searches the DB for a FeedMedia of the given id. * - * @param context A context that is used for opening a database connection. * @param mediaId The id of the object * @return The found object */ - public static FeedMedia getFeedMedia(final Context context, final long mediaId) { - PodDBAdapter adapter = new PodDBAdapter(context); + public static FeedMedia getFeedMedia(final long mediaId) { + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor mediaCursor = adapter.getSingleFeedMediaCursor(mediaId); FeedMedia media = null; if (mediaCursor.moveToFirst()) { - final long itemId = mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); - media = extractFeedMediaFromCursorRow(mediaCursor); - FeedItem item = getFeedItem(context, itemId); + int indexFeedItem = mediaCursor.getColumnIndex(PodDBAdapter.KEY_FEEDITEM); + final long itemId = mediaCursor.getLong(indexFeedItem); + media = FeedMedia.fromCursor(mediaCursor); + FeedItem item = getFeedItem(itemId); if (media != null && item != null) { media.setItem(item); item.setMedia(media); @@ -1076,13 +919,13 @@ public final class DBReader { /** * Returns the flattr queue as a List of FlattrThings. The list consists of Feeds and FeedItems. * - * @param context A context that is used for opening a database connection. * @return The flattr queue as a List. */ - public static List<FlattrThing> getFlattrQueue(Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); + public static List<FlattrThing> getFlattrQueue() { + Log.d(TAG, "getFlattrQueue() called with: " + ""); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - List<FlattrThing> result = new ArrayList<FlattrThing>(); + List<FlattrThing> result = new ArrayList<>(); // load feeds Cursor feedCursor = adapter.getFeedsInFlattrQueueCursor(); @@ -1103,56 +946,78 @@ public final class DBReader { return result; } - - /** - * Returns true if the flattr queue is empty. - * - * @param context A context that is used for opening a database connection. - */ - public static boolean getFlattrQueueEmpty(Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - boolean empty = adapter.getFlattrQueueSize() == 0; - adapter.close(); - return empty; - } - /** * Returns data necessary for displaying the navigation drawer. This includes * the list of subscriptions, the number of items in the queue and the number of unread * items. * - * @param context A context that is used for opening a database connection. */ - public static NavDrawerData getNavDrawerData(Context context) { - PodDBAdapter adapter = new PodDBAdapter(context); + public static NavDrawerData getNavDrawerData() { + Log.d(TAG, "getNavDrawerData() called with: " + ""); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); List<Feed> feeds = getFeedList(adapter); long[] feedIds = new long[feeds.size()]; for(int i=0; i < feeds.size(); i++) { feedIds[i] = feeds.get(i).getId(); } - final LongIntMap numUnreadFeedItems = adapter.getNumberOfUnreadFeedItems(feedIds); - Collections.sort(feeds, new Comparator<Feed>() { - @Override - public int compare(Feed lhs, Feed rhs) { - long numUnreadLhs = numUnreadFeedItems.get(lhs.getId()); - Log.d(TAG, "feed with id " + lhs.getId() + " has " + numUnreadLhs + " unread items"); - long numUnreadRhs = numUnreadFeedItems.get(rhs.getId()); - Log.d(TAG, "feed with id " + rhs.getId() + " has " + numUnreadRhs + " unread items"); - if(numUnreadLhs > numUnreadRhs) { + final LongIntMap feedCounters = adapter.getFeedCounters(feedIds); + + Comparator<Feed> comparator; + int feedOrder = UserPreferences.getFeedOrder(); + if(feedOrder == UserPreferences.FEED_ORDER_COUNTER) { + comparator = (lhs, rhs) -> { + long counterLhs = feedCounters.get(lhs.getId()); + long counterRhs = feedCounters.get(rhs.getId()); + if(counterLhs > counterRhs) { // reverse natural order: podcast with most unplayed episodes first return -1; - } else if(numUnreadLhs == numUnreadRhs) { + } else if(counterLhs == counterRhs) { return lhs.getTitle().compareTo(rhs.getTitle()); } else { return 1; } - } - }); + }; + } else if(feedOrder == UserPreferences.FEED_ORDER_ALPHABETICAL) { + comparator = (lhs, rhs) -> { + String t1 = lhs.getTitle(); + String t2 = rhs.getTitle(); + if(t1 == null) { + return 1; + } else if(t2 == null) { + return -1; + } else { + return t1.toLowerCase().compareTo(t2.toLowerCase()); + } + }; + } else { + comparator = (lhs, rhs) -> { + if(lhs.getItems() == null || lhs.getItems().size() == 0) { + List<FeedItem> items = DBReader.getFeedItemList(lhs); + lhs.setItems(items); + } + if(rhs.getItems() == null || rhs.getItems().size() == 0) { + List<FeedItem> items = DBReader.getFeedItemList(rhs); + rhs.setItems(items); + } + if(lhs.getMostRecentItem() == null) { + return 1; + } else if(rhs.getMostRecentItem() == null) { + return -1; + } else { + Date d1 = lhs.getMostRecentItem().getPubDate(); + Date d2 = rhs.getMostRecentItem().getPubDate(); + return d2.compareTo(d1); + } + }; + } + + Collections.sort(feeds, comparator); int queueSize = adapter.getQueueSize(); int numNewItems = adapter.getNumberOfNewItems(); - NavDrawerData result = new NavDrawerData(feeds, queueSize, numNewItems, numUnreadFeedItems); + int numDownloadedItems = adapter.getNumberOfDownloadedEpisodes(); + + NavDrawerData result = new NavDrawerData(feeds, queueSize, numNewItems, numDownloadedItems, feedCounters); adapter.close(); return result; } @@ -1161,14 +1026,19 @@ public final class DBReader { public List<Feed> feeds; public int queueSize; public int numNewItems; - public LongIntMap numUnreadFeedItems; - - public NavDrawerData(List<Feed> feeds, int queueSize, int numNewItems, - LongIntMap numUnreadFeedItems) { + public int numDownloadedItems; + public LongIntMap feedCounters; + + public NavDrawerData(List<Feed> feeds, + int queueSize, + int numNewItems, + int numDownloadedItems, + LongIntMap feedIndicatorValues) { this.feeds = feeds; this.queueSize = queueSize; this.numNewItems = numNewItems; - this.numUnreadFeedItems = numUnreadFeedItems; + this.numDownloadedItems = numDownloadedItems; + this.feedCounters = feedIndicatorValues; } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index e570ee709..efc60bfc2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -6,7 +6,6 @@ import android.database.Cursor; import android.util.Log; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.Iterator; @@ -25,10 +24,9 @@ import de.danoeh.antennapod.core.asynctask.FlattrClickWorker; import de.danoeh.antennapod.core.asynctask.FlattrStatusFetcher; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; -import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.service.GpodnetSyncService; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.service.playback.PlaybackService; @@ -70,7 +68,7 @@ public final class DBTasks { * @param downloadUrl URL of the feed. */ public static void removeFeedWithDownloadUrl(Context context, String downloadUrl) { - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); Cursor cursor = adapter.getFeedCursorDownloadUrls(); long feedID = 0; @@ -165,7 +163,7 @@ public final class DBTasks { if (feeds != null) { refreshFeeds(context, feeds); } else { - refreshFeeds(context, DBReader.getFeedList(context)); + refreshFeeds(context, DBReader.getFeedList()); } isRefreshing.set(false); @@ -180,6 +178,7 @@ public final class DBTasks { if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { GpodnetSyncService.sendSyncIntent(context); } + Log.d(TAG, "refreshAllFeeds autodownload"); autodownloadUndownloadedItems(context); } }.start(); @@ -189,62 +188,29 @@ public final class DBTasks { } /** - * Used by refreshExpiredFeeds to determine which feeds should be refreshed. - * This method will use the value specified in the UserPreferences as the - * expiration time. - * - * @param context Used for DB access. - * @return A list of expired feeds. An empty list will be returned if there - * are no expired feeds. - */ - public static List<Feed> getExpiredFeeds(final Context context) { - long millis = UserPreferences.getUpdateInterval(); - - if (millis > 0) { - - List<Feed> feedList = DBReader.getExpiredFeedsList(context, - millis); - if (feedList.size() > 0) { - refreshFeeds(context, feedList); - } - return feedList; - } else { - return new ArrayList<Feed>(); - } - } - - /** - * Refreshes expired Feeds in the list returned by the getExpiredFeedsList(Context, long) method in DBReader. - * The expiration date parameter is determined by the update interval specified in {@link UserPreferences}. - * - * @param context Used for DB access. + * @param context + * @param feedList the list of feeds to refresh */ - public static void refreshExpiredFeeds(final Context context) { - Log.d(TAG, "Refreshing expired feeds"); - - new Thread() { - public void run() { - refreshFeeds(context, getExpiredFeeds(context)); - } - }.start(); - } - private static void refreshFeeds(final Context context, final List<Feed> feedList) { for (Feed feed : feedList) { - try { - refreshFeed(context, feed); - } catch (DownloadRequestException e) { - e.printStackTrace(); - DBWriter.addDownloadStatus( - context, - new DownloadStatus(feed, feed - .getHumanReadableIdentifier(), - DownloadError.ERROR_REQUEST_ERROR, false, e - .getMessage() - ) - ); + FeedPreferences prefs = feed.getPreferences(); + // feeds with !getKeepUpdated can only be refreshed + // directly from the FeedActivity + if (prefs.getKeepUpdated()) { + try { + refreshFeed(context, feed); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus( + new DownloadStatus(feed, feed + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, false, e + .getMessage() + ) + ); + } } } @@ -262,7 +228,6 @@ public final class DBTasks { } catch (DownloadRequestException e) { e.printStackTrace(); DBWriter.addDownloadStatus( - context, new DownloadStatus(feed, feed .getHumanReadableIdentifier(), DownloadError.ERROR_REQUEST_ERROR, false, e @@ -302,16 +267,17 @@ public final class DBTasks { */ public static void refreshFeed(Context context, Feed feed) throws DownloadRequestException { - Log.d(TAG, "id " + feed.getId()); + Log.d(TAG, "refreshFeed(feed.id: " + feed.getId() +")"); refreshFeed(context, feed, false); } private static void refreshFeed(Context context, Feed feed, boolean loadAllPages) throws DownloadRequestException { Feed f; + Date lastUpdate = feed.hasLastUpdateFailed() ? new Date(0) : feed.getLastUpdate(); if (feed.getPreferences() == null) { - f = new Feed(feed.getDownload_url(), feed.getLastUpdate(), feed.getTitle()); + f = new Feed(feed.getDownload_url(), lastUpdate, feed.getTitle()); } else { - f = new Feed(feed.getDownload_url(), feed.getLastUpdate(), feed.getTitle(), + f = new Feed(feed.getDownload_url(), lastUpdate, feed.getTitle(), feed.getPreferences().getUsername(), feed.getPreferences().getPassword()); } f.setId(feed.getId()); @@ -319,24 +285,6 @@ public final class DBTasks { } /** - * Notifies the database about a missing FeedImage file. This method will attempt to re-download the file. - * - * @param context Used for requesting the download. - * @param image The FeedImage object. - */ - public static void notifyInvalidImageFile(final Context context, - final FeedImage image) { - Log.i(TAG, - "The DB was notified about an invalid image download. It will now try to re-download the image file"); - try { - DownloadRequester.getInstance().downloadImage(context, image); - } catch (DownloadRequestException e) { - e.printStackTrace(); - Log.w(TAG, "Failed to download invalid feed image"); - } - } - - /** * Notifies the database about a missing FeedMedia file. This method will correct the FeedMedia object's values in the * DB and send a FeedUpdateBroadcast. */ @@ -346,7 +294,7 @@ public final class DBTasks { "The feedmanager was notified about a missing episode. It will update its database now."); media.setDownloaded(false); media.setFile_url(null); - DBWriter.setFeedMedia(context, media); + DBWriter.setFeedMedia(media); EventDistributor.getInstance().sendFeedUpdateBroadcast(); } @@ -358,7 +306,7 @@ public final class DBTasks { public static void downloadAllItemsInQueue(final Context context) { new Thread() { public void run() { - List<FeedItem> queue = DBReader.getQueue(context); + List<FeedItem> queue = DBReader.getQueue(); if (!queue.isEmpty()) { try { downloadFeedItems(context, @@ -393,9 +341,7 @@ public final class DBTasks { @Override public void run() { ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm() - .performCleanup(context, - ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm() - .getPerformCleanupParameter(context, Arrays.asList(items))); + .makeRoomForEpisodes(context, items.length); } }.start(); @@ -409,7 +355,7 @@ public final class DBTasks { requester.downloadMedia(context, item.getMedia()); } catch (DownloadRequestException e) { e.printStackTrace(); - DBWriter.addDownloadStatus(context, + DBWriter.addDownloadStatus( new DownloadStatus(item.getMedia(), item .getMedia() .getHumanReadableIdentifier(), @@ -433,13 +379,12 @@ public final class DBTasks { * This method is executed on an internal single thread executor. * * @param context Used for accessing the DB. - * @param mediaIds If this list is not empty, the method will only download a candidate for automatic downloading if - * its media ID is in the mediaIds list. * @return A Future that can be used for waiting for the methods completion. */ - public static Future<?> autodownloadUndownloadedItems(final Context context, final long... mediaIds) { + public static Future<?> autodownloadUndownloadedItems(final Context context) { + Log.d(TAG, "autodownloadUndownloadedItems"); return autodownloadExec.submit(ClientConfig.dbTasksCallbacks.getAutomaticDownloadAlgorithm() - .autoDownloadUndownloadedItems(context, mediaIds)); + .autoDownloadUndownloadedItems(context)); } @@ -452,24 +397,21 @@ public final class DBTasks { * @param context Used for accessing the DB. */ public static void performAutoCleanup(final Context context) { - ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm().performCleanup(context, - ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm().getDefaultCleanupParameter(context)); + ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm().performCleanup(context); } /** * Returns the successor of a FeedItem in the queue. * - * @param context Used for accessing the DB. * @param itemId ID of the FeedItem * @param queue Used for determining the successor of the item. If this parameter is null, the method will load * the queue from the database in the same thread. * @return Successor of the FeedItem or null if the FeedItem is not in the queue or has no successor. */ - public static FeedItem getQueueSuccessorOfItem(Context context, - final long itemId, List<FeedItem> queue) { + public static FeedItem getQueueSuccessorOfItem(final long itemId, List<FeedItem> queue) { FeedItem result = null; if (queue == null) { - queue = DBReader.getQueue(context); + queue = DBReader.getQueue(); } if (queue != null) { Iterator<FeedItem> iterator = queue.iterator(); @@ -494,19 +436,19 @@ public final class DBTasks { * @param feedItemId ID of the FeedItem */ public static boolean isInQueue(Context context, final long feedItemId) { - LongList queue = DBReader.getQueueIDList(context); + LongList queue = DBReader.getQueueIDList(); return queue.contains(feedItemId); } - private static Feed searchFeedByIdentifyingValueOrID(Context context, PodDBAdapter adapter, + private static Feed searchFeedByIdentifyingValueOrID(PodDBAdapter adapter, Feed feed) { if (feed.getId() != 0) { - return DBReader.getFeed(context, feed.getId(), adapter); + return DBReader.getFeed(feed.getId(), adapter); } else { - List<Feed> feeds = DBReader.getFeedList(context); + List<Feed> feeds = DBReader.getFeedList(); for (Feed f : feeds) { if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) { - f.setItems(DBReader.getFeedItemList(context, f)); + f.setItems(DBReader.getFeedItemList(f)); return f; } } @@ -545,7 +487,7 @@ public final class DBTasks { List<Feed> newFeedsList = new ArrayList<Feed>(); List<Feed> updatedFeedsList = new ArrayList<Feed>(); Feed[] resultFeeds = new Feed[newFeeds.length]; - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); for (int feedIdx = 0; feedIdx < newFeeds.length; feedIdx++) { @@ -553,7 +495,7 @@ public final class DBTasks { final Feed newFeed = newFeeds[feedIdx]; // Look up feed in the feedslist - final Feed savedFeed = searchFeedByIdentifyingValueOrID(context, adapter, + final Feed savedFeed = searchFeedByIdentifyingValueOrID(adapter, newFeed); if (savedFeed == null) { Log.d(TAG, "Found no existing Feed with title " @@ -563,7 +505,7 @@ public final class DBTasks { // all new feeds will have the most recent item marked as unplayed FeedItem mostRecent = newFeed.getMostRecentItem(); if (mostRecent != null) { - mostRecent.setRead(false); + mostRecent.setNew(); } newFeedsList.add(newFeed); @@ -574,22 +516,27 @@ public final class DBTasks { Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator()); - final boolean markNewItemsAsUnread; if (newFeed.getPageNr() == savedFeed.getPageNr()) { if (savedFeed.compareWithOther(newFeed)) { Log.d(TAG, "Feed has updated attribute values. Updating old feed's attributes"); savedFeed.updateFromOther(newFeed); } - markNewItemsAsUnread = true; } else { - Log.d(TAG, "New feed has a higher page number. Merging without marking as unread"); - markNewItemsAsUnread = false; + Log.d(TAG, "New feed has a higher page number."); savedFeed.setNextPageLink(newFeed.getNextPageLink()); } if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) { Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences"); savedFeed.getPreferences().updateFromOther(newFeed.getPreferences()); } + + // get the most recent date now, before we start changing the list + FeedItem priorMostRecent = savedFeed.getMostRecentItem(); + Date priorMostRecentDate = null; + if (priorMostRecent != null) { + priorMostRecentDate = priorMostRecent.getPubDate(); + } + // Look for new or updated Items for (int idx = 0; idx < newFeed.getItems().size(); idx++) { final FeedItem item = newFeed.getItems().get(idx); @@ -597,12 +544,19 @@ public final class DBTasks { item.getIdentifyingValue()); if (oldItem == null) { // item is new - final int i = idx; item.setFeed(savedFeed); item.setAutoDownload(savedFeed.getPreferences().getAutoDownload()); - savedFeed.getItems().add(i, item); - if (markNewItemsAsUnread) { - item.setRead(false); + savedFeed.getItems().add(idx, item); + + // only mark the item new if it actually occurs + // before the most recent item (before we started adding things) + // (if the most recent date is null then we can assume there are no items + // and this is the first, hence 'new') + if (priorMostRecentDate == null || + priorMostRecentDate.before(item.getPubDate())) { + Log.d(TAG, "Marking item published on " + item.getPubDate() + + " new, prior most recent date = " + priorMostRecentDate); + item.setNew(); } } else { oldItem.updateFromOther(item); @@ -622,7 +576,7 @@ public final class DBTasks { try { DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[newFeedsList.size()])).get(); - DBWriter.setCompleteFeed(context, updatedFeedsList.toArray(new Feed[updatedFeedsList.size()])).get(); + DBWriter.setCompleteFeed(updatedFeedsList.toArray(new Feed[updatedFeedsList.size()])).get(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { @@ -650,8 +604,8 @@ public final class DBTasks { public void execute(PodDBAdapter adapter) { Cursor searchResult = adapter.searchItemTitles(feedID, query); - List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); - DBReader.loadFeedDataOfFeedItemlist(context, items); + List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult); + DBReader.loadAdditionalFeedItemListData(items); setResult(items); searchResult.close(); } @@ -674,8 +628,8 @@ public final class DBTasks { public void execute(PodDBAdapter adapter) { Cursor searchResult = adapter.searchItemDescriptions(feedID, query); - List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); - DBReader.loadFeedDataOfFeedItemlist(context, items); + List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult); + DBReader.loadAdditionalFeedItemListData(items); setResult(items); searchResult.close(); } @@ -698,8 +652,8 @@ public final class DBTasks { public void execute(PodDBAdapter adapter) { Cursor searchResult = adapter.searchItemContentEncoded(feedID, query); - List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); - DBReader.loadFeedDataOfFeedItemlist(context, items); + List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult); + DBReader.loadAdditionalFeedItemListData(items); setResult(items); searchResult.close(); } @@ -721,8 +675,8 @@ public final class DBTasks { public void execute(PodDBAdapter adapter) { Cursor searchResult = adapter.searchItemChapters(feedID, query); - List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); - DBReader.loadFeedDataOfFeedItemlist(context, items); + List<FeedItem> items = DBReader.extractItemlistFromCursor(searchResult); + DBReader.loadAdditionalFeedItemListData(items); setResult(items); searchResult.close(); } @@ -745,7 +699,7 @@ public final class DBTasks { @Override public T call() throws Exception { - PodDBAdapter adapter = new PodDBAdapter(context); + PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); execute(adapter); adapter.close(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index fe5d0dfd3..e728abc3b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -13,20 +13,22 @@ import org.shredzone.flattr4j.model.Flattr; import java.io.File; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.Date; -import java.util.LinkedList; import java.util.List; -import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; -import java.util.concurrent.ThreadFactory; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.asynctask.FlattrClickWorker; +import de.danoeh.antennapod.core.event.FavoritesEvent; +import de.danoeh.antennapod.core.event.FeedItemEvent; +import de.danoeh.antennapod.core.event.QueueEvent; import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedEvent; @@ -34,7 +36,6 @@ import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; -import de.danoeh.antennapod.core.feed.QueueEvent; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; @@ -56,19 +57,16 @@ import de.greenrobot.event.EventBus; * This class will use the {@link EventDistributor} to notify listeners about changes in the database. */ public class DBWriter { + private static final String TAG = "DBWriter"; private static final ExecutorService dbExec; static { - dbExec = Executors.newSingleThreadExecutor(new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } + dbExec = Executors.newSingleThreadExecutor(r -> { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; }); } @@ -83,62 +81,59 @@ public class DBWriter { */ public static Future<?> deleteFeedMediaOfItem(final Context context, final long mediaId) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - - final FeedMedia media = DBReader.getFeedMedia(context, mediaId); - if (media != null) { - Log.i(TAG, String.format("Requested to delete FeedMedia [id=%d, title=%s, downloaded=%s", - media.getId(), media.getEpisodeTitle(), String.valueOf(media.isDownloaded()))); - boolean result = false; - if (media.isDownloaded()) { - // delete downloaded media file - File mediaFile = new File(media.getFile_url()); - if (mediaFile.exists()) { - result = mediaFile.delete(); - } - media.setDownloaded(false); - media.setFile_url(null); - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setMedia(media); - adapter.close(); - - // If media is currently being played, change playback - // type to 'stream' and shutdown playback service - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context); - if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA) { - if (media.getId() == PlaybackPreferences - .getCurrentlyPlayingFeedMediaId()) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean( - PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, - true); - editor.commit(); - } - if (PlaybackPreferences - .getCurrentlyPlayingFeedMediaId() == media - .getId()) { - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - } + return dbExec.submit(() -> { + final FeedMedia media = DBReader.getFeedMedia(mediaId); + if (media != null) { + Log.i(TAG, String.format("Requested to delete FeedMedia [id=%d, title=%s, downloaded=%s", + media.getId(), media.getEpisodeTitle(), String.valueOf(media.isDownloaded()))); + boolean result = false; + if (media.isDownloaded()) { + // delete downloaded media file + File mediaFile = new File(media.getFile_url()); + if (mediaFile.exists()) { + result = mediaFile.delete(); + } + media.setDownloaded(false); + media.setFile_url(null); + media.setHasEmbeddedPicture(false); + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setMedia(media); + adapter.close(); + + // If media is currently being played, change playback + // type to 'stream' and shutdown playback service + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context); + if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA) { + if (media.getId() == PlaybackPreferences + .getCurrentlyPlayingFeedMediaId()) { + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean( + PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, + true); + editor.commit(); } - // Gpodder: queue delete action for synchronization - if(GpodnetPreferences.loggedIn()) { - FeedItem item = media.getItem(); - GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.DELETE) - .currentDeviceId() - .currentTimestamp() - .build(); - GpodnetPreferences.enqueueEpisodeAction(action); + if (PlaybackPreferences + .getCurrentlyPlayingFeedMediaId() == media + .getId()) { + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); } } - Log.d(TAG, "Deleting File. Result: " + result); - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.DELETED_MEDIA, media.getItem())); - EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + // Gpodder: queue delete action for synchronization + if(GpodnetPreferences.loggedIn()) { + FeedItem item = media.getItem(); + GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, GpodnetEpisodeAction.Action.DELETE) + .currentDeviceId() + .currentTimestamp() + .build(); + GpodnetPreferences.enqueueEpisodeAction(action); + } } + Log.d(TAG, "Deleting File. Result: " + result); + EventBus.getDefault().post(FeedItemEvent.deletedMedia(Arrays.asList(media.getItem()))); + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); } }); } @@ -150,83 +145,91 @@ public class DBWriter { * @param feedId ID of the Feed that should be deleted. */ public static Future<?> deleteFeed(final Context context, final long feedId) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - DownloadRequester requester = DownloadRequester.getInstance(); - SharedPreferences prefs = PreferenceManager - .getDefaultSharedPreferences(context - .getApplicationContext()); - final Feed feed = DBReader.getFeed(context, feedId); - if (feed != null) { - if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA - && PlaybackPreferences.getLastPlayedFeedId() == feed - .getId()) { - context.sendBroadcast(new Intent( - PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); - SharedPreferences.Editor editor = prefs.edit(); - editor.putLong( - PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, - -1); - editor.commit(); + return dbExec.submit(() -> { + DownloadRequester requester = DownloadRequester.getInstance(); + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context + .getApplicationContext()); + final Feed feed = DBReader.getFeed(feedId); + + if (feed != null) { + if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA + && PlaybackPreferences.getLastPlayedFeedId() == feed + .getId()) { + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + SharedPreferences.Editor editor = prefs.edit(); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + -1); + editor.commit(); + } + + // delete image file + if (feed.getImage() != null) { + if (feed.getImage().isDownloaded() + && feed.getImage().getFile_url() != null) { + File imageFile = new File(feed.getImage() + .getFile_url()); + imageFile.delete(); + } else if (requester.isDownloadingFile(feed.getImage())) { + requester.cancelDownload(context, feed.getImage()); } + } + // delete stored media files and mark them as read + List<FeedItem> queue = DBReader.getQueue(); + List<FeedItem> removed = new ArrayList<>(); + if (feed.getItems() == null) { + DBReader.getFeedItemList(feed); + } - // delete image file - if (feed.getImage() != null) { - if (feed.getImage().isDownloaded() - && feed.getImage().getFile_url() != null) { - File imageFile = new File(feed.getImage() - .getFile_url()); - imageFile.delete(); - } else if (requester.isDownloadingFile(feed.getImage())) { - requester.cancelDownload(context, feed.getImage()); - } + for (FeedItem item : feed.getItems()) { + if(queue.remove(item)) { + removed.add(item); } - // delete stored media files and mark them as read - List<FeedItem> queue = DBReader.getQueue(context); - boolean queueWasModified = false; - if (feed.getItems() == null) { - DBReader.getFeedItemList(context, feed); + if (item.getMedia() != null + && item.getMedia().isDownloaded()) { + File mediaFile = new File(item.getMedia() + .getFile_url()); + mediaFile.delete(); + } else if (item.getMedia() != null + && requester.isDownloadingFile(item.getMedia())) { + requester.cancelDownload(context, item.getMedia()); } - for (FeedItem item : feed.getItems()) { - queueWasModified |= queue.remove(item); - if (item.getMedia() != null - && item.getMedia().isDownloaded()) { - File mediaFile = new File(item.getMedia() - .getFile_url()); - mediaFile.delete(); - } else if (item.getMedia() != null - && requester.isDownloadingFile(item.getMedia())) { - requester.cancelDownload(context, item.getMedia()); + if (item.hasItemImage()) { + FeedImage image = item.getImage(); + if (image.isDownloaded() && image.getFile_url() != null) { + File imgFile = new File(image.getFile_url()); + imgFile.delete(); + } else if (requester.isDownloadingFile(image)) { + requester.cancelDownload(context, item.getImage()); } - - if (item.hasItemImage()) { - FeedImage image = item.getImage(); - if (image.isDownloaded() && image.getFile_url() != null) { - File imgFile = new File(image.getFile_url()); - imgFile.delete(); - } else if (requester.isDownloadingFile(image)) { - requester.cancelDownload(context, item.getImage()); - } - } - } - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - if (queueWasModified) { - adapter.setQueue(queue); } - adapter.removeFeed(feed); - adapter.close(); - - if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { - GpodnetPreferences.addRemovedFeed(feed.getDownload_url()); + } + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + if (removed.size() > 0) { + adapter.setQueue(queue); + for(FeedItem item : removed) { + EventBus.getDefault().post(QueueEvent.irreversibleRemoved(item)); } - EventDistributor.getInstance().sendFeedUpdateBroadcast(); + } + adapter.removeFeed(feed); + adapter.close(); - BackupManager backupManager = new BackupManager(context); - backupManager.dataChanged(); + if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { + GpodnetPreferences.addRemovedFeed(feed.getDownload_url()); } + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + + // we assume we also removed download log entries for the feed or its media files. + // especially important if download or refresh failed, as the user should not be able + // to retry these + EventDistributor.getInstance().sendDownloadLogUpdateBroadcast(); + + BackupManager backupManager = new BackupManager(context); + backupManager.dataChanged(); } }); } @@ -234,39 +237,27 @@ public class DBWriter { /** * Deletes the entire playback history. * - * @param context A context that is used for opening a database connection. */ - public static Future<?> clearPlaybackHistory(final Context context) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.clearPlaybackHistory(); - adapter.close(); - EventDistributor.getInstance() - .sendPlaybackHistoryUpdateBroadcast(); - } + public static Future<?> clearPlaybackHistory() { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.clearPlaybackHistory(); + adapter.close(); + EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast(); }); } /** * Deletes the entire download log. - * - * @param context A context that is used for opening a database connection. */ - public static Future<?> clearDownloadLog(final Context context) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.clearDownloadLog(); - adapter.close(); - EventDistributor.getInstance() - .sendDownloadLogUpdateBroadcast(); - } + public static Future<?> clearDownloadLog() { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.clearDownloadLog(); + adapter.close(); + EventDistributor.getInstance().sendDownloadLogUpdateBroadcast(); }); } @@ -276,58 +267,36 @@ public class DBWriter { * its playback completion date is set to a non-null value. This method will set the playback completion date to the * current date regardless of the current value. * - * @param context A context that is used for opening a database connection. * @param media FeedMedia that should be added to the playback history. */ - public static Future<?> addItemToPlaybackHistory(final Context context, - final FeedMedia media) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Adding new item to playback history"); - media.setPlaybackCompletionDate(new Date()); - // reset played_duration to 0 so that it behaves correctly when the episode is played again - media.setPlayedDuration(0); - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedMediaPlaybackCompletionDate(media); - adapter.close(); - EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast(); + public static Future<?> addItemToPlaybackHistory(final FeedMedia media) { + return dbExec.submit(() -> { + Log.d(TAG, "Adding new item to playback history"); + media.setPlaybackCompletionDate(new Date()); + // reset played_duration to 0 so that it behaves correctly when the episode is played again + media.setPlayedDuration(0); + + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedMediaPlaybackCompletionDate(media); + adapter.close(); + EventDistributor.getInstance().sendPlaybackHistoryUpdateBroadcast(); - } }); } - private static void cleanupDownloadLog(final PodDBAdapter adapter) { - final long logSize = adapter.getDownloadLogSize(); - if (logSize > DBReader.DOWNLOAD_LOG_SIZE) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Cleaning up download log"); - adapter.removeDownloadLogItems(logSize - DBReader.DOWNLOAD_LOG_SIZE); - } - } - /** * Adds a Download status object to the download log. * - * @param context A context that is used for opening a database connection. * @param status The DownloadStatus object. */ - public static Future<?> addDownloadStatus(final Context context, - final DownloadStatus status) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setDownloadStatus(status); - adapter.close(); - EventDistributor.getInstance().sendDownloadLogUpdateBroadcast(); - } + public static Future<?> addDownloadStatus(final DownloadStatus status) { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setDownloadStatus(status); + adapter.close(); + EventDistributor.getInstance().sendDownloadLogUpdateBroadcast(); }); } @@ -344,108 +313,118 @@ public class DBWriter { */ public static Future<?> addQueueItemAt(final Context context, final long itemId, final int index, final boolean performAutoDownload) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - final List<FeedItem> queue = DBReader.getQueue(context, adapter); - FeedItem item = null; - - if (queue != null) { - if (!itemListContains(queue, itemId)) { - item = DBReader.getFeedItem(context, itemId); - if (item != null) { - queue.add(index, item); - adapter.setQueue(queue); - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED, item, index)); + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + final List<FeedItem> queue = DBReader.getQueue(adapter); + FeedItem item; + + if (queue != null) { + if (!itemListContains(queue, itemId)) { + item = DBReader.getFeedItem(itemId); + if (item != null) { + queue.add(index, item); + adapter.setQueue(queue); + item.addTag(FeedItem.TAG_QUEUE); + EventBus.getDefault().post(QueueEvent.added(item, index)); + if (item.isNew()) { + DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.getId()); } } } + } - adapter.close(); - if (performAutoDownload) { - DBTasks.autodownloadUndownloadedItems(context); - } - + adapter.close(); + if (performAutoDownload) { + DBTasks.autodownloadUndownloadedItems(context); } + }); } + public static Future<?> addQueueItem(final Context context, + final FeedItem... items) { + LongList itemIds = new LongList(items.length); + for (FeedItem item : items) { + itemIds.add(item.getId()); + item.addTag(FeedItem.TAG_QUEUE); + } + return addQueueItem(context, false, itemIds.toArray()); + } + /** * Appends FeedItem objects to the end of the queue. The 'read'-attribute of all items will be set to true. * If a FeedItem is already in the queue, the FeedItem will not change its position in the queue. * * @param context A context that is used for opening a database connection. + * @param performAutoDownload true if an auto-download process should be started after the operation. * @param itemIds IDs of the FeedItem objects that should be added to the queue. */ - public static Future<?> addQueueItem(final Context context, + public static Future<?> addQueueItem(final Context context, final boolean performAutoDownload, final long... itemIds) { - return dbExec.submit(new Runnable() { + return dbExec.submit(() -> { + if (itemIds.length > 0) { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + final List<FeedItem> queue = DBReader.getQueue(adapter); - @Override - public void run() { - if (itemIds.length > 0) { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - final List<FeedItem> queue = DBReader.getQueue(context, - adapter); - - if (queue != null) { - boolean queueModified = false; - boolean unreadItemsModified = false; - List<FeedItem> itemsToSave = new LinkedList<FeedItem>(); - for (int i = 0; i < itemIds.length; i++) { - if (!itemListContains(queue, itemIds[i])) { - final FeedItem item = DBReader.getFeedItem( - context, itemIds[i]); - - if (item != null) { - // add item to either front ot back of queue - boolean addToFront = UserPreferences.enqueueAtFront(); - - if(addToFront){ - queue.add(0, item); - } else { - queue.add(item); - } - - queueModified = true; + if (queue != null) { + boolean queueModified = false; + LongList markAsUnplayedIds = new LongList(); + List<QueueEvent> events = new ArrayList<QueueEvent>(); + for (int i = 0; i < itemIds.length; i++) { + if (!itemListContains(queue, itemIds[i])) { + final FeedItem item = DBReader.getFeedItem(itemIds[i]); + + + if (item != null) { + // add item to either front ot back of queue + boolean addToFront = UserPreferences.enqueueAtFront(); + if (addToFront) { + queue.add(0 + i, item); + events.add(QueueEvent.added(item, 0 + i)); + } else { + queue.add(item); + events.add(QueueEvent.added(item, queue.size() - 1)); + } + queueModified = true; + if (item.isNew()) { + markAsUnplayedIds.add(item.getId()); } } } - if (queueModified) { - adapter.setQueue(queue); - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.ADDED_ITEMS, queue)); + } + if (queueModified) { + adapter.setQueue(queue); + for (QueueEvent event : events) { + EventBus.getDefault().post(event); + } + if (markAsUnplayedIds.size() > 0) { + DBWriter.markItemPlayed(FeedItem.UNPLAYED, markAsUnplayedIds.toArray()); } } - adapter.close(); + } + adapter.close(); + if (performAutoDownload) { DBTasks.autodownloadUndownloadedItems(context); } } }); - } /** * Removes all FeedItem objects from the queue. * - * @param context A context that is used for opening a database connection. */ - public static Future<?> clearQueue(final Context context) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.clearQueue(); - adapter.close(); - - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.CLEARED)); - } + public static Future<?> clearQueue() { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.clearQueue(); + adapter.close(); + + EventBus.getDefault().post(QueueEvent.cleared()); }); } @@ -458,79 +437,99 @@ public class DBWriter { */ public static Future<?> removeQueueItem(final Context context, final FeedItem item, final boolean performAutoDownload) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - final List<FeedItem> queue = DBReader.getQueue(context, adapter); - - if (queue != null) { - int position = queue.indexOf(item); - if(position >= 0) { - queue.remove(position); - adapter.setQueue(queue); - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.REMOVED, item, position)); - } else { - Log.w(TAG, "Queue was not modified by call to removeQueueItem"); - } + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + final List<FeedItem> queue = DBReader.getQueue(adapter); + + if (queue != null) { + int position = queue.indexOf(item); + if (position >= 0) { + queue.remove(position); + adapter.setQueue(queue); + item.removeTag(FeedItem.TAG_QUEUE); + EventBus.getDefault().post(QueueEvent.removed(item)); } else { - Log.e(TAG, "removeQueueItem: Could not load queue"); - } - adapter.close(); - if (performAutoDownload) { - DBTasks.autodownloadUndownloadedItems(context); + Log.w(TAG, "Queue was not modified by call to removeQueueItem"); } + } else { + Log.e(TAG, "removeQueueItem: Could not load queue"); + } + adapter.close(); + if (performAutoDownload) { + DBTasks.autodownloadUndownloadedItems(context); + } + }); + + } + + public static Future<?> addFavoriteItem(final FeedItem item) { + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance().open(); + adapter.addFavoriteItem(item); + adapter.close(); + item.addTag(FeedItem.TAG_FAVORITE); + EventBus.getDefault().post(FavoritesEvent.added(item)); + }); + } + + public static Future<?> addFavoriteItemById(final long itemId) { + return dbExec.submit(() -> { + final FeedItem item = DBReader.getFeedItem(itemId); + if (item == null) { + Log.d(TAG, "Can't find item for itemId " + itemId); + return; } + final PodDBAdapter adapter = PodDBAdapter.getInstance().open(); + adapter.addFavoriteItem(item); + adapter.close(); + item.addTag(FeedItem.TAG_FAVORITE); + EventBus.getDefault().post(FavoritesEvent.added(item)); }); + } + public static Future<?> removeFavoriteItem(final FeedItem item) { + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance().open(); + adapter.removeFavoriteItem(item); + adapter.close(); + item.removeTag(FeedItem.TAG_FAVORITE); + EventBus.getDefault().post(FavoritesEvent.removed(item)); + }); } /** * Moves the specified item to the top of the queue. - * - * @param context A context that is used for opening a database connection. - * @param itemId The item to move to the top of the queue + * @param itemId The item to move to the top of the queue * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - * false if the caller wants to avoid unexpected updates of the GUI. */ - public static Future<?> moveQueueItemToTop(final Context context, final long itemId, final boolean broadcastUpdate) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - LongList queueIdList = DBReader.getQueueIDList(context); - int index = queueIdList.indexOf(itemId); - if (index >=0) { - moveQueueItemHelper(context, index, 0, broadcastUpdate); - } else { - Log.e(TAG, "moveQueueItemToTop: item not found"); - } + public static Future<?> moveQueueItemToTop(final long itemId, final boolean broadcastUpdate) { + return dbExec.submit(() -> { + LongList queueIdList = DBReader.getQueueIDList(); + int index = queueIdList.indexOf(itemId); + if (index >=0) { + moveQueueItemHelper(index, 0, broadcastUpdate); + } else { + Log.e(TAG, "moveQueueItemToTop: item not found"); } }); } /** * Moves the specified item to the bottom of the queue. - * - * @param context A context that is used for opening a database connection. - * @param itemId The item to move to the bottom of the queue + * @param itemId The item to move to the bottom of the queue * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - * false if the caller wants to avoid unexpected updates of the GUI. */ - public static Future<?> moveQueueItemToBottom(final Context context, final long itemId, + public static Future<?> moveQueueItemToBottom(final long itemId, final boolean broadcastUpdate) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - LongList queueIdList = DBReader.getQueueIDList(context); - int index = queueIdList.indexOf(itemId); - if(index >= 0) { - moveQueueItemHelper(context, index, queueIdList.size() - 1, - broadcastUpdate); - } else { - Log.e(TAG, "moveQueueItemToBottom: item not found"); - } + return dbExec.submit(() -> { + LongList queueIdList = DBReader.getQueueIDList(); + int index = queueIdList.indexOf(itemId); + if (index >= 0) { + moveQueueItemHelper(index, queueIdList.size() - 1, + broadcastUpdate); + } else { + Log.e(TAG, "moveQueueItemToBottom: item not found"); } }); } @@ -538,21 +537,16 @@ public class DBWriter { /** * Changes the position of a FeedItem in the queue. * - * @param context A context that is used for opening a database connection. * @param from Source index. Must be in range 0..queue.size()-1. * @param to Destination index. Must be in range 0..queue.size()-1. * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to * false if the caller wants to avoid unexpected updates of the GUI. * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) */ - public static Future<?> moveQueueItem(final Context context, final int from, + public static Future<?> moveQueueItem(final int from, final int to, final boolean broadcastUpdate) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - moveQueueItemHelper(context, from, to, broadcastUpdate); - } + return dbExec.submit(() -> { + moveQueueItemHelper(from, to, broadcastUpdate); }); } @@ -561,32 +555,27 @@ public class DBWriter { * <p/> * This function must be run using the ExecutorService (dbExec). * - * @param context A context that is used for opening a database connection. * @param from Source index. Must be in range 0..queue.size()-1. * @param to Destination index. Must be in range 0..queue.size()-1. * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to * false if the caller wants to avoid unexpected updates of the GUI. * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) */ - private static void moveQueueItemHelper(final Context context, final int from, + private static void moveQueueItemHelper(final int from, final int to, final boolean broadcastUpdate) { - final PodDBAdapter adapter = new PodDBAdapter(context); + final PodDBAdapter adapter = PodDBAdapter.getInstance(); adapter.open(); - final List<FeedItem> queue = DBReader - .getQueue(context, adapter); + final List<FeedItem> queue = DBReader.getQueue(adapter); if (queue != null) { - if (from >= 0 && from < queue.size() && to >= 0 - && to < queue.size()) { - + if (from >= 0 && from < queue.size() && to >= 0 && to < queue.size()) { final FeedItem item = queue.remove(from); queue.add(to, item); adapter.setQueue(queue); if (broadcastUpdate) { - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.MOVED, item, to)); + EventBus.getDefault().post(QueueEvent.moved(item, to)); } - } } else { Log.e(TAG, "moveQueueItemHelper: Could not load queue"); @@ -594,183 +583,178 @@ public class DBWriter { adapter.close(); } - /** - * Sets the 'read'-attribute of a FeedItem to the specified value. + /* + * Sets the 'read'-attribute of all specified FeedItems * * @param context A context that is used for opening a database connection. - * @param itemId ID of the FeedItem - * @param read New value of the 'read'-attribute + * @param played New value of the 'read'-attribute, one of FeedItem.PLAYED, FeedItem.NEW, + * FeedItem.UNPLAYED + * @param itemIds IDs of the FeedItems. */ - public static Future<?> markItemRead(final Context context, final long itemId, - final boolean read) { - return markItemRead(context, itemId, read, 0, false); + public static Future<?> markItemPlayed(final int played, final long... itemIds) { + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedItemRead(played, itemIds); + adapter.close(); + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + }); } /** * Sets the 'read'-attribute of a FeedItem to the specified value. - * - * @param context A context that is used for opening a database connection. - * @param item The FeedItem object - * @param read New value of the 'read'-attribute + * @param item The FeedItem object + * @param played New value of the 'read'-attribute one of FeedItem.PLAYED, + * FeedItem.NEW, FeedItem.UNPLAYED * @param resetMediaPosition true if this method should also reset the position of the FeedItem's FeedMedia object. - * If the FeedItem has no FeedMedia object, this parameter will be ignored. */ - public static Future<?> markItemRead(Context context, FeedItem item, boolean read, boolean resetMediaPosition) { + public static Future<?> markItemPlayed(FeedItem item, int played, boolean resetMediaPosition) { long mediaId = (item.hasMedia()) ? item.getMedia().getId() : 0; - return markItemRead(context, item.getId(), read, mediaId, resetMediaPosition); + return markItemPlayed(item.getId(), played, mediaId, resetMediaPosition); } - private static Future<?> markItemRead(final Context context, final long itemId, - final boolean read, final long mediaId, - final boolean resetMediaPosition) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedItemRead(read, itemId, mediaId, - resetMediaPosition); - adapter.close(); - - EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); - } + private static Future<?> markItemPlayed(final long itemId, + final int played, + final long mediaId, + final boolean resetMediaPosition) { + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedItemRead(played, itemId, mediaId, + resetMediaPosition); + adapter.close(); + + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); }); } /** * Sets the 'read'-attribute of all FeedItems of a specific Feed to true. * - * @param context A context that is used for opening a database connection. * @param feedId ID of the Feed. */ - public static Future<?> markFeedRead(final Context context, final long feedId) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - Cursor itemCursor = adapter.getAllItemsOfFeedCursor(feedId); - long[] itemIds = new long[itemCursor.getCount()]; - itemCursor.moveToFirst(); - for (int i = 0; i < itemIds.length; i++) { - itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX); - itemCursor.moveToNext(); - } - itemCursor.close(); - adapter.setFeedItemRead(true, itemIds); - adapter.close(); - - EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + public static Future<?> markFeedSeen(final long feedId) { + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + Cursor itemCursor = adapter.getNewItemsIdsCursor(feedId); + long[] ids = new long[itemCursor.getCount()]; + itemCursor.moveToFirst(); + for (int i = 0; i < ids.length; i++) { + ids[i] = itemCursor.getLong(0); + itemCursor.moveToNext(); } - }); + itemCursor.close(); + adapter.setFeedItemRead(FeedItem.UNPLAYED, ids); + adapter.close(); + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + }); } /** - * Sets the 'read'-attribute of all FeedItems to true. + * Sets the 'read'-attribute of all FeedItems of a specific Feed to true. * - * @param context A context that is used for opening a database connection. + * @param feedId ID of the Feed. */ - public static Future<?> markAllItemsRead(final Context context) { - return dbExec.submit(new Runnable() { + public static Future<?> markFeedRead(final long feedId) { + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + Cursor itemCursor = adapter.getAllItemsOfFeedCursor(feedId); + long[] itemIds = new long[itemCursor.getCount()]; + itemCursor.moveToFirst(); + for (int i = 0; i < itemIds.length; i++) { + int indexId = itemCursor.getColumnIndex(PodDBAdapter.KEY_ID); + itemIds[i] = itemCursor.getLong(indexId); + itemCursor.moveToNext(); + } + itemCursor.close(); + adapter.setFeedItemRead(FeedItem.PLAYED, itemIds); + adapter.close(); - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - Cursor itemCursor = adapter.getUnreadItemsCursor(); - long[] itemIds = new long[itemCursor.getCount()]; - itemCursor.moveToFirst(); - for (int i = 0; i < itemIds.length; i++) { - itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX); - itemCursor.moveToNext(); - } - itemCursor.close(); - adapter.setFeedItemRead(true, itemIds); - adapter.close(); + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + }); + } - EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + /** + * Sets the 'read'-attribute of all FeedItems to true. + */ + public static Future<?> markAllItemsRead() { + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + Cursor itemCursor = adapter.getUnreadItemsCursor(); + long[] itemIds = new long[itemCursor.getCount()]; + itemCursor.moveToFirst(); + for (int i = 0; i < itemIds.length; i++) { + int indexId = itemCursor.getColumnIndex(PodDBAdapter.KEY_ID); + itemIds[i] = itemCursor.getLong(indexId); + itemCursor.moveToNext(); } + itemCursor.close(); + adapter.setFeedItemRead(FeedItem.PLAYED, itemIds); + adapter.close(); + + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); }); } static Future<?> addNewFeed(final Context context, final Feed... feeds) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setCompleteFeed(feeds); - adapter.close(); - - if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { - for (Feed feed : feeds) { - GpodnetPreferences.addAddedFeed(feed.getDownload_url()); - } + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setCompleteFeed(feeds); + adapter.close(); + + if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { + for (Feed feed : feeds) { + GpodnetPreferences.addAddedFeed(feed.getDownload_url()); } - - BackupManager backupManager = new BackupManager(context); - backupManager.dataChanged(); } + + BackupManager backupManager = new BackupManager(context); + backupManager.dataChanged(); }); } - static Future<?> setCompleteFeed(final Context context, final Feed... feeds) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setCompleteFeed(feeds); - adapter.close(); - - } + static Future<?> setCompleteFeed(final Feed... feeds) { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setCompleteFeed(feeds); + adapter.close(); }); - } /** * Saves a FeedMedia object in the database. This method will save all attributes of the FeedMedia object. The * contents of FeedComponent-attributes (e.g. the FeedMedia's 'item'-attribute) will not be saved. * - * @param context A context that is used for opening a database connection. * @param media The FeedMedia object. */ - public static Future<?> setFeedMedia(final Context context, - final FeedMedia media) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setMedia(media); - adapter.close(); - } + public static Future<?> setFeedMedia(final FeedMedia media) { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setMedia(media); + adapter.close(); }); } /** - * Saves the 'position' and 'duration' attributes of a FeedMedia object + * Saves the 'position', 'duration' and 'last played time' attributes of a FeedMedia object * - * @param context A context that is used for opening a database connection. * @param media The FeedMedia object. */ - public static Future<?> setFeedMediaPlaybackInformation(final Context context, final FeedMedia media) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedMediaPlaybackInformation(media); - adapter.close(); - } + public static Future<?> setFeedMediaPlaybackInformation(final FeedMedia media) { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedMediaPlaybackInformation(media); + adapter.close(); }); } @@ -778,20 +762,15 @@ public class DBWriter { * Saves a FeedItem object in the database. This method will save all attributes of the FeedItem object including * the content of FeedComponent-attributes. * - * @param context A context that is used for opening a database connection. * @param item The FeedItem object. */ - public static Future<?> setFeedItem(final Context context, - final FeedItem item) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setSingleFeedItem(item); - adapter.close(); - } + public static Future<?> setFeedItem(final FeedItem item) { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setSingleFeedItem(item); + adapter.close(); + EventBus.getDefault().post(FeedItemEvent.updated(item)); }); } @@ -799,60 +778,42 @@ public class DBWriter { * Saves a FeedImage object in the database. This method will save all attributes of the FeedImage object. The * contents of FeedComponent-attributes (e.g. the FeedImages's 'feed'-attribute) will not be saved. * - * @param context A context that is used for opening a database connection. * @param image The FeedImage object. */ - public static Future<?> setFeedImage(final Context context, - final FeedImage image) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setImage(image); - adapter.close(); - } + public static Future<?> setFeedImage(final FeedImage image) { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setImage(image); + adapter.close(); }); } /** - * Updates download URLs of feeds from a given Map. The key of the Map is the original URL of the feed - * and the value is the updated URL + * Updates download URL of a feed */ - public static Future<?> updateFeedDownloadURLs(final Context context, final Map<String, String> urls) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - for (String key : urls.keySet()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Replacing URL " + key + " with url " + urls.get(key)); - - adapter.setFeedDownloadUrl(key, urls.get(key)); - } - adapter.close(); - } + public static Future<?> updateFeedDownloadURL(final String original, final String updated) { + Log.d(TAG, "updateFeedDownloadURL(original: " + original + ", updated: " + updated +")"); + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedDownloadUrl(original, updated); + adapter.close(); }); } /** * Saves a FeedPreferences object in the database. The Feed ID of the FeedPreferences-object MUST NOT be 0. * - * @param context Used for opening a database connection. * @param preferences The FeedPreferences object. */ - public static Future<?> setFeedPreferences(final Context context, final FeedPreferences preferences) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedPreferences(preferences); - adapter.close(); - EventDistributor.getInstance().sendFeedUpdateBroadcast(); - } + public static Future<?> setFeedPreferences(final FeedPreferences preferences) { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedPreferences(preferences); + adapter.close(); + EventDistributor.getInstance().sendFeedUpdateBroadcast(); }); } @@ -873,17 +834,13 @@ public class DBWriter { public static Future<?> setFeedItemFlattrStatus(final Context context, final FeedItem item, final boolean startFlattrClickWorker) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedItemFlattrStatus(item); - adapter.close(); - if (startFlattrClickWorker) { - new FlattrClickWorker(context).executeAsync(); - } + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedItemFlattrStatus(item); + adapter.close(); + if (startFlattrClickWorker) { + new FlattrClickWorker(context).executeAsync(); } }); } @@ -896,17 +853,13 @@ public class DBWriter { private static Future<?> setFeedFlattrStatus(final Context context, final Feed feed, final boolean startFlattrClickWorker) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedFlattrStatus(feed); - adapter.close(); - if (startFlattrClickWorker) { - new FlattrClickWorker(context).executeAsync(); - } + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedFlattrStatus(feed); + adapter.close(); + if (startFlattrClickWorker) { + new FlattrClickWorker(context).executeAsync(); } }); } @@ -916,18 +869,13 @@ public class DBWriter { * * @param lastUpdateFailed true if last update failed */ - public static Future<?> setFeedLastUpdateFailed(final Context context, - final long feedId, - final boolean lastUpdateFailed) { - return dbExec.submit(new Runnable() { - - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedLastUpdateFailed(feedId, lastUpdateFailed); - adapter.close(); - } + public static Future<?> setFeedLastUpdateFailed(final long feedId, + final boolean lastUpdateFailed) { + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedLastUpdateFailed(feedId, lastUpdateFailed); + adapter.close(); }); } @@ -955,14 +903,15 @@ public class DBWriter { */ public static Future<?> setFlattredStatus(Context context, FlattrThing thing, boolean startFlattrClickWorker) { // must propagate this to back db - if (thing instanceof FeedItem) + if (thing instanceof FeedItem) { return setFeedItemFlattrStatus(context, (FeedItem) thing, startFlattrClickWorker); - else if (thing instanceof Feed) + } else if (thing instanceof Feed) { return setFeedFlattrStatus(context, (Feed) thing, startFlattrClickWorker); - else if (thing instanceof SimpleFlattrThing) { - } // SimpleFlattrThings are generated on the fly and do not have DB backing - else + } else if (thing instanceof SimpleFlattrThing) { + // SimpleFlattrThings are generated on the fly and do not have DB backing + } else { Log.e(TAG, "flattrQueue processing - thing is neither FeedItem nor Feed nor SimpleFlattrThing"); + } return null; } @@ -970,16 +919,13 @@ public class DBWriter { /** * Reset flattr status to unflattrd for all items */ - public static Future<?> clearAllFlattrStatus(final Context context) { + public static Future<?> clearAllFlattrStatus() { Log.d(TAG, "clearAllFlattrStatus()"); - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.clearAllFlattrStatus(); - adapter.close(); - } + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.clearAllFlattrStatus(); + adapter.close(); }); } @@ -987,99 +933,115 @@ public class DBWriter { * Set flattr status of the feeds/feeditems in flattrList to flattred at the given timestamp, * where the information has been retrieved from the flattr API */ - public static Future<?> setFlattredStatus(final Context context, final List<Flattr> flattrList) { + public static Future<?> setFlattredStatus(final List<Flattr> flattrList) { Log.d(TAG, "setFlattredStatus to status retrieved from flattr api running with " + flattrList.size() + " items"); // clear flattr status in db - clearAllFlattrStatus(context); + clearAllFlattrStatus(); // submit list with flattred things having normalized URLs to db - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - for (Flattr flattr : flattrList) { - adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime())); - } - adapter.close(); + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + for (Flattr flattr : flattrList) { + adapter.setItemFlattrStatus(formatURIForQuery(flattr.getThing().getUrl()), new FlattrStatus(flattr.getCreated().getTime())); } + adapter.close(); }); } /** * Sort the FeedItems in the queue with the given Comparator. - * - * @param context A context that is used for opening a database connection. * @param comparator FeedItem comparator * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to - * false if the caller wants to avoid unexpected updates of the GUI. */ - public static Future<?> sortQueue(final Context context, final Comparator<FeedItem> comparator, final boolean broadcastUpdate) { - return dbExec.submit(new Runnable() { - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - final List<FeedItem> queue = DBReader.getQueue(context, adapter); - - if (queue != null) { - Collections.sort(queue, comparator); - adapter.setQueue(queue); - if (broadcastUpdate) { - EventBus.getDefault().post(new QueueEvent(QueueEvent.Action.SORTED)); - } - } else { - Log.e(TAG, "sortQueue: Could not load queue"); + public static Future<?> sortQueue(final Comparator<FeedItem> comparator, final boolean broadcastUpdate) { + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + final List<FeedItem> queue = DBReader.getQueue(adapter); + + if (queue != null) { + Collections.sort(queue, comparator); + adapter.setQueue(queue); + if (broadcastUpdate) { + EventBus.getDefault().post(QueueEvent.sorted(queue)); } - adapter.close(); + } else { + Log.e(TAG, "sortQueue: Could not load queue"); } + adapter.close(); }); } /** * Sets the 'auto_download'-attribute of specific FeedItem. * - * @param context A context that is used for opening a database connection. * @param feedItem FeedItem. + * @param autoDownload true enables auto download, false disables it */ - public static Future<?> setFeedItemAutoDownload(final Context context, final FeedItem feedItem, + public static Future<?> setFeedItemAutoDownload(final FeedItem feedItem, final boolean autoDownload) { - Log.d(TAG, "FeedItem[id=" + feedItem.getId() + "] SET auto_download " + autoDownload); - return dbExec.submit(new Runnable() { - - @Override - public void run() { - final PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedItemAutoDownload(feedItem, autoDownload); - adapter.close(); + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedItemAutoDownload(feedItem, autoDownload ? 1 : 0); + adapter.close(); + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + }); + } - EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + public static Future<?> saveFeedItemAutoDownloadFailed(final FeedItem feedItem) { + return dbExec.submit(() -> { + int failedAttempts = feedItem.getFailedAutoDownloadAttempts() + 1; + long autoDownload; + if(!feedItem.getAutoDownload() || failedAttempts >= 10) { + autoDownload = 0; // giving up, disable auto download + feedItem.setAutoDownload(false); + } else { + long now = System.currentTimeMillis(); + autoDownload = (now / 10) * 10 + failedAttempts; } + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedItemAutoDownload(feedItem, autoDownload); + adapter.close(); + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); }); + } + /** + * Sets the 'auto_download'-attribute of specific FeedItem. + * + * @param feed This feed's episodes will be processed. + * @param autoDownload If true, auto download will be enabled for the feed's episodes. Else, + */ + public static Future<?> setFeedsItemsAutoDownload(final Feed feed, + final boolean autoDownload) { + Log.d(TAG, (autoDownload ? "Enabling" : "Disabling") + " auto download for items of feed " + feed.getId()); + return dbExec.submit(() -> { + final PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedsItemsAutoDownload(feed, autoDownload); + adapter.close(); + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + }); } + /** * Set filter of the feed - * - * @param context Used for opening a database connection. - * @param feedId The feed's ID + * @param feedId The feed's ID * @param filterValues Values that represent properties to filter by */ - public static Future<?> setFeedItemsFilter(final Context context, final long feedId, - final List<String> filterValues) { - Log.d(TAG, "setFeedFilter"); - - return dbExec.submit(new Runnable() { - @Override - public void run() { - PodDBAdapter adapter = new PodDBAdapter(context); - adapter.open(); - adapter.setFeedItemFilter(feedId, filterValues); - adapter.close(); - EventBus.getDefault().post(new FeedEvent(FeedEvent.Action.FILTER_CHANGED, feedId)); - } + public static Future<?> setFeedItemsFilter(final long feedId, + final Set<String> filterValues) { + Log.d(TAG, "setFeedItemsFilter() called with: " + "feedId = [" + feedId + "], filterValues = [" + filterValues + "]"); + return dbExec.submit(() -> { + PodDBAdapter adapter = PodDBAdapter.getInstance(); + adapter.open(); + adapter.setFeedItemFilter(feedId, filterValues); + adapter.close(); + EventBus.getDefault().post(new FeedEvent(FeedEvent.Action.FILTER_CHANGED, feedId)); }); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java index ca6aa0178..0dc1dadeb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java @@ -3,22 +3,20 @@ package de.danoeh.antennapod.core.storage; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.annotation.NonNull; +import android.text.TextUtils; import android.util.Log; import android.webkit.URLUtil; import org.apache.commons.io.FilenameUtils; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; import java.io.File; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.feed.EventDistributor; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedFile; -import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadRequest; @@ -73,10 +71,8 @@ public class DownloadRequester { * call will return false. * @return True if the download request was accepted, false otherwise. */ - public synchronized boolean download(Context context, DownloadRequest request) { - Validate.notNull(context); - Validate.notNull(request); - + public synchronized boolean download(@NonNull Context context, + @NonNull DownloadRequest request) { if (downloads.containsKey(request.getSource())) { if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored."); return false; @@ -86,7 +82,7 @@ public class DownloadRequester { Intent launchIntent = new Intent(context, DownloadService.class); launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request); context.startService(launchIntent); - EventDistributor.getInstance().sendDownloadQueuedBroadcast(); + return true; } @@ -147,7 +143,7 @@ public class DownloadRequester { private boolean isFilenameAvailable(String path) { for (String key : downloads.keySet()) { DownloadRequest r = downloads.get(key); - if (StringUtils.equals(r.getDestination(), path)) { + if (TextUtils.equals(r.getDestination(), path)) { if (BuildConfig.DEBUG) Log.d(TAG, path + " is already used by another requested download"); @@ -171,7 +167,7 @@ public class DownloadRequester { if (feedFileValid(feed)) { String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null; String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null; - long ifModifiedSince = feed.getLastUpdate().getTime(); + long ifModifiedSince = feed.isPaged() ? 0 : feed.getLastUpdate().getTime(); Bundle args = new Bundle(); args.putInt(REQUEST_ARG_PAGE_NR, feed.getPageNr()); @@ -186,15 +182,6 @@ public class DownloadRequester { downloadFeed(context, feed, false); } - public synchronized void downloadImage(Context context, FeedImage image) - throws DownloadRequestException { - if (feedFileValid(image)) { - FeedFile container = (image.getOwner() instanceof FeedFile) ? (FeedFile) image.getOwner() : null; - download(context, image, container, new File(getImagefilePath(context), - getImagefileName(image)), false, null, null, 0, false, null); - } - } - public synchronized void downloadMedia(Context context, FeedMedia feedmedia) throws DownloadRequestException { if (feedFileValid(feedmedia)) { @@ -332,20 +319,6 @@ public class DownloadRequester { return "feed-" + FileNameGenerator.generateFileName(filename); } - public synchronized String getImagefilePath(Context context) - throws DownloadRequestException { - return getExternalFilesDirOrThrowException(context, IMAGE_DOWNLOADPATH) - .toString() + "/"; - } - - public synchronized String getImagefileName(FeedImage image) { - String filename = image.getDownload_url(); - if (image.getOwner() != null && image.getOwner().getHumanReadableIdentifier() != null) { - filename = image.getOwner().getHumanReadableIdentifier(); - } - return "image-" + FileNameGenerator.generateFileName(filename); - } - public synchronized String getMediafilePath(Context context, FeedMedia media) throws DownloadRequestException { File externalStorage = getExternalFilesDirOrThrowException( @@ -359,7 +332,7 @@ public class DownloadRequester { private File getExternalFilesDirOrThrowException(Context context, String type) throws DownloadRequestException { - File result = UserPreferences.getDataFolder(context, type); + File result = UserPreferences.getDataFolder(type); if (result == null) { throw new DownloadRequestException( "Failed to access external storage"); @@ -375,7 +348,7 @@ public class DownloadRequester { if (media.getItem() != null && media.getItem().getTitle() != null) { String title = media.getItem().getTitle(); // Delete reserved characters - titleBaseFilename = title.replaceAll("[\\\\/%\\?\\*:|<>\"\\p{Cntrl}]", ""); + titleBaseFilename = title.replaceAll("[^a-zA-Z0-9 ._()-]", ""); titleBaseFilename = titleBaseFilename.trim(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java index 6a8b4a441..0f402745c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/EpisodeCleanupAlgorithm.java @@ -2,35 +2,60 @@ package de.danoeh.antennapod.core.storage; import android.content.Context; -import java.util.List; +import de.danoeh.antennapod.core.preferences.UserPreferences; -import de.danoeh.antennapod.core.feed.FeedItem; - -public interface EpisodeCleanupAlgorithm<T> { +public abstract class EpisodeCleanupAlgorithm { /** * Deletes downloaded episodes that are no longer needed. What episodes are deleted and how many * of them depends on the implementation. * - * @param context Can be used for accessing the database - * @param parameter An additional parameter. This parameter is either returned by getDefaultCleanupParameter - * or getPerformCleanupParameter. + * @param context Can be used for accessing the database + * @param numToRemove An additional parameter. This parameter is either returned by getDefaultCleanupParameter + * or getPerformCleanupParameter. * @return The number of episodes that were deleted. */ - public int performCleanup(Context context, T parameter); + public abstract int performCleanup(Context context, int numToRemove); + + public int performCleanup(Context context) { + return performCleanup(context, getDefaultCleanupParameter()); + } /** * Returns a parameter for performCleanup. The implementation of this interface should decide how much * space to free to satisfy the episode cache conditions. If the conditions are already satisfied, this * method should not have any effects. */ - public T getDefaultCleanupParameter(Context context); + public abstract int getDefaultCleanupParameter(); /** - * Returns a parameter for performCleanup. + * Cleans up just enough episodes to make room for the requested number * - * @param items A list of FeedItems that are about to be downloaded. The implementation of this interface - * should decide how much space to free to satisfy the episode cache conditions. + * @param context Can be used for accessing the database + * @param amountOfRoomNeeded the number of episodes we need space for + * @return The number of epiosdes that were deleted + */ + public int makeRoomForEpisodes(Context context, int amountOfRoomNeeded) { + return performCleanup(context, getNumEpisodesToCleanup(amountOfRoomNeeded)); + } + + /** + * @param amountOfRoomNeeded the number of episodes we want to download + * @return the number of episodes to delete in order to make room */ - public T getPerformCleanupParameter(Context context, List<FeedItem> items); + protected int getNumEpisodesToCleanup(final int amountOfRoomNeeded) { + if (amountOfRoomNeeded >= 0 + && UserPreferences.getEpisodeCacheSize() != UserPreferences + .getEpisodeCacheSizeUnlimited()) { + int downloadedEpisodes = DBReader + .getNumberOfDownloadedEpisodes(); + if (downloadedEpisodes + amountOfRoomNeeded >= UserPreferences + .getEpisodeCacheSize()) { + + return downloadedEpisodes + amountOfRoomNeeded + - UserPreferences.getEpisodeCacheSize(); + } + } + return 0; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java index f6a59836b..09949b87e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java @@ -1,5 +1,7 @@ package de.danoeh.antennapod.core.storage; +import android.database.Cursor; + import java.util.Date; /** @@ -36,6 +38,15 @@ public class FeedItemStatistics { } } + public static FeedItemStatistics fromCursor(Cursor cursor) { + return new FeedItemStatistics( + cursor.getLong(0), + cursor.getInt(1), + cursor.getInt(2), + cursor.getInt(4), + new Date(cursor.getLong(3))); + } + public long getFeedID() { return feedID; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java index 4780098e0..85ff8fc8c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -9,16 +9,16 @@ import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; +import android.media.MediaMetadataRetriever; +import android.text.TextUtils; import android.util.Log; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; - import java.util.Arrays; import java.util.List; +import java.util.Set; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.event.ProgressEvent; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedComponent; @@ -26,11 +26,11 @@ import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.util.LongIntMap; import de.danoeh.antennapod.core.util.flattr.FlattrStatus; - -; +import de.greenrobot.event.EventBus; // TODO Remove media column from feeditem table @@ -38,6 +38,7 @@ import de.danoeh.antennapod.core.util.flattr.FlattrStatus; * Implements methods for accessing the database */ public class PodDBAdapter { + private static final String TAG = "PodDBAdapter"; public static final String DATABASE_NAME = "Antennapod.db"; @@ -51,63 +52,6 @@ public class PodDBAdapter { */ public static final int SEARCH_LIMIT = 30; - // ----------- Column indices - // ----------- General indices - public static final int KEY_ID_INDEX = 0; - public static final int KEY_TITLE_INDEX = 1; - public static final int KEY_FILE_URL_INDEX = 2; - public static final int KEY_DOWNLOAD_URL_INDEX = 3; - public static final int KEY_DOWNLOADED_INDEX = 4; - public static final int KEY_LINK_INDEX = 5; - public static final int KEY_DESCRIPTION_INDEX = 6; - public static final int KEY_PAYMENT_LINK_INDEX = 7; - // ----------- Feed indices - public static final int KEY_LAST_UPDATE_INDEX = 8; - public static final int KEY_LANGUAGE_INDEX = 9; - public static final int KEY_AUTHOR_INDEX = 10; - public static final int KEY_IMAGE_INDEX = 11; - public static final int KEY_TYPE_INDEX = 12; - public static final int KEY_FEED_IDENTIFIER_INDEX = 13; - public static final int KEY_FEED_FLATTR_STATUS_INDEX = 14; - public static final int KEY_FEED_USERNAME_INDEX = 15; - public static final int KEY_FEED_PASSWORD_INDEX = 16; - public static final int KEY_IS_PAGED_INDEX = 17; - public static final int KEY_LOAD_ALL_PAGES_INDEX = 18; - public static final int KEY_NEXT_PAGE_LINK_INDEX = 19; - // ----------- FeedItem indices - public static final int KEY_CONTENT_ENCODED_INDEX = 2; - public static final int KEY_PUBDATE_INDEX = 3; - public static final int KEY_READ_INDEX = 4; - public static final int KEY_MEDIA_INDEX = 8; - public static final int KEY_FEED_INDEX = 9; - public static final int KEY_HAS_SIMPLECHAPTERS_INDEX = 10; - public static final int KEY_ITEM_IDENTIFIER_INDEX = 11; - public static final int KEY_ITEM_FLATTR_STATUS_INDEX = 12; - // ---------- FeedMedia indices - public static final int KEY_DURATION_INDEX = 1; - public static final int KEY_POSITION_INDEX = 5; - public static final int KEY_SIZE_INDEX = 6; - public static final int KEY_MIME_TYPE_INDEX = 7; - public static final int KEY_PLAYBACK_COMPLETION_DATE_INDEX = 8; - public static final int KEY_MEDIA_FEEDITEM_INDEX = 9; - public static final int KEY_PLAYED_DURATION_INDEX = 10; - // --------- Download log indices - public static final int KEY_FEEDFILE_INDEX = 1; - public static final int KEY_FEEDFILETYPE_INDEX = 2; - public static final int KEY_REASON_INDEX = 3; - public static final int KEY_SUCCESSFUL_INDEX = 4; - public static final int KEY_COMPLETION_DATE_INDEX = 5; - public static final int KEY_REASON_DETAILED_INDEX = 6; - public static final int KEY_DOWNLOADSTATUS_TITLE_INDEX = 7; - // --------- Queue indices - public static final int KEY_FEEDITEM_INDEX = 1; - public static final int KEY_QUEUE_FEED_INDEX = 2; - // --------- Chapters indices - public static final int KEY_CHAPTER_START_INDEX = 2; - public static final int KEY_CHAPTER_FEEDITEM_INDEX = 3; - public static final int KEY_CHAPTER_LINK_INDEX = 4; - public static final int KEY_CHAPTER_TYPE_INDEX = 5; - // Key-constants public static final String KEY_ID = "id"; public static final String KEY_TITLE = "title"; @@ -148,6 +92,8 @@ public class PodDBAdapter { public static final String KEY_CHAPTER_TYPE = "type"; public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date"; public static final String KEY_AUTO_DOWNLOAD = "auto_download"; + public static final String KEY_KEEP_UPDATED = "keep_updated"; + public static final String KEY_AUTO_DELETE_ACTION = "auto_delete_action"; public static final String KEY_PLAYED_DURATION = "played_duration"; public static final String KEY_USERNAME = "username"; public static final String KEY_PASSWORD = "password"; @@ -155,6 +101,10 @@ public class PodDBAdapter { public static final String KEY_NEXT_PAGE_LINK = "next_page_link"; public static final String KEY_HIDE = "hide"; public static final String KEY_LAST_UPDATE_FAILED = "last_update_failed"; + public static final String KEY_HAS_EMBEDDED_PICTURE = "has_embedded_picture"; + public static final String KEY_LAST_PLAYED_TIME = "last_played_time"; + public static final String KEY_INCLUDE_FILTER = "include_filter"; + public static final String KEY_EXCLUDE_FILTER = "exclude_filter"; // Table names public static final String TABLE_NAME_FEEDS = "Feeds"; @@ -164,6 +114,7 @@ public class PodDBAdapter { public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog"; public static final String TABLE_NAME_QUEUE = "Queue"; public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters"; + public static final String TABLE_NAME_FAVORITES = "Favorites"; // SQL Statements for creating new tables private static final String TABLE_PRIMARY_KEY = KEY_ID @@ -180,10 +131,14 @@ public class PodDBAdapter { + KEY_FLATTR_STATUS + " INTEGER," + KEY_USERNAME + " TEXT," + KEY_PASSWORD + " TEXT," + + KEY_INCLUDE_FILTER + " TEXT DEFAULT ''," + + KEY_EXCLUDE_FILTER + " TEXT DEFAULT ''," + + KEY_KEEP_UPDATED + " INTEGER DEFAULT 1," + KEY_IS_PAGED + " INTEGER DEFAULT 0," + KEY_NEXT_PAGE_LINK + " TEXT," + KEY_HIDE + " TEXT," - + KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0)"; + + KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0," + + KEY_AUTO_DELETE_ACTION + " INTEGER DEFAULT 0)"; public static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE @@ -209,7 +164,8 @@ public class PodDBAdapter { + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER," + KEY_FEEDITEM + " INTEGER," + KEY_PLAYED_DURATION + " INTEGER," - + KEY_AUTO_DOWNLOAD + " INTEGER)"; + + KEY_HAS_EMBEDDED_PICTURE + " INTEGER," + + KEY_LAST_PLAYED_TIME + " INTEGER)"; public static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE " + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE @@ -236,6 +192,15 @@ public class PodDBAdapter { + TABLE_NAME_FEED_ITEMS + "_" + KEY_IMAGE + " ON " + TABLE_NAME_FEED_ITEMS + " (" + KEY_IMAGE + ")"; + public static final String CREATE_INDEX_FEEDITEMS_PUBDATE = "CREATE INDEX IF NOT EXISTS " + + TABLE_NAME_FEED_ITEMS + "_" + KEY_PUBDATE + " ON " + TABLE_NAME_FEED_ITEMS + " (" + + KEY_PUBDATE + ")"; + + public static final String CREATE_INDEX_FEEDITEMS_READ = "CREATE INDEX IF NOT EXISTS " + + TABLE_NAME_FEED_ITEMS + "_" + KEY_READ + " ON " + TABLE_NAME_FEED_ITEMS + " (" + + KEY_READ + ")"; + + public static final String CREATE_INDEX_QUEUE_FEEDITEM = "CREATE INDEX " + TABLE_NAME_QUEUE + "_" + KEY_FEEDITEM + " ON " + TABLE_NAME_QUEUE + " (" + KEY_FEEDITEM + ")"; @@ -248,11 +213,10 @@ public class PodDBAdapter { + TABLE_NAME_SIMPLECHAPTERS + "_" + KEY_FEEDITEM + " ON " + TABLE_NAME_SIMPLECHAPTERS + " (" + KEY_FEEDITEM + ")"; - - private SQLiteDatabase db; - private final Context context; - private PodDBHelper helper; - + public static final String CREATE_TABLE_FAVORITES = "CREATE TABLE " + + TABLE_NAME_FAVORITES + "(" + KEY_ID + " INTEGER PRIMARY KEY," + + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)"; + /** * Select all columns from the feed-table */ @@ -272,6 +236,7 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_TYPE, TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER, TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD, + TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED, TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS, TABLE_NAME_FEEDS + "." + KEY_IS_PAGED, TABLE_NAME_FEEDS + "." + KEY_NEXT_PAGE_LINK, @@ -279,30 +244,11 @@ public class PodDBAdapter { TABLE_NAME_FEEDS + "." + KEY_PASSWORD, TABLE_NAME_FEEDS + "." + KEY_HIDE, TABLE_NAME_FEEDS + "." + KEY_LAST_UPDATE_FAILED, + TABLE_NAME_FEEDS + "." + KEY_AUTO_DELETE_ACTION, + TABLE_NAME_FEEDS + "." + KEY_INCLUDE_FILTER, + TABLE_NAME_FEEDS + "." + KEY_EXCLUDE_FILTER }; - - // column indices for FEED_SEL_STD - public static final int IDX_FEED_SEL_STD_ID = 0; - public static final int IDX_FEED_SEL_STD_TITLE = 1; - public static final int IDX_FEED_SEL_STD_FILE_URL = 2; - public static final int IDX_FEED_SEL_STD_DOWNLOAD_URL = 3; - public static final int IDX_FEED_SEL_STD_DOWNLOADED = 4; - public static final int IDX_FEED_SEL_STD_LINK = 5; - public static final int IDX_FEED_SEL_STD_DESCRIPTION = 6; - public static final int IDX_FEED_SEL_STD_PAYMENT_LINK = 7; - public static final int IDX_FEED_SEL_STD_LASTUPDATE = 8; - public static final int IDX_FEED_SEL_STD_LANGUAGE = 9; - public static final int IDX_FEED_SEL_STD_AUTHOR = 10; - public static final int IDX_FEED_SEL_STD_IMAGE = 11; - public static final int IDX_FEED_SEL_STD_TYPE = 12; - public static final int IDX_FEED_SEL_STD_FEED_IDENTIFIER = 13; - public static final int IDX_FEED_SEL_PREFERENCES_AUTO_DOWNLOAD = 14; - public static final int IDX_FEED_SEL_STD_FLATTR_STATUS = 15; - public static final int IDX_FEED_SEL_STD_IS_PAGED = 16; - public static final int IDX_FEED_SEL_STD_NEXT_PAGE_LINK = 17; - public static final int IDX_FEED_SEL_PREFERENCES_USERNAME = 18; - public static final int IDX_FEED_SEL_PREFERENCES_PASSWORD = 19; - + /** * Select all columns from the feeditems-table except description and * content-encoded. @@ -313,7 +259,8 @@ public class PodDBAdapter { TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE, TABLE_NAME_FEED_ITEMS + "." + KEY_READ, TABLE_NAME_FEED_ITEMS + "." + KEY_LINK, - TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK, KEY_MEDIA, + TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK, + TABLE_NAME_FEED_ITEMS + "." + KEY_MEDIA, TABLE_NAME_FEED_ITEMS + "." + KEY_FEED, TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS, TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER, @@ -323,6 +270,20 @@ public class PodDBAdapter { }; /** + * All the tables in the database + */ + private static final String[] ALL_TABLES = { + TABLE_NAME_FEEDS, + TABLE_NAME_FEED_ITEMS, + TABLE_NAME_FEED_IMAGES, + TABLE_NAME_FEED_MEDIA, + TABLE_NAME_DOWNLOAD_LOG, + TABLE_NAME_QUEUE, + TABLE_NAME_SIMPLECHAPTERS, + TABLE_NAME_FAVORITES + }; + + /** * Contains FEEDITEM_SEL_FI_SMALL as comma-separated list. Useful for raw queries. */ private static final String SEL_FI_SMALL_STR; @@ -332,74 +293,56 @@ public class PodDBAdapter { SEL_FI_SMALL_STR = selFiSmall.substring(1, selFiSmall.length() - 1); } - // column indices for FEEDITEM_SEL_FI_SMALL - - public static final int IDX_FI_SMALL_ID = 0; - public static final int IDX_FI_SMALL_TITLE = 1; - public static final int IDX_FI_SMALL_PUBDATE = 2; - public static final int IDX_FI_SMALL_READ = 3; - public static final int IDX_FI_SMALL_LINK = 4; - public static final int IDX_FI_SMALL_PAYMENT_LINK = 5; - public static final int IDX_FI_SMALL_MEDIA = 6; - public static final int IDX_FI_SMALL_FEED = 7; - public static final int IDX_FI_SMALL_HAS_CHAPTERS = 8; - public static final int IDX_FI_SMALL_ITEM_IDENTIFIER = 9; - public static final int IDX_FI_SMALL_FLATTR_STATUS = 10; - public static final int IDX_FI_SMALL_IMAGE = 11; - /** * Select id, description and content-encoded column from feeditems. */ private static final String[] SEL_FI_EXTRA = {KEY_ID, KEY_DESCRIPTION, KEY_CONTENT_ENCODED, KEY_FEED}; - // column indices for SEL_FI_EXTRA - public static final int IDX_FI_EXTRA_ID = 0; - public static final int IDX_FI_EXTRA_DESCRIPTION = 1; - public static final int IDX_FI_EXTRA_CONTENT_ENCODED = 2; - public static final int IDX_FI_EXTRA_FEED = 3; + private static SQLiteDatabase db; + private static Context context; + private static PodDBHelper dbHelper; + private static int counter = 0; - static PodDBHelper dbHelperSingleton; + public static void init(Context context) { + PodDBAdapter.context = context.getApplicationContext(); + } - private static synchronized PodDBHelper getDbHelperSingleton(Context appContext) { - if (dbHelperSingleton == null) { - dbHelperSingleton = new PodDBHelper(appContext, DATABASE_NAME, null, - ClientConfig.storageCallbacks.getDatabaseVersion()); + public static synchronized PodDBAdapter getInstance() { + if(dbHelper == null) { + dbHelper = new PodDBHelper(PodDBAdapter.context, DATABASE_NAME, null); } - return dbHelperSingleton; + return new PodDBAdapter(); } - public PodDBAdapter(Context c) { - this.context = c; - helper = getDbHelperSingleton(c.getApplicationContext()); - } + private PodDBAdapter() {} public PodDBAdapter open() { if (db == null || !db.isOpen() || db.isReadOnly()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Opening DB"); + Log.v(TAG, "Opening DB"); try { - db = helper.getWritableDatabase(); + db = dbHelper.getWritableDatabase(); } catch (SQLException ex) { - ex.printStackTrace(); - db = helper.getReadableDatabase(); + Log.e(TAG, Log.getStackTraceString(ex)); + db = dbHelper.getReadableDatabase(); } } return this; } public void close() { - if (BuildConfig.DEBUG) - Log.d(TAG, "Closing DB"); - //db.close(); + // do nothing } - public static boolean deleteDatabase(Context context) { - Log.w(TAG, "Deleting database"); - dbHelperSingleton.close(); - dbHelperSingleton = null; - return context.deleteDatabase(DATABASE_NAME); + public static boolean deleteDatabase() { + PodDBAdapter adapter = getInstance(); + adapter.open(); + for (String tableName : ALL_TABLES) { + db.delete(tableName, "1", null); + } + adapter.close(); + return true; } /** @@ -435,7 +378,7 @@ public class PodDBAdapter { values.put(KEY_IS_PAGED, feed.isPaged()); values.put(KEY_NEXT_PAGE_LINK, feed.getNextPageLink()); if(feed.getItemFilter() != null && feed.getItemFilter().getValues().length > 0) { - values.put(KEY_HIDE, StringUtils.join(feed.getItemFilter().getValues(), ",")); + values.put(KEY_HIDE, TextUtils.join( ",", feed.getItemFilter().getValues())); } else { values.put(KEY_HIDE, ""); } @@ -458,15 +401,20 @@ public class PodDBAdapter { } ContentValues values = new ContentValues(); values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload()); + values.put(KEY_KEEP_UPDATED, prefs.getKeepUpdated()); + values.put(KEY_AUTO_DELETE_ACTION,prefs.getAutoDeleteAction().ordinal()); values.put(KEY_USERNAME, prefs.getUsername()); values.put(KEY_PASSWORD, prefs.getPassword()); + values.put(KEY_INCLUDE_FILTER, prefs.getFilter().getIncludeFilter()); + values.put(KEY_EXCLUDE_FILTER, prefs.getFilter().getExcludeFilter()); db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())}); } - public void setFeedItemFilter(long feedId, List<String> filterValues) { + public void setFeedItemFilter(long feedId, Set<String> filterValues) { + Log.d(TAG, "setFeedItemFilter() called with: " + "feedId = [" + feedId + "], " + + "filterValues = [" + TextUtils.join(",", filterValues) + "]"); ContentValues values = new ContentValues(); - values.put(KEY_HIDE, StringUtils.join(filterValues, ",")); - Log.d(TAG, StringUtils.join(filterValues, ",")); + values.put(KEY_HIDE, TextUtils.join(",", filterValues)); db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feedId)}); } @@ -476,7 +424,12 @@ public class PodDBAdapter { * @return the id of the entry */ public long setImage(FeedImage image) { - db.beginTransaction(); + boolean startedTransaction = false; + if(false == db.inTransaction()) { + db.beginTransaction(); + startedTransaction = true; + } + ContentValues values = new ContentValues(); values.put(KEY_TITLE, image.getTitle()); values.put(KEY_DOWNLOAD_URL, image.getDownload_url()); @@ -497,8 +450,10 @@ public class PodDBAdapter { db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(image.getOwner().getId())}); } } - db.setTransactionSuccessful(); - db.endTransaction(); + if(startedTransaction) { + db.setTransactionSuccessful(); + db.endTransaction(); + } return image.getId(); } @@ -516,10 +471,11 @@ public class PodDBAdapter { values.put(KEY_DOWNLOAD_URL, media.getDownload_url()); values.put(KEY_DOWNLOADED, media.isDownloaded()); values.put(KEY_FILE_URL, media.getFile_url()); + values.put(KEY_HAS_EMBEDDED_PICTURE, media.hasEmbeddedPicture()); + values.put(KEY_LAST_PLAYED_TIME, media.getLastPlayedTime()); if (media.getPlaybackCompletionDate() != null) { - values.put(KEY_PLAYBACK_COMPLETION_DATE, media - .getPlaybackCompletionDate().getTime()); + values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime()); } else { values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); } @@ -541,6 +497,7 @@ public class PodDBAdapter { values.put(KEY_POSITION, media.getPosition()); values.put(KEY_DURATION, media.getDuration()); values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); + values.put(KEY_LAST_PLAYED_TIME, media.getLastPlayedTime()); db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(media.getId())}); } else { @@ -736,7 +693,13 @@ public class PodDBAdapter { setFeed(item.getFeed()); } values.put(KEY_FEED, item.getFeed().getId()); - values.put(KEY_READ, item.isRead()); + if(item.isNew()) { + values.put(KEY_READ, FeedItem.NEW); + } else if(item.isPlayed()) { + values.put(KEY_READ, FeedItem.PLAYED); + } else { + values.put(KEY_READ, FeedItem.UNPLAYED); + } values.put(KEY_HAS_CHAPTERS, item.getChapters() != null || item.hasChapters()); values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); values.put(KEY_FLATTR_STATUS, item.getFlattrStatus().toLong()); @@ -763,12 +726,12 @@ public class PodDBAdapter { return item.getId(); } - public void setFeedItemRead(boolean read, long itemId, long mediaId, + public void setFeedItemRead(int played, long itemId, long mediaId, boolean resetMediaPosition) { db.beginTransaction(); ContentValues values = new ContentValues(); - values.put(KEY_READ, read); + values.put(KEY_READ, played); db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(itemId)}); if (resetMediaPosition) { @@ -781,7 +744,12 @@ public class PodDBAdapter { db.endTransaction(); } - public void setFeedItemRead(boolean read, long... itemIds) { + /** + * Sets the 'read' attribute of the item. + * @param read must be one of FeedItem.PLAYED, FeedItem.NEW, FeedItem.UNPLAYED + * @param itemIds items to change the value of + */ + public void setFeedItemRead(int read, long... itemIds) { db.beginTransaction(); ContentValues values = new ContentValues(); for (long id : itemIds) { @@ -802,8 +770,7 @@ public class PodDBAdapter { values.put(KEY_LINK, chapter.getLink()); values.put(KEY_CHAPTER_TYPE, chapter.getChapterType()); if (chapter.getId() == 0) { - chapter.setId(db - .insert(TABLE_NAME_SIMPLECHAPTERS, null, values)); + chapter.setId(db.insert(TABLE_NAME_SIMPLECHAPTERS, null, values)); } else { db.update(TABLE_NAME_SIMPLECHAPTERS, values, KEY_ID + "=?", new String[]{String.valueOf(chapter.getId())}); @@ -839,11 +806,65 @@ public class PodDBAdapter { return status.getId(); } - public void setFeedItemAutoDownload(FeedItem feedItem, boolean autoDownload) { + public void setFeedItemAutoDownload(FeedItem feedItem, long autoDownload) { ContentValues values = new ContentValues(); values.put(KEY_AUTO_DOWNLOAD, autoDownload); db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", - new String[] { String.valueOf(feedItem.getId()) } ); + new String[]{String.valueOf(feedItem.getId())}); + } + + public void setFeedsItemsAutoDownload(Feed feed, boolean autoDownload) { + final String sql = "UPDATE " + TABLE_NAME_FEED_ITEMS + + " SET " + KEY_AUTO_DOWNLOAD + "="+ (autoDownload ? "1" : "0") + + " WHERE " + KEY_FEED + "=" + feed.getId(); + db.execSQL(sql); + } + + public void setFavorites(List<FeedItem> favorites) { + ContentValues values = new ContentValues(); + db.beginTransaction(); + db.delete(TABLE_NAME_FAVORITES, null, null); + for (int i = 0; i < favorites.size(); i++) { + FeedItem item = favorites.get(i); + values.put(KEY_ID, i); + values.put(KEY_FEEDITEM, item.getId()); + values.put(KEY_FEED, item.getFeed().getId()); + db.insertWithOnConflict(TABLE_NAME_FAVORITES, null, values, SQLiteDatabase.CONFLICT_REPLACE); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + /** + * Adds the item to favorites + */ + public void addFavoriteItem(FeedItem item) { + // don't add an item that's already there... + if (isItemInFavorites(item)) { + Log.d(TAG, "item already in favorites"); + return; + } + ContentValues values = new ContentValues(); + values.put(KEY_FEEDITEM, item.getId()); + values.put(KEY_FEED, item.getFeedId()); + db.insert(TABLE_NAME_FAVORITES, null, values); + } + + public void removeFavoriteItem(FeedItem item) { + String deleteClause = String.format("DELETE FROM %s WHERE %s=%s AND %s=%s", + TABLE_NAME_FAVORITES, + KEY_FEEDITEM, item.getId(), + KEY_FEED, item.getFeedId()); + db.execSQL(deleteClause); + } + + public boolean isItemInFavorites(FeedItem item) { + String query = String.format("SELECT %s from %s WHERE %s=%d", + KEY_ID, TABLE_NAME_FAVORITES, KEY_FEEDITEM, item.getId()); + Cursor c = db.rawQuery(query, null); + int count = c.getCount(); + c.close(); + return count > 0; } public long getDownloadLogSize() { @@ -857,14 +878,6 @@ public class PodDBAdapter { return count; } - public void removeDownloadLogItems(long count) { - if (count > 0) { - final String sql = String.format("DELETE FROM %s WHERE %s in (SELECT %s from %s ORDER BY %s ASC LIMIT %d)", - TABLE_NAME_DOWNLOAD_LOG, KEY_ID, KEY_ID, TABLE_NAME_DOWNLOAD_LOG, KEY_COMPLETION_DATE, count); - db.execSQL(sql, null); - } - } - public void setQueue(List<FeedItem> queue) { ContentValues values = new ContentValues(); db.beginTransaction(); @@ -874,8 +887,7 @@ public class PodDBAdapter { values.put(KEY_ID, i); values.put(KEY_FEEDITEM, item.getId()); values.put(KEY_FEED, item.getFeed().getId()); - db.insertWithOnConflict(TABLE_NAME_QUEUE, null, values, - SQLiteDatabase.CONFLICT_REPLACE); + db.insertWithOnConflict(TABLE_NAME_QUEUE, null, values, SQLiteDatabase.CONFLICT_REPLACE); } db.setTransactionSuccessful(); db.endTransaction(); @@ -886,6 +898,10 @@ public class PodDBAdapter { } public void removeFeedMedia(FeedMedia media) { + // delete download log entries for feed media + db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILE + "=? AND " + KEY_FEEDFILETYPE +"=?", + new String[] { String.valueOf(media.getId()), String.valueOf(FeedMedia.FEEDFILETYPE_FEEDMEDIA) }); + db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?", new String[]{String.valueOf(media.getId())}); } @@ -930,6 +946,9 @@ public class PodDBAdapter { removeFeedItem(item); } } + // delete download log entries for feed + db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_FEEDFILE + "=? AND " + KEY_FEEDFILETYPE +"=?", + new String[] { String.valueOf(feed.getId()), String.valueOf(Feed.FEEDFILETYPE_FEED) }); db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())}); @@ -937,11 +956,6 @@ public class PodDBAdapter { db.endTransaction(); } - public void removeDownloadStatus(DownloadStatus remove) { - db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_ID + "=?", - new String[]{String.valueOf(remove.getId())}); - } - public void clearPlaybackHistory() { ContentValues values = new ContentValues(); values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); @@ -967,13 +981,6 @@ public class PodDBAdapter { return db.query(TABLE_NAME_FEEDS, new String[]{KEY_ID, KEY_DOWNLOAD_URL}, null, null, null, null, null); } - public final Cursor getExpiredFeedsCursor(long expirationTime) { - Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_LASTUPDATE + " < " + String.valueOf(System.currentTimeMillis() - expirationTime), - null, null, null, - null); - return c; - } - /** * Returns a cursor with all FeedItems of a Feed. Uses FEEDITEM_SEL_FI_SMALL * @@ -1017,15 +1024,44 @@ public class PodDBAdapter { } /** - * Returns a cursor for a DB query in the FeedImages table for a given ID. + * Returns a cursor for a DB query in the FeedImages table for given IDs. * - * @param id ID of the FeedImage + * @param imageIds IDs of the images * @return The cursor of the query */ - public final Cursor getImageCursor(final long id) { - Cursor c = db.query(TABLE_NAME_FEED_IMAGES, null, KEY_ID + "=?", - new String[]{String.valueOf(id)}, null, null, null); - return c; + public final Cursor getImageCursor(String... imageIds) { + int length = imageIds.length; + if (length > IN_OPERATOR_MAXIMUM) { + Log.w(TAG, "Length of id array is larger than " + + IN_OPERATOR_MAXIMUM + ". Creating multiple cursors"); + int numCursors = (int) (((double) length) / (IN_OPERATOR_MAXIMUM)) + 1; + Cursor[] cursors = new Cursor[numCursors]; + for (int i = 0; i < numCursors; i++) { + int neededLength; + String[] parts; + final int elementsLeft = length - i * IN_OPERATOR_MAXIMUM; + + if (elementsLeft >= IN_OPERATOR_MAXIMUM) { + neededLength = IN_OPERATOR_MAXIMUM; + parts = Arrays.copyOfRange(imageIds, i + * IN_OPERATOR_MAXIMUM, (i + 1) + * IN_OPERATOR_MAXIMUM); + } else { + neededLength = elementsLeft; + parts = Arrays.copyOfRange(imageIds, i + * IN_OPERATOR_MAXIMUM, (i * IN_OPERATOR_MAXIMUM) + + neededLength); + } + + cursors[i] = db.rawQuery("SELECT * FROM " + + TABLE_NAME_FEED_IMAGES + " WHERE " + KEY_ID + " IN " + + buildInOperator(neededLength), parts); + } + return new MergeCursor(cursors); + } else { + return db.query(TABLE_NAME_FEED_IMAGES, null, KEY_ID + " IN " + + buildInOperator(length), imageIds, null, null, null); + } } public final Cursor getSimpleChaptersOfFeedItemCursor(final FeedItem item) { @@ -1053,23 +1089,17 @@ public class PodDBAdapter { /** * Returns a cursor which contains all feed items in the queue. The returned * cursor uses the FEEDITEM_SEL_FI_SMALL selection. + * cursor uses the FEEDITEM_SEL_FI_SMALL selection. */ public final Cursor getQueueCursor() { - Object[] args = (Object[]) new String[]{ - SEL_FI_SMALL_STR + "," + TABLE_NAME_QUEUE + "." + KEY_ID, + Object[] args = new String[] { + SEL_FI_SMALL_STR, TABLE_NAME_FEED_ITEMS, TABLE_NAME_QUEUE, TABLE_NAME_FEED_ITEMS + "." + KEY_ID, TABLE_NAME_QUEUE + "." + KEY_FEEDITEM, - TABLE_NAME_QUEUE + "." + KEY_ID}; - String query = String.format( - "SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s", args); + TABLE_NAME_QUEUE + "." + KEY_ID }; + String query = String.format("SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s", args); Cursor c = db.rawQuery(query, null); - /* - * Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, - * "INNER JOIN ? ON ?=?", new String[] { TABLE_NAME_QUEUE, - * TABLE_NAME_FEED_ITEMS + "." + KEY_ID, TABLE_NAME_QUEUE + "." + - * KEY_FEEDITEM }, null, null, TABLE_NAME_QUEUE + "." + KEY_FEEDITEM); - */ return c; } @@ -1078,51 +1108,58 @@ public class PodDBAdapter { return c; } + + public final Cursor getFavoritesCursor() { + Object[] args = new String[] { + SEL_FI_SMALL_STR, + TABLE_NAME_FEED_ITEMS, TABLE_NAME_FAVORITES, + TABLE_NAME_FEED_ITEMS + "." + KEY_ID, + TABLE_NAME_FAVORITES + "." + KEY_FEEDITEM, + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE }; + String query = String.format("SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s DESC", args); + Cursor c = db.rawQuery(query, null); + return c; + } + /** * Returns a cursor which contains all feed items in the unread items list. * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection. */ public final Cursor getUnreadItemsCursor() { Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_READ - + "=0", null, null, null, KEY_PUBDATE + " DESC"); + + "<" + FeedItem.PLAYED, null, null, null, KEY_PUBDATE + " DESC"); return c; } - public final Cursor getNewItemIdsCursor() { - final String query = "SELECT " + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + /** + * Returns a cursor which contains all items of a feed that are considered new. + * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection. + */ + public final Cursor getNewItemsIdsCursor(long feedId) { + final String query = "SELECT " + KEY_ID + " FROM " + TABLE_NAME_FEED_ITEMS - + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON " - + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" - + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM - + " LEFT OUTER JOIN " + TABLE_NAME_QUEUE + " ON " - + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" - + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM - + " WHERE " - + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed - + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded - + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played - + TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL"; // not in queue - return db.rawQuery(query, null); + + " WHERE " + KEY_FEED + "=" + feedId + + " AND " + KEY_READ + "=" + FeedItem.NEW + + " ORDER BY " + KEY_PUBDATE + " DESC"; + Cursor c = db.rawQuery(query, null); + return c; } /** * Returns a cursor which contains all feed items that are considered new. + * Excludes those feeds that do not have 'Keep Updated' enabled. * The returned cursor uses the FEEDITEM_SEL_FI_SMALL selection. */ public final Cursor getNewItemsCursor() { - final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS - + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON " - + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" - + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM - + " LEFT OUTER JOIN " + TABLE_NAME_QUEUE + " ON " - + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" - + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM - + " WHERE " - + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed - + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded - + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played - + TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL" // not in queue - + " ORDER BY " + KEY_PUBDATE + " DESC"; + String[] args = new String[] { + SEL_FI_SMALL_STR, + TABLE_NAME_FEED_ITEMS, + TABLE_NAME_FEEDS, + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID, + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + "=" + FeedItem.NEW + " AND " + TABLE_NAME_FEEDS + "." + KEY_KEEP_UPDATED + " > 0", + KEY_PUBDATE + " DESC" + }; + final String query = String.format("SELECT %s FROM %s INNER JOIN %s ON %s WHERE %s ORDER BY %s", args); Cursor c = db.rawQuery(query, null); return c; } @@ -1133,11 +1170,11 @@ public class PodDBAdapter { } public Cursor getDownloadedItemsCursor() { - final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS - + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON " - + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" - + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + " WHERE " - + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0"; + final String query = "SELECT " + SEL_FI_SMALL_STR + + " FROM " + TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + + " WHERE " + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0"; Cursor c = db.rawQuery(query, null); return c; } @@ -1151,7 +1188,9 @@ public class PodDBAdapter { * @throws IllegalArgumentException if limit < 0 */ public final Cursor getCompletedMediaCursor(int limit) { - Validate.isTrue(limit >= 0, "Limit must be >= 0"); + if(limit < 0) { + throw new IllegalArgumentException("Limit must be >= 0"); + } Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, KEY_PLAYBACK_COMPLETION_DATE + " > 0", null, null, @@ -1163,26 +1202,26 @@ public class PodDBAdapter { return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", new String[]{String.valueOf(id)}, null, null, null); } - public final Cursor getFeedMediaCursorByItemID(String... mediaIds) { - int length = mediaIds.length; + public final Cursor getFeedMediaCursor(String... itemIds) { + int length = itemIds.length; if (length > IN_OPERATOR_MAXIMUM) { Log.w(TAG, "Length of id array is larger than " + IN_OPERATOR_MAXIMUM + ". Creating multiple cursors"); int numCursors = (int) (((double) length) / (IN_OPERATOR_MAXIMUM)) + 1; Cursor[] cursors = new Cursor[numCursors]; for (int i = 0; i < numCursors; i++) { - int neededLength = 0; - String[] parts = null; + int neededLength; + String[] parts; final int elementsLeft = length - i * IN_OPERATOR_MAXIMUM; if (elementsLeft >= IN_OPERATOR_MAXIMUM) { neededLength = IN_OPERATOR_MAXIMUM; - parts = Arrays.copyOfRange(mediaIds, i + parts = Arrays.copyOfRange(itemIds, i * IN_OPERATOR_MAXIMUM, (i + 1) * IN_OPERATOR_MAXIMUM); } else { neededLength = elementsLeft; - parts = Arrays.copyOfRange(mediaIds, i + parts = Arrays.copyOfRange(itemIds, i * IN_OPERATOR_MAXIMUM, (i * IN_OPERATOR_MAXIMUM) + neededLength); } @@ -1194,7 +1233,7 @@ public class PodDBAdapter { return new MergeCursor(cursors); } else { return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_FEEDITEM + " IN " - + buildInOperator(length), mediaIds, null, null, null); + + buildInOperator(length), itemIds, null, null, null); } } @@ -1237,25 +1276,31 @@ public class PodDBAdapter { } public final Cursor getFeedItemCursor(final String podcastUrl, final String episodeUrl) { - final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS - + " INNER JOIN " + - TABLE_NAME_FEEDS + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + - TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + "='" + - episodeUrl + "' AND " + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL + "='" + podcastUrl + "'"; + String downloadUrl = DatabaseUtils.sqlEscapeString(podcastUrl); + String itemIdentifier = DatabaseUtils.sqlEscapeString(episodeUrl); + final String query = "" + + "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + TABLE_NAME_FEEDS + + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID + + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER + "=" + itemIdentifier + + " AND " + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL + "=" + downloadUrl; return db.rawQuery(query, null); } public Cursor getImageAuthenticationCursor(final String imageUrl) { - final String query = "SELECT " + KEY_USERNAME + "," + KEY_PASSWORD + " FROM " - + TABLE_NAME_FEED_IMAGES + " INNER JOIN " + TABLE_NAME_FEEDS + " ON " + - TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" + TABLE_NAME_FEEDS + "." + KEY_IMAGE + " WHERE " - + TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "='" + imageUrl + "' UNION SELECT " - + KEY_USERNAME + "," + KEY_PASSWORD + " FROM " + TABLE_NAME_FEED_IMAGES + " INNER JOIN " - + TABLE_NAME_FEED_ITEMS + " ON " + TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" + - TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE + " INNER JOIN " + TABLE_NAME_FEEDS + " ON " - + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID + " WHERE " - + TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "='" + imageUrl + "'"; - Log.d(TAG, "Query: " + query); + String downloadUrl = DatabaseUtils.sqlEscapeString(imageUrl); + final String query = "" + + "SELECT " + KEY_USERNAME + "," + KEY_PASSWORD + " FROM " + TABLE_NAME_FEED_IMAGES + + " INNER JOIN " + TABLE_NAME_FEEDS + + " ON " + TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" + TABLE_NAME_FEEDS + "." + KEY_IMAGE + + " WHERE " + TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "=" + downloadUrl + + " UNION SELECT " + KEY_USERNAME + "," + KEY_PASSWORD + + " FROM " + TABLE_NAME_FEED_IMAGES + + " INNER JOIN " + TABLE_NAME_FEED_ITEMS + + " ON " + TABLE_NAME_FEED_IMAGES + "." + KEY_ID + "=" + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE + + " INNER JOIN " + TABLE_NAME_FEEDS + + " ON " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + TABLE_NAME_FEEDS + "." + KEY_ID + + " WHERE " + TABLE_NAME_FEED_IMAGES + "." + KEY_DOWNLOAD_URL + "=" + downloadUrl; return db.rawQuery(query, null); } @@ -1271,19 +1316,9 @@ public class PodDBAdapter { } public final int getNumberOfNewItems() { - final String query = "SELECT COUNT(" + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + ")" - +" FROM " + TABLE_NAME_FEED_ITEMS - + " LEFT JOIN " + TABLE_NAME_FEED_MEDIA + " ON " - + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" - + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM - + " LEFT JOIN " + TABLE_NAME_QUEUE + " ON " - + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" - + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM - + " WHERE " - + TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed - + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded - + TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played - + TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL"; // not in queue + final String query = "SELECT COUNT(" + KEY_ID + ")" + + " FROM " + TABLE_NAME_FEED_ITEMS + + " WHERE " + KEY_READ + "=" + FeedItem.NEW; Cursor c = db.rawQuery(query, null); int result = 0; if (c.moveToFirst()) { @@ -1293,12 +1328,37 @@ public class PodDBAdapter { return result; } - public final LongIntMap getNumberOfUnreadFeedItems(long... feedIds) { + public final LongIntMap getFeedCounters(long... feedIds) { + int setting = UserPreferences.getFeedCounterSetting(); + String whereRead; + if(setting == UserPreferences.FEED_COUNTER_SHOW_NEW_UNPLAYED_SUM) { + whereRead = "(" + KEY_READ + "=" + FeedItem.NEW + + " OR " + KEY_READ + "=" + FeedItem.UNPLAYED + ")"; + } else if(setting == UserPreferences.FEED_COUNTER_SHOW_NEW) { + whereRead = KEY_READ + "=" + FeedItem.NEW; + } else if(setting == UserPreferences.FEED_COUNTER_SHOW_UNPLAYED) { + whereRead = KEY_READ + "=" + FeedItem.UNPLAYED; + } else { // NONE + return new LongIntMap(0); + } + + // work around TextUtils.join wanting only boxed items + // and StringUtils.join() causing NoSuchMethodErrors on MIUI + StringBuilder builder = new StringBuilder(); + for (long id : feedIds) { + builder.append(id); + builder.append(','); + } + if (feedIds.length > 0) { + // there's an extra ',', get rid of it + builder.deleteCharAt(builder.length() - 1); + } + final String query = "SELECT " + KEY_FEED + ", COUNT(" + KEY_ID + ") AS count " + " FROM " + TABLE_NAME_FEED_ITEMS - + " WHERE " + KEY_FEED + " IN (" + StringUtils.join(feedIds, ',') + ") " - + " AND " + KEY_READ + " = 0" - + " GROUP BY " + KEY_FEED; + + " WHERE " + KEY_FEED + " IN (" + builder.toString() + ") " + + " AND " + whereRead + " GROUP BY " + KEY_FEED; + Cursor c = db.rawQuery(query, null); LongIntMap result = new LongIntMap(c.getCount()); if (c.moveToFirst()) { @@ -1451,17 +1511,22 @@ public class PodDBAdapter { * Helper class for opening the Antennapod database. */ private static class PodDBHelper extends SQLiteOpenHelper { + + private final static int VERSION = 1050003; + + private Context context; + /** * Constructor. * * @param context Context to use * @param name Name of the database * @param factory to use for creating cursor objects - * @param version number of the database */ public PodDBHelper(final Context context, final String name, - final CursorFactory factory, final int version) { - super(context, name, factory, version); + final CursorFactory factory) { + super(context, name, factory, VERSION); + this.context = context; } @Override @@ -1473,9 +1538,12 @@ public class PodDBAdapter { db.execSQL(CREATE_TABLE_DOWNLOAD_LOG); db.execSQL(CREATE_TABLE_QUEUE); db.execSQL(CREATE_TABLE_SIMPLECHAPTERS); + db.execSQL(CREATE_TABLE_FAVORITES); db.execSQL(CREATE_INDEX_FEEDITEMS_FEED); db.execSQL(CREATE_INDEX_FEEDITEMS_IMAGE); + db.execSQL(CREATE_INDEX_FEEDITEMS_PUBDATE); + db.execSQL(CREATE_INDEX_FEEDITEMS_READ); db.execSQL(CREATE_INDEX_FEEDMEDIA_FEEDITEM); db.execSQL(CREATE_INDEX_QUEUE_FEEDITEM); db.execSQL(CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM); @@ -1485,7 +1553,259 @@ public class PodDBAdapter { @Override public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) { - ClientConfig.storageCallbacks.onUpgrade(db, oldVersion, newVersion); + EventBus.getDefault().post(ProgressEvent.start(context.getString(R.string.progress_upgrading_database))); + Log.w("DBAdapter", "Upgrading from version " + oldVersion + " to " + + newVersion + "."); + if (oldVersion <= 1) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + + KEY_TYPE + " TEXT"); + } + if (oldVersion <= 2) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + KEY_LINK + " TEXT"); + } + if (oldVersion <= 3) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + KEY_ITEM_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 4) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + " ADD COLUMN " + + KEY_FEED_IDENTIFIER + " TEXT"); + } + if (oldVersion <= 5) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + KEY_REASON_DETAILED + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_DOWNLOAD_LOG + + " ADD COLUMN " + KEY_DOWNLOADSTATUS_TITLE + " TEXT"); + } + if (oldVersion <= 6) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS + + " ADD COLUMN " + KEY_CHAPTER_TYPE + " INTEGER"); + } + if (oldVersion <= 7) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + KEY_PLAYBACK_COMPLETION_DATE + + " INTEGER"); + } + if (oldVersion <= 8) { + final int KEY_ID_POSITION = 0; + final int KEY_MEDIA_POSITION = 1; + + // Add feeditem column to feedmedia table + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + KEY_FEEDITEM + + " INTEGER"); + Cursor feeditemCursor = db.query(PodDBAdapter.TABLE_NAME_FEED_ITEMS, + new String[]{KEY_ID, KEY_MEDIA}, "? > 0", + new String[]{KEY_MEDIA}, null, null, null); + if (feeditemCursor.moveToFirst()) { + db.beginTransaction(); + ContentValues contentValues = new ContentValues(); + do { + long mediaId = feeditemCursor.getLong(KEY_MEDIA_POSITION); + contentValues.put(KEY_FEEDITEM, feeditemCursor.getLong(KEY_ID_POSITION)); + db.update(PodDBAdapter.TABLE_NAME_FEED_MEDIA, contentValues, KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); + contentValues.clear(); + } while (feeditemCursor.moveToNext()); + db.setTransactionSuccessful(); + db.endTransaction(); + } + feeditemCursor.close(); + } + if (oldVersion <= 9) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_AUTO_DOWNLOAD + + " INTEGER DEFAULT 1"); + } + if (oldVersion <= 10) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + KEY_FLATTR_STATUS + + " INTEGER"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + KEY_PLAYED_DURATION + + " INTEGER"); + } + if (oldVersion <= 11) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_USERNAME + + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_PASSWORD + + " TEXT"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + KEY_IMAGE + + " INTEGER"); + } + if (oldVersion <= 12) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_IS_PAGED + " INTEGER DEFAULT 0"); + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_NEXT_PAGE_LINK + " TEXT"); + } + if (oldVersion <= 13) { + // remove duplicate rows in "Chapters" table that were created because of a bug. + db.execSQL(String.format("DELETE FROM %s WHERE %s NOT IN " + + "(SELECT MIN(%s) as %s FROM %s GROUP BY %s,%s,%s,%s,%s)", + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS, + KEY_ID, + KEY_ID, + KEY_ID, + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS, + KEY_TITLE, + KEY_START, + KEY_FEEDITEM, + KEY_LINK, + KEY_CHAPTER_TYPE)); + } + if(oldVersion <= 14) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " ADD COLUMN " + KEY_AUTO_DOWNLOAD + " INTEGER"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " SET " + KEY_AUTO_DOWNLOAD + " = " + + "(SELECT " + KEY_AUTO_DOWNLOAD + + " FROM " + PodDBAdapter.TABLE_NAME_FEEDS + + " WHERE " + PodDBAdapter.TABLE_NAME_FEEDS + "." + KEY_ID + + " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + ")"); + + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_HIDE + " TEXT"); + + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + KEY_LAST_UPDATE_FAILED + " INTEGER DEFAULT 0"); + + // create indexes + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_FEED); + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_IMAGE); + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDMEDIA_FEEDITEM); + db.execSQL(PodDBAdapter.CREATE_INDEX_QUEUE_FEEDITEM); + db.execSQL(PodDBAdapter.CREATE_INDEX_SIMPLECHAPTERS_FEEDITEM); + } + if(oldVersion <= 15) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + KEY_HAS_EMBEDDED_PICTURE + " INTEGER DEFAULT -1"); + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " SET " + KEY_HAS_EMBEDDED_PICTURE + "=0" + + " WHERE " + KEY_DOWNLOADED + "=0"); + Cursor c = db.rawQuery("SELECT " + KEY_FILE_URL + + " FROM " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " WHERE " + KEY_DOWNLOADED + "=1 " + + " AND " + KEY_HAS_EMBEDDED_PICTURE + "=-1", null); + if(c.moveToFirst()) { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + do { + String fileUrl = c.getString(0); + try { + mmr.setDataSource(fileUrl); + byte[] image = mmr.getEmbeddedPicture(); + if (image != null) { + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " SET " + KEY_HAS_EMBEDDED_PICTURE + "=1" + + " WHERE " + KEY_FILE_URL + "='"+ fileUrl + "'"); + } else { + db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " SET " + KEY_HAS_EMBEDDED_PICTURE + "=0" + + " WHERE " + KEY_FILE_URL + "='"+ fileUrl + "'"); + } + } catch(Exception e) { + e.printStackTrace(); + } + } while(c.moveToNext()); + } + c.close(); + } + if(oldVersion <= 16) { + String selectNew = "SELECT " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + KEY_ID + + " FROM " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + " ON " + + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + + " LEFT OUTER JOIN " + PodDBAdapter.TABLE_NAME_QUEUE + " ON " + + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + PodDBAdapter.TABLE_NAME_QUEUE + "." + KEY_FEEDITEM + + " WHERE " + + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + KEY_READ + " = 0 AND " // unplayed + + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + " = 0 AND " // undownloaded + + PodDBAdapter.TABLE_NAME_FEED_MEDIA + "." + KEY_POSITION + " = 0 AND " // not partially played + + PodDBAdapter.TABLE_NAME_QUEUE + "." + KEY_ID + " IS NULL"; // not in queue + String sql = "UPDATE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + + " SET " + KEY_READ + "=" + FeedItem.NEW + + " WHERE " + KEY_ID + " IN (" + selectNew + ")"; + Log.d("Migration", "SQL: " + sql); + db.execSQL(sql); + } + if(oldVersion <= 17) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_AUTO_DELETE_ACTION + " INTEGER DEFAULT 0"); + } + if(oldVersion < 1030005) { + db.execSQL("UPDATE FeedItems SET auto_download=0 WHERE " + + "(read=1 OR id IN (SELECT feeditem FROM FeedMedia WHERE position>0 OR downloaded=1)) " + + "AND id NOT IN (SELECT feeditem FROM Queue)"); + } + if(oldVersion < 1040001) { + db.execSQL(CREATE_TABLE_FAVORITES); + } + if (oldVersion < 1040002) { + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_MEDIA + + " ADD COLUMN " + PodDBAdapter.KEY_LAST_PLAYED_TIME + " INTEGER DEFAULT 0"); + } + if(oldVersion < 1040013) { + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_PUBDATE); + db.execSQL(PodDBAdapter.CREATE_INDEX_FEEDITEMS_READ); + } + + if (oldVersion < 1050003) { + // Migrates feed list filter data + + db.beginTransaction(); + + // Change to intermediate values to avoid overwriting in the following find/replace + db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + + "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'unplayed', 'noplay')"); + db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + + "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'not_queued', 'noqueue')"); + db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + + "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'not_downloaded', 'nodl')"); + + // Replace played, queued, and downloaded with their opposites + db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + + "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'played', 'unplayed')"); + db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + + "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'queued', 'not_queued')"); + db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + + "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'downloaded', 'not_downloaded')"); + + // Now replace intermediates for unplayed, not queued, etc. with their opposites + db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + + "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'noplay', 'played')"); + db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + + "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'noqueue', 'queued')"); + db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + + "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'nodl', 'downloaded')"); + + // Paused doesn't have an opposite, so unplayed is the next best option + db.execSQL("UPDATE " + TABLE_NAME_FEEDS + "\n" + + "SET " + KEY_HIDE + " = replace(" + KEY_HIDE + ", 'paused', 'unplayed')"); + + db.setTransactionSuccessful(); + db.endTransaction(); + + // and now get ready for autodownload filters + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_INCLUDE_FILTER + " TEXT DEFAULT ''"); + + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_EXCLUDE_FILTER + " TEXT DEFAULT ''"); + + // and now auto refresh + db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS + + " ADD COLUMN " + PodDBAdapter.KEY_KEEP_UPDATED + " INTEGER DEFAULT 1"); + } + + EventBus.getDefault().post(ProgressEvent.end()); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java index 413a11f8e..9280db8a3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java @@ -1,8 +1,8 @@ package de.danoeh.antennapod.core.syndication.handler; +import android.support.v4.util.ArrayMap; + import java.util.ArrayList; -import java.util.HashMap; -import java.util.LinkedHashMap; import java.util.Map; import java.util.Stack; @@ -32,7 +32,7 @@ public class HandlerState { /** * Namespaces that have been defined so far. */ - protected HashMap<String, Namespace> namespaces; + protected Map<String, Namespace> namespaces; protected Stack<Namespace> defaultNamespaces; /** * Buffer for saving characters. @@ -42,16 +42,16 @@ public class HandlerState { /** * Temporarily saved objects. */ - protected HashMap<String, Object> tempObjects; + protected Map<String, Object> tempObjects; public HandlerState(Feed feed) { this.feed = feed; - alternateUrls = new LinkedHashMap<String, String>(); + alternateUrls = new ArrayMap<>(); items = new ArrayList<FeedItem>(); tagstack = new Stack<SyndElement>(); - namespaces = new HashMap<String, Namespace>(); + namespaces = new ArrayMap<>(); defaultNamespaces = new Stack<Namespace>(); - tempObjects = new HashMap<String, Object>(); + tempObjects = new ArrayMap<>(); } public Feed getFeed() { @@ -105,7 +105,7 @@ public class HandlerState { alternateUrls.put(url, title); } - public HashMap<String, Object> getTempObjects() { + public Map<String, Object> getTempObjects() { return tempObjects; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java index 32cd538d5..4d56e1365 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java @@ -1,8 +1,7 @@ package de.danoeh.antennapod.core.syndication.handler; import android.util.Log; -import de.danoeh.antennapod.core.BuildConfig; -import de.danoeh.antennapod.core.feed.Feed; + import org.apache.commons.io.input.XmlStreamReader; import org.jsoup.Jsoup; import org.xmlpull.v1.XmlPullParser; @@ -14,6 +13,8 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.Reader; +import de.danoeh.antennapod.core.feed.Feed; + /** Gets the type of a specific feed by reading the root element. */ public class TypeGetter { private static final String TAG = "TypeGetter"; @@ -28,11 +29,13 @@ public class TypeGetter { public Type getType(Feed feed) throws UnsupportedFeedtypeException { XmlPullParserFactory factory; if (feed.getFile_url() != null) { + Reader reader = null; try { factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); XmlPullParser xpp = factory.newPullParser(); - xpp.setInput(createReader(feed)); + reader = createReader(feed); + xpp.setInput(reader); int eventType = xpp.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { @@ -40,38 +43,30 @@ public class TypeGetter { String tag = xpp.getName(); if (tag.equals(ATOM_ROOT)) { feed.setType(Feed.TYPE_ATOM1); - if (BuildConfig.DEBUG) - Log.d(TAG, "Recognized type Atom"); + Log.d(TAG, "Recognized type Atom"); return Type.ATOM; } else if (tag.equals(RSS_ROOT)) { - String strVersion = xpp.getAttributeValue(null, - "version"); + String strVersion = xpp.getAttributeValue(null, "version"); if (strVersion != null) { - if (strVersion.equals("2.0")) { feed.setType(Feed.TYPE_RSS2); - if (BuildConfig.DEBUG) - Log.d(TAG, "Recognized type RSS 2.0"); + Log.d(TAG, "Recognized type RSS 2.0"); return Type.RSS20; } else if (strVersion.equals("0.91") || strVersion.equals("0.92")) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Recognized type RSS 0.91/0.92"); + Log.d(TAG, "Recognized type RSS 0.91/0.92"); return Type.RSS091; } } throw new UnsupportedFeedtypeException(Type.INVALID); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Type is invalid"); + Log.d(TAG, "Type is invalid"); throw new UnsupportedFeedtypeException(Type.INVALID, tag); } } else { eventType = xpp.next(); } } - } catch (XmlPullParserException e) { e.printStackTrace(); // XML document might actually be a HTML document -> try to parse as HTML @@ -88,10 +83,17 @@ public class TypeGetter { } catch (IOException e) { e.printStackTrace(); + } finally { + if(reader != null) { + try { + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } } } - if (BuildConfig.DEBUG) - Log.d(TAG, "Type is invalid"); + Log.d(TAG, "Type is invalid"); throw new UnsupportedFeedtypeException(Type.INVALID); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java index ff9828eba..99c4cd67a 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java @@ -1,11 +1,14 @@ package de.danoeh.antennapod.core.syndication.namespace; -import de.danoeh.antennapod.core.feed.FeedImage; -import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import android.text.TextUtils; + import org.xml.sax.Attributes; import java.util.concurrent.TimeUnit; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; + public class NSITunes extends Namespace { public static final String NSTAG = "itunes"; public static final String NSURI = "http://www.itunes.com/dtds/podcast-1.0.dtd"; @@ -16,6 +19,8 @@ public class NSITunes extends Namespace { private static final String AUTHOR = "author"; public static final String DURATION = "duration"; + public static final String SUBTITLE = "subtitle"; + public static final String SUMMARY = "summary"; @Override @@ -34,7 +39,8 @@ public class NSITunes extends Namespace { } else { // this is the feed image - if (state.getFeed().getImage() == null) { + // prefer to all other images + if(!TextUtils.isEmpty(image.getDownload_url())) { image.setOwner(state.getFeed()); state.getFeed().setImage(image); } @@ -63,13 +69,28 @@ public class NSITunes extends Namespace { } else { return; } - state.getTempObjects().put(DURATION, duration); } catch (NumberFormatException e) { e.printStackTrace(); } + } else if (localName.equals(SUBTITLE)) { + String subtitle = state.getContentBuf().toString(); + if (state.getCurrentItem() != null) { + if (TextUtils.isEmpty(state.getCurrentItem().getDescription())) { + state.getCurrentItem().setDescription(subtitle); + } + } else { + if (TextUtils.isEmpty(state.getFeed().getDescription())) { + state.getFeed().setDescription(subtitle); + } + } + } else if (localName.equals(SUMMARY)) { + String summary = state.getContentBuf().toString(); + if (state.getCurrentItem() != null) { + state.getCurrentItem().setDescription(summary); + } else { + state.getFeed().setDescription(summary); + } } - } - } diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java index 6455332be..7e19213be 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java @@ -5,6 +5,7 @@ import android.util.Log; import org.xml.sax.Attributes; import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; @@ -57,6 +58,10 @@ public class NSRSS20 extends Namespace { long size = 0; try { size = Long.parseLong(attributes.getValue(ENC_LEN)); + if(size < 16384) { + // less than 16kb is suspicious, check manually + size = 0; + } } catch (NumberFormatException e) { if (BuildConfig.DEBUG) Log.d(TAG, "Length attribute could not be parsed."); @@ -69,8 +74,11 @@ public class NSRSS20 extends Namespace { if (state.getTagstack().size() >= 1) { String parent = state.getTagstack().peek().getName(); if (parent.equals(CHANNEL)) { - state.getFeed().setImage(new FeedImage()); - state.getFeed().getImage().setOwner(state.getFeed()); + Feed feed = state.getFeed(); + if(feed.getImage() == null) { + feed.setImage(new FeedImage()); + feed.getImage().setOwner(state.getFeed()); + } } } } @@ -115,13 +123,16 @@ public class NSRSS20 extends Namespace { state.getCurrentItem().setItemIdentifier(content); } } else if (top.equals(TITLE)) { + String title = content.trim(); if (second.equals(ITEM)) { - state.getCurrentItem().setTitle(content); + state.getCurrentItem().setTitle(title); } else if (second.equals(CHANNEL)) { - state.getFeed().setTitle(content); + state.getFeed().setTitle(title); } else if (second.equals(IMAGE) && third != null && third.equals(CHANNEL)) { - state.getFeed().getImage().setTitle(content); + if(state.getFeed().getImage().getTitle() == null) { + state.getFeed().getImage().setTitle(title); + } } } else if (top.equals(LINK)) { if (second.equals(CHANNEL)) { @@ -134,7 +145,9 @@ public class NSRSS20 extends Namespace { DateUtils.parse(content)); } else if (top.equals(URL) && second.equals(IMAGE) && third != null && third.equals(CHANNEL)) { - state.getFeed().getImage().setDownload_url(content); + if(state.getFeed().getImage().getDownload_url() == null) { // prefer itunes:image + state.getFeed().getImage().setDownload_url(content); + } } else if (localName.equals(DESCR)) { if (second.equals(CHANNEL)) { state.getFeed().setDescription(content); diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java index abff5b2db..b23a142af 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java @@ -4,7 +4,6 @@ import android.util.Log; import org.xml.sax.Attributes; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.feed.FeedImage; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; @@ -88,16 +87,15 @@ public class NSAtom extends Namespace { size = Long.parseLong(strSize); } } catch (NumberFormatException e) { - if (BuildConfig.DEBUG) Log.d(TAG, "Length attribute could not be parsed."); + Log.d(TAG, "Length attribute could not be parsed."); } String type = attributes.getValue(LINK_TYPE); if (SyndTypeUtils.enclosureTypeValid(type) - || (type = SyndTypeUtils - .getValidMimeTypeFromUrl(href)) != null) { - state.getCurrentItem().setMedia( - new FeedMedia(state.getCurrentItem(), href, - size, type) - ); + || (type = SyndTypeUtils.getValidMimeTypeFromUrl(href)) != null) { + FeedItem currItem = state.getCurrentItem(); + if(!currItem.hasMedia()) { + currItem.setMedia(new FeedMedia(currItem, href, size, type)); + } } } else if (rel.equals(LINK_REL_PAYMENT)) { state.getCurrentItem().setPaymentLink(href); @@ -111,9 +109,11 @@ public class NSAtom extends Namespace { * LINK_TYPE_HTML or LINK_TYPE_XHTML */ if ((type == null && state.getFeed().getLink() == null) - || (type != null && (type.equals(LINK_TYPE_HTML) || type.equals(LINK_TYPE_XHTML)))) { + || (type != null && (type.equals(LINK_TYPE_HTML) + || type.equals(LINK_TYPE_XHTML)))) { state.getFeed().setLink(href); - } else if (type != null && (type.equals(LINK_TYPE_ATOM) || type.equals(LINK_TYPE_RSS))) { + } else if (type != null && (type.equals(LINK_TYPE_ATOM) + || type.equals(LINK_TYPE_RSS))) { // treat as podlove alternate feed String title = attributes.getValue(LINK_TITLE); if (title == null) { @@ -199,7 +199,9 @@ public class NSAtom extends Namespace { DateUtils.parse(content)); } } else if (top.equals(IMAGE)) { - state.getFeed().setImage(new FeedImage(state.getFeed(), content, null)); + if(state.getFeed().getImage() == null) { + state.getFeed().setImage(new FeedImage(state.getFeed(), content, null)); + } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java index a0b514bd6..1b929b214 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java @@ -1,7 +1,10 @@ package de.danoeh.antennapod.core.util; +import android.content.Context; import android.util.Log; +import de.danoeh.antennapod.core.R; + /** Provides methods for converting various units. */ public final class Converter { /** Class shall not be instantiated. */ @@ -23,7 +26,7 @@ public final class Converter { /** Determines the length of the number for best readability.*/ private static final int NUM_LENGTH = 1024; - + private static final int DAYS_MIL = 86400000; private static final int HOURS_MIL = 3600000; private static final int MINUTES_MIL = 60000; private static final int SECONDS_MIL = 1000; @@ -99,5 +102,21 @@ public final class Converter { return Integer.valueOf(parts[0]) * 3600 * 1000 + Integer.valueOf(parts[1]) * 1000 * 60; } + + /** Converts milliseconds to a localized string containing hours and minutes */ + public static String getDurationStringLocalized(Context context, long duration) { + int h = (int)(duration / HOURS_MIL); + int rest = (int)(duration - h * HOURS_MIL); + int m = rest / MINUTES_MIL; + + String result = ""; + if(h > 0) { + String hours = context.getResources().getQuantityString(R.plurals.time_hours_quantified, h, h); + result += hours + " "; + } + String minutes = context.getResources().getQuantityString(R.plurals.time_minutes_quantified, m, m); + result += minutes; + return result; + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java index b6df2dc85..4b4201b50 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.core.util; +import android.content.Context; import android.util.Log; import org.apache.commons.lang3.StringUtils; @@ -7,7 +8,9 @@ import org.apache.commons.lang3.StringUtils; import java.text.ParsePosition; import java.text.SimpleDateFormat; import java.util.Date; +import java.util.GregorianCalendar; import java.util.Locale; +import java.util.TimeZone; /** * Parses several date formats. @@ -16,60 +19,88 @@ public class DateUtils { private static final String TAG = "DateUtils"; + private static final SimpleDateFormat parser = new SimpleDateFormat("", Locale.US); + private static final TimeZone defaultTimezone = TimeZone.getTimeZone("GMT"); + + static { + parser.setLenient(false); + parser.setTimeZone(defaultTimezone); + } + public static Date parse(final String input) { if(input == null) { - throw new IllegalArgumentException("Date most not be null"); + throw new IllegalArgumentException("Date must not be null"); } - String date = input.replace('/', '-'); + String date = input.trim().replace('/', '-').replaceAll("( ){2,}+", " "); + + // if datetime is more precise than seconds, make sure the value is in ms if(date.contains(".")) { int start = date.indexOf('.'); int current = start+1; while(current < date.length() && Character.isDigit(date.charAt(current))) { current++; } + // even more precise than microseconds: discard further decimal places if(current - start > 4) { if(current < date.length()-1) { date = date.substring(0, start + 4) + date.substring(current); } else { date = date.substring(0, start + 4); } + // less than 4 decimal places: pad to have a consistent format for the parser } else if(current - start < 4) { if(current < date.length()-1) { date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start)) + date.substring(current); } else { date = date.substring(0, current) + StringUtils.repeat("0", 4-(current-start)); } - } } String[] patterns = { "dd MMM yy HH:mm:ss Z", "dd MMM yy HH:mm Z", "EEE, dd MMM yyyy HH:mm:ss Z", + "EEE, dd MMM yyyy HH:mm:ss", "EEE, dd MMMM yyyy HH:mm:ss Z", + "EEE, dd MMMM yyyy HH:mm:ss", + "EEEE, dd MMM yyyy HH:mm:ss Z", "EEEE, dd MMM yy HH:mm:ss Z", + "EEEE, dd MMM yyyy HH:mm:ss", + "EEEE, dd MMM yy HH:mm:ss", "EEE MMM d HH:mm:ss yyyy", + "EEE, dd MMM yyyy HH:mm Z", + "EEE, dd MMM yyyy HH:mm", + "EEE, dd MMMM yyyy HH:mm Z", + "EEE, dd MMMM yyyy HH:mm", + "EEEE, dd MMM yyyy HH:mm Z", + "EEEE, dd MMM yy HH:mm Z", + "EEEE, dd MMM yyyy HH:mm", + "EEEE, dd MMM yy HH:mm", + "EEE MMM d HH:mm yyyy", "yyyy-MM-dd'T'HH:mm:ss", - "yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'HH:mm:ss.SSS Z", + "yyyy-MM-dd'T'HH:mm:ss.SSS", "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss'Z'", "yyyy-MM-ddZ", "yyyy-MM-dd" }; - SimpleDateFormat parser = new SimpleDateFormat("", Locale.US); - parser.setLenient(false); + ParsePosition pos = new ParsePosition(0); for(String pattern : patterns) { parser.applyPattern(pattern); pos.setIndex(0); - Date result = parser.parse(date, pos); - if(result != null && pos.getIndex() == date.length()) { - return result; + try { + Date result = parser.parse(date, pos); + if (result != null && pos.getIndex() == date.length()) { + return result; + } + } catch(Exception e) { + Log.e(TAG, Log.getStackTraceString(e)); } } - Log.d(TAG, "Could not parse date string \"" + input + "\""); + Log.d(TAG, "Could not parse date string \"" + input + "\" [" + date + "]"); return null; } @@ -109,6 +140,20 @@ public class DateUtils { public static String formatRFC3339UTC(Date date) { SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US); + format.setTimeZone(defaultTimezone); return format.format(date); } + + public static String formatAbbrev(final Context context, final Date date) { + GregorianCalendar cal = new GregorianCalendar(); + cal.add(GregorianCalendar.YEAR, -1); + // some padding, because no one really remembers what day of the month it is + cal.add(GregorianCalendar.DAY_OF_MONTH, 10); + boolean withinLastYear = date.after(cal.getTime()); + int format = android.text.format.DateUtils.FORMAT_ABBREV_ALL; + if(withinLastYear) { + format |= android.text.format.DateUtils.FORMAT_NO_YEAR; + } + return android.text.format.DateUtils.formatDateTime(context, date.getTime(), format); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java new file mode 100644 index 000000000..892e5ff38 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedItemUtil.java @@ -0,0 +1,78 @@ +package de.danoeh.antennapod.core.util; + +import java.util.List; + +import de.danoeh.antennapod.core.feed.FeedItem; + +public class FeedItemUtil { + + public static int indexOfItemWithDownloadUrl(List<FeedItem> items, String downloadUrl) { + if(items == null) { + return -1; + } + for(int i=0; i < items.size(); i++) { + FeedItem item = items.get(i); + if(item.hasMedia() && item.getMedia().getDownload_url().equals(downloadUrl)) { + return i; + } + } + return -1; + } + + public static int indexOfItemWithId(List<FeedItem> items, long id) { + for(int i=0; i < items.size(); i++) { + FeedItem item = items.get(i); + if(item != null && item.getId() == id) { + return i; + } + } + return -1; + } + + public static int indexOfItemWithMediaId(List<FeedItem> items, long mediaId) { + for(int i=0; i < items.size(); i++) { + FeedItem item = items.get(i); + if(item != null && item.getMedia() != null && item.getMedia().getId() == mediaId) { + return i; + } + } + return -1; + } + + public static long[] getIds(FeedItem... items) { + if(items == null || items.length == 0) { + return new long[0]; + } + long[] result = new long[items.length]; + for(int i=0; i < items.length; i++) { + result[i] = items[i].getId(); + } + return result; + } + + public static long[] getIds(List<FeedItem> items) { + if(items == null || items.size() == 0) { + return new long[0]; + } + long[] result = new long[items.size()]; + for(int i=0; i < items.size(); i++) { + result[i] = items.get(i).getId(); + } + return result; + } + + public static boolean containsAnyId(List<FeedItem> items, long[] ids) { + if(items == null || items.size() == 0) { + return false; + } + for(FeedItem item : items) { + for(long id : ids) { + if(item.getId() == id) { + return true; + } + } + } + return false; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java new file mode 100644 index 000000000..2d5a6e5a1 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/IntentUtils.java @@ -0,0 +1,18 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; + +import java.util.List; + +public class IntentUtils { + + public static boolean isCallable(final Context context, final Intent intent) { + List<ResolveInfo> list = context.getPackageManager().queryIntentActivities(intent, + PackageManager.MATCH_DEFAULT_ONLY); + return list.size() > 0; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java index 07432d28a..287ec4d0c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java @@ -1,14 +1,15 @@ package de.danoeh.antennapod.core.util; +import android.support.v4.util.ArrayMap; + import java.nio.charset.Charset; -import java.util.HashMap; public class LangUtils { public static final Charset UTF_8 = Charset.forName("UTF-8"); - private static HashMap<String, String> languages; + private static ArrayMap<String, String> languages; static { - languages = new HashMap<String, String>(); + languages = new ArrayMap<>(); languages.put("af", "Afrikaans"); languages.put("sq", "Albanian"); languages.put("sq", "Albanian"); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java b/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java index 8934f3272..6ed8b820e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LongList.java @@ -30,6 +30,17 @@ public final class LongList { size = 0; } + public static LongList of(long... values) { + if(values == null || values.length == 0) { + return new LongList(0); + } + LongList result = new LongList(values.length); + for(long value : values) { + result.add(value); + } + return result; + } + @Override public int hashCode() { int hashCode = 1; @@ -166,6 +177,28 @@ public final class LongList { } /** + * Removes values from this list. + * + * @param values values to remove + */ + public void removeAll(long[] values) { + for(long value : values) { + remove(value); + } + } + + /** + * Removes values from this list. + * + * @param list List with values to remove + */ + public void removeAll(LongList list) { + for(long value : list.values) { + remove(value); + } + } + + /** * Removes an element at a given index, shifting elements at greater * indicies down one. * diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java index 3a349e221..c2cd273b8 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java @@ -5,19 +5,35 @@ import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.wifi.WifiInfo; import android.net.wifi.WifiManager; +import android.text.TextUtils; import android.util.Log; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; + +import java.io.File; +import java.io.IOException; import java.util.Arrays; import java.util.List; -import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; +import de.danoeh.antennapod.core.storage.DBWriter; +import rx.Observable; +import rx.Subscriber; +import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; public class NetworkUtils { - private static final String TAG = "NetworkUtils"; - private NetworkUtils() { + private static final String TAG = NetworkUtils.class.getSimpleName(); + + private static Context context; + public static void init(Context context) { + NetworkUtils.context = context; } /** @@ -26,18 +42,16 @@ public class NetworkUtils { * network that is on the 'selected networks' list of the Wi-Fi filter for * automatic downloads and false otherwise. * */ - public static boolean autodownloadNetworkAvailable(Context context) { + public static boolean autodownloadNetworkAvailable() { ConnectivityManager cm = (ConnectivityManager) context .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo networkInfo = cm.getActiveNetworkInfo(); if (networkInfo != null) { if (networkInfo.getType() == ConnectivityManager.TYPE_WIFI) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Device is connected to Wi-Fi"); + Log.d(TAG, "Device is connected to Wi-Fi"); if (networkInfo.isConnected()) { if (!UserPreferences.isEnableAutodownloadWifiFilter()) { - if (BuildConfig.DEBUG) - Log.d(TAG, "Auto-dl filter is disabled"); + Log.d(TAG, "Auto-dl filter is disabled"); return true; } else { WifiManager wm = (WifiManager) context @@ -48,31 +62,28 @@ public class NetworkUtils { .getAutodownloadSelectedNetworks()); if (selectedNetworks.contains(Integer.toString(wifiInfo .getNetworkId()))) { - if (BuildConfig.DEBUG) - Log.d(TAG, - "Current network is on the selected networks list"); + Log.d(TAG, "Current network is on the selected networks list"); return true; } } } } } - if (BuildConfig.DEBUG) - Log.d(TAG, "Network for auto-dl is not available"); + Log.d(TAG, "Network for auto-dl is not available"); return false; } - public static boolean networkAvailable(Context context) { + public static boolean networkAvailable() { ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo info = cm.getActiveNetworkInfo(); return info != null && info.isConnected(); } - public static boolean isDownloadAllowed(Context context) { - return UserPreferences.isAllowMobileUpdate() || NetworkUtils.connectedToWifi(context); + public static boolean isDownloadAllowed() { + return UserPreferences.isAllowMobileUpdate() || NetworkUtils.connectedToWifi(); } - public static boolean connectedToWifi(Context context) { + public static boolean connectedToWifi() { ConnectivityManager connManager = (ConnectivityManager) context .getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo mWifi = connManager @@ -81,4 +92,67 @@ public class NetworkUtils { return mWifi.isConnected(); } + public static Observable<Long> getFeedMediaSizeObservable(FeedMedia media) { + return Observable.create(new Observable.OnSubscribe<Long>() { + @Override + public void call(Subscriber<? super Long> subscriber) { + if (false == NetworkUtils.isDownloadAllowed()) { + subscriber.onNext(0L); + subscriber.onCompleted(); + return; + } + long size = Integer.MIN_VALUE; + if (media.isDownloaded()) { + File mediaFile = new File(media.getLocalMediaUrl()); + if (mediaFile.exists()) { + size = mediaFile.length(); + } + } else if (false == media.checkedOnSizeButUnknown()) { + // only query the network if we haven't already checked + + String url = media.getDownload_url(); + if(TextUtils.isEmpty(url)) { + subscriber.onNext(0L); + subscriber.onCompleted(); + return; + } + + OkHttpClient client = AntennapodHttpClient.getHttpClient(); + Request.Builder httpReq = new Request.Builder() + .url(url) + .header("Accept-Encoding", "identity") + .head(); + try { + Response response = client.newCall(httpReq.build()).execute(); + if (response.isSuccessful()) { + String contentLength = response.header("Content-Length"); + try { + size = Integer.parseInt(contentLength); + } catch (NumberFormatException e) { + Log.e(TAG, Log.getStackTraceString(e)); + } + } + } catch (IOException e) { + subscriber.onNext(0L); + subscriber.onCompleted(); + Log.e(TAG, Log.getStackTraceString(e)); + return; // better luck next time + } + } + Log.d(TAG, "new size: " + size); + if (size <= 0) { + // they didn't tell us the size, but we don't want to keep querying on it + media.setCheckedOnSizeButUnknown(); + } else { + media.setSize(size); + } + subscriber.onNext(size); + subscriber.onCompleted(); + DBWriter.setFeedMedia(media); + } + }) + .subscribeOn(Schedulers.newThread()) + .observeOn(AndroidSchedulers.mainThread()); + } + } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java b/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java index 9a1496b75..71d6040ba 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/QueueSorter.java @@ -83,7 +83,7 @@ public class QueueSorter { } if (comparator != null) { - DBWriter.sortQueue(context, comparator, broadcastUpdate); + DBWriter.sortQueue(comparator, broadcastUpdate); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java new file mode 100644 index 000000000..ee306a401 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/RewindAfterPauseUtils.java @@ -0,0 +1,47 @@ +package de.danoeh.antennapod.core.util; + +import java.util.concurrent.TimeUnit; + +/** + * This class calculates the proper rewind time after the pause and resume. + * <p> + * User might loose context if he/she pauses and resumes the media after longer time. + * Media file should be "rewinded" x seconds after user resumes the playback. + */ +public class RewindAfterPauseUtils { + + public static final long ELAPSED_TIME_FOR_SHORT_REWIND = TimeUnit.MINUTES.toMillis(1); + public static final long ELAPSED_TIME_FOR_MEDIUM_REWIND = TimeUnit.HOURS.toMillis(1); + public static final long ELAPSED_TIME_FOR_LONG_REWIND = TimeUnit.DAYS.toMillis(1); + + public static final long SHORT_REWIND = TimeUnit.SECONDS.toMillis(3); + public static final long MEDIUM_REWIND = TimeUnit.SECONDS.toMillis(10); + public static final long LONG_REWIND = TimeUnit.SECONDS.toMillis(20); + + /** + * @param currentPosition current position in a media file in ms + * @param lastPlayedTime timestamp when was media paused + * @return new rewinded position for playback in milliseconds + */ + public static int calculatePositionWithRewind(int currentPosition, long lastPlayedTime) { + if (currentPosition > 0 && lastPlayedTime > 0) { + long elapsedTime = System.currentTimeMillis() - lastPlayedTime; + long rewindTime = 0; + + if (elapsedTime > ELAPSED_TIME_FOR_LONG_REWIND) { + rewindTime = LONG_REWIND; + } else if (elapsedTime > ELAPSED_TIME_FOR_MEDIUM_REWIND) { + rewindTime = MEDIUM_REWIND; + } else if (elapsedTime > ELAPSED_TIME_FOR_SHORT_REWIND) { + rewindTime = SHORT_REWIND; + } + + int newPosition = currentPosition - (int) rewindTime; + + return newPosition > 0 ? newPosition : 0; + } + else { + return currentPosition; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java index 85f32ed50..35916a604 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java @@ -2,6 +2,8 @@ package de.danoeh.antennapod.core.util; import android.content.Context; import android.content.Intent; + +import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; @@ -11,24 +13,49 @@ public class ShareUtils { private ShareUtils() {} - public static void shareLink(Context context, String link) { + public static void shareLink(Context context, String text) { Intent i = new Intent(Intent.ACTION_SEND); i.setType("text/plain"); - i.putExtra(Intent.EXTRA_SUBJECT, "Sharing URL"); - i.putExtra(Intent.EXTRA_TEXT, link); - context.startActivity(Intent.createChooser(i, "Share URL")); + i.putExtra(Intent.EXTRA_TEXT, text); + context.startActivity(Intent.createChooser(i, context.getString(R.string.share_url_label))); } - - public static void shareFeedItemLink(Context context, FeedItem item) { - shareLink(context, item.getLink()); + + public static void shareFeedlink(Context context, Feed feed) { + shareLink(context, feed.getTitle() + ": " + feed.getLink()); } public static void shareFeedDownloadLink(Context context, Feed feed) { - shareLink(context, feed.getDownload_url()); + shareLink(context, feed.getTitle() + ": " + feed.getDownload_url()); } - - public static void shareFeedlink(Context context, Feed feed) { - shareLink(context, feed.getLink()); + + public static void shareFeedItemLink(Context context, FeedItem item) { + shareFeedItemLink(context, item, false); + } + + public static void shareFeedItemDownloadLink(Context context, FeedItem item) { + shareFeedItemDownloadLink(context, item, false); + } + + private static String getItemShareText(FeedItem item) { + return item.getFeed().getTitle() + ": " + item.getTitle(); + } + + public static void shareFeedItemLink(Context context, FeedItem item, boolean withPosition) { + String text = getItemShareText(item) + " " + item.getLink(); + if(withPosition) { + int pos = item.getMedia().getPosition(); + text = item.getLink() + " [" + Converter.getDurationStringLong(pos) + "]"; + } + shareLink(context, text); + } + + public static void shareFeedItemDownloadLink(Context context, FeedItem item, boolean withPosition) { + String text = getItemShareText(item) + " " + item.getMedia().getDownload_url(); + if(withPosition) { + int pos = item.getMedia().getPosition(); + text += " [" + Converter.getDurationStringLong(pos) + "]"; + } + shareLink(context, text); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java index dea380937..1ef81bf64 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java @@ -1,14 +1,12 @@ package de.danoeh.antennapod.core.util; import android.app.Activity; -import android.content.Context; import android.os.Build; import android.os.StatFs; import android.util.Log; import java.io.File; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.preferences.UserPreferences; @@ -18,13 +16,12 @@ import de.danoeh.antennapod.core.preferences.UserPreferences; public class StorageUtils { private static final String TAG = "StorageUtils"; - public static boolean storageAvailable(Context context) { - File dir = UserPreferences.getDataFolder(context, null); + public static boolean storageAvailable() { + File dir = UserPreferences.getDataFolder(null); if (dir != null) { return dir.exists() && dir.canRead() && dir.canWrite(); } else { - if (BuildConfig.DEBUG) - Log.d(TAG, "Storage not available: data folder is null"); + Log.d(TAG, "Storage not available: data folder is null"); return false; } } @@ -39,7 +36,7 @@ public class StorageUtils { * @return true if external storage is available */ public static boolean checkStorageAvailability(Activity activity) { - boolean storageAvailable = storageAvailable(activity); + boolean storageAvailable = storageAvailable(); if (!storageAvailable) { activity.finish(); activity.startActivity(ClientConfig.applicationCallbacks.getStorageErrorActivity(activity)); @@ -51,8 +48,19 @@ public class StorageUtils { * Get the number of free bytes that are available on the external storage. */ public static long getFreeSpaceAvailable() { - StatFs stat = new StatFs(UserPreferences.getDataFolder( - ClientConfig.applicationCallbacks.getApplicationInstance(), null).getAbsolutePath()); + File dataFolder = UserPreferences.getDataFolder(null); + if (dataFolder != null) { + return getFreeSpaceAvailable(dataFolder.getAbsolutePath()); + } else { + return 0; + } + } + + /** + * Get the number of free bytes that are available on the external storage. + */ + public static long getFreeSpaceAvailable(String path) { + StatFs stat = new StatFs(path); long availableBlocks; long blockSize; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java index 4300556d2..415a1d3a2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java @@ -3,8 +3,6 @@ package de.danoeh.antennapod.core.util; import android.net.Uri; import android.util.Log; -import org.apache.commons.lang3.StringUtils; - import de.danoeh.antennapod.core.BuildConfig; /** @@ -32,19 +30,19 @@ public final class URLChecker { * @return The prepared url */ public static String prepareURL(String url) { - url = StringUtils.trim(url); + url = url.trim(); if (url.startsWith("feed://")) { if (BuildConfig.DEBUG) Log.d(TAG, "Replacing feed:// with http://"); return url.replaceFirst("feed://", "http://"); } else if (url.startsWith("pcast://")) { if (BuildConfig.DEBUG) Log.d(TAG, "Removing pcast://"); - return prepareURL(StringUtils.removeStart(url, "pcast://")); + return prepareURL(url.substring("pcast://".length())); } else if (url.startsWith("itpc")) { if (BuildConfig.DEBUG) Log.d(TAG, "Replacing itpc:// with http://"); return url.replaceFirst("itpc://", "http://"); } else if (url.startsWith(AP_SUBSCRIBE)) { if (BuildConfig.DEBUG) Log.d(TAG, "Removing antennapod-subscribe://"); - return prepareURL(StringUtils.removeStart(url, AP_SUBSCRIBE)); + return prepareURL(url.substring(AP_SUBSCRIBE.length())); } else if (!(url.startsWith("http://") || url.startsWith("https://"))) { if (BuildConfig.DEBUG) Log.d(TAG, "Adding http:// at the beginning of the URL"); return "http://" + url; @@ -66,7 +64,7 @@ public final class URLChecker { if (base == null) { return prepareURL(url); } - url = StringUtils.trim(url); + url = url.trim(); base = prepareURL(base); Uri urlUri = Uri.parse(url); Uri baseUri = Uri.parse(base); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java index 50792ae26..6ddfb0366 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java @@ -1,6 +1,5 @@ package de.danoeh.antennapod.core.util.flattr; -import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; @@ -8,9 +7,10 @@ import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.preference.PreferenceManager; +import android.support.v7.app.AlertDialog; +import android.text.TextUtils; import android.util.Log; -import org.apache.commons.lang3.StringUtils; import org.shredzone.flattr4j.FlattrService; import org.shredzone.flattr4j.exception.FlattrException; import org.shredzone.flattr4j.model.Flattr; @@ -42,12 +42,6 @@ public class FlattrUtils { private static final String PREF_ACCESS_TOKEN = "de.danoeh.antennapod.preference.flattrAccessToken"; - // Flattr URL for this app. - public static final String APP_URL = "http://antennapod.com"; - // Human-readable flattr-page. - public static final String APP_LINK = "https://flattr.com/thing/745609/"; - public static final String APP_THING_ID = "745609"; - private static volatile AccessToken cachedToken; private static AndroidAuthenticator createAuthenticator() { @@ -84,8 +78,8 @@ public class FlattrUtils { * Returns true if FLATTR_APP_KEY and FLATTR_APP_SECRET in BuildConfig are not null and not empty */ public static boolean hasAPICredentials() { - return StringUtils.isNotEmpty(ClientConfig.flattrCallbacks.getFlattrAppKey()) - && StringUtils.isNotEmpty(ClientConfig.flattrCallbacks.getFlattrAppSecret()); + return !TextUtils.isEmpty(ClientConfig.flattrCallbacks.getFlattrAppKey()) + && !TextUtils.isEmpty(ClientConfig.flattrCallbacks.getFlattrAppSecret()); } public static boolean hasToken() { @@ -110,18 +104,6 @@ public class FlattrUtils { storeToken(null); } - public static Thing getAppThing(Context context) { - FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); - try { - Thing thing = fs.getThing(Thing.withId(APP_THING_ID)); - return thing; - } catch (FlattrException e) { - e.printStackTrace(); - showErrorDialog(context, e.getMessage()); - return null; - } - } - public static void clickUrl(Context context, String url) throws FlattrException { if (hasToken()) { @@ -185,7 +167,7 @@ public class FlattrUtils { deleteToken(); FlattrServiceCreator.deleteFlattrService(); showRevokeDialog(context); - DBWriter.clearAllFlattrStatus(context); + DBWriter.clearAllFlattrStatus(); } // ------------------------------------------------ DIALOGS @@ -245,37 +227,6 @@ public class FlattrUtils { } } - public static void showForbiddenDialog(final Context context, - final String url) { - AlertDialog.Builder builder = new AlertDialog.Builder(context); - builder.setTitle(R.string.action_forbidden_title); - builder.setMessage(R.string.action_forbidden_msg); - builder.setPositiveButton(R.string.authenticate_now_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - context.startActivity( - ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context)); - } - - } - ); - builder.setNegativeButton(R.string.visit_website_label, - new OnClickListener() { - - @Override - public void onClick(DialogInterface dialog, int which) { - Uri uri = Uri.parse(url); - context.startActivity(new Intent(Intent.ACTION_VIEW, - uri)); - } - - } - ); - builder.create().show(); - } - public static void showErrorDialog(final Context context, final String msg) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.error_label); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java deleted file mode 100644 index 26c712af3..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/UndoBarController.java +++ /dev/null @@ -1,125 +0,0 @@ -package de.danoeh.antennapod.core.util.gui; - -import android.os.Handler; -import android.view.View; -import android.widget.TextView; - -import com.nineoldandroids.animation.Animator; -import com.nineoldandroids.animation.AnimatorListenerAdapter; -import com.nineoldandroids.view.ViewHelper; -import com.nineoldandroids.view.ViewPropertyAnimator; - -import de.danoeh.antennapod.core.R; - -import static com.nineoldandroids.view.ViewPropertyAnimator.animate; - -public class UndoBarController<T> { - private View mBarView; - private TextView mMessageView; - private ViewPropertyAnimator mBarAnimator; - private Handler mHideHandler = new Handler(); - - private UndoListener<T> mUndoListener; - - // State objects - private T mUndoToken; - private CharSequence mUndoMessage; - - public interface UndoListener<T> { - /** - * This callback function is called when the undo button is pressed - * - * @param token - */ - void onUndo(T token); - - /** - * - * This callback function is called when the bar fades out without button press - * - * @param token - */ - void onHide(T token); - } - - public UndoBarController(View undoBarView, UndoListener<T> undoListener) { - mBarView = undoBarView; - mBarAnimator = animate(mBarView); - mUndoListener = undoListener; - - mMessageView = (TextView) mBarView.findViewById(R.id.undobar_message); - mBarView.findViewById(R.id.undobar_button) - .setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - hideUndoBar(false); - mUndoListener.onUndo(mUndoToken); - } - }); - - hideUndoBar(true); - } - - public void showUndoBar(boolean immediate, CharSequence message, T undoToken) { - mUndoToken = undoToken; - mUndoMessage = message; - mMessageView.setText(mUndoMessage); - - mHideHandler.removeCallbacks(mHideRunnable); - mHideHandler.postDelayed(mHideRunnable, - mBarView.getResources().getInteger(R.integer.undobar_hide_delay)); - - mBarView.setVisibility(View.VISIBLE); - if (immediate) { - ViewHelper.setAlpha(mBarView, 1); - } else { - mBarAnimator.cancel(); - mBarAnimator - .alpha(1) - .setDuration( - mBarView.getResources() - .getInteger(android.R.integer.config_shortAnimTime)) - .setListener(null); - } - } - - public boolean isShowing() { - return mBarView.getVisibility() == View.VISIBLE; - } - - public void close() { - hideUndoBar(true); - mUndoListener.onHide(mUndoToken); - } - - public void hideUndoBar(boolean immediate) { - mHideHandler.removeCallbacks(mHideRunnable); - if (immediate) { - mBarView.setVisibility(View.GONE); - ViewHelper.setAlpha(mBarView, 0); - mUndoMessage = null; - } else { - mBarAnimator.cancel(); - mBarAnimator - .alpha(0) - .setDuration(mBarView.getResources() - .getInteger(android.R.integer.config_shortAnimTime)) - .setListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - mBarView.setVisibility(View.GONE); - mUndoMessage = null; - mUndoToken = null; - } - }); - } - } - - private Runnable mHideRunnable = new Runnable() { - @Override - public void run() { - hideUndoBar(false); - mUndoListener.onHide(mUndoToken); - } - }; -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java index aafcea307..f0850e6df 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java @@ -3,7 +3,9 @@ package de.danoeh.antennapod.core.util.playback; import android.content.Context; import android.util.Log; import android.view.SurfaceHolder; -import com.aocate.media.MediaPlayer; +import org.antennapod.audio.MediaPlayer; + +import de.danoeh.antennapod.core.preferences.UserPreferences; public class AudioPlayer extends MediaPlayer implements IPlayer { private static final String TAG = "AudioPlayer"; @@ -16,7 +18,6 @@ public class AudioPlayer extends MediaPlayer implements IPlayer { public void setScreenOnWhilePlaying(boolean screenOn) { Log.e(TAG, "Setting screen on while playing not supported in Audio Player"); throw new UnsupportedOperationException("Setting screen on while playing not supported in Audio Player"); - } @Override @@ -31,4 +32,14 @@ public class AudioPlayer extends MediaPlayer implements IPlayer { public void setVideoScalingMode(int mode) { throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player"); } + + @Override + protected boolean useSonic() { + return UserPreferences.useSonic(); + } + + @Override + protected boolean downmix() { + return UserPreferences.stereoToMono(); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java index 49769f4f0..ec50dce7c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java @@ -20,6 +20,7 @@ public class ExternalMedia implements Playable { public static final String PREF_SOURCE_URL = "ExternalMedia.PrefSourceUrl"; public static final String PREF_POSITION = "ExternalMedia.PrefPosition"; public static final String PREF_MEDIA_TYPE = "ExternalMedia.PrefMediaType"; + public static final String PREF_LAST_PLAYED_TIME = "ExternalMedia.PrefLastPlayedTime"; private String source; @@ -29,6 +30,7 @@ public class ExternalMedia implements Playable { private List<Chapter> chapters; private int duration; private int position; + private long lastPlayedTime; public ExternalMedia(String source, MediaType mediaType) { super(); @@ -36,9 +38,10 @@ public class ExternalMedia implements Playable { this.mediaType = mediaType; } - public ExternalMedia(String source, MediaType mediaType, int position) { + public ExternalMedia(String source, MediaType mediaType, int position, long lastPlayedTime) { this(source, mediaType); this.position = position; + this.lastPlayedTime = lastPlayedTime; } @Override @@ -51,6 +54,7 @@ public class ExternalMedia implements Playable { dest.writeString(source); dest.writeString(mediaType.toString()); dest.writeInt(position); + dest.writeLong(lastPlayedTime); } @Override @@ -58,6 +62,7 @@ public class ExternalMedia implements Playable { prefEditor.putString(PREF_SOURCE_URL, source); prefEditor.putString(PREF_MEDIA_TYPE, mediaType.toString()); prefEditor.putInt(PREF_POSITION, position); + prefEditor.putLong(PREF_LAST_PLAYED_TIME, lastPlayedTime); } @Override @@ -145,6 +150,11 @@ public class ExternalMedia implements Playable { } @Override + public long getLastPlayedTime() { + return lastPlayedTime; + } + + @Override public MediaType getMediaType() { return mediaType; } @@ -170,10 +180,12 @@ public class ExternalMedia implements Playable { } @Override - public void saveCurrentPosition(SharedPreferences pref, int newPosition) { + public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timestamp) { SharedPreferences.Editor editor = pref.edit(); editor.putInt(PREF_POSITION, newPosition); + editor.putLong(PREF_LAST_PLAYED_TIME, timestamp); position = newPosition; + lastPlayedTime = timestamp; editor.commit(); } @@ -188,6 +200,11 @@ public class ExternalMedia implements Playable { } @Override + public void setLastPlayedTime(long lastPlayedTime) { + this.lastPlayedTime = lastPlayedTime; + } + + @Override public void onPlaybackStart() { } @@ -215,8 +232,12 @@ public class ExternalMedia implements Playable { if (in.dataAvail() > 0) { position = in.readInt(); } - ExternalMedia extMedia = new ExternalMedia(source, type, position); - return extMedia; + long lastPlayedTime = 0; + if (in.dataAvail() > 0) { + lastPlayedTime = in.readLong(); + } + + return new ExternalMedia(source, type, position, lastPlayedTime); } public ExternalMedia[] newArray(int size) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java index 147c7848d..d67153a4e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java @@ -10,6 +10,8 @@ public interface IPlayer { boolean canSetSpeed(); + boolean canDownmix(); + float getCurrentPitchStepsAdjustment(); int getCurrentPosition(); @@ -57,6 +59,8 @@ public interface IPlayer { void setPlaybackSpeed(float f); + void setDownmix(boolean enable); + void setVolume(float left, float right); void start(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java index 7ebd580f7..86ec4fbd0 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java @@ -7,7 +7,7 @@ import android.util.Log; import java.util.List; -import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.asynctask.ImageResource; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; @@ -18,7 +18,7 @@ import de.danoeh.antennapod.core.util.ShownotesProvider; * Interface for objects that can be played by the PlaybackService. */ public interface Playable extends Parcelable, - ShownotesProvider, PicassoImageResource { + ShownotesProvider, ImageResource { /** * Save information about the playable in a preference so that it can be @@ -82,6 +82,12 @@ public interface Playable extends Parcelable, public int getPosition(); /** + * Returns last time (in ms) when this playable was played or 0 + * if last played time is unknown. + */ + public long getLastPlayedTime(); + + /** * Returns the type of media. This method should return the correct value * BEFORE loadMetadata() is called. */ @@ -115,14 +121,23 @@ public interface Playable extends Parcelable, * Saves the current position of this object. Implementations can use the * provided SharedPreference to save this information and retrieve it later * via PlayableUtils.createInstanceFromPreferences. + * + * @param pref shared prefs that might be used to store this object + * @param newPosition new playback position in ms + * @param timestamp current time in ms */ - public void saveCurrentPosition(SharedPreferences pref, int newPosition); + public void saveCurrentPosition(SharedPreferences pref, int newPosition, long timestamp); public void setPosition(int newPosition); public void setDuration(int newDuration); /** + * @param lastPlayedTimestamp timestamp in ms + */ + public void setLastPlayedTime(long lastPlayedTimestamp); + + /** * Is called by the PlaybackService when playback starts. */ public void onPlaybackStart(); @@ -159,28 +174,42 @@ public interface Playable extends Parcelable, */ public static Playable createInstanceFromPreferences(Context context, int type, SharedPreferences pref) { + Playable result = null; // ADD new Playable types here: switch (type) { case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA: - long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1); - if (mediaId != -1) { - return DBReader.getFeedMedia(context, mediaId); - } + result = createFeedMediaInstance(pref); break; case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA: - String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, - null); - String mediaType = pref.getString( - ExternalMedia.PREF_MEDIA_TYPE, null); - if (source != null && mediaType != null) { - int position = pref.getInt(ExternalMedia.PREF_POSITION, 0); - return new ExternalMedia(source, - MediaType.valueOf(mediaType), position); - } + result = createExternalMediaInstance(pref); break; } - Log.e(TAG, "Could not restore Playable object from preferences"); - return null; + if (result == null) { + Log.e(TAG, "Could not restore Playable object from preferences"); + } + return result; + } + + private static Playable createFeedMediaInstance(SharedPreferences pref) { + Playable result = null; + long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1); + if (mediaId != -1) { + result = DBReader.getFeedMedia(mediaId); + } + return result; + } + + private static Playable createExternalMediaInstance(SharedPreferences pref) { + Playable result = null; + String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, null); + String mediaType = pref.getString(ExternalMedia.PREF_MEDIA_TYPE, null); + if (source != null && mediaType != null) { + int position = pref.getInt(ExternalMedia.PREF_POSITION, 0); + long lastPlayedTime = pref.getLong(ExternalMedia.PREF_LAST_PLAYED_TIME, 0); + result = new ExternalMedia(source, MediaType.valueOf(mediaType), + position, lastPlayedTime); + } + return result; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index a0d12d3e7..27935978c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -11,24 +11,22 @@ import android.content.SharedPreferences; import android.content.res.TypedArray; import android.media.MediaPlayer; import android.os.AsyncTask; +import android.os.Build; import android.os.IBinder; import android.preference.PreferenceManager; +import android.support.annotation.NonNull; +import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.SurfaceHolder; -import android.view.View; import android.view.View.OnClickListener; import android.widget.ImageButton; import android.widget.SeekBar; import android.widget.TextView; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.Validate; - 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; @@ -37,6 +35,7 @@ import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.playback.PlaybackService; import de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer; import de.danoeh.antennapod.core.service.playback.PlayerStatus; @@ -49,6 +48,7 @@ import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils; * control playback instead of communicating with the PlaybackService directly. */ public abstract class PlaybackController { + private static final String TAG = "PlaybackController"; public static final int INVALID_TIME = -1; @@ -56,7 +56,7 @@ public abstract class PlaybackController { private final Activity activity; private PlaybackService playbackService; - private Playable media; + protected Playable media; private PlayerStatus status; private ScheduledThreadPoolExecutor schedExecutor; @@ -74,22 +74,16 @@ public abstract class PlaybackController { */ private boolean reinitOnPause; - public PlaybackController(Activity activity, boolean reinitOnPause) { - Validate.notNull(activity); + public PlaybackController(@NonNull Activity activity, boolean reinitOnPause) { this.activity = activity; this.reinitOnPause = reinitOnPause; schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE, - new ThreadFactory() { - - @Override - public Thread newThread(Runnable r) { - Thread t = new Thread(r); - t.setPriority(Thread.MIN_PRIORITY); - return t; - } + r -> { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; }, new RejectedExecutionHandler() { - @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { @@ -106,10 +100,10 @@ public abstract class PlaybackController { */ public void init() { activity.registerReceiver(statusUpdate, new IntentFilter( - PlaybackService.ACTION_PLAYER_STATUS_CHANGED)); + PlaybackService.ACTION_PLAYER_STATUS_CHANGED)); activity.registerReceiver(notificationReceiver, new IntentFilter( - PlaybackService.ACTION_PLAYER_NOTIFICATION)); + PlaybackService.ACTION_PLAYER_NOTIFICATION)); activity.registerReceiver(shutdownReceiver, new IntentFilter( PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); @@ -120,6 +114,7 @@ public abstract class PlaybackController { throw new IllegalStateException( "Can't call init() after release() has been called"); } + checkMediaInfoLoaded(); } /** @@ -240,7 +235,7 @@ public abstract class PlaybackController { return null; } - public abstract void setupGUI(); + private void setupPositionObserver() { if ((positionObserverFuture != null && positionObserverFuture @@ -264,8 +259,6 @@ public abstract class PlaybackController { } } - public abstract void onPositionObserverUpdate(); - private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { playbackService = ((PlaybackService.LocalBinder) service) @@ -359,7 +352,7 @@ public abstract class PlaybackController { @Override public void onReceive(Context context, Intent intent) { if (isConnectedToPlaybackService()) { - if (StringUtils.equals(intent.getAction(), + if (TextUtils.equals(intent.getAction(), PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { release(); onShutdownNotification(); @@ -368,26 +361,31 @@ public abstract class PlaybackController { } }; - public abstract void onPlaybackSpeedChange(); + public void setupGUI() {}; + + public void onPositionObserverUpdate() {}; + - public abstract void onShutdownNotification(); + public void onPlaybackSpeedChange() {}; + + public void onShutdownNotification() {}; /** * Called when the currently displayed information should be refreshed. */ - public abstract void onReloadNotification(int code); + public void onReloadNotification(int code) {}; - public abstract void onBufferStart(); + public void onBufferStart() {}; - public abstract void onBufferEnd(); + public void onBufferEnd() {}; - public abstract void onBufferUpdate(float progress); + public void onBufferUpdate(float progress) {}; - public abstract void onSleepTimerUpdate(); + public void onSleepTimerUpdate() {}; - public abstract void handleError(int code); + public void handleError(int code) {}; - public abstract void onPlaybackEnd(); + public void onPlaybackEnd() {}; public void repeatHandleStatus() { if (status != null && playbackService != null) { @@ -418,7 +416,6 @@ public abstract class PlaybackController { Log.d(TAG, "status: " + status.toString()); switch (status) { - case ERROR: postStatusMsg(R.string.player_error_msg); handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN); @@ -479,19 +476,25 @@ public abstract class PlaybackController { private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) { ImageButton butPlay = getPlayButton(); - butPlay.setImageResource(resource); - butPlay.setContentDescription(contentDescription); + if(butPlay != null) { + butPlay.setImageResource(resource); + butPlay.setContentDescription(contentDescription); + } } - public abstract ImageButton getPlayButton(); + public ImageButton getPlayButton() { + return null; + }; - public abstract void postStatusMsg(int msg); + public void postStatusMsg(int msg) {}; - public abstract void clearStatusMsg(); + public void clearStatusMsg() {}; - public abstract boolean loadMediaInfo(); + public boolean loadMediaInfo() { + return false; + }; - public abstract void onAwaitingVideoSurface(); + public void onAwaitingVideoSurface() {}; /** * Called when connection to playback service has been established or @@ -525,7 +528,7 @@ public abstract class PlaybackController { } } - public abstract void onServiceQueried(); + public void onServiceQueried() {}; /** * Should be used by classes which implement the OnSeekBarChanged interface. @@ -555,7 +558,7 @@ public abstract class PlaybackController { * Should be used by classes which implement the OnSeekBarChanged interface. */ public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) { - if (playbackService != null) { + if (playbackService != null && media != null) { playbackService.seekTo((int) (prog * media.getDuration())); setupPositionObserver(); } @@ -572,37 +575,32 @@ public abstract class PlaybackController { } public OnClickListener newOnPlayButtonClickListener() { - return new OnClickListener() { - @Override - public void onClick(View v) { - if (playbackService != null) { - switch (status) { - case PLAYING: - playbackService.pause(true, reinitOnPause); - break; - case PAUSED: - case PREPARED: - playbackService.resume(); - break; - case PREPARING: - playbackService.setStartWhenPrepared(!playbackService - .isStartWhenPrepared()); - if (reinitOnPause - && playbackService.isStartWhenPrepared() == false) { - playbackService.reinit(); - } - break; - case INITIALIZED: - playbackService.setStartWhenPrepared(true); - playbackService.prepare(); - break; + return v -> { + if (playbackService == null) { + Log.w(TAG, "Play/Pause button was pressed, but playbackservice was null!"); + return; + } + switch (status) { + case PLAYING: + playbackService.pause(true, reinitOnPause); + break; + case PAUSED: + case PREPARED: + playbackService.resume(); + break; + case PREPARING: + playbackService.setStartWhenPrepared(!playbackService + .isStartWhenPrepared()); + if (reinitOnPause + && playbackService.isStartWhenPrepared() == false) { + playbackService.reinit(); } - } else { - Log.w(TAG, - "Play/Pause button was pressed, but playbackservice was null!"); - } + break; + case INITIALIZED: + playbackService.setStartWhenPrepared(true); + playbackService.prepare(); + break; } - }; } @@ -652,9 +650,9 @@ public abstract class PlaybackController { } } - public void setSleepTimer(long time) { + public void setSleepTimer(long time, boolean shakeToReset, boolean vibrate) { if (playbackService != null) { - playbackService.setSleepTimer(time); + playbackService.setSleepTimer(time, shakeToReset, vibrate); } } @@ -681,6 +679,11 @@ public abstract class PlaybackController { } public boolean canSetPlaybackSpeed() { + if (org.antennapod.audio.MediaPlayer.isPrestoLibraryInstalled(activity.getApplicationContext()) + || UserPreferences.useSonic() + || Build.VERSION.SDK_INT >= 23) { + return true; + } return playbackService != null && playbackService.canSetSpeed(); } @@ -690,6 +693,12 @@ public abstract class PlaybackController { } } + public void setVolume(float leftVolume, float rightVolume) { + if (playbackService != null) { + playbackService.setVolume(leftVolume, rightVolume); + } + } + public float getCurrentPlaybackSpeedMultiplier() { if (canSetPlaybackSpeed()) { return playbackService.getCurrentPlaybackSpeed(); @@ -698,6 +707,16 @@ public abstract class PlaybackController { } } + public boolean canDownmix() { + return playbackService != null && playbackService.canDownmix(); + } + + public void setDownmix(boolean enable) { + if(playbackService != null) { + playbackService.setDownmix(enable); + } + } + public boolean isPlayingVideo() { if (playbackService != null) { return PlaybackService.getCurrentMediaType() == MediaType.VIDEO; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java index f31297b41..2eee1ac87 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java @@ -2,10 +2,10 @@ package de.danoeh.antennapod.core.util.playback; import android.content.Context; import android.content.res.TypedArray; +import android.support.annotation.NonNull; import android.util.Log; import android.util.TypedValue; -import org.apache.commons.lang3.Validate; import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -14,7 +14,6 @@ import org.jsoup.select.Elements; import java.util.regex.Matcher; import java.util.regex.Pattern; -import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.util.Converter; import de.danoeh.antennapod.core.util.ShownotesProvider; @@ -59,6 +58,8 @@ public class Timeline { private static final Pattern TIMECODE_LINK_REGEX = Pattern.compile("antennapod://timecode/((\\d+))"); private static final String TIMECODE_LINK = "<a class=\"timecode\" href=\"antennapod://timecode/%d\">%s</a>"; private static final Pattern TIMECODE_REGEX = Pattern.compile("\\b(?:(?:(([0-9][0-9])):))?(([0-9][0-9])):(([0-9][0-9]))\\b"); + private static final Pattern LINE_BREAK_REGEX = Pattern.compile("<br *\\/?>"); + /** * Applies an app-specific CSS stylesheet and adds timecode links (optional). @@ -82,11 +83,15 @@ public class Timeline { return null; } if (shownotes == null) { - if (BuildConfig.DEBUG) - Log.d(TAG, "shownotesProvider contained no shownotes. Returning empty string"); + Log.d(TAG, "shownotesProvider contained no shownotes. Returning empty string"); return ""; } + // replace ASCII line breaks with HTML ones if shownotes don't contain HTML line breaks already + if(!LINE_BREAK_REGEX.matcher(shownotes).find() && !shownotes.contains("<p>")) { + shownotes = shownotes.replace("\n", "<br />"); + } + Document document = Jsoup.parse(shownotes); // apply style @@ -97,10 +102,9 @@ public class Timeline { // apply timecode links if (addTimecodes) { Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX); - if (BuildConfig.DEBUG) - Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes"); + Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes"); for (Element element : elementsWithTimeCodes) { - Matcher matcherLong = TIMECODE_REGEX.matcher(element.text()); + Matcher matcherLong = TIMECODE_REGEX.matcher(element.html()); StringBuffer buffer = new StringBuffer(); while (matcherLong.find()) { String h = matcherLong.group(1); @@ -154,8 +158,7 @@ public class Timeline { } - public void setShownotesProvider(ShownotesProvider shownotesProvider) { - Validate.notNull(shownotesProvider); + public void setShownotesProvider(@NonNull ShownotesProvider shownotesProvider) { this.shownotesProvider = shownotesProvider; } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java index dc5270d8f..368379509 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java @@ -17,6 +17,11 @@ public class VideoPlayer extends MediaPlayer implements IPlayer { } @Override + public boolean canDownmix() { + return false; + } + + @Override public float getCurrentPitchStepsAdjustment() { return 1; } @@ -60,6 +65,12 @@ public class VideoPlayer extends MediaPlayer implements IPlayer { throw new UnsupportedOperationException("Setting playback speed unsupported in video player"); } + @Override + public void setDownmix(boolean b) { + Log.e(TAG, "Setting downmix unsupported in video player"); + throw new UnsupportedOperationException("Setting downmix unsupported in video player"); + } + @Override public void setVideoScalingMode(int mode) { super.setVideoScalingMode(mode); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java index 9588265b8..13cb9f002 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java @@ -1,7 +1,9 @@ package de.danoeh.antennapod.core.util.syndication; import android.net.Uri; -import org.apache.commons.lang3.StringUtils; +import android.support.v4.util.ArrayMap; +import android.text.TextUtils; + import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -9,7 +11,6 @@ import org.jsoup.select.Elements; import java.io.File; import java.io.IOException; -import java.util.LinkedHashMap; import java.util.Map; /** @@ -45,12 +46,12 @@ public class FeedDiscoverer { } private Map<String, String> findLinks(Document document, String baseUrl) { - Map<String, String> res = new LinkedHashMap<String, String>(); + Map<String, String> res = new ArrayMap<>(); Elements links = document.head().getElementsByTag("link"); for (Element link : links) { String rel = link.attr("rel"); String href = link.attr("href"); - if (!StringUtils.isEmpty(href) && + if (!TextUtils.isEmpty(href) && (rel.equals("alternate") || rel.equals("feed"))) { String type = link.attr("type"); if (type.equals(MIME_RSS) || type.equals(MIME_ATOM)) { @@ -58,7 +59,7 @@ public class FeedDiscoverer { String processedUrl = processURL(baseUrl, href); if (processedUrl != null) { res.put(processedUrl, - (StringUtils.isEmpty(title)) ? href : title); + (TextUtils.isEmpty(title)) ? href : title); } } } |