diff options
author | daniel oeh <daniel.oeh@gmail.com> | 2014-11-19 16:50:50 +0100 |
---|---|---|
committer | daniel oeh <daniel.oeh@gmail.com> | 2014-11-19 16:50:50 +0100 |
commit | 563ceaf39332943b1733036f5c3b9087c481d313 (patch) | |
tree | 2a77340ec440b030524f28e0e1fcb98196ba94fb /core/src/main | |
parent | baa7d5f11283cb7668d45b561af5d38f0ccb9632 (diff) | |
parent | 9588747edda50cd98374a599b47d4be60c562d59 (diff) | |
download | AntennaPod-563ceaf39332943b1733036f5c3b9087c481d313.zip |
Merge branch 'develop'0.9.9.5
Diffstat (limited to 'core/src/main')
497 files changed, 32869 insertions, 0 deletions
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml new file mode 100644 index 000000000..c660cd805 --- /dev/null +++ b/core/src/main/AndroidManifest.xml @@ -0,0 +1,52 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="de.danoeh.antennapod.core"> + + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.WAKE_LOCK" /> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /> + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> + + <application + android:allowBackup="true" + android:icon="@drawable/ic_launcher" + android:label="@string/app_name"> + + <service + android:name=".service.download.DownloadService" + android:enabled="true" /> + <service + android:name=".service.playback.PlaybackService" + android:enabled="true" + android:exported="true"/> + <service + android:name=".service.GpodnetSyncService" + android:enabled="true"/> + + <receiver + android:name=".receiver.MediaButtonReceiver" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MEDIA_BUTTON" /> + </intent-filter> + <intent-filter> + <action android:name="de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER" /> + </intent-filter> + </receiver> + + <receiver android:name=".receiver.AlarmUpdateReceiver"> + <intent-filter> + <action android:name="android.intent.action.BOOT_COMPLETED" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.PACKAGE_REPLACED" /> + <data + android:path="de.danoeh.antennapod" + android:scheme="package" /> + </intent-filter> + </receiver> + + </application> + +</manifest> diff --git a/core/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl new file mode 100644 index 000000000..6bdc76801 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IDeathCallback_0_8.aidl @@ -0,0 +1,18 @@ +// 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.presto.service; + +oneway interface IDeathCallback_0_8 { +}
\ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl new file mode 100644 index 000000000..7357e402e --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnBufferingUpdateListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// 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.presto.service; + +interface IOnBufferingUpdateListenerCallback_0_8 { + void onBufferingUpdate(int percent); +}
\ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl new file mode 100644 index 000000000..d5edea729 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnCompletionListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// 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.presto.service; + +interface IOnCompletionListenerCallback_0_8 { + void onCompletion(); +}
\ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl new file mode 100644 index 000000000..2c4f2df3e --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnErrorListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// 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.presto.service; + +interface IOnErrorListenerCallback_0_8 { + boolean onError(int what, int extra); +} diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl new file mode 100644 index 000000000..9dbd1d260 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnInfoListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// 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.presto.service; + +interface IOnInfoListenerCallback_0_8 { + boolean onInfo(int what, int extra); +}
\ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl new file mode 100644 index 000000000..41223a97b --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnPitchAdjustmentAvailableChangedListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// 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.presto.service; + +interface IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 { + void onPitchAdjustmentAvailableChanged(boolean pitchAdjustmentAvailable); +}
\ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl new file mode 100644 index 000000000..7be8f1237 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnPreparedListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// 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.presto.service; + +interface IOnPreparedListenerCallback_0_8 { + void onPrepared(); +}
\ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl new file mode 100644 index 000000000..5bdda98b6 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnSeekCompleteListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// 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.presto.service; + +interface IOnSeekCompleteListenerCallback_0_8 { + void onSeekComplete(); +}
\ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl new file mode 100644 index 000000000..a69c1cf34 --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8.aidl @@ -0,0 +1,19 @@ +// 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.presto.service; + +interface IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 { + void onSpeedAdjustmentAvailableChanged(boolean speedAdjustmentAvailable); +}
\ No newline at end of file diff --git a/core/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl b/core/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl new file mode 100644 index 000000000..12a6047de --- /dev/null +++ b/core/src/main/aidl/com/aocate/presto/service/IPlayMedia_0_8.aidl @@ -0,0 +1,75 @@ +// 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.presto.service; + +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.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.IOnInfoListenerCallback_0_8; + +interface IPlayMedia_0_8 { + boolean canSetPitch(long sessionId); + boolean canSetSpeed(long sessionId); + float getCurrentPitchStepsAdjustment(long sessionId); + int getCurrentPosition(long sessionId); + float getCurrentSpeedMultiplier(long sessionId); + int getDuration(long sessionId); + float getMaxSpeedMultiplier(long sessionId); + float getMinSpeedMultiplier(long sessionId); + int getVersionCode(); + String getVersionName(); + boolean isLooping(long sessionId); + boolean isPlaying(long sessionId); + void pause(long sessionId); + void prepare(long sessionId); + void prepareAsync(long sessionId); + void registerOnBufferingUpdateCallback(long sessionId, IOnBufferingUpdateListenerCallback_0_8 cb); + void registerOnCompletionCallback(long sessionId, IOnCompletionListenerCallback_0_8 cb); + void registerOnErrorCallback(long sessionId, IOnErrorListenerCallback_0_8 cb); + void registerOnInfoCallback(long sessionId, IOnInfoListenerCallback_0_8 cb); + void registerOnPitchAdjustmentAvailableChangedCallback(long sessionId, IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 cb); + void registerOnPreparedCallback(long sessionId, IOnPreparedListenerCallback_0_8 cb); + void registerOnSeekCompleteCallback(long sessionId, IOnSeekCompleteListenerCallback_0_8 cb); + void registerOnSpeedAdjustmentAvailableChangedCallback(long sessionId, IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 cb); + void release(long sessionId); + void reset(long sessionId); + void seekTo(long sessionId, int msec); + void setAudioStreamType(long sessionId, int streamtype); + void setDataSourceString(long sessionId, String path); + void setDataSourceUri(long sessionId, in Uri uri); + void setEnableSpeedAdjustment(long sessionId, boolean enableSpeedAdjustment); + void setLooping(long sessionId, boolean looping); + void setPitchStepsAdjustment(long sessionId, float pitchSteps); + void setPlaybackPitch(long sessionId, float f); + void setPlaybackSpeed(long sessionId, float f); + void setSpeedAdjustmentAlgorithm(long sessionId, int algorithm); + void setVolume(long sessionId, float left, float right); + void start(long sessionId); + long startSession(IDeathCallback_0_8 cb); + void stop(long sessionId); + void unregisterOnBufferingUpdateCallback(long sessionId, IOnBufferingUpdateListenerCallback_0_8 cb); + void unregisterOnCompletionCallback(long sessionId, IOnCompletionListenerCallback_0_8 cb); + void unregisterOnErrorCallback(long sessionId, IOnErrorListenerCallback_0_8 cb); + void unregisterOnInfoCallback(long sessionId, IOnInfoListenerCallback_0_8 cb); + void unregisterOnPitchAdjustmentAvailableChangedCallback(long sessionId, IOnPitchAdjustmentAvailableChangedListenerCallback_0_8 cb); + void unregisterOnPreparedCallback(long sessionId, IOnPreparedListenerCallback_0_8 cb); + void unregisterOnSeekCompleteCallback(long sessionId, IOnSeekCompleteListenerCallback_0_8 cb); + void unregisterOnSpeedAdjustmentAvailableChangedCallback(long sessionId, IOnSpeedAdjustmentAvailableChangedListenerCallback_0_8 cb); +}
\ No newline at end of file diff --git a/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java new file mode 100644 index 000000000..17ee74a13 --- /dev/null +++ b/core/src/main/java/com/aocate/media/AndroidMediaPlayer.java @@ -0,0 +1,470 @@ +// 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 android.content.Context; +import android.media.MediaPlayer; +import android.net.Uri; +import android.util.Log; + +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 new file mode 100644 index 000000000..c73c5219e --- /dev/null +++ b/core/src/main/java/com/aocate/media/MediaPlayer.java @@ -0,0 +1,1278 @@ +// 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 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; + } + + /** + * 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 new file mode 100644 index 000000000..856ab47ce --- /dev/null +++ b/core/src/main/java/com/aocate/media/MediaPlayerImpl.java @@ -0,0 +1,118 @@ +// 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 new file mode 100644 index 000000000..702a23b0f --- /dev/null +++ b/core/src/main/java/com/aocate/media/ServiceBackedMediaPlayer.java @@ -0,0 +1,1201 @@ +// 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 = + new Intent(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()) { + 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) { + 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 new file mode 100644 index 000000000..d337a0452 --- /dev/null +++ b/core/src/main/java/com/aocate/media/SpeedAdjustmentAlgorithm.java @@ -0,0 +1,31 @@ +// 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 new file mode 100644 index 000000000..3bc1ce4eb --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/ApplicationCallbacks.java @@ -0,0 +1,24 @@ +package de.danoeh.antennapod.core; + +import android.app.Application; +import android.content.Context; +import android.content.Intent; + +/** + * Callbacks related to the application in general + */ +public interface ApplicationCallbacks { + + /** + * Returns a non-null instance of the application class + */ + public Application getApplicationInstance(); + + /** + * Returns a non-null intent that starts the storage error + * activity. + */ + 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 new file mode 100644 index 000000000..e5e609f5f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/ClientConfig.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core; + +/** + * Stores callbacks for core classes like Services, DB classes etc. and other configuration variables. + * Apps using the core module of AntennaPod should register implementations of all interfaces here. + */ +public class ClientConfig { + + /** + * Should be used when setting User-Agent header for HTTP-requests. + */ + public static String USER_AGENT; + + public static ApplicationCallbacks applicationCallbacks; + + public static DownloadServiceCallbacks downloadServiceCallbacks; + + public static PlaybackServiceCallbacks playbackServiceCallbacks; + + public static GpodnetCallbacks gpodnetCallbacks; + + public static FlattrCallbacks flattrCallbacks; + + public static StorageCallbacks storageCallbacks; +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java new file mode 100644 index 000000000..286e830c5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java @@ -0,0 +1,58 @@ +package de.danoeh.antennapod.core; + +import android.app.PendingIntent; +import android.content.Context; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.service.download.DownloadRequest; + +/** + * Callbacks for the DownloadService of the core module + */ +public interface DownloadServiceCallbacks { + + /** + * Returns a PendingIntent for a notification the main notification of the DownloadService. + * <p/> + * The PendingIntent takes the users to a screen where they can observe all currently running + * downloads. + * + * @return A non-null PendingIntent for the notification. + */ + public PendingIntent getNotificationContentIntent(Context context); + + /** + * Returns a PendingIntent for a notification that tells the user to enter a username + * or a password for a requested download. + * <p/> + * The PendingIntent takes users to an Activity that lets the user enter their username + * and password to retry the download. + * + * @return A non-null PendingIntent for the notification. + */ + public PendingIntent getAuthentificationNotificationContentIntent(Context context, DownloadRequest request); + + /** + * Returns a PendingIntent for notification that notifies the user about the completion of downloads + * along with information about failed and successful downloads. + * <p/> + * The PendingIntent takes users to an activity where they can look at all successful and failed downloads. + * + * @return A non-null PendingIntent for the notification or null if shouldCreateReport()==false + */ + public PendingIntent getReportNotificationContentIntent(Context context); + + /** + * Called by the FeedSyncThread after a feed has been downloaded and parsed. + * + * @param feed The non-null feed that has been parsed. + */ + public void onFeedParsed(Context context, Feed feed); + + /** + * Returns true if the DownloadService should create a report that shows the number of failed + * downloads when the service shuts down. + * */ + public boolean shouldCreateReport(); +} + diff --git a/core/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java new file mode 100644 index 000000000..cee1029d8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/FlattrCallbacks.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.core; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; + +import org.shredzone.flattr4j.oauth.AccessToken; + +/** + * Callbacks for the flattr integration of the app. + */ +public interface FlattrCallbacks { + + /** + * Returns if true if the flattr integration should be activated, + * false otherwise. + */ + public boolean flattrEnabled(); + + /** + * Returns an intent that starts the activity that is responsible for + * letting users log into their flattr account. + * + * @return The intent that starts the authentication activity or null + * if flattr integration is disabled (i.e. flattrEnabled() == false). + */ + public Intent getFlattrAuthenticationActivityIntent(Context context); + + public PendingIntent getFlattrFailedNotificationContentIntent(Context context); + + public String getFlattrAppKey(); + + public String getFlattrAppSecret(); + + public void handleFlattrAuthenticationSuccess(AccessToken token); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java new file mode 100644 index 000000000..6174bce29 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/GpodnetCallbacks.java @@ -0,0 +1,27 @@ +package de.danoeh.antennapod.core; + +import android.app.PendingIntent; +import android.content.Context; + +/** + * Callbacks related to the gpodder.net integration of the core module + */ +public interface GpodnetCallbacks { + + + /** + * Returns if true if the gpodder.net integration should be activated, + * false otherwise. + */ + public boolean gpodnetEnabled(); + + /** + * Returns a PendingIntent for the error notification of the GpodnetSyncService. + * <p/> + * What the PendingIntent does may be implementation-specific. + * + * @return A PendingIntent for the notification or null if gpodder.net integration + * has been disabled (i.e. gpodnetEnabled() == false). + */ + public PendingIntent getGpodnetSyncServiceErrorNotificationPendingIntent(Context context); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java new file mode 100644 index 000000000..fb01fa703 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java @@ -0,0 +1,33 @@ +package de.danoeh.antennapod.core; + +import android.content.Context; +import android.content.Intent; + +import de.danoeh.antennapod.core.feed.MediaType; + +/** + * Callbacks for the PlaybackService of the core module + */ +public interface PlaybackServiceCallbacks { + + /** + * Returns an intent which starts an audio- or videoplayer, depending on the + * type of media that is being played. + * + * @param mediaType The type of media that is being played. + * @return A non-null activity intent. + */ + public Intent getPlayerActivityIntent(Context context, MediaType mediaType); + + /** + * Returns true if the PlaybackService should load new episodes from the queue when playback ends + * and false if the PlaybackService should ignore the queue and load no more episodes when playback + * finishes. + */ + public boolean useQueue(); + + /** + * Returns a drawable resource that is used for the notification of the playback service. + */ + public int getNotificationIconResource(Context context); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java new file mode 100644 index 000000000..5d1a0fffc --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/StorageCallbacks.java @@ -0,0 +1,27 @@ +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 new file mode 100644 index 000000000..a13130082 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/DownloadObserver.java @@ -0,0 +1,177 @@ +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 new file mode 100644 index 000000000..255b95119 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java @@ -0,0 +1,74 @@ +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.os.AsyncTask; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.Feed; +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 FeedRemover(Context context, Feed feed) { + super(); + this.context = context; + this.feed = feed; + } + + @Override + protected Void doInBackground(Void... params) { + try { + DBWriter.deleteFeed(context, feed.getId()).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + return null; + } + + @Override + protected void onCancelled() { + dialog.dismiss(); + } + + @Override + protected void onPostExecute(Void result) { + dialog.dismiss(); + } + + @Override + protected void onPreExecute() { + dialog = new ProgressDialog(context); + dialog.setMessage(context.getString(R.string.feed_remover_msg)); + dialog.setOnCancelListener(new OnCancelListener() { + + @Override + public void onCancel(DialogInterface dialog) { + cancel(true); + + } + + }); + dialog.show(); + } + + @SuppressLint("NewApi") + public void executeAsync() { + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } + +} 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 new file mode 100644 index 000000000..5d2d5d441 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrClickWorker.java @@ -0,0 +1,237 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.annotation.TargetApi; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.os.AsyncTask; +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; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; + +/** + * Performs a click action in a background thread. + * <p/> + * When started, the flattr click worker will try to flattr every item that is in the flattr queue. If no network + * connection is available it will shut down immediately. The FlattrClickWorker can also be given one additional + * FlattrThing which will be flattrd immediately. + * <p/> + * The FlattrClickWorker will display a toast notification for every item that has been flattrd. If the FlattrClickWorker failed + * to flattr something, a notification will be displayed. + */ +public class FlattrClickWorker extends AsyncTask<Void, Integer, FlattrClickWorker.ExitCode> { + protected static final String TAG = "FlattrClickWorker"; + + private static final int NOTIFICATION_ID = 4; + + private final Context context; + + public static enum ExitCode {EXIT_NORMAL, NO_TOKEN, NO_NETWORK, NO_THINGS} + + private volatile int countFailed = 0; + private volatile int countSuccess = 0; + + private volatile FlattrThing extraFlattrThing; + + /** + * Only relevant if just one thing is flattrd + */ + private volatile FlattrException exception; + + /** + * Creates a new FlattrClickWorker which will only flattr all things in the queue. + * <p/> + * The FlattrClickWorker has to be started by calling executeAsync(). + * + * @param context A context for accessing the database and posting notifications. Must not be null. + */ + public FlattrClickWorker(Context context) { + Validate.notNull(context); + this.context = context.getApplicationContext(); + } + + /** + * Creates a new FlattrClickWorker which will flattr all things in the queue and one additional + * FlattrThing. + * <p/> + * The FlattrClickWorker has to be started by calling executeAsync(). + * + * @param context A context for accessing the database and posting notifications. Must not be null. + * @param extraFlattrThing The additional thing to flattr + */ + public FlattrClickWorker(Context context, FlattrThing extraFlattrThing) { + this(context); + this.extraFlattrThing = extraFlattrThing; + } + + + @Override + protected ExitCode doInBackground(Void... params) { + + if (!FlattrUtils.hasToken()) { + return ExitCode.NO_TOKEN; + } + + if (!NetworkUtils.networkAvailable(context)) { + return ExitCode.NO_NETWORK; + } + + final List<FlattrThing> flattrQueue = DBReader.getFlattrQueue(context); + if (extraFlattrThing != null) { + flattrQueue.add(extraFlattrThing); + } else if (flattrQueue.size() == 1) { + // if only one item is flattrd, the report can specifically mentioned that this item has failed + extraFlattrThing = flattrQueue.get(0); + } + + if (flattrQueue.isEmpty()) { + return ExitCode.NO_THINGS; + } + + List<Future> dbFutures = new LinkedList<Future>(); + for (FlattrThing thing : flattrQueue) { + if (BuildConfig.DEBUG) Log.d(TAG, "Processing " + thing.getTitle()); + + try { + thing.getFlattrStatus().setUnflattred(); // pop from queue to prevent unflattrable things from getting stuck in flattr queue infinitely + FlattrUtils.clickUrl(context, thing.getPaymentLink()); + thing.getFlattrStatus().setFlattred(); + publishProgress(R.string.flattr_click_success); + countSuccess++; + + } catch (FlattrException e) { + e.printStackTrace(); + countFailed++; + if (countFailed == 1) { + exception = e; + } + } + + Future<?> f = DBWriter.setFlattredStatus(context, thing, false); + if (f != null) { + dbFutures.add(f); + } + } + + for (Future f : dbFutures) { + try { + f.get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + return ExitCode.EXIT_NORMAL; + } + + @Override + protected void onPostExecute(ExitCode exitCode) { + super.onPostExecute(exitCode); + switch (exitCode) { + case EXIT_NORMAL: + if (countFailed > 0) { + postFlattrFailedNotification(); + } + break; + case NO_NETWORK: + postToastNotification(R.string.flattr_click_enqueued); + break; + case NO_TOKEN: + postNoTokenNotification(); + break; + case NO_THINGS: // nothing to notify here + break; + } + } + + @Override + protected void onProgressUpdate(Integer... values) { + super.onProgressUpdate(values); + postToastNotification(values[0]); + } + + private void postToastNotification(int msg) { + Toast.makeText(context, context.getString(msg), Toast.LENGTH_LONG).show(); + } + + private void postNoTokenNotification() { + PendingIntent contentIntent = PendingIntent.getActivity(context, 0, + ClientConfig.flattrCallbacks.getFlattrAuthenticationActivityIntent(context), 0); + + Notification notification = new NotificationCompat.Builder(context) + .setStyle(new NotificationCompat.BigTextStyle().bigText(context.getString(R.string.no_flattr_token_notification_msg))) + .setContentIntent(contentIntent) + .setContentTitle(context.getString(R.string.no_flattr_token_title)) + .setTicker(context.getString(R.string.no_flattr_token_title)) + .setSmallIcon(R.drawable.stat_notify_sync_error) + .setOngoing(false) + .setAutoCancel(true) + .build(); + ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification); + } + + private void postFlattrFailedNotification() { + if (countFailed == 0) { + return; + } + + PendingIntent contentIntent = ClientConfig.flattrCallbacks.getFlattrFailedNotificationContentIntent(context); + String title; + String subtext; + + if (countFailed == 1) { + title = context.getString(R.string.flattrd_failed_label); + String exceptionMsg = (exception.getMessage() != null) ? exception.getMessage() : ""; + subtext = context.getString(R.string.flattr_click_failure, extraFlattrThing.getTitle()) + + "\n" + exceptionMsg; + } else { + title = context.getString(R.string.flattrd_label); + subtext = context.getString(R.string.flattr_click_success_count, countSuccess) + "\n" + + context.getString(R.string.flattr_click_failure_count, countFailed); + } + + Notification notification = new NotificationCompat.Builder(context) + .setStyle(new NotificationCompat.BigTextStyle().bigText(subtext)) + .setContentIntent(contentIntent) + .setContentTitle(title) + .setTicker(title) + .setSmallIcon(R.drawable.stat_notify_sync_error) + .setOngoing(false) + .setAutoCancel(true) + .build(); + ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).notify(NOTIFICATION_ID, notification); + } + + + /** + * Starts the FlattrClickWorker as an AsyncTask. + */ + @TargetApi(11) + public void executeAsync() { + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } +} 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 new file mode 100644 index 000000000..c4aa76ac7 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrStatusFetcher.java @@ -0,0 +1,47 @@ +package de.danoeh.antennapod.core.asynctask; + +import android.content.Context; +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; +import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.model.Flattr; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +/** + * Fetch list of flattred things and flattr status in database in a background thread. + */ + +public class FlattrStatusFetcher extends Thread { + protected static final String TAG = "FlattrStatusFetcher"; + protected Context context; + + public FlattrStatusFetcher(Context context) { + super(); + this.context = context; + } + + @Override + public void run() { + if (BuildConfig.DEBUG) Log.d(TAG, "Starting background work: Retrieving Flattr status"); + + Thread.currentThread().setPriority(Thread.MIN_PRIORITY); + + try { + List<Flattr> flattredThings = FlattrUtils.retrieveFlattredThings(); + DBWriter.setFlattredStatus(context, flattredThings).get(); + } catch (FlattrException e) { + e.printStackTrace(); + Log.d(TAG, "flattrQueue exception retrieving list with flattred items " + e.getMessage()); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + + if (BuildConfig.DEBUG) Log.d(TAG, "Finished background work: Retrieved Flattr status"); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java new file mode 100644 index 000000000..2513d1abd --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FlattrTokenFetcher.java @@ -0,0 +1,92 @@ +package de.danoeh.antennapod.core.asynctask; + + +import android.annotation.SuppressLint; +import android.app.ProgressDialog; +import android.content.Context; +import android.net.Uri; +import android.os.AsyncTask; +import android.util.Log; + +import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.oauth.AccessToken; +import org.shredzone.flattr4j.oauth.AndroidAuthenticator; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; + +/** + * Fetches the access token in the background in order to avoid networkOnMainThread exception. + */ + +public class FlattrTokenFetcher extends AsyncTask<Void, Void, AccessToken> { + private static final String TAG = "FlattrTokenFetcher"; + Context context; + AndroidAuthenticator auth; + AccessToken token; + Uri uri; + ProgressDialog dialog; + FlattrException exception; + + public FlattrTokenFetcher(Context context, AndroidAuthenticator auth, Uri uri) { + super(); + this.context = context; + this.auth = auth; + this.uri = uri; + } + + @Override + protected void onPostExecute(AccessToken result) { + if (result != null) { + FlattrUtils.storeToken(result); + } + dialog.dismiss(); + if (exception == null) { + ClientConfig.flattrCallbacks.handleFlattrAuthenticationSuccess(result); + } else { + FlattrUtils.showErrorDialog(context, exception.getMessage()); + } + } + + + @Override + protected void onPreExecute() { + super.onPreExecute(); + dialog = new ProgressDialog(context); + dialog.setMessage(context.getString(R.string.processing_label)); + dialog.setIndeterminate(true); + dialog.setCancelable(false); + dialog.show(); + } + + + @Override + protected AccessToken doInBackground(Void... params) { + try { + token = auth.fetchAccessToken(uri); + } catch (FlattrException e) { + e.printStackTrace(); + exception = e; + return null; + } + if (token != null) { + if (BuildConfig.DEBUG) Log.d(TAG, "Successfully got token"); + return token; + } else { + Log.w(TAG, "Flattr token was null"); + return null; + } + } + + @SuppressLint("NewApi") + public void executeAsync() { + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + execute(); + } + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java new file mode 100644 index 000000000..c0d8049db --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoImageResource.java @@ -0,0 +1,37 @@ +package de.danoeh.antennapod.core.asynctask; + +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 { + + /** + * This scheme should be used by PicassoImageResources to + * indicate that the image Uri points to a file that is not an image + * (e.g. a media file). This workaround is needed so that the Picasso library + * loads these Uri with a Downloader instead of trying to load it directly. + * <p/> + * For example implementations, see FeedMedia or ExternalMedia. + */ + public static final String SCHEME_MEDIA = "media"; + + + /** + * Parameter key for an encoded fallback Uri. This Uri MUST point to a local image file + */ + public static final String PARAM_FALLBACK = "fallback"; + + /** + * Returns a Uri to the image or null if no image is available. + * <p/> + * The Uri can either be an HTTP-URL, a URL pointing to a local image file or + * a non-image file (see SCHEME_MEDIA for more details). + * <p/> + * The Uri can also have an optional fallback-URL if loading the default URL + * failed (see PARAM_FALLBACK). + */ + public Uri getImageUri(); +} 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 new file mode 100644 index 000000000..1ed29c23a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/asynctask/PicassoProvider.java @@ -0,0 +1,212 @@ +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.util.Log; + +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 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.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * 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; + } + Picasso picasso = new Picasso.Builder(appContext) + .indicatorsEnabled(DEBUG) + .loggingEnabled(DEBUG) + .downloader(new OkHttpDownloader(appContext)) + .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 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) 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) { + // check for fallback Uri + String fallbackParam = data.uri.getQueryParameter(PicassoImageResource.PARAM_FALLBACK); + + if (fallbackParam != null) { + Uri fallback = Uri.parse(fallbackParam); + bitmap = decodeStreamFromFile(data, fallback); + } + } + 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; + } + } +} 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 new file mode 100644 index 000000000..1535e2e9a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java @@ -0,0 +1,211 @@ +package de.danoeh.antennapod.core.backup; + +import android.app.backup.BackupAgentHelper; +import android.app.backup.BackupDataInputStream; +import android.app.backup.BackupDataOutput; +import android.app.backup.BackupHelper; +import android.content.Context; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import de.danoeh.antennapod.core.BuildConfig; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.io.Writer; +import java.math.BigInteger; +import java.security.DigestInputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.opml.OpmlElement; +import de.danoeh.antennapod.core.opml.OpmlReader; +import de.danoeh.antennapod.core.opml.OpmlWriter; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.LangUtils; + +public class OpmlBackupAgent extends BackupAgentHelper { + private static final String OPML_BACKUP_KEY = "opml"; + + @Override + public void onCreate() { + addHelper(OPML_BACKUP_KEY, new OpmlBackupHelper(this)); + } + + private static final void LOGD(String tag, String msg) { + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, msg); + } + } + + private static final void LOGD(String tag, String msg, Throwable tr) { + if (BuildConfig.DEBUG && Log.isLoggable(tag, Log.DEBUG)) { + Log.d(tag, msg, tr); + } + } + + /** Class for backing up and restoring the OPML file. */ + private static class OpmlBackupHelper implements BackupHelper { + private static final String TAG = "OpmlBackupHelper"; + + private static final String OPML_ENTITY_KEY = "antennapod-feeds.opml"; + + private final Context mContext; + + /** Checksum of restored OPML file */ + private byte[] mChecksum; + + public OpmlBackupHelper(Context context) { + mContext = context; + } + + @Override + public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) { + Log.d(TAG, "Performing backup"); + ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); + MessageDigest digester = null; + Writer writer; + + try { + digester = MessageDigest.getInstance("MD5"); + writer = new OutputStreamWriter(new DigestOutputStream(byteStream, digester), + LangUtils.UTF_8); + } catch (NoSuchAlgorithmException e) { + writer = new OutputStreamWriter(byteStream, LangUtils.UTF_8); + } + + try { + // Write OPML + new OpmlWriter().writeDocument(DBReader.getFeedList(mContext), writer); + + // Compare checksum of new and old file to see if we need to perform a backup at all + if (digester != null) { + byte[] newChecksum = digester.digest(); + LOGD(TAG, "New checksum: " + new BigInteger(1, newChecksum).toString(16)); + + // Get the old checksum + if (oldState != null) { + FileInputStream inState = new FileInputStream(oldState.getFileDescriptor()); + int len = inState.read(); + + if (len != -1) { + byte[] oldChecksum = new byte[len]; + inState.read(oldChecksum); + LOGD(TAG, "Old checksum: " + new BigInteger(1, oldChecksum).toString(16)); + + if (Arrays.equals(oldChecksum, newChecksum)) { + LOGD(TAG, "Checksums are the same; won't backup"); + return; + } + } + } + + writeNewStateDescription(newState, newChecksum); + } + + LOGD(TAG, "Backing up OPML"); + byte[] bytes = byteStream.toByteArray(); + data.writeEntityHeader(OPML_ENTITY_KEY, bytes.length); + data.writeEntityData(bytes, bytes.length); + } catch (IOException e) { + Log.e(TAG, "Error during backup", e); + } finally { + if (writer != null) { + try { + writer.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void restoreEntity(BackupDataInputStream data) { + LOGD(TAG, "Backup restore"); + + if (!OPML_ENTITY_KEY.equals(data.getKey())) { + LOGD(TAG, "Unknown entity key: " + data.getKey()); + return; + } + + MessageDigest digester = null; + Reader reader; + + try { + digester = MessageDigest.getInstance("MD5"); + reader = new InputStreamReader(new DigestInputStream(data, digester), + LangUtils.UTF_8); + } catch (NoSuchAlgorithmException e) { + reader = new InputStreamReader(data, LangUtils.UTF_8); + } + + try { + ArrayList<OpmlElement> opmlElements = new OpmlReader().readDocument(reader); + mChecksum = digester == null ? null : digester.digest(); + DownloadRequester downloader = DownloadRequester.getInstance(); + Date lastUpdated = new Date(); + + for (OpmlElement opmlElem : opmlElements) { + Feed feed = new Feed(opmlElem.getXmlUrl(), lastUpdated, opmlElem.getText()); + + try { + downloader.downloadFeed(mContext, feed); + } catch (DownloadRequestException e) { + LOGD(TAG, "Error while restoring/downloading feed", e); + } + } + } catch (XmlPullParserException e) { + Log.e(TAG, "Error while parsing the OPML file", e); + } catch (IOException e) { + Log.e(TAG, "Failed to restore OPML backup", e); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + } + } + } + } + + @Override + public void writeNewStateDescription(ParcelFileDescriptor newState) { + writeNewStateDescription(newState, mChecksum); + } + + /** + * Writes the new state description, which is the checksum of the OPML file. + * + * @param newState + * @param checksum + */ + private void writeNewStateDescription(ParcelFileDescriptor newState, byte[] checksum) { + if (checksum == null) { + return; + } + + try { + FileOutputStream outState = new FileOutputStream(newState.getFileDescriptor()); + outState.write(checksum.length); + outState.write(checksum); + outState.flush(); + outState.close(); + } catch (IOException e) { + Log.e(TAG, "Failed to write new state description", e); + } + } + } +} 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 new file mode 100644 index 000000000..ba1add895 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/dialog/ConfirmationDialog.java @@ -0,0 +1,64 @@ +package de.danoeh.antennapod.core.dialog; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.R; + +/** + * Creates an AlertDialog which asks the user to confirm something. Other + * classes can handle events like confirmation or cancellation. + */ +public abstract class ConfirmationDialog { + private static final String TAG = "ConfirmationDialog"; + + Context context; + int titleId; + int messageId; + + public ConfirmationDialog(Context context, int titleId, int messageId) { + this.context = context; + this.titleId = titleId; + this.messageId = messageId; + } + + public void onCancelButtonPressed(DialogInterface dialog) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Dialog was cancelled"); + dialog.dismiss(); + } + + 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, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + onConfirmButtonPressed(dialog); + } + }); + builder.setNegativeButton(R.string.cancel_label, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + onCancelButtonPressed(dialog); + } + }); + builder.setOnCancelListener(new DialogInterface.OnCancelListener() { + + @Override + public void onCancel(DialogInterface dialog) { + onCancelButtonPressed(dialog); + } + }); + return builder.create(); + } +} 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 new file mode 100644 index 000000000..3d174bd8e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/dialog/DownloadRequestErrorDialogCreator.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.core.dialog; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import de.danoeh.antennapod.core.R; + +/** Creates Alert Dialogs if a DownloadRequestException has happened. */ +public class DownloadRequestErrorDialogCreator { + private DownloadRequestErrorDialogCreator() { + } + + public static void newRequestErrorDialog(Context context, + String errorMessage) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setNeutralButton(android.R.string.ok, + new DialogInterface.OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }) + .setTitle(R.string.download_error_request_error) + .setMessage( + context.getString(R.string.download_request_error_dialog_message_prefix) + + errorMessage); + builder.create().show(); + } +} 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 new file mode 100644 index 000000000..ce3352ed6 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Chapter.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.core.feed; + +public abstract class Chapter extends FeedComponent { + + /** Defines starting point in milliseconds. */ + protected long start; + protected String title; + protected String link; + + public Chapter() { + } + + public Chapter(long start) { + super(); + this.start = start; + } + + public Chapter(long start, String title, FeedItem item, String link) { + super(); + this.start = start; + this.title = title; + this.link = link; + } + + public abstract int getChapterType(); + + public long getStart() { + return start; + } + + public String getTitle() { + return title; + } + + public String getLink() { + return link; + } + + public void setStart(long start) { + this.start = start; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setLink(String link) { + this.link = link; + } + + @Override + public String getHumanReadableIdentifier() { + return title; + } +} 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 new file mode 100644 index 000000000..f8815dcf0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/EventDistributor.java @@ -0,0 +1,140 @@ +package de.danoeh.antennapod.core.feed; + +import android.os.Handler; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.core.BuildConfig; + +import java.util.AbstractQueue; +import java.util.Observable; +import java.util.Observer; +import java.util.concurrent.ConcurrentLinkedQueue; + +/** + * Notifies its observers about changes in the feed database. Observers can + * register by retrieving an instance of this class and registering an + * EventListener. When new events arrive, the EventDistributor will process the + * event queue in a handler that runs on the main thread. The observers will only + * be notified once if the event queue contains multiple elements. + * + * Events can be sent with the send* methods. + */ +public class EventDistributor extends Observable { + private static final String TAG = "EventDistributor"; + + public static final int FEED_LIST_UPDATE = 1; + public static final int UNREAD_ITEMS_UPDATE = 2; + public static final int QUEUE_UPDATE = 4; + 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; + + private Handler handler; + private AbstractQueue<Integer> events; + + private static EventDistributor instance; + + private EventDistributor() { + this.handler = new Handler(); + events = new ConcurrentLinkedQueue<Integer>(); + } + + public static synchronized EventDistributor getInstance() { + if (instance == null) { + instance = new EventDistributor(); + } + return instance; + } + + public void register(EventListener el) { + addObserver(el); + } + + public void unregister(EventListener el) { + deleteObserver(el); + } + + public void addEvent(Integer i) { + events.offer(i); + handler.post(new Runnable() { + + @Override + public void run() { + processEventQueue(); + } + }); + } + + private void processEventQueue() { + Integer result = 0; + if (BuildConfig.DEBUG) + Log.d(TAG, + "Processing event queue. Number of events: " + + events.size()); + for (Integer current = events.poll(); current != null; current = events + .poll()) { + result |= current; + } + if (result != 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Notifying observers. Data: " + result); + setChanged(); + notifyObservers(result); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Event queue didn't contain any new events. Observers will not be notified."); + } + } + + @Override + public void addObserver(Observer observer) { + super.addObserver(observer); + Validate.isInstanceOf(EventListener.class, observer); + } + + public void sendDownloadQueuedBroadcast() { + addEvent(DOWNLOAD_QUEUED); + } + + public void sendUnreadItemsUpdateBroadcast() { + addEvent(UNREAD_ITEMS_UPDATE); + } + + public void sendQueueUpdateBroadcast() { + addEvent(QUEUE_UPDATE); + } + + public void sendFeedUpdateBroadcast() { + addEvent(FEED_LIST_UPDATE); + } + + public void sendPlaybackHistoryUpdateBroadcast() { + addEvent(PLAYBACK_HISTORY_UPDATE); + } + + public void sendDownloadLogUpdateBroadcast() { + addEvent(DOWNLOADLOG_UPDATE); + } + + public void sendDownloadHandledBroadcast() { + addEvent(DOWNLOAD_HANDLED); + } + + public static abstract class EventListener implements Observer { + + @Override + public void update(Observable observable, Object data) { + if (observable instanceof EventDistributor + && data instanceof Integer) { + update((EventDistributor) observable, (Integer) data); + } + } + + public abstract void update(EventDistributor eventDistributor, + Integer arg); + } +} 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 new file mode 100644 index 000000000..c0f71ed55 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java @@ -0,0 +1,506 @@ +package de.danoeh.antennapod.core.feed; + +import android.content.Context; +import android.net.Uri; + +import org.apache.commons.lang3.StringUtils; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.EpisodeFilter; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; + +/** + * Data Object for a whole feed + * + * @author daniel + */ +public class Feed extends FeedFile implements FlattrThing, PicassoImageResource { + public static final int FEEDFILETYPE_FEED = 0; + public static final String TYPE_RSS2 = "rss"; + public static final String TYPE_RSS091 = "rss"; + public static final String TYPE_ATOM1 = "atom"; + + private String title; + /** + * Contains 'id'-element in Atom feed. + */ + private String feedIdentifier; + /** + * Link to the website. + */ + private String link; + private String description; + private String language; + /** + * Name of the author + */ + private String author; + private FeedImage image; + private List<FeedItem> items; + /** + * Date of last refresh. + */ + private Date lastUpdate; + private FlattrStatus flattrStatus; + private String paymentLink; + /** + * Feed type, for example RSS 2 or Atom + */ + private String type; + + /** + * Feed preferences + */ + private FeedPreferences preferences; + + /** + * The page number that this feed is on. Only feeds with page number "0" should be stored in the + * database, feed objects with a higher page number only exist temporarily and should be merged + * into feeds with page number "0". + * <p/> + * This attribute's value is not saved in the database + */ + private int pageNr; + + /** + * True if this is a "paged feed", i.e. there exist other feed files that belong to the same + * logical feed. + */ + private boolean paged; + + /** + * Link to the next page of this feed. If this feed object represents a logical feed (i.e. a feed + * that is saved in the database) this might be null while still being a paged feed. + */ + private String nextPageLink; + + /** + * This constructor is used for restoring a feed from the database. + */ + public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink, + String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl, + String downloadUrl, boolean downloaded, FlattrStatus status, boolean paged, String nextPageLink) { + super(fileUrl, downloadUrl, downloaded); + this.id = id; + this.title = title; + if (lastUpdate != null) { + this.lastUpdate = (Date) lastUpdate.clone(); + } else { + this.lastUpdate = null; + } + this.link = link; + this.description = description; + this.paymentLink = paymentLink; + this.author = author; + this.language = language; + this.type = type; + this.feedIdentifier = feedIdentifier; + this.image = image; + this.flattrStatus = status; + this.paged = paged; + this.nextPageLink = nextPageLink; + + items = new ArrayList<FeedItem>(); + } + + /** + * This constructor is used for test purposes and uses a default flattr status object. + */ + public Feed(long id, Date lastUpdate, String title, String link, String description, String paymentLink, + String author, String language, String type, String feedIdentifier, FeedImage image, String fileUrl, + String downloadUrl, boolean downloaded) { + this(id, lastUpdate, title, link, description, paymentLink, author, language, type, feedIdentifier, image, + fileUrl, downloadUrl, downloaded, new FlattrStatus(), false, null); + } + + /** + * This constructor can be used when parsing feed data. Only the 'lastUpdate' and 'items' field are initialized. + */ + public Feed() { + super(); + items = new ArrayList<FeedItem>(); + lastUpdate = new Date(); + this.flattrStatus = new FlattrStatus(); + } + + /** + * This constructor is used for requesting a feed download (it must not be used for anything else!). It should NOT be + * used if the title of the feed is already known. + */ + public Feed(String url, Date lastUpdate) { + super(null, url, false); + this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null; + this.flattrStatus = new FlattrStatus(); + } + + /** + * This constructor is used for requesting a feed download (it must not be used for anything else!). It should be + * used if the title of the feed is already known. + */ + public Feed(String url, Date lastUpdate, String title) { + this(url, lastUpdate); + this.title = title; + this.flattrStatus = new FlattrStatus(); + } + + /** + * This constructor is used for requesting a feed download (it must not be used for anything else!). It should be + * used if the title of the feed is already known. + */ + public Feed(String url, Date lastUpdate, String title, String username, String password) { + this(url, lastUpdate, title); + preferences = new FeedPreferences(0, true, username, password); + } + + /** + * Returns the number of FeedItems where 'read' is false. If the 'display + * only episodes' - preference is set to true, this method will only count + * items with episodes. + */ + public int getNumOfNewItems() { + int count = 0; + for (FeedItem item : items) { + if (item.getState() == FeedItem.State.NEW) { + if (!UserPreferences.isDisplayOnlyEpisodes() + || item.getMedia() != null) { + count++; + } + } + } + return count; + } + + /** + * Returns the number of FeedItems where the media started to play but + * wasn't finished yet. + */ + public int getNumOfStartedItems() { + int count = 0; + + for (FeedItem item : items) { + FeedItem.State state = item.getState(); + if (state == FeedItem.State.IN_PROGRESS + || state == FeedItem.State.PLAYING) { + count++; + } + } + return count; + } + + /** + * Returns true if at least one item in the itemlist is unread. + * + * @param enableEpisodeFilter true if this method should only count items with episodes if + * the 'display only episodes' - preference is set to true by the + * user. + */ + public boolean hasNewItems(boolean enableEpisodeFilter) { + for (FeedItem item : items) { + if (item.getState() == FeedItem.State.NEW) { + if (!(enableEpisodeFilter && UserPreferences + .isDisplayOnlyEpisodes()) || item.getMedia() != null) { + return true; + } + } + } + return false; + } + + /** + * Returns the number of FeedItems. + * + * @param enableEpisodeFilter true if this method should only count items with episodes if + * the 'display only episodes' - preference is set to true by the + * user. + */ + public int getNumOfItems(boolean enableEpisodeFilter) { + if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) { + return EpisodeFilter.countItemsWithEpisodes(items); + } else { + return items.size(); + } + } + + /** + * Returns the item at the specified index. + * + * @param enableEpisodeFilter true if this method should ignore items without episdodes if + * the episodes filter has been enabled by the user. + */ + public FeedItem getItemAtIndex(boolean enableEpisodeFilter, int position) { + if (enableEpisodeFilter && UserPreferences.isDisplayOnlyEpisodes()) { + return EpisodeFilter.accessEpisodeByIndex(items, position); + } else { + return items.get(position); + } + } + + /** + * Returns the value that uniquely identifies this Feed. If the + * feedIdentifier attribute is not null, it will be returned. Else it will + * try to return the title. If the title is not given, it will use the link + * of the feed. + */ + public String getIdentifyingValue() { + if (feedIdentifier != null && !feedIdentifier.isEmpty()) { + return feedIdentifier; + } else if (download_url != null && !download_url.isEmpty()) { + return download_url; + } else if (title != null && !title.isEmpty()) { + return title; + } else { + return link; + } + } + + @Override + public String getHumanReadableIdentifier() { + if (title != null) { + return title; + } else { + return download_url; + } + } + + public void updateFromOther(Feed other) { + super.updateFromOther(other); + if (other.title != null) { + title = other.title; + } + if (other.feedIdentifier != null) { + feedIdentifier = other.feedIdentifier; + } + if (other.link != null) { + link = other.link; + } + if (other.description != null) { + description = other.description; + } + if (other.language != null) { + language = other.language; + } + if (other.author != null) { + author = other.author; + } + if (other.paymentLink != null) { + paymentLink = other.paymentLink; + } + if (other.flattrStatus != null) { + flattrStatus = other.flattrStatus; + } + // this feed's nextPage might already point to a higher page, so we only update the nextPage value + // if this feed is not paged and the other feed is. + if (!this.paged && other.paged) { + this.paged = other.paged; + this.nextPageLink = other.nextPageLink; + } + } + + public boolean compareWithOther(Feed other) { + if (super.compareWithOther(other)) { + return true; + } + if (!title.equals(other.title)) { + return true; + } + if (other.feedIdentifier != null) { + if (feedIdentifier == null + || !feedIdentifier.equals(other.feedIdentifier)) { + return true; + } + } + if (other.link != null) { + if (link == null || !link.equals(other.link)) { + return true; + } + } + if (other.description != null) { + if (description == null || !description.equals(other.description)) { + return true; + } + } + if (other.language != null) { + if (language == null || !language.equals(other.language)) { + return true; + } + } + if (other.author != null) { + if (author == null || !author.equals(other.author)) { + return true; + } + } + if (other.paymentLink != null) { + if (paymentLink == null || !paymentLink.equals(other.paymentLink)) { + return true; + } + } + if (other.isPaged() && !this.isPaged()) { + return true; + } + if (!StringUtils.equals(other.getNextPageLink(), this.getNextPageLink())) { + return true; + } + return false; + } + + @Override + public int getTypeAsInt() { + return FEEDFILETYPE_FEED; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public FeedImage getImage() { + return image; + } + + public void setImage(FeedImage image) { + this.image = image; + } + + public List<FeedItem> getItems() { + return items; + } + + public void setItems(List<FeedItem> list) { + this.items = list; + } + + public Date getLastUpdate() { + return (lastUpdate != null) ? (Date) lastUpdate.clone() : null; + } + + public void setLastUpdate(Date lastUpdate) { + this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null; + } + + public String getFeedIdentifier() { + return feedIdentifier; + } + + public void setFeedIdentifier(String feedIdentifier) { + this.feedIdentifier = feedIdentifier; + } + + public void setFlattrStatus(FlattrStatus status) { + this.flattrStatus = status; + } + + public FlattrStatus getFlattrStatus() { + return flattrStatus; + } + + public String getPaymentLink() { + return paymentLink; + } + + public void setPaymentLink(String paymentLink) { + this.paymentLink = paymentLink; + } + + public String getLanguage() { + return language; + } + + public void setLanguage(String language) { + this.language = language; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public void setPreferences(FeedPreferences preferences) { + this.preferences = preferences; + } + + public FeedPreferences getPreferences() { + return preferences; + } + + public void savePreferences(Context context) { + DBWriter.setFeedPreferences(context, preferences); + } + + @Override + public void setId(long id) { + super.setId(id); + if (preferences != null) { + preferences.setFeedID(id); + } + } + + @Override + public Uri getImageUri() { + if (image != null) { + return image.getImageUri(); + } else { + return null; + } + } + + public int getPageNr() { + return pageNr; + } + + public void setPageNr(int pageNr) { + this.pageNr = pageNr; + } + + public boolean isPaged() { + return paged; + } + + public void setPaged(boolean paged) { + this.paged = paged; + } + + public String getNextPageLink() { + return nextPageLink; + } + + public void setNextPageLink(String nextPageLink) { + this.nextPageLink = nextPageLink; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java new file mode 100644 index 000000000..05115c1ea --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedComponent.java @@ -0,0 +1,66 @@ +package de.danoeh.antennapod.core.feed; + +/** + * Represents every possible component of a feed + * + * @author daniel + */ +public abstract class FeedComponent { + + protected long id; + + public FeedComponent() { + super(); + } + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + /** + * Update this FeedComponent's attributes with the attributes from another + * FeedComponent. This method should only update attributes which where read from + * the feed. + */ + public void updateFromOther(FeedComponent other) { + } + + /** + * Compare's this FeedComponent's attribute values with another FeedComponent's + * attribute values. This method will only compare attributes which were + * read from the feed. + * + * @return true if attribute values are different, false otherwise + */ + public boolean compareWithOther(FeedComponent other) { + return false; + } + + + /** + * Should return a non-null, human-readable String so that the item can be + * identified by the user. Can be title, download-url, etc. + */ + public abstract String getHumanReadableIdentifier(); + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FeedComponent that = (FeedComponent) o; + + if (id != that.id) return false; + + return true; + } + + @Override + public int hashCode() { + return (int) (id ^ (id >>> 32)); + } +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java new file mode 100644 index 000000000..3dc58654b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedFile.java @@ -0,0 +1,105 @@ +package de.danoeh.antennapod.core.feed; + +import java.io.File; + +/** + * Represents a component of a Feed that has to be downloaded + */ +public abstract class FeedFile extends FeedComponent { + + protected String file_url; + protected String download_url; + protected boolean downloaded; + + /** + * Creates a new FeedFile object. + * + * @param file_url The location of the FeedFile. If this is null, the downloaded-attribute + * will automatically be set to false. + * @param download_url The location where the FeedFile can be downloaded. + * @param downloaded true if the FeedFile has been downloaded, false otherwise. This parameter + * will automatically be interpreted as false if the file_url is null. + */ + public FeedFile(String file_url, String download_url, boolean downloaded) { + super(); + this.file_url = file_url; + this.download_url = download_url; + this.downloaded = (file_url != null) && downloaded; + } + + public FeedFile() { + this(null, null, false); + } + + public abstract int getTypeAsInt(); + + /** + * Update this FeedFile's attributes with the attributes from another + * FeedFile. This method should only update attributes which where read from + * the feed. + */ + public void updateFromOther(FeedFile other) { + super.updateFromOther(other); + this.download_url = other.download_url; + } + + /** + * Compare's this FeedFile's attribute values with another FeedFile's + * attribute values. This method will only compare attributes which were + * read from the feed. + * + * @return true if attribute values are different, false otherwise + */ + public boolean compareWithOther(FeedFile other) { + if (super.compareWithOther(other)) { + return true; + } + if (!download_url.equals(other.download_url)) { + return true; + } + return false; + } + + /** + * Returns true if the file exists at file_url. + */ + public boolean fileExists() { + if (file_url == null) { + return false; + } else { + File f = new File(file_url); + return f.exists(); + } + } + + public String getFile_url() { + return file_url; + } + + /** + * Changes the file_url of this FeedFile. Setting this value to + * null will also set the downloaded-attribute to false. + */ + public void setFile_url(String file_url) { + this.file_url = file_url; + if (file_url == null) { + downloaded = false; + } + } + + public String getDownload_url() { + return download_url; + } + + public void setDownload_url(String download_url) { + this.download_url = download_url; + } + + public boolean isDownloaded() { + return downloaded; + } + + public void setDownloaded(boolean downloaded) { + this.downloaded = downloaded; + } +} 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 new file mode 100644 index 000000000..b01747f7f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedImage.java @@ -0,0 +1,72 @@ +package de.danoeh.antennapod.core.feed; + +import android.net.Uri; + +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; + +import java.io.File; + + +public class FeedImage extends FeedFile implements PicassoImageResource { + public static final int FEEDFILETYPE_FEEDIMAGE = 1; + + protected String title; + protected FeedComponent owner; + + public FeedImage(FeedComponent owner, String download_url, String title) { + super(null, download_url, false); + this.download_url = download_url; + this.title = title; + this.owner = owner; + } + + public FeedImage(long id, String title, String file_url, + String download_url, boolean downloaded) { + super(file_url, download_url, downloaded); + this.id = id; + this.title = title; + } + + public FeedImage() { + super(); + } + + @Override + public String getHumanReadableIdentifier() { + if (owner != null && owner.getHumanReadableIdentifier() != null) { + return owner.getHumanReadableIdentifier(); + } else { + return download_url; + } + } + + @Override + public int getTypeAsInt() { + return FEEDFILETYPE_FEEDIMAGE; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public FeedComponent getOwner() { + return owner; + } + + public void setOwner(FeedComponent owner) { + this.owner = owner; + } + + @Override + public Uri getImageUri() { + if (file_url != null && downloaded) { + return Uri.fromFile(new File(file_url)); + } else { + return 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 new file mode 100644 index 000000000..d056917e1 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItem.java @@ -0,0 +1,334 @@ +package de.danoeh.antennapod.core.feed; + +import android.net.Uri; + +import java.util.Date; +import java.util.List; +import java.util.concurrent.Callable; + +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.ShownotesProvider; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; + +/** + * Data Object for a XML message + * + * @author daniel + */ +public class FeedItem extends FeedComponent implements ShownotesProvider, FlattrThing, PicassoImageResource { + + /** + * The id/guid that can be found in the rss/atom feed. Might not be set. + */ + private String itemIdentifier; + private String title; + /** + * The description of a feeditem. + */ + private String description; + /** + * The content of the content-encoded tag of a feeditem. + */ + private String contentEncoded; + + private String link; + private Date pubDate; + private FeedMedia media; + + private Feed feed; + private long feedId; + + private boolean read; + private String paymentLink; + private FlattrStatus flattrStatus; + private List<Chapter> chapters; + private FeedImage image; + + public FeedItem() { + this.read = true; + this.flattrStatus = new FlattrStatus(); + } + + /** + * 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) { + this.id = id; + this.title = title; + this.itemIdentifier = itemIdentifier; + this.link = link; + this.pubDate = (pubDate != null) ? (Date) pubDate.clone() : null; + this.read = read; + this.feed = feed; + this.flattrStatus = new FlattrStatus(); + } + + public void updateFromOther(FeedItem other) { + super.updateFromOther(other); + if (other.title != null) { + title = other.title; + } + if (other.getDescription() != null) { + description = other.getDescription(); + } + if (other.getContentEncoded() != null) { + contentEncoded = other.contentEncoded; + } + if (other.link != null) { + link = other.link; + } + if (other.pubDate != null && other.pubDate != pubDate) { + pubDate = other.pubDate; + } + if (other.media != null) { + if (media == null) { + setMedia(other.media); + } else if (media.compareWithOther(other)) { + media.updateFromOther(other); + } + } + if (other.paymentLink != null) { + paymentLink = other.paymentLink; + } + if (other.chapters != null) { + if (chapters == null) { + chapters = other.chapters; + } + } + if (image == null) { + image = other.image; + } + } + + /** + * Returns the value that uniquely identifies this FeedItem. If the + * itemIdentifier attribute is not null, it will be returned. Else it will + * try to return the title. If the title is not given, it will use the link + * of the entry. + */ + public String getIdentifyingValue() { + if (itemIdentifier != null && !itemIdentifier.isEmpty()) { + return itemIdentifier; + } else if (title != null && !title.isEmpty()) { + return title; + } else { + return link; + } + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } + + public Date getPubDate() { + if (pubDate != null) { + return (Date) pubDate.clone(); + } else { + return null; + } + } + + public void setPubDate(Date pubDate) { + if (pubDate != null) { + this.pubDate = (Date) pubDate.clone(); + } else { + this.pubDate = null; + } + } + + public FeedMedia getMedia() { + return media; + } + + /** + * Sets the media object of this FeedItem. If the given + * FeedMedia object is not null, it's 'item'-attribute value + * will also be set to this item. + */ + public void setMedia(FeedMedia media) { + this.media = media; + if (media != null && media.getItem() != this) { + media.setItem(this); + } + } + + public Feed getFeed() { + return feed; + } + + public void setFeed(Feed feed) { + this.feed = feed; + } + + public boolean isRead() { + return read || isInProgress(); + } + + public void setRead(boolean read) { + this.read = read; + } + + private boolean isInProgress() { + return (media != null && media.isInProgress()); + } + + public String getContentEncoded() { + return contentEncoded; + } + + public void setContentEncoded(String contentEncoded) { + this.contentEncoded = contentEncoded; + } + + public void setFlattrStatus(FlattrStatus status) { + this.flattrStatus = status; + } + + public FlattrStatus getFlattrStatus() { + return flattrStatus; + } + + public String getPaymentLink() { + return paymentLink; + } + + public void setPaymentLink(String paymentLink) { + this.paymentLink = paymentLink; + } + + public List<Chapter> getChapters() { + return chapters; + } + + public void setChapters(List<Chapter> chapters) { + this.chapters = chapters; + } + + public String getItemIdentifier() { + return itemIdentifier; + } + + public void setItemIdentifier(String itemIdentifier) { + this.itemIdentifier = itemIdentifier; + } + + public boolean hasMedia() { + return media != null; + } + + private boolean isPlaying() { + if (media != null) { + return media.isPlaying(); + } + return false; + } + + @Override + public Callable<String> loadShownotes() { + return new Callable<String>() { + @Override + public String call() throws Exception { + + if (contentEncoded == null || description == null) { + DBReader.loadExtraInformationOfFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), FeedItem.this); + + } + return (contentEncoded != null) ? contentEncoded : description; + } + }; + } + + @Override + public Uri getImageUri() { + if (hasItemImageDownloaded()) { + return image.getImageUri(); + } else if (hasMedia()) { + return media.getImageUri(); + } else if (feed != null) { + return feed.getImageUri(); + } else { + return null; + } + } + + public enum State { + NEW, IN_PROGRESS, READ, PLAYING + } + + public State getState() { + if (hasMedia()) { + if (isPlaying()) { + return State.PLAYING; + } + if (isInProgress()) { + return State.IN_PROGRESS; + } + } + return (isRead() ? State.READ : State.NEW); + } + + public long getFeedId() { + return feedId; + } + + public void setFeedId(long feedId) { + this.feedId = feedId; + } + + /** + * Returns the image of this item or the image of the feed if this item does + * not have its own image. + */ + public FeedImage getImage() { + return (hasItemImage()) ? image : feed.getImage(); + } + + public void setImage(FeedImage image) { + this.image = image; + if (image != null) { + image.setOwner(this); + } + } + + /** + * Returns true if this FeedItem has its own image, false otherwise. + */ + public boolean hasItemImage() { + return image != null; + } + + /** + * Returns true if this FeedItem has its own image and the image has been downloaded. + */ + public boolean hasItemImageDownloaded() { + return image != null && image.isDownloaded(); + } + + @Override + public String getHumanReadableIdentifier() { + return title; + } +} 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 new file mode 100644 index 000000000..defcfd598 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -0,0 +1,412 @@ +package de.danoeh.antennapod.core.feed; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +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.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.util.ChapterUtils; +import de.danoeh.antennapod.core.util.playback.Playable; + +public class FeedMedia extends FeedFile implements Playable { + private static final String TAG = "FeedMedia"; + + public static final int FEEDFILETYPE_FEEDMEDIA = 2; + public static final int PLAYABLE_TYPE_FEEDMEDIA = 1; + + public static final String PREF_MEDIA_ID = "FeedMedia.PrefMediaId"; + public static final String PREF_FEED_ID = "FeedMedia.PrefFeedId"; + + private int duration; + private int position; // Current position in file + private 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; + private Date playbackCompletionDate; + + /* Used for loading item when restoring from parcel. */ + private long itemID; + + public FeedMedia(FeedItem i, String download_url, long size, + String mime_type) { + super(null, download_url, false); + this.item = i; + this.size = size; + this.mime_type = mime_type; + } + + public FeedMedia(long id, FeedItem item, int duration, int position, + long size, String mime_type, String file_url, String download_url, + boolean downloaded, Date playbackCompletionDate, int played_duration) { + super(file_url, download_url, downloaded); + this.id = id; + this.item = item; + this.duration = duration; + this.position = position; + this.played_duration = played_duration; + this.size = size; + this.mime_type = mime_type; + this.playbackCompletionDate = playbackCompletionDate == null + ? null : (Date) playbackCompletionDate.clone(); + } + + public FeedMedia(long id, FeedItem item) { + super(); + this.id = id; + this.item = item; + } + + @Override + public String getHumanReadableIdentifier() { + if (item != null && item.getTitle() != null) { + return item.getTitle(); + } else { + return download_url; + } + } + + /** + * Uses mimetype to determine the type of media. + */ + public MediaType getMediaType() { + if (mime_type == null || mime_type.isEmpty()) { + return MediaType.UNKNOWN; + } else { + if (mime_type.startsWith("audio")) { + return MediaType.AUDIO; + } else if (mime_type.startsWith("video")) { + return MediaType.VIDEO; + } else if (mime_type.equals("application/ogg")) { + return MediaType.AUDIO; + } + } + return MediaType.UNKNOWN; + } + + public void updateFromOther(FeedMedia other) { + super.updateFromOther(other); + if (other.size > 0) { + size = other.size; + } + if (other.mime_type != null) { + mime_type = other.mime_type; + } + } + + public boolean compareWithOther(FeedMedia other) { + if (super.compareWithOther(other)) { + return true; + } + if (other.mime_type != null) { + if (mime_type == null || !mime_type.equals(other.mime_type)) { + return true; + } + } + if (other.size > 0 && other.size != size) { + return true; + } + return false; + } + + /** + * Reads playback preferences to determine whether this FeedMedia object is + * currently being played. + */ + public boolean isPlaying() { + return PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA + && PlaybackPreferences.getCurrentlyPlayingFeedMediaId() == id; + } + + @Override + public int getTypeAsInt() { + return FEEDFILETYPE_FEEDMEDIA; + } + + public int getDuration() { + return duration; + } + + public void setDuration(int duration) { + this.duration = duration; + } + + public int getPlayedDuration() { + return played_duration; + } + + public void setPlayedDuration(int played_duration) { + this.played_duration = played_duration; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public String getMime_type() { + return mime_type; + } + + public void setMime_type(String mime_type) { + this.mime_type = mime_type; + } + + public FeedItem getItem() { + return item; + } + + /** + * Sets the item object of this FeedMedia. If the given + * FeedItem object is not null, it's 'media'-attribute value + * will also be set to this media object. + */ + public void setItem(FeedItem item) { + this.item = item; + if (item != null && item.getMedia() != this) { + item.setMedia(this); + } + } + + public Date getPlaybackCompletionDate() { + return playbackCompletionDate == null + ? null : (Date) playbackCompletionDate.clone(); + } + + public void setPlaybackCompletionDate(Date playbackCompletionDate) { + this.playbackCompletionDate = playbackCompletionDate == null + ? null : (Date) playbackCompletionDate.clone(); + } + + public boolean isInProgress() { + return (this.position > 0); + } + + public FeedImage getImage() { + if (item != null) { + return (item.hasItemImageDownloaded()) ? item.getImage() : item.getFeed().getImage(); + } + return null; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(id); + dest.writeLong(item.getId()); + + dest.writeInt(duration); + dest.writeInt(position); + dest.writeLong(size); + dest.writeString(mime_type); + dest.writeString(file_url); + dest.writeString(download_url); + dest.writeByte((byte) ((downloaded) ? 1 : 0)); + dest.writeLong((playbackCompletionDate != null) ? playbackCompletionDate.getTime() : 0); + dest.writeInt(played_duration); + } + + @Override + public void writeToPreferences(Editor prefEditor) { + prefEditor.putLong(PREF_FEED_ID, item.getFeed().getId()); + prefEditor.putLong(PREF_MEDIA_ID, id); + } + + @Override + public void loadMetadata() throws PlayableException { + if (item == null && itemID != 0) { + item = DBReader.getFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), itemID); + } + } + + @Override + public void loadChapterMarks() { + if (getChapters() == null && !localFileAvailable()) { + ChapterUtils.loadChaptersFromStreamUrl(this); + if (getChapters() != null && item != null) { + DBWriter.setFeedItem(ClientConfig.applicationCallbacks.getApplicationInstance(), + item); + } + } + + } + + @Override + public String getEpisodeTitle() { + if (item == null) { + return null; + } + if (getItem().getTitle() != null) { + return getItem().getTitle(); + } else { + return getItem().getIdentifyingValue(); + } + } + + @Override + public List<Chapter> getChapters() { + if (item == null) { + return null; + } + return getItem().getChapters(); + } + + @Override + public String getWebsiteLink() { + if (item == null) { + return null; + } + return getItem().getLink(); + } + + @Override + public String getFeedTitle() { + if (item == null) { + return null; + } + return getItem().getFeed().getTitle(); + } + + @Override + public Object getIdentifier() { + return id; + } + + @Override + public String getLocalMediaUrl() { + return file_url; + } + + @Override + public String getStreamUrl() { + return download_url; + } + + @Override + public String getPaymentLink() { + if (item == null) { + return null; + } + return getItem().getPaymentLink(); + } + + @Override + public boolean localFileAvailable() { + return isDownloaded() && file_url != null; + } + + @Override + public boolean streamAvailable() { + return download_url != null; + } + + @Override + public void saveCurrentPosition(SharedPreferences pref, int newPosition) { + setPosition(newPosition); + DBWriter.setFeedMediaPlaybackInformation(ClientConfig.applicationCallbacks.getApplicationInstance(), this); + } + + @Override + public void onPlaybackStart() { + } + + @Override + public void onPlaybackCompleted() { + + } + + @Override + public int getPlayableType() { + return PLAYABLE_TYPE_FEEDMEDIA; + } + + @Override + public void setChapters(List<Chapter> chapters) { + getItem().setChapters(chapters); + } + + @Override + public Callable<String> loadShownotes() { + return new Callable<String>() { + @Override + public String call() throws Exception { + if (item == null) { + item = DBReader.getFeedItem( + ClientConfig.applicationCallbacks.getApplicationInstance(), itemID); + } + if (item.getContentEncoded() == null || item.getDescription() == null) { + DBReader.loadExtraInformationOfFeedItem( + ClientConfig.applicationCallbacks.getApplicationInstance(), item); + + } + return (item.getContentEncoded() != null) ? item.getContentEncoded() : item.getDescription(); + } + }; + } + + public static final Parcelable.Creator<FeedMedia> CREATOR = new Parcelable.Creator<FeedMedia>() { + public FeedMedia createFromParcel(Parcel in) { + final long id = in.readLong(); + final long itemID = in.readLong(); + FeedMedia result = new FeedMedia(id, null, in.readInt(), in.readInt(), in.readLong(), in.readString(), in.readString(), + in.readString(), in.readByte() != 0, new Date(in.readLong()), in.readInt()); + result.itemID = itemID; + return result; + } + + public FeedMedia[] newArray(int size) { + return new FeedMedia[size]; + } + }; + + @Override + public Uri getImageUri() { + final Uri feedImgUri = getFeedImageUri(); + + if (localFileAvailable()) { + Uri.Builder builder = new Uri.Builder(); + builder.scheme(SCHEME_MEDIA) + .encodedPath(getLocalMediaUrl()); + if (feedImgUri != null) { + builder.appendQueryParameter(PARAM_FALLBACK, feedImgUri.toString()); + } + return builder.build(); + } else if (item.hasItemImageDownloaded()) { + return item.getImage().getImageUri(); + } else { + return feedImgUri; + } + } + + private Uri getFeedImageUri() { + if (item != null && item.getFeed() != null) { + return item.getFeed().getImageUri(); + } else { + return null; + } + } +} 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 new file mode 100644 index 000000000..2f0304182 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedPreferences.java @@ -0,0 +1,89 @@ +package de.danoeh.antennapod.core.feed; + +import android.content.Context; +import de.danoeh.antennapod.core.storage.DBWriter; +import org.apache.commons.lang3.StringUtils; + +/** + * Contains preferences for a single feed. + */ +public class FeedPreferences { + + private long feedID; + private boolean autoDownload; + private String username; + private String password; + + public FeedPreferences(long feedID, boolean autoDownload, String username, String password) { + this.feedID = feedID; + this.autoDownload = autoDownload; + this.username = username; + this.password = password; + } + + + /** + * Compare another FeedPreferences with this one. The feedID and autoDownload attribute are excluded from the + * comparison. + * + * @return True if the two objects are different. + */ + public boolean compareWithOther(FeedPreferences other) { + if (other == null) + return true; + if (!StringUtils.equals(username, other.username)) { + return true; + } + if (!StringUtils.equals(password, other.password)) { + return true; + } + return false; + } + + /** + * Update this FeedPreferences object from another one. The feedID and autoDownload attributes are excluded + * from the update. + */ + public void updateFromOther(FeedPreferences other) { + if (other == null) + return; + this.username = other.username; + this.password = other.password; + } + + public long getFeedID() { + return feedID; + } + + public void setFeedID(long feedID) { + this.feedID = feedID; + } + + public boolean getAutoDownload() { + return autoDownload; + } + + public void setAutoDownload(boolean autoDownload) { + this.autoDownload = autoDownload; + } + + public void save(Context context) { + DBWriter.setFeedPreferences(context, this); + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java new file mode 100644 index 000000000..f0ff03a93 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/ID3Chapter.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.core.feed; + +public class ID3Chapter extends Chapter { + public static final int CHAPTERTYPE_ID3CHAPTER = 2; + + /** + * Identifies the chapter in its ID3 tag. This attribute does not have to be + * store in the DB and is only used for parsing. + */ + private String id3ID; + + public ID3Chapter(String id3ID, long start) { + super(start); + this.id3ID = id3ID; + } + + public ID3Chapter(long start, String title, FeedItem item, String link) { + super(start, title, item, link); + } + + @Override + public String toString() { + return "ID3Chapter [id3ID=" + id3ID + ", title=" + title + ", start=" + + start + ", url=" + link + "]"; + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_ID3CHAPTER; + } + + public String getId3ID() { + return id3ID; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java b/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java new file mode 100644 index 000000000..7b3cb829d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/MediaType.java @@ -0,0 +1,5 @@ +package de.danoeh.antennapod.core.feed; + +public enum MediaType { + AUDIO, VIDEO, UNKNOWN +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java new file mode 100644 index 000000000..9aa8d3170 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SearchResult.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.feed; + +public class SearchResult { + private FeedComponent component; + /** Additional information (e.g. where it was found) */ + private String subtitle; + /** Higher value means more importance */ + private int value; + + public SearchResult(FeedComponent component, int value, String subtitle) { + super(); + this.component = component; + this.value = value; + this.subtitle = subtitle; + } + + public FeedComponent getComponent() { + return component; + } + + public String getSubtitle() { + return subtitle; + } + + public void setSubtitle(String subtitle) { + this.subtitle = subtitle; + } + + public int getValue() { + return value; + } + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java new file mode 100644 index 000000000..2dadd3ec8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/SimpleChapter.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.feed; + +public class SimpleChapter extends Chapter { + public static final int CHAPTERTYPE_SIMPLECHAPTER = 0; + + public SimpleChapter(long start, String title, FeedItem item, String link) { + super(start, title, item, link); + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_SIMPLECHAPTER; + } + + public void updateFromOther(SimpleChapter other) { + super.updateFromOther(other); + start = other.start; + if (other.title != null) { + title = other.title; + } + if (other.link != null) { + link = other.link; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java new file mode 100644 index 000000000..5b54a2d59 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/VorbisCommentChapter.java @@ -0,0 +1,109 @@ +package de.danoeh.antennapod.core.feed; + +import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException; + +import java.util.concurrent.TimeUnit; + +public class VorbisCommentChapter extends Chapter { + public static final int CHAPTERTYPE_VORBISCOMMENT_CHAPTER = 3; + + private static final int CHAPTERXXX_LENGTH = "chapterxxx".length(); + + private int vorbisCommentId; + + public VorbisCommentChapter(int vorbisCommentId) { + this.vorbisCommentId = vorbisCommentId; + } + + public VorbisCommentChapter(long start, String title, FeedItem item, + String link) { + super(start, title, item, link); + } + + @Override + public String toString() { + return "VorbisCommentChapter [id=" + id + ", title=" + title + + ", link=" + link + ", start=" + start + "]"; + } + + public static long getStartTimeFromValue(String value) + throws VorbisCommentReaderException { + String[] parts = value.split(":"); + if (parts.length >= 3) { + try { + long hours = TimeUnit.MILLISECONDS.convert( + Long.parseLong(parts[0]), TimeUnit.HOURS); + long minutes = TimeUnit.MILLISECONDS.convert( + Long.parseLong(parts[1]), TimeUnit.MINUTES); + if (parts[2].contains("-->")) { + parts[2] = parts[2].substring(0, parts[2].indexOf("-->")); + } + long seconds = TimeUnit.MILLISECONDS.convert( + ((long) Float.parseFloat(parts[2])), TimeUnit.SECONDS); + return hours + minutes + seconds; + } catch (NumberFormatException e) { + throw new VorbisCommentReaderException(e); + } + } else { + throw new VorbisCommentReaderException("Invalid time string"); + } + } + + /** + * Return the id of a vorbiscomment chapter from a string like CHAPTERxxx* + * + * @return the id of the chapter key or -1 if the id couldn't be read. + * @throws VorbisCommentReaderException + * */ + public static int getIDFromKey(String key) + throws VorbisCommentReaderException { + if (key.length() >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx + try { + String strId = key.substring(8, 10); + return Integer.parseInt(strId); + } catch (NumberFormatException e) { + throw new VorbisCommentReaderException(e); + } + } + throw new VorbisCommentReaderException("key is too short (" + key + ")"); + } + + /** + * Get the string that comes after 'CHAPTERxxx', for example 'name' or + * 'url'. + */ + public static String getAttributeTypeFromKey(String key) { + if (key.length() > CHAPTERXXX_LENGTH) { + return key.substring(CHAPTERXXX_LENGTH, key.length()); + } + return null; + } + + @Override + public int getChapterType() { + return CHAPTERTYPE_VORBISCOMMENT_CHAPTER; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setLink(String link) { + this.link = link; + } + + public void setStart(long start) { + this.start = start; + } + + public int getVorbisCommentId() { + return vorbisCommentId; + } + + public void setVorbisCommentId(int vorbisCommentId) { + this.vorbisCommentId = vorbisCommentId; + } + + + +} 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 new file mode 100644 index 000000000..5ee40186f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetService.java @@ -0,0 +1,684 @@ +package de.danoeh.antennapod.core.gpoddernet; + +import com.squareup.okhttp.Credentials; +import com.squareup.okhttp.MediaType; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +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; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; + +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetDevice; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetPodcast; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetTag; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; + +/** + * Communicates with the gpodder.net service. + */ +public class GpodnetService { + + private static final String BASE_SCHEME = "https"; + + public static final String DEFAULT_BASE_HOST = "gpodder.net"; + private final String BASE_HOST; + + private static final MediaType TEXT = MediaType.parse("plain/text; charset=utf-8"); + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + private final OkHttpClient httpClient; + + + public GpodnetService() { + httpClient = AntennapodHttpClient.getHttpClient(); + BASE_HOST = GpodnetPreferences.getHostname(); + } + + /** + * Returns the [count] most used tags. + */ + public List<GpodnetTag> getTopTags(int count) + throws GpodnetServiceException { + URL url; + try { + url = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/api/2/tags/%d.json", count), null).toURL(); + } catch (MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + + Request.Builder request = new Request.Builder().url(url); + String response = executeRequest(request); + try { + JSONArray jsonTagList = new JSONArray(response); + List<GpodnetTag> tagList = new ArrayList<GpodnetTag>( + jsonTagList.length()); + for (int i = 0; i < jsonTagList.length(); i++) { + JSONObject jObj = jsonTagList.getJSONObject(i); + String name = jObj.getString("tag"); + int usage = jObj.getInt("usage"); + tagList.add(new GpodnetTag(name, usage)); + } + return tagList; + } catch (JSONException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Returns the [count] most subscribed podcasts for the given tag. + * + * @throws IllegalArgumentException if tag is null + */ + public List<GpodnetPodcast> getPodcastsForTag(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.getName(), count), null).toURL(); + Request.Builder request = new Request.Builder().url(url); + String response = executeRequest(request); + + JSONArray jsonArray = new JSONArray(response); + return readPodcastListFromJSONArray(jsonArray); + + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Returns the toplist of podcast. + * + * @param count of elements that should be returned. Must be in range 1..100. + * @throws IllegalArgumentException if count is out of range. + */ + public List<GpodnetPodcast> getPodcastToplist(int count) + throws GpodnetServiceException { + Validate.isTrue(count >= 1 && count <= 100, "Count must be in range 1..100"); + + try { + URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/toplist/%d.json", count), null).toURL(); + Request.Builder request = new Request.Builder().url(url); + String response = executeRequest(request); + + JSONArray jsonArray = new JSONArray(response); + return readPodcastListFromJSONArray(jsonArray); + + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Returns a list of suggested podcasts for the user that is currently + * logged in. + * <p/> + * This method requires authentication. + * + * @param count The + * number of elements that should be returned. Must be in range + * 1..100. + * @throws IllegalArgumentException if count is out of range. + * @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"); + + try { + URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/suggestions/%d.json", count), null).toURL(); + Request.Builder request = new Request.Builder().url(url); + String response = executeRequest(request); + + JSONArray jsonArray = new JSONArray(response); + return readPodcastListFromJSONArray(jsonArray); + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Searches the podcast directory for a given string. + * + * @param query The search query + * @param scaledLogoSize The size of the logos that are returned by the search query. + * Must be in range 1..256. If the value is out of range, the + * default value defined by the gpodder.net API will be used. + */ + public List<GpodnetPodcast> searchPodcasts(String query, int scaledLogoSize) + throws GpodnetServiceException { + String parameters = (scaledLogoSize > 0 && scaledLogoSize <= 256) ? String + .format("q=%s&scale_logo=%d", query, scaledLogoSize) : String + .format("q=%s", query); + try { + URL url = new URI(BASE_SCHEME, null, BASE_HOST, -1, "/search.json", + parameters, null).toURL(); + Request.Builder request = new Request.Builder().url(url); + String response = executeRequest(request); + + JSONArray jsonArray = new JSONArray(response); + return readPodcastListFromJSONArray(jsonArray); + + } catch (JSONException | MalformedURLException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } + } + + /** + * Returns all devices of a given user. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @throws IllegalArgumentException If username is null. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public List<GpodnetDevice> getDevices(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(); + Request.Builder request = new Request.Builder().url(url); + String response = executeRequest(request); + JSONArray devicesArray = new JSONArray(response); + return readDeviceListFromJSONArray(devicesArray); + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Configures the device of a given user. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @param deviceId The ID of the device that should be configured. + * @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) + 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(); + String content; + if (caption != null || type != null) { + JSONObject jsonContent = new JSONObject(); + if (caption != null) { + jsonContent.put("caption", caption); + } + if (type != null) { + jsonContent.put("type", type.toString()); + } + content = jsonContent.toString(); + } else { + content = ""; + } + RequestBody body = RequestBody.create(JSON, content); + Request.Builder request = new Request.Builder().post(body).url(url); + executeRequest(request); + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Returns the subscriptions of a specific device. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @param deviceId The ID of the device whose subscriptions should be returned. + * @return A list of subscriptions in OPML format. + * @throws IllegalArgumentException If username or deviceId is null. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public String getSubscriptionsOfDevice(String username, 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(); + Request.Builder request = new Request.Builder().url(url); + return executeRequest(request); + } catch (MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Returns all subscriptions of a specific user. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @return A list of subscriptions in OPML format. + * @throws IllegalArgumentException If username is null. + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public String getSubscriptionsOfUser(String username) + throws GpodnetServiceException { + Validate.notNull(username); + + try { + URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/subscriptions/%s.opml", username), null).toURL(); + Request.Builder request = new Request.Builder().url(url); + String response = executeRequest(request); + return response; + } catch (MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Uploads the subscriptions of a specific device. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @param deviceId The ID of the device whose subscriptions should be updated. + * @param subscriptions A list of feed URLs containing all subscriptions of the + * device. + * @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"); + } + try { + URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/subscriptions/%s/%s.txt", username, deviceId), null).toURL(); + StringBuilder builder = new StringBuilder(); + for (String s : subscriptions) { + builder.append(s); + builder.append("\n"); + } + RequestBody body = RequestBody.create(TEXT, builder.toString()); + Request.Builder request = new Request.Builder().put(body).url(url); + executeRequest(request); + } catch (MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** + * Updates the subscription list of a specific device. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @param deviceId The ID of the device whose subscriptions should be updated. + * @param added Collection of feed URLs of added feeds. This Collection MUST NOT contain any duplicates + * @param removed Collection of feed URLs of removed feeds. This Collection MUST NOT contain any duplicates + * @return a GpodnetUploadChangesResponse. See {@link de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse} + * for details. + * @throws java.lang.IllegalArgumentException if username, deviceId, added or removed is null. + * @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); + + try { + URL url = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/api/2/subscriptions/%s/%s.json", username, deviceId), null).toURL(); + + final JSONObject requestObject = new JSONObject(); + requestObject.put("add", new JSONArray(added)); + requestObject.put("remove", new JSONArray(removed)); + + RequestBody body = RequestBody.create(JSON, requestObject.toString()); + Request.Builder request = new Request.Builder().post(body).url(url); + + final String response = executeRequest(request); + return GpodnetUploadChangesResponse.fromJSONObject(response); + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + + } + + /** + * Returns all subscription changes of a specific device. + * <p/> + * This method requires authentication. + * + * @param username The username. Must be the same user as the one which is + * currently logged in. + * @param deviceId The ID of the device whose subscription changes should be + * downloaded. + * @param timestamp A timestamp that can be used to receive all changes since a + * specific point in time. + * @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); + + String params = String.format("since=%d", timestamp); + String path = String.format("/api/2/subscriptions/%s/%s.json", + username, deviceId); + try { + URL url = new URI(BASE_SCHEME, null, BASE_HOST, -1, path, params, + null).toURL(); + Request.Builder request = new Request.Builder().url(url); + + String response = executeRequest(request); + JSONObject changes = new JSONObject(response); + return readSubscriptionChangesFromJSONObject(changes); + } catch (URISyntaxException e) { + e.printStackTrace(); + throw new IllegalStateException(e); + } catch (JSONException | MalformedURLException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + + } + + /** + * Logs in a specific user. This method must be called if any of the methods + * that require authentication is used. + * + * @throws IllegalArgumentException If username or password is null. + */ + public void authenticate(String username, String password) + throws GpodnetServiceException { + Validate.notNull(username); + Validate.notNull(password); + + URL url; + try { + url = new URI(BASE_SCHEME, BASE_HOST, String.format( + "/api/2/auth/%s/login.json", username), null).toURL(); + } catch (MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + Request.Builder request = new Request.Builder().url(url).post(null); + executeRequestWithAuthentication(request, username, password); + } + + /** + * Shuts down the GpodnetService's HTTP client. The service will be shut down in a separate thread to avoid + * NetworkOnMainThreadExceptions. + */ + public void shutdown() { + new Thread() { + @Override + public void run() { + AntennapodHttpClient.cleanup(); + } + }.start(); + } + + private String executeRequest(Request.Builder requestB) + throws GpodnetServiceException { + Validate.notNull(requestB); + + Request request = requestB.header("User-Agent", ClientConfig.USER_AGENT).build(); + String responseString = null; + Response response = null; + ResponseBody body = null; + try { + + response = httpClient.newCall(request).execute(); + checkStatusCode(response); + body = response.body(); + responseString = getStringFromResponseBody(body); + } catch (IOException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } finally { + if (response != null && body != null) { + try { + body.close(); + } catch (IOException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + } + return responseString; + } + + private String executeRequestWithAuthentication(Request.Builder requestB, + String username, String password) throws GpodnetServiceException { + if (requestB == null || username == null || password == null) { + throw new IllegalArgumentException( + "request and credentials must not be null"); + } + + Request request = requestB.header("User-Agent", ClientConfig.USER_AGENT).build(); + String result = null; + ResponseBody body = null; + try { + String credential = Credentials.basic(username, password); + Request authRequest = request.newBuilder().header("Authorization", credential).build(); + Response response = httpClient.newCall(authRequest).execute(); + checkStatusCode(response); + body = response.body(); + result = getStringFromResponseBody(body); + } catch (ClientProtocolException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } catch (IOException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } finally { + if (body != null) { + try { + body.close(); + } catch (IOException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + } + return result; + } + + private String getStringFromResponseBody(ResponseBody body) + throws GpodnetServiceException { + Validate.notNull(body); + + ByteArrayOutputStream outputStream; + int contentLength = (int) body.contentLength(); + if (contentLength > 0) { + outputStream = new ByteArrayOutputStream(contentLength); + } else { + outputStream = new ByteArrayOutputStream(); + } + try { + byte[] buffer = new byte[8 * 1024]; + InputStream in = body.byteStream(); + int count; + while ((count = in.read(buffer)) > 0) { + outputStream.write(buffer, 0, count); + } + } catch (IOException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + return outputStream.toString(); + } + + private void checkStatusCode(Response response) + throws GpodnetServiceException { + Validate.notNull(response); + int responseCode = response.code(); + if (responseCode != HttpStatus.SC_OK) { + if (responseCode == HttpStatus.SC_UNAUTHORIZED) { + throw new GpodnetServiceAuthenticationException("Wrong username or password"); + } else { + throw new GpodnetServiceBadStatusCodeException( + "Bad response code: " + responseCode, responseCode); + } + } + } + + private List<GpodnetPodcast> readPodcastListFromJSONArray(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) + throws JSONException { + String url = object.getString("url"); + + String title; + Object titleObj = object.opt("title"); + if (titleObj != null && titleObj instanceof String) { + title = (String) titleObj; + } else { + title = url; + } + + String description; + Object descriptionObj = object.opt("description"); + if (descriptionObj != null && descriptionObj instanceof String) { + description = (String) descriptionObj; + } else { + description = ""; + } + + int subscribers = object.getInt("subscribers"); + + Object logoUrlObj = object.opt("logo_url"); + String logoUrl = (logoUrlObj instanceof String) ? (String) logoUrlObj + : null; + if (logoUrl == null) { + Object scaledLogoUrl = object.opt("scaled_logo_url"); + if (scaledLogoUrl != null && scaledLogoUrl instanceof String) { + logoUrl = (String) scaledLogoUrl; + } + } + + String website = null; + Object websiteObj = object.opt("website"); + if (websiteObj != null && websiteObj instanceof String) { + website = (String) websiteObj; + } + String mygpoLink = object.getString("mygpo_link"); + return new GpodnetPodcast(url, title, description, subscribers, + logoUrl, website, mygpoLink); + } + + private List<GpodnetDevice> readDeviceListFromJSONArray(JSONArray array) + throws JSONException { + Validate.notNull(array); + + List<GpodnetDevice> result = new ArrayList<GpodnetDevice>( + array.length()); + for (int i = 0; i < array.length(); i++) { + result.add(readDeviceFromJSONObject(array.getJSONObject(i))); + } + return result; + } + + private GpodnetDevice readDeviceFromJSONObject(JSONObject object) + throws JSONException { + String id = object.getString("id"); + String caption = object.getString("caption"); + String type = object.getString("type"); + int subscriptions = object.getInt("subscriptions"); + return new GpodnetDevice(id, caption, type, subscriptions); + } + + private GpodnetSubscriptionChange readSubscriptionChangesFromJSONObject( + JSONObject object) throws JSONException { + Validate.notNull(object); + + List<String> added = new LinkedList<String>(); + JSONArray jsonAdded = object.getJSONArray("add"); + for (int i = 0; i < jsonAdded.length(); i++) { + added.add(jsonAdded.getString(i)); + } + + List<String> removed = new LinkedList<String>(); + JSONArray jsonRemoved = object.getJSONArray("remove"); + for (int i = 0; i < jsonRemoved.length(); i++) { + removed.add(jsonRemoved.getString(i)); + } + + long timestamp = object.getLong("timestamp"); + return new GpodnetSubscriptionChange(added, removed, timestamp); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java new file mode 100644 index 000000000..8bd56218c --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceAuthenticationException.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.gpoddernet; + +public class GpodnetServiceAuthenticationException extends GpodnetServiceException { + + public GpodnetServiceAuthenticationException() { + super(); + } + + public GpodnetServiceAuthenticationException(String message, Throwable cause) { + super(message, cause); + } + + public GpodnetServiceAuthenticationException(String message) { + super(message); + } + + public GpodnetServiceAuthenticationException(Throwable cause) { + super(cause); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java new file mode 100644 index 000000000..16f01f0f4 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceBadStatusCodeException.java @@ -0,0 +1,12 @@ +package de.danoeh.antennapod.core.gpoddernet; + +public class GpodnetServiceBadStatusCodeException extends GpodnetServiceException { + int statusCode; + + public GpodnetServiceBadStatusCodeException(String message, int statusCode) { + super(message); + this.statusCode = statusCode; + } + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java new file mode 100644 index 000000000..ce704f7e3 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/GpodnetServiceException.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.gpoddernet; + +public class GpodnetServiceException extends Exception { + + public GpodnetServiceException() { + } + + public GpodnetServiceException(String message) { + super(message); + } + + public GpodnetServiceException(Throwable cause) { + super(cause); + } + + public GpodnetServiceException(String message, Throwable cause) { + super(message, cause); + } +} 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 new file mode 100644 index 000000000..4885a243a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetDevice.java @@ -0,0 +1,72 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +public class GpodnetDevice { + + private String id; + private String caption; + private DeviceType type; + private int subscriptions; + + public GpodnetDevice(String id, String caption, String type, + int subscriptions) { + Validate.notNull(id); + + this.id = id; + this.caption = caption; + this.type = DeviceType.fromString(type); + this.subscriptions = subscriptions; + } + + @Override + public String toString() { + return "GpodnetDevice [id=" + id + ", caption=" + caption + ", type=" + + type + ", subscriptions=" + subscriptions + "]"; + } + + public static enum DeviceType { + DESKTOP, LAPTOP, MOBILE, SERVER, OTHER; + + static DeviceType fromString(String s) { + if (s == null) { + return OTHER; + } + + if (s.equals("desktop")) { + return DESKTOP; + } else if (s.equals("laptop")) { + return LAPTOP; + } else if (s.equals("mobile")) { + return MOBILE; + } else if (s.equals("server")) { + return SERVER; + } else { + return OTHER; + } + } + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + + } + + public String getId() { + return id; + } + + public String getCaption() { + return caption; + } + + public DeviceType getType() { + return type; + } + + public int getSubscriptions() { + return subscriptions; + } + +} 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 new file mode 100644 index 000000000..afebf66ac --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetPodcast.java @@ -0,0 +1,65 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +public class GpodnetPodcast { + private String url; + private String title; + private String description; + private int subscribers; + private String logoUrl; + 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); + + this.url = url; + this.title = title; + this.description = description; + this.subscribers = subscribers; + this.logoUrl = logoUrl; + this.website = website; + this.mygpoLink = mygpoLink; + } + + @Override + public String toString() { + return "GpodnetPodcast [url=" + url + ", title=" + title + + ", description=" + description + ", subscribers=" + + subscribers + ", logoUrl=" + logoUrl + ", website=" + website + + ", mygpoLink=" + mygpoLink + "]"; + } + + public String getUrl() { + return url; + } + + public String getTitle() { + return title; + } + + public String getDescription() { + return description; + } + + public int getSubscribers() { + return subscribers; + } + + public String getLogoUrl() { + return logoUrl; + } + + public String getWebsite() { + return website; + } + + public String getMygpoLink() { + return mygpoLink; + } + +} 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 new file mode 100644 index 000000000..a5cb8c0f0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetSubscriptionChange.java @@ -0,0 +1,41 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +import java.util.List; + +public class GpodnetSubscriptionChange { + private List<String> added; + private List<String> removed; + private long timestamp; + + public GpodnetSubscriptionChange(List<String> added, List<String> removed, + long timestamp) { + Validate.notNull(added); + Validate.notNull(removed); + + this.added = added; + this.removed = removed; + this.timestamp = timestamp; + } + + @Override + public String toString() { + return "GpodnetSubscriptionChange [added=" + added.toString() + + ", removed=" + removed.toString() + ", timestamp=" + + timestamp + "]"; + } + + public List<String> getAdded() { + return added; + } + + public List<String> getRemoved() { + return removed; + } + + public long getTimestamp() { + return 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 new file mode 100644 index 000000000..7178f4be5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetTag.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.apache.commons.lang3.Validate; + +import java.util.Comparator; + +public class GpodnetTag { + + private String name; + private int usage; + + public GpodnetTag(String name, int usage) { + Validate.notNull(name); + + this.name = name; + this.usage = usage; + } + + public GpodnetTag(String name) { + super(); + this.name = name; + } + + @Override + public String toString() { + return "GpodnetTag [name=" + name + ", usage=" + usage + "]"; + } + + public String getName() { + return name; + } + + public int getUsage() { + return usage; + } + + public static class UsageComparator implements Comparator<GpodnetTag> { + + @Override + public int compare(GpodnetTag o1, GpodnetTag o2) { + return o1.usage - o2.usage; + } + + } + +} 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 new file mode 100644 index 000000000..5a37efa5e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/gpoddernet/model/GpodnetUploadChangesResponse.java @@ -0,0 +1,56 @@ +package de.danoeh.antennapod.core.gpoddernet.model; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +/** + * Object returned by {@link de.danoeh.antennapod.core.gpoddernet.GpodnetService} in uploadChanges method. + */ +public class GpodnetUploadChangesResponse { + + /** + * timestamp/ID that can be used for requesting changes since this upload. + */ + public final long timestamp; + + /** + * URLs that should be updated. The key of the map is the original URL, the value of the map + * is the sanitized URL. + */ + public final Map<String, String> updatedUrls; + + public GpodnetUploadChangesResponse(long timestamp, Map<String, String> updatedUrls) { + this.timestamp = timestamp; + this.updatedUrls = updatedUrls; + } + + /** + * Creates a new GpodnetUploadChangesResponse-object from a JSON object that was + * returned by an uploadChanges call. + * + * @throws org.json.JSONException If the method could not parse the JSONObject. + */ + 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>(); + JSONArray urls = object.getJSONArray("update_urls"); + for (int i = 0; i < urls.length(); i++) { + JSONArray urlPair = urls.getJSONArray(i); + updatedUrls.put(urlPair.getString(0), urlPair.getString(1)); + } + return new GpodnetUploadChangesResponse(timestamp, updatedUrls); + } + + @Override + public String toString() { + return "GpodnetUploadChangesResponse{" + + "timestamp=" + timestamp + + ", updatedUrls=" + updatedUrls + + '}'; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java new file mode 100644 index 000000000..8d0a4a842 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlElement.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.opml; + +/** Represents a single feed in an OPML file. */ +public class OpmlElement { + private String text; + private String xmlUrl; + private String htmlUrl; + private String type; + + public OpmlElement() { + + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public String getXmlUrl() { + return xmlUrl; + } + + public void setXmlUrl(String xmlUrl) { + this.xmlUrl = xmlUrl; + } + + public String getHtmlUrl() { + return htmlUrl; + } + + public void setHtmlUrl(String htmlUrl) { + this.htmlUrl = htmlUrl; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java new file mode 100644 index 000000000..775129d09 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlReader.java @@ -0,0 +1,87 @@ +package de.danoeh.antennapod.core.opml; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.IOException; +import java.io.Reader; +import java.util.ArrayList; + +/** Reads OPML documents. */ +public class OpmlReader { + private static final String TAG = "OpmlReader"; + + // ATTRIBUTES + private boolean isInOpml = false; + private ArrayList<OpmlElement> elementList; + + /** + * Reads an Opml document and returns a list of all OPML elements it can + * find + * + * @throws IOException + * @throws XmlPullParserException + */ + public ArrayList<OpmlElement> readDocument(Reader reader) + throws XmlPullParserException, IOException { + elementList = new ArrayList<OpmlElement>(); + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + XmlPullParser xpp = factory.newPullParser(); + xpp.setInput(reader); + int eventType = xpp.getEventType(); + + while (eventType != XmlPullParser.END_DOCUMENT) { + switch (eventType) { + case XmlPullParser.START_DOCUMENT: + if (BuildConfig.DEBUG) + Log.d(TAG, "Reached beginning of document"); + break; + case XmlPullParser.START_TAG: + if (xpp.getName().equals(OpmlSymbols.OPML)) { + isInOpml = true; + if (BuildConfig.DEBUG) + Log.d(TAG, "Reached beginning of OPML tree."); + } else if (isInOpml && xpp.getName().equals(OpmlSymbols.OUTLINE)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Found new Opml element"); + OpmlElement element = new OpmlElement(); + + final String title = xpp.getAttributeValue(null, OpmlSymbols.TITLE); + if (title != null) { + Log.i(TAG, "Using title: " + title); + element.setText(title); + } else { + Log.i(TAG, "Title not found, using text"); + element.setText(xpp.getAttributeValue(null, OpmlSymbols.TEXT)); + } + element.setXmlUrl(xpp.getAttributeValue(null, OpmlSymbols.XMLURL)); + element.setHtmlUrl(xpp.getAttributeValue(null, OpmlSymbols.HTMLURL)); + element.setType(xpp.getAttributeValue(null, OpmlSymbols.TYPE)); + if (element.getXmlUrl() != null) { + if (element.getText() == null) { + Log.i(TAG, "Opml element has no text attribute."); + element.setText(element.getXmlUrl()); + } + elementList.add(element); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Skipping element because of missing xml url"); + } + } + break; + } + eventType = xpp.next(); + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "Parsing finished."); + + return elementList; + } + +} 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 new file mode 100644 index 000000000..2b831ca2a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlSymbols.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.opml; + +/** Contains symbols for reading and writing OPML documents. */ +public final class OpmlSymbols { + + public static final String OPML = "opml"; + public static final String BODY = "body"; + public static final String OUTLINE = "outline"; + public static final String TEXT = "text"; + public static final String XMLURL = "xmlUrl"; + public static final String HTMLURL = "htmlUrl"; + public static final String TYPE = "type"; + public static final String VERSION = "version"; + public static final String HEAD = "head"; + public static final String TITLE = "title"; + + 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 new file mode 100644 index 000000000..641190f62 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/opml/OpmlWriter.java @@ -0,0 +1,65 @@ +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.List; + +/** Writes OPML documents. */ +public class OpmlWriter { + private static final String TAG = "OpmlWriter"; + private static final String ENCODING = "UTF-8"; + private static final String OPML_VERSION = "2.0"; + private static final String OPML_TITLE = "AntennaPod Subscriptions"; + + /** + * Takes a list of feeds and a writer and writes those into an OPML + * document. + * + * @throws IOException + * @throws IllegalStateException + * @throws IllegalArgumentException + */ + public void writeDocument(List<Feed> feeds, Writer writer) + throws IllegalArgumentException, IllegalStateException, IOException { + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting to write document"); + XmlSerializer xs = Xml.newSerializer(); + xs.setOutput(writer); + + xs.startDocument(ENCODING, false); + xs.startTag(null, OpmlSymbols.OPML); + xs.attribute(null, OpmlSymbols.VERSION, OPML_VERSION); + + xs.startTag(null, OpmlSymbols.HEAD); + xs.startTag(null, OpmlSymbols.TITLE); + xs.text(OPML_TITLE); + xs.endTag(null, OpmlSymbols.TITLE); + xs.endTag(null, OpmlSymbols.HEAD); + + xs.startTag(null, OpmlSymbols.BODY); + for (Feed feed : feeds) { + xs.startTag(null, OpmlSymbols.OUTLINE); + xs.attribute(null, OpmlSymbols.TEXT, feed.getTitle()); + xs.attribute(null, OpmlSymbols.TITLE, feed.getTitle()); + if (feed.getType() != null) { + xs.attribute(null, OpmlSymbols.TYPE, feed.getType()); + } + xs.attribute(null, OpmlSymbols.XMLURL, feed.getDownload_url()); + if (feed.getLink() != null) { + xs.attribute(null, OpmlSymbols.HTMLURL, feed.getLink()); + } + xs.endTag(null, OpmlSymbols.OUTLINE); + } + xs.endTag(null, OpmlSymbols.BODY); + xs.endTag(null, OpmlSymbols.OPML); + xs.endDocument(); + if (BuildConfig.DEBUG) + 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 new file mode 100644 index 000000000..af04df017 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/GpodnetPreferences.java @@ -0,0 +1,247 @@ +package de.danoeh.antennapod.core.preferences; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.locks.ReentrantLock; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.service.GpodnetSyncService; + +/** + * Manages preferences for accessing gpodder.net service + */ +public class GpodnetPreferences { + + private static final String TAG = "GpodnetPreferences"; + + private static final String PREF_NAME = "gpodder.net"; + public static final String PREF_GPODNET_USERNAME = "de.danoeh.antennapod.preferences.gpoddernet.username"; + public static final String PREF_GPODNET_PASSWORD = "de.danoeh.antennapod.preferences.gpoddernet.password"; + public static final String PREF_GPODNET_DEVICEID = "de.danoeh.antennapod.preferences.gpoddernet.deviceID"; + public static final String PREF_GPODNET_HOSTNAME = "prefGpodnetHostname"; + + + public static final String PREF_LAST_SYNC_TIMESTAMP = "de.danoeh.antennapod.preferences.gpoddernet.last_sync_timestamp"; + public static final String PREF_SYNC_ADDED = "de.danoeh.antennapod.preferences.gpoddernet.sync_added"; + public static final String PREF_SYNC_REMOVED = "de.danoeh.antennapod.preferences.gpoddernet.sync_removed"; + + private static String username; + private static String password; + private static String deviceID; + private static String hostname; + + private static ReentrantLock feedListLock = new ReentrantLock(); + private static Set<String> addedFeeds; + private static Set<String> removedFeeds; + + /** + * Last value returned by getSubscriptionChanges call. Will be used for all subsequent calls of getSubscriptionChanges. + */ + private static long lastSyncTimestamp; + + private static boolean preferencesLoaded = false; + + private static SharedPreferences getPreferences() { + return ClientConfig.applicationCallbacks.getApplicationInstance().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE); + } + + private static synchronized void ensurePreferencesLoaded() { + if (!preferencesLoaded) { + SharedPreferences prefs = getPreferences(); + username = prefs.getString(PREF_GPODNET_USERNAME, null); + password = prefs.getString(PREF_GPODNET_PASSWORD, null); + deviceID = prefs.getString(PREF_GPODNET_DEVICEID, null); + lastSyncTimestamp = prefs.getLong(PREF_LAST_SYNC_TIMESTAMP, 0); + addedFeeds = readListFromString(prefs.getString(PREF_SYNC_ADDED, "")); + removedFeeds = readListFromString(prefs.getString(PREF_SYNC_REMOVED, "")); + hostname = checkGpodnetHostname(prefs.getString(PREF_GPODNET_HOSTNAME, GpodnetService.DEFAULT_BASE_HOST)); + + preferencesLoaded = true; + } + } + + private static void writePreference(String key, String value) { + SharedPreferences.Editor editor = getPreferences().edit(); + editor.putString(key, value); + editor.commit(); + } + + private static void writePreference(String key, long value) { + SharedPreferences.Editor editor = getPreferences().edit(); + editor.putLong(key, value); + editor.commit(); + } + + private static void writePreference(String key, Collection<String> value) { + SharedPreferences.Editor editor = getPreferences().edit(); + editor.putString(key, writeListToString(value)); + editor.commit(); + } + + public static String getUsername() { + ensurePreferencesLoaded(); + return username; + } + + public static void setUsername(String username) { + GpodnetPreferences.username = username; + writePreference(PREF_GPODNET_USERNAME, username); + } + + public static String getPassword() { + ensurePreferencesLoaded(); + return password; + } + + public static void setPassword(String password) { + GpodnetPreferences.password = password; + writePreference(PREF_GPODNET_PASSWORD, password); + } + + public static String getDeviceID() { + ensurePreferencesLoaded(); + return deviceID; + } + + public static void setDeviceID(String deviceID) { + GpodnetPreferences.deviceID = deviceID; + writePreference(PREF_GPODNET_DEVICEID, deviceID); + } + + public static long getLastSyncTimestamp() { + ensurePreferencesLoaded(); + return lastSyncTimestamp; + } + + public static void setLastSyncTimestamp(long lastSyncTimestamp) { + GpodnetPreferences.lastSyncTimestamp = lastSyncTimestamp; + writePreference(PREF_LAST_SYNC_TIMESTAMP, lastSyncTimestamp); + } + + public static String getHostname() { + ensurePreferencesLoaded(); + return hostname; + } + + public static void setHostname(String value) { + value = checkGpodnetHostname(value); + if (!value.equals(hostname)) { + logout(); + writePreference(PREF_GPODNET_HOSTNAME, value); + hostname = value; + } + } + + public static void addAddedFeed(String feed) { + ensurePreferencesLoaded(); + feedListLock.lock(); + if (addedFeeds.add(feed)) { + writePreference(PREF_SYNC_ADDED, addedFeeds); + } + if (removedFeeds.remove(feed)) { + writePreference(PREF_SYNC_REMOVED, removedFeeds); + } + feedListLock.unlock(); + GpodnetSyncService.sendSyncIntent(ClientConfig.applicationCallbacks.getApplicationInstance()); + } + + public static void addRemovedFeed(String feed) { + ensurePreferencesLoaded(); + feedListLock.lock(); + if (removedFeeds.add(feed)) { + writePreference(PREF_SYNC_REMOVED, removedFeeds); + } + if (addedFeeds.remove(feed)) { + writePreference(PREF_SYNC_ADDED, addedFeeds); + } + feedListLock.unlock(); + GpodnetSyncService.sendSyncIntent(ClientConfig.applicationCallbacks.getApplicationInstance()); + } + + public static Set<String> getAddedFeedsCopy() { + ensurePreferencesLoaded(); + Set<String> copy = new HashSet<String>(); + feedListLock.lock(); + copy.addAll(addedFeeds); + feedListLock.unlock(); + return copy; + } + + public static void removeAddedFeeds(Collection<String> removed) { + ensurePreferencesLoaded(); + feedListLock.lock(); + addedFeeds.removeAll(removed); + writePreference(PREF_SYNC_ADDED, addedFeeds); + feedListLock.unlock(); + } + + public static Set<String> getRemovedFeedsCopy() { + ensurePreferencesLoaded(); + Set<String> copy = new HashSet<String>(); + feedListLock.lock(); + copy.addAll(removedFeeds); + feedListLock.unlock(); + return copy; + } + + public static void removeRemovedFeeds(Collection<String> removed) { + ensurePreferencesLoaded(); + removedFeeds.removeAll(removed); + writePreference(PREF_SYNC_REMOVED, removedFeeds); + + } + + /** + * Returns true if device ID, username and password have a non-null value + */ + public static boolean loggedIn() { + ensurePreferencesLoaded(); + return deviceID != null && username != null && password != null; + } + + public static synchronized void logout() { + if (BuildConfig.DEBUG) Log.d(TAG, "Logout: Clearing preferences"); + setUsername(null); + setPassword(null); + setDeviceID(null); + addedFeeds.clear(); + writePreference(PREF_SYNC_ADDED, addedFeeds); + removedFeeds.clear(); + writePreference(PREF_SYNC_REMOVED, removedFeeds); + setLastSyncTimestamp(0); + } + + private static Set<String> readListFromString(String s) { + Set<String> result = new HashSet<String>(); + for (String item : s.split(" ")) { + result.add(item); + } + return result; + } + + private static String writeListToString(Collection<String> c) { + StringBuilder result = new StringBuilder(); + for (String item : c) { + result.append(item); + result.append(" "); + } + return result.toString().trim(); + } + + private static String checkGpodnetHostname(String value) { + int startIndex = 0; + if (value.startsWith("http://")) { + startIndex = "http://".length(); + } else if (value.startsWith("https://")) { + startIndex = "https://".length(); + } + return value.substring(startIndex); + } +} 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 new file mode 100644 index 000000000..d88543f73 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java @@ -0,0 +1,146 @@ +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; + +/** + * Provides access to preferences set by the playback service. A private + * 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"; + + /** Value of PREF_CURRENTLY_PLAYING_MEDIA if no media is playing. */ + public static final long NO_MEDIA_PLAYING = -1; + + private long currentlyPlayingFeedId; + private long currentlyPlayingFeedMediaId; + private long currentlyPlayingMedia; + private boolean currentEpisodeIsStream; + private boolean currentEpisodeIsVideo; + + private static PlaybackPreferences instance; + private Context context; + + private PlaybackPreferences(Context context) { + this.context = context; + loadPreferences(); + } + + /** + * 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 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); + } + + @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); + } + } + + 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; + } + + public static long getCurrentlyPlayingMedia() { + instanceAvailable(); + return instance.currentlyPlayingMedia; + } + + public static long getCurrentlyPlayingFeedMediaId() { + return instance.currentlyPlayingFeedMediaId; + } + + public static boolean getCurrentEpisodeIsStream() { + instanceAvailable(); + return instance.currentEpisodeIsStream; + } + + public static boolean getCurrentEpisodeIsVideo() { + instanceAvailable(); + return instance.currentEpisodeIsVideo; + } + +} 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 new file mode 100644 index 000000000..f4d44c4da --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -0,0 +1,607 @@ +package de.danoeh.antennapod.core.preferences; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.json.JSONArray; +import org.json.JSONException; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.receiver.FeedUpdateReceiver; + +/** + * 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 + * when called. + */ +public class UserPreferences implements + SharedPreferences.OnSharedPreferenceChangeListener { + public static final String IMPORT_DIR = "import/"; + private static final String TAG = "UserPreferences"; + + public static final String PREF_PAUSE_ON_HEADSET_DISCONNECT = "prefPauseOnHeadsetDisconnect"; + public static final String PREF_FOLLOW_QUEUE = "prefFollowQueue"; + public static final String PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY = "prefDownloadMediaOnWifiOnly"; + public static final String PREF_UPDATE_INTERVAL = "prefAutoUpdateIntervall"; + public static final String PREF_MOBILE_UPDATE = "prefMobileUpdate"; + public static final String PREF_DISPLAY_ONLY_EPISODES = "prefDisplayOnlyEpisodes"; + public static final String PREF_AUTO_DELETE = "prefAutoDelete"; + public static final String PREF_AUTO_FLATTR = "pref_auto_flattr"; + public static final String PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD = "prefAutoFlattrPlayedDurationThreshold"; + public static final String PREF_THEME = "prefTheme"; + public static final String PREF_DATA_FOLDER = "prefDataFolder"; + public static final String PREF_ENABLE_AUTODL = "prefEnableAutoDl"; + public static final String PREF_ENABLE_AUTODL_WIFI_FILTER = "prefEnableAutoDownloadWifiFilter"; + private static final String PREF_AUTODL_SELECTED_NETWORKS = "prefAutodownloadSelectedNetworks"; + public static final String PREF_EPISODE_CACHE_SIZE = "prefEpisodeCacheSize"; + private static final String PREF_PLAYBACK_SPEED = "prefPlaybackSpeed"; + private static final String PREF_PLAYBACK_SPEED_ARRAY = "prefPlaybackSpeedArray"; + public static final String PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS = "prefPauseForFocusLoss"; + private static final String PREF_SEEK_DELTA_SECS = "prefSeekDeltaSecs"; + private static final String PREF_EXPANDED_NOTIFICATION = "prefExpandNotify"; + private static final String PREF_PERSISTENT_NOTIFICATION = "prefPersistNotify"; + + // TODO: Make this value configurable + private static final float PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT = 0.8f; + + private static int EPISODE_CACHE_SIZE_UNLIMITED = -1; + + private static UserPreferences instance; + private final Context context; + + // Preferences + private boolean pauseOnHeadsetDisconnect; + private boolean followQueue; + private boolean downloadMediaOnWifiOnly; + private long updateInterval; + private boolean allowMobileUpdate; + private boolean displayOnlyEpisodes; + private boolean autoDelete; + private boolean autoFlattr; + private float autoFlattrPlayedDurationThreshold; + private int theme; + private boolean enableAutodownload; + private boolean enableAutodownloadWifiFilter; + private String[] autodownloadSelectedNetworks; + private int episodeCacheSize; + private String playbackSpeed; + private String[] playbackSpeedArray; + private boolean pauseForFocusLoss; + private int seekDeltaSecs; + private boolean isFreshInstall; + private int notifyPriority; + private boolean persistNotify; + + private UserPreferences(Context context) { + this.context = context; + loadPreferences(); + } + + /** + * 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 UserPreferences(context); + + createImportDirectory(); + createNoMediaFile(); + PreferenceManager.getDefaultSharedPreferences(context) + .registerOnSharedPreferenceChangeListener(instance); + + } + + private void loadPreferences() { + SharedPreferences sp = PreferenceManager + .getDefaultSharedPreferences(context); + EPISODE_CACHE_SIZE_UNLIMITED = context.getResources().getInteger( + R.integer.episode_cache_size_unlimited); + pauseOnHeadsetDisconnect = sp.getBoolean( + PREF_PAUSE_ON_HEADSET_DISCONNECT, true); + followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false); + downloadMediaOnWifiOnly = sp.getBoolean( + PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true); + updateInterval = readUpdateInterval(sp.getString(PREF_UPDATE_INTERVAL, + "0")); + allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); + displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, false); + autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); + autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); + autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, + PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT); + theme = readThemeValue(sp.getString(PREF_THEME, "0")); + enableAutodownloadWifiFilter = sp.getBoolean( + PREF_ENABLE_AUTODL_WIFI_FILTER, false); + autodownloadSelectedNetworks = StringUtils.split( + sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); + episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString( + PREF_EPISODE_CACHE_SIZE, "20")); + enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false); + playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0"); + playbackSpeedArray = readPlaybackSpeedArray(sp.getString( + PREF_PLAYBACK_SPEED_ARRAY, null)); + pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); + seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30")); + if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) { + notifyPriority = NotificationCompat.PRIORITY_MAX; + } + else { + notifyPriority = NotificationCompat.PRIORITY_DEFAULT; + } + persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, 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"); + } + } + + public static boolean isPauseOnHeadsetDisconnect() { + instanceAvailable(); + return instance.pauseOnHeadsetDisconnect; + } + + public static boolean isFollowQueue() { + instanceAvailable(); + return instance.followQueue; + } + + public static boolean isDownloadMediaOnWifiOnly() { + instanceAvailable(); + return instance.downloadMediaOnWifiOnly; + } + + public static long getUpdateInterval() { + instanceAvailable(); + return instance.updateInterval; + } + + public static boolean isAllowMobileUpdate() { + instanceAvailable(); + return instance.allowMobileUpdate; + } + + public static boolean isDisplayOnlyEpisodes() { + instanceAvailable(); + //return instance.displayOnlyEpisodes; + return false; + } + + public static boolean isAutoDelete() { + instanceAvailable(); + return instance.autoDelete; + } + + public static boolean isAutoFlattr() { + instanceAvailable(); + return instance.autoFlattr; + } + + public static int getNotifyPriority() { + instanceAvailable(); + return instance.notifyPriority; + } + + public static boolean isPersistNotify() { + instanceAvailable(); + return instance.persistNotify; + } + + + /** + * Returns the time after which an episode should be auto-flattr'd in percent of the episode's + * duration. + */ + public static float getAutoFlattrPlayedDurationThreshold() { + instanceAvailable(); + return instance.autoFlattrPlayedDurationThreshold; + } + + public static int getTheme() { + instanceAvailable(); + return instance.theme; + } + + public static boolean isEnableAutodownloadWifiFilter() { + instanceAvailable(); + return instance.enableAutodownloadWifiFilter; + } + + public static String[] getAutodownloadSelectedNetworks() { + instanceAvailable(); + return instance.autodownloadSelectedNetworks; + } + + public static int getEpisodeCacheSizeUnlimited() { + return EPISODE_CACHE_SIZE_UNLIMITED; + } + + public static String getPlaybackSpeed() { + instanceAvailable(); + return instance.playbackSpeed; + } + + public static String[] getPlaybackSpeedArray() { + instanceAvailable(); + return instance.playbackSpeedArray; + } + + public static int getSeekDeltaMs() { + instanceAvailable(); + return 1000 * instance.seekDeltaSecs; + } + + /** + * Returns the capacity of the episode cache. This method will return the + * negative integer EPISODE_CACHE_SIZE_UNLIMITED if the cache size is set to + * 'unlimited'. + */ + public static int getEpisodeCacheSize() { + instanceAvailable(); + return instance.episodeCacheSize; + } + + public static boolean isEnableAutodownload() { + instanceAvailable(); + return instance.enableAutodownload; + } + + public static boolean shouldPauseForFocusLoss() { + instanceAvailable(); + return instance.pauseForFocusLoss; + } + + public static boolean isFreshInstall() { + instanceAvailable(); + return instance.isFreshInstall; + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sp, String key) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Registered change of user preferences. Key: " + key); + + if (key.equals(PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY)) { + downloadMediaOnWifiOnly = sp.getBoolean( + PREF_DOWNLOAD_MEDIA_ON_WIFI_ONLY, true); + + } else if (key.equals(PREF_MOBILE_UPDATE)) { + allowMobileUpdate = sp.getBoolean(PREF_MOBILE_UPDATE, false); + + } else if (key.equals(PREF_FOLLOW_QUEUE)) { + followQueue = sp.getBoolean(PREF_FOLLOW_QUEUE, false); + + } else if (key.equals(PREF_UPDATE_INTERVAL)) { + updateInterval = readUpdateInterval(sp.getString( + PREF_UPDATE_INTERVAL, "0")); + ClientConfig.applicationCallbacks.setUpdateInterval(updateInterval); + + } else if (key.equals(PREF_AUTO_DELETE)) { + autoDelete = sp.getBoolean(PREF_AUTO_DELETE, false); + + } else if (key.equals(PREF_AUTO_FLATTR)) { + autoFlattr = sp.getBoolean(PREF_AUTO_FLATTR, false); + } else if (key.equals(PREF_DISPLAY_ONLY_EPISODES)) { + displayOnlyEpisodes = sp.getBoolean(PREF_DISPLAY_ONLY_EPISODES, + false); + } else if (key.equals(PREF_THEME)) { + theme = readThemeValue(sp.getString(PREF_THEME, "")); + } else if (key.equals(PREF_ENABLE_AUTODL_WIFI_FILTER)) { + enableAutodownloadWifiFilter = sp.getBoolean( + PREF_ENABLE_AUTODL_WIFI_FILTER, false); + } else if (key.equals(PREF_AUTODL_SELECTED_NETWORKS)) { + autodownloadSelectedNetworks = StringUtils.split( + sp.getString(PREF_AUTODL_SELECTED_NETWORKS, ""), ','); + } else if (key.equals(PREF_EPISODE_CACHE_SIZE)) { + episodeCacheSize = readEpisodeCacheSizeInternal(sp.getString( + PREF_EPISODE_CACHE_SIZE, "20")); + } else if (key.equals(PREF_ENABLE_AUTODL)) { + enableAutodownload = sp.getBoolean(PREF_ENABLE_AUTODL, false); + } else if (key.equals(PREF_PLAYBACK_SPEED)) { + playbackSpeed = sp.getString(PREF_PLAYBACK_SPEED, "1.0"); + } else if (key.equals(PREF_PLAYBACK_SPEED_ARRAY)) { + playbackSpeedArray = readPlaybackSpeedArray(sp.getString( + PREF_PLAYBACK_SPEED_ARRAY, null)); + } else if (key.equals(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS)) { + pauseForFocusLoss = sp.getBoolean(PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, false); + } else if (key.equals(PREF_SEEK_DELTA_SECS)) { + seekDeltaSecs = Integer.valueOf(sp.getString(PREF_SEEK_DELTA_SECS, "30")); + } else if (key.equals(PREF_PAUSE_ON_HEADSET_DISCONNECT)) { + pauseOnHeadsetDisconnect = sp.getBoolean(PREF_PAUSE_ON_HEADSET_DISCONNECT, true); + } else if (key.equals(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD)) { + autoFlattrPlayedDurationThreshold = sp.getFloat(PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD, + PREF_AUTO_FLATTR_PLAYED_DURATION_THRESHOLD_DEFAULT); + } else if (key.equals(PREF_EXPANDED_NOTIFICATION)) { + if (sp.getBoolean(PREF_EXPANDED_NOTIFICATION, false)) { + notifyPriority = NotificationCompat.PRIORITY_MAX; + } + else { + notifyPriority = NotificationCompat.PRIORITY_DEFAULT; + } + } else if (key.equals(PREF_PERSISTENT_NOTIFICATION)) { + persistNotify = sp.getBoolean(PREF_PERSISTENT_NOTIFICATION, false); + } + } + + public static void setPlaybackSpeed(String speed) { + PreferenceManager.getDefaultSharedPreferences(instance.context).edit() + .putString(PREF_PLAYBACK_SPEED, speed).apply(); + } + + public static void setPlaybackSpeedArray(String[] speeds) { + JSONArray jsonArray = new JSONArray(); + for (String speed : speeds) { + jsonArray.put(speed); + } + PreferenceManager.getDefaultSharedPreferences(instance.context).edit() + .putString(PREF_PLAYBACK_SPEED_ARRAY, jsonArray.toString()) + .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. Should only be used for testing purposes! + */ + 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; + } + + /** + * 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; + } + + /** + * Return the folder where the app stores all of its data. This method will + * return the standard data folder if none has been set by the user. + * + * @param type The name of the folder inside the data folder. May be null + * when accessing the root of the data folder. + * @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()); + String strDir = prefs.getString(PREF_DATA_FOLDER, null); + if (strDir == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Using default data folder"); + return context.getExternalFilesDir(type); + } else { + File dataDir = new File(strDir); + if (!dataDir.exists()) { + if (!dataDir.mkdir()) { + Log.w(TAG, "Could not create data folder"); + return null; + } + } + + if (type == null) { + return dataDir; + } else { + // handle path separators + String[] dirs = type.split("/"); + for (int i = 0; i < dirs.length; i++) { + if (dirs.length > 0) { + if (i < dirs.length - 1) { + dataDir = getDataFolder(context, dirs[i]); + if (dataDir == null) { + return null; + } + } + type = dirs[i]; + } + } + File typeDir = new File(dataDir, type); + if (!typeDir.exists()) { + if (dataDir.canWrite()) { + if (!typeDir.mkdir()) { + Log.e(TAG, "Could not create data folder named " + + type); + return null; + } + } + } + return typeDir; + } + + } + } + + public static void setDataFolder(String dir) { + if (BuildConfig.DEBUG) + 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(); + createImportDirectory(); + } + + /** + * Create a .nomedia file to prevent scanning by the media scanner. + */ + private static void createNoMediaFile() { + File f = new File(instance.context.getExternalFilesDir(null), + ".nomedia"); + if (!f.exists()) { + try { + f.createNewFile(); + } catch (IOException e) { + Log.e(TAG, "Could not create .nomedia file"); + e.printStackTrace(); + } + if (BuildConfig.DEBUG) + Log.d(TAG, ".nomedia file created"); + } + } + + /** + * Creates the import directory if it doesn't exist and if storage is + * available + */ + private static void createImportDirectory() { + File importDir = getDataFolder(instance.context, + IMPORT_DIR); + if (importDir != null) { + if (importDir.exists()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Import directory already exists"); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Creating import directory"); + importDir.mkdir(); + } + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Could not access external storage."); + } + } + + /** + * Updates alarm registered with the AlarmManager service or deactivates it. + */ + public static void restartUpdateAlarm(long triggerAtMillis, long intervalMillis) { + instanceAvailable(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Restarting update alarm."); + AlarmManager alarmManager = (AlarmManager) instance.context + .getSystemService(Context.ALARM_SERVICE); + PendingIntent updateIntent = PendingIntent.getBroadcast( + instance.context, 0, new Intent( + FeedUpdateReceiver.ACTION_REFRESH_FEEDS), 0); + alarmManager.cancel(updateIntent); + if (intervalMillis != 0) { + alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, triggerAtMillis, intervalMillis, + updateIntent); + if (BuildConfig.DEBUG) + Log.d(TAG, "Changed alarm to new interval"); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Automatic update was deactivated"); + } + } + + + /** + * 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); + } +} 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 new file mode 100644 index 000000000..84277b6d5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/AlarmUpdateReceiver.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +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.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"); + } + + ClientConfig.applicationCallbacks.setUpdateInterval(UserPreferences.getUpdateInterval()); + + } + +} 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 new file mode 100644 index 000000000..6ce30763d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/FeedUpdateReceiver.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.storage.DBTasks; + +/** Refreshes all feeds when it receives an intent */ +public class FeedUpdateReceiver extends BroadcastReceiver { + private static final String TAG = "FeedUpdateReceiver"; + public static final String ACTION_REFRESH_FEEDS = "de.danoeh.antennapod.feedupdatereceiver.refreshFeeds"; + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ACTION_REFRESH_FEEDS)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received intent"); + boolean mobileUpdate = UserPreferences.isAllowMobileUpdate(); + if (mobileUpdate || connectedToWifi(context)) { + DBTasks.refreshExpiredFeeds(context); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Blocking automatic update: no wifi available / no mobile updates allowed"); + } + } + } + + private boolean connectedToWifi(Context context) { + ConnectivityManager connManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo mWifi = connManager + .getNetworkInfo(ConnectivityManager.TYPE_WIFI); + + return mWifi.isConnected(); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java new file mode 100644 index 000000000..a900248d2 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/MediaButtonReceiver.java @@ -0,0 +1,32 @@ +package de.danoeh.antennapod.core.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.KeyEvent; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.service.playback.PlaybackService; + +/** Receives media button events. */ +public class MediaButtonReceiver extends BroadcastReceiver { + private static final String TAG = "MediaButtonReceiver"; + public static final String EXTRA_KEYCODE = "de.danoeh.antennapod.core.service.extra.MediaButtonReceiver.KEYCODE"; + + public static final String NOTIFY_BUTTON_RECEIVER = "de.danoeh.antennapod.NOTIFY_BUTTON_RECEIVER"; + + @Override + public void onReceive(Context context, Intent intent) { + if (BuildConfig.DEBUG) Log.d(TAG, "Received intent"); + KeyEvent event = (KeyEvent) intent.getExtras().get( + Intent.EXTRA_KEY_EVENT); + if (event.getAction() == KeyEvent.ACTION_DOWN) { + Intent serviceIntent = new Intent(context, PlaybackService.class); + int keycode = event.getKeyCode(); + serviceIntent.putExtra(EXTRA_KEYCODE, keycode); + context.startService(serviceIntent); + } + + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java new file mode 100644 index 000000000..0f2a81dfb --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/GpodnetSyncService.java @@ -0,0 +1,251 @@ +package de.danoeh.antennapod.core.service; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.IBinder; +import android.support.v4.app.NotificationCompat; +import android.util.Log; + +import java.util.Date; +import java.util.LinkedList; +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.feed.Feed; +import de.danoeh.antennapod.core.gpoddernet.GpodnetService; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceAuthenticationException; +import de.danoeh.antennapod.core.gpoddernet.GpodnetServiceException; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetSubscriptionChange; +import de.danoeh.antennapod.core.gpoddernet.model.GpodnetUploadChangesResponse; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +import de.danoeh.antennapod.core.storage.DownloadRequester; +import de.danoeh.antennapod.core.util.NetworkUtils; + +/** + * Synchronizes local subscriptions with gpodder.net service. The service should be started with ACTION_SYNC as an action argument. + * This class also provides static methods for starting the GpodnetSyncService. + */ +public class GpodnetSyncService extends Service { + private static final String TAG = "GpodnetSyncService"; + + private static final long WAIT_INTERVAL = 5000L; + + public static final String ARG_ACTION = "action"; + + public static final String ACTION_SYNC = "de.danoeh.antennapod.intent.action.sync"; + + private GpodnetService service; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + final String action = (intent != null) ? intent.getStringExtra(ARG_ACTION) : null; + if (action != null && action.equals(ACTION_SYNC)) { + Log.d(TAG, String.format("Waiting %d milliseconds before uploading changes", WAIT_INTERVAL)); + syncWaiterThread.restart(); + } else { + Log.e(TAG, "Received invalid intent: action argument is null or invalid"); + } + return START_FLAG_REDELIVERY; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (BuildConfig.DEBUG) Log.d(TAG, "onDestroy"); + syncWaiterThread.interrupt(); + + } + + @Override + public IBinder onBind(Intent intent) { + return null; + } + + private synchronized GpodnetService tryLogin() throws GpodnetServiceException { + if (service == null) { + service = new GpodnetService(); + service.authenticate(GpodnetPreferences.getUsername(), GpodnetPreferences.getPassword()); + } + return service; + } + + private synchronized void syncChanges() { + if (GpodnetPreferences.loggedIn() && NetworkUtils.networkAvailable(this)) { + final long timestamp = GpodnetPreferences.getLastSyncTimestamp(); + try { + final List<String> localSubscriptions = DBReader.getFeedListDownloadUrls(this); + GpodnetService service = tryLogin(); + + if (timestamp == 0) { + // first sync: download all subscriptions... + GpodnetSubscriptionChange changes = + service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), 0); + if (BuildConfig.DEBUG) + Log.d(TAG, "Downloaded subscription changes: " + changes); + processSubscriptionChanges(localSubscriptions, changes); + + // ... then upload all local subscriptions + if (BuildConfig.DEBUG) + Log.d(TAG, "Uploading subscription list: " + localSubscriptions); + GpodnetUploadChangesResponse uploadChangesResponse = + service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), localSubscriptions, new LinkedList<String>()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Uploading changes response: " + uploadChangesResponse); + GpodnetPreferences.removeAddedFeeds(localSubscriptions); + GpodnetPreferences.removeRemovedFeeds(GpodnetPreferences.getRemovedFeedsCopy()); + GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp); + } else { + Set<String> added = GpodnetPreferences.getAddedFeedsCopy(); + Set<String> removed = GpodnetPreferences.getRemovedFeedsCopy(); + + // download remote changes first... + GpodnetSubscriptionChange subscriptionChanges = service.getSubscriptionChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), timestamp); + if (BuildConfig.DEBUG) + Log.d(TAG, "Downloaded subscription changes: " + subscriptionChanges); + processSubscriptionChanges(localSubscriptions, subscriptionChanges); + + // ... then upload changes local changes + if (BuildConfig.DEBUG) + Log.d(TAG, String.format("Uploading subscriptions, Added: %s\nRemoved: %s", + added.toString(), removed)); + GpodnetUploadChangesResponse uploadChangesResponse = service.uploadChanges(GpodnetPreferences.getUsername(), GpodnetPreferences.getDeviceID(), added, removed); + if (BuildConfig.DEBUG) + Log.d(TAG, "Upload subscriptions response: " + uploadChangesResponse); + + GpodnetPreferences.removeAddedFeeds(added); + GpodnetPreferences.removeRemovedFeeds(removed); + GpodnetPreferences.setLastSyncTimestamp(uploadChangesResponse.timestamp); + } + clearErrorNotifications(); + } catch (GpodnetServiceException e) { + e.printStackTrace(); + updateErrorNotification(e); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + stopSelf(); + } + + private synchronized void processSubscriptionChanges(List<String> localSubscriptions, GpodnetSubscriptionChange changes) throws DownloadRequestException { + for (String downloadUrl : changes.getAdded()) { + if (!localSubscriptions.contains(downloadUrl)) { + Feed feed = new Feed(downloadUrl, new Date()); + DownloadRequester.getInstance().downloadFeed(this, feed); + } + } + for (String downloadUrl : changes.getRemoved()) { + DBTasks.removeFeedWithDownloadUrl(GpodnetSyncService.this, downloadUrl); + } + } + + private void clearErrorNotifications() { + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(R.id.notification_gpodnet_sync_error); + nm.cancel(R.id.notification_gpodnet_sync_autherror); + } + + private void updateErrorNotification(GpodnetServiceException exception) { + if (BuildConfig.DEBUG) Log.d(TAG, "Posting error notification"); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(this); + final String title; + final String description; + final int id; + if (exception instanceof GpodnetServiceAuthenticationException) { + title = getString(R.string.gpodnetsync_auth_error_title); + description = getString(R.string.gpodnetsync_auth_error_descr); + id = R.id.notification_gpodnet_sync_autherror; + } else { + title = getString(R.string.gpodnetsync_error_title); + description = getString(R.string.gpodnetsync_error_descr) + exception.getMessage(); + id = R.id.notification_gpodnet_sync_error; + } + + PendingIntent activityIntent = ClientConfig.gpodnetCallbacks.getGpodnetSyncServiceErrorNotificationPendingIntent(this); + Notification notification = builder.setContentTitle(title) + .setContentText(description) + .setContentIntent(activityIntent) + .setSmallIcon(R.drawable.stat_notify_sync_error) + .setAutoCancel(true) + .build(); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(id, notification); + } + + private WaiterThread syncWaiterThread = new WaiterThread(WAIT_INTERVAL) { + @Override + public void onWaitCompleted() { + syncChanges(); + } + }; + + private abstract class WaiterThread { + private long waitInterval; + private Thread thread; + + private WaiterThread(long waitInterval) { + this.waitInterval = waitInterval; + reinit(); + } + + public abstract void onWaitCompleted(); + + public void exec() { + if (!thread.isAlive()) { + thread.start(); + } + } + + private void reinit() { + if (thread != null && thread.isAlive()) { + Log.d(TAG, "Interrupting waiter thread"); + thread.interrupt(); + } + thread = new Thread() { + @Override + public void run() { + try { + Thread.sleep(waitInterval); + } catch (InterruptedException e) { + e.printStackTrace(); + } + if (!isInterrupted()) { + synchronized (this) { + onWaitCompleted(); + } + } + } + }; + } + + public void restart() { + reinit(); + exec(); + } + + public void interrupt() { + if (thread != null && thread.isAlive()) { + thread.interrupt(); + } + } + } + + public static void sendSyncIntent(Context context) { + if (GpodnetPreferences.loggedIn()) { + Intent intent = new Intent(context, GpodnetSyncService.class); + intent.putExtra(ARG_ACTION, ACTION_SYNC); + context.startService(intent); + } + } +} diff --git a/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 new file mode 100644 index 000000000..3efcf4da8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/APRedirectHandler.java @@ -0,0 +1,54 @@ +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 new file mode 100644 index 000000000..ec3d3e2fe --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/AntennapodHttpClient.java @@ -0,0 +1,67 @@ +package de.danoeh.antennapod.core.service.download; + +import android.util.Log; + +import com.squareup.okhttp.OkHttpClient; + +import java.net.CookieManager; +import java.net.CookiePolicy; +import java.util.concurrent.TimeUnit; + +import de.danoeh.antennapod.core.BuildConfig; + +/** + * Provides access to a HttpClient singleton. + */ +public class AntennapodHttpClient { + private static final String TAG = "AntennapodHttpClient"; + + public static final int CONNECTION_TIMEOUT = 30000; + public static final int READ_TIMEOUT = 30000; + + public static final int MAX_CONNECTIONS = 8; + + + private static volatile OkHttpClient httpClient = null; + + /** + * Returns the HttpClient singleton. + */ + 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; + } + return httpClient; + } + + /** + * Closes expired connections. This method should be called by the using class once has finished its work with + * the HTTP client. + */ + public static synchronized void cleanup() { + if (httpClient != null) { + // does nothing at the moment + } + } +} 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 new file mode 100644 index 000000000..75d6570b2 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadRequest.java @@ -0,0 +1,220 @@ +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; + +public class DownloadRequest implements Parcelable { + + private final String destination; + private final String source; + private final String title; + private String username; + private String password; + private boolean deleteOnFailure; + private final long feedfileId; + private final int feedfileType; + private final Bundle arguments; + + protected int progressPercent; + protected long soFar; + protected long size; + protected int statusMsg; + + public DownloadRequest(String destination, String source, String title, + long feedfileId, int feedfileType, String username, String password, boolean deleteOnFailure, Bundle arguments) { + Validate.notNull(destination); + Validate.notNull(source); + Validate.notNull(title); + + this.destination = destination; + this.source = source; + this.title = title; + this.feedfileId = feedfileId; + this.feedfileType = feedfileType; + this.username = username; + this.password = password; + this.deleteOnFailure = deleteOnFailure; + this.arguments = (arguments != null) ? arguments : new Bundle(); + } + + public DownloadRequest(String destination, String source, String title, + long feedfileId, int feedfileType) { + this(destination, source, title, feedfileId, feedfileType, null, null, true, null); + } + + private DownloadRequest(Parcel in) { + destination = in.readString(); + source = in.readString(); + title = in.readString(); + feedfileId = in.readLong(); + feedfileType = in.readInt(); + deleteOnFailure = (in.readByte() > 0); + arguments = in.readBundle(); + if (in.dataAvail() > 0) { + username = in.readString(); + } else { + username = null; + } + if (in.dataAvail() > 0) { + password = in.readString(); + } else { + password = null; + } + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(destination); + dest.writeString(source); + dest.writeString(title); + dest.writeLong(feedfileId); + dest.writeInt(feedfileType); + dest.writeByte((deleteOnFailure) ? (byte) 1 : 0); + dest.writeBundle(arguments); + if (username != null) { + dest.writeString(username); + } + if (password != null) { + dest.writeString(password); + } + } + + public static final Parcelable.Creator<DownloadRequest> CREATOR = new Parcelable.Creator<DownloadRequest>() { + public DownloadRequest createFromParcel(Parcel in) { + return new DownloadRequest(in); + } + + public DownloadRequest[] newArray(int size) { + return new DownloadRequest[size]; + } + }; + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DownloadRequest that = (DownloadRequest) o; + + if (deleteOnFailure != that.deleteOnFailure) return false; + if (feedfileId != that.feedfileId) return false; + if (feedfileType != that.feedfileType) return false; + if (progressPercent != that.progressPercent) return false; + if (size != that.size) return false; + if (soFar != that.soFar) return false; + if (statusMsg != that.statusMsg) return false; + if (!arguments.equals(that.arguments)) return false; + if (!destination.equals(that.destination)) return false; + if (password != null ? !password.equals(that.password) : that.password != null) + return false; + if (!source.equals(that.source)) return false; + if (title != null ? !title.equals(that.title) : that.title != null) return false; + if (username != null ? !username.equals(that.username) : that.username != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = destination.hashCode(); + result = 31 * result + source.hashCode(); + result = 31 * result + (title != null ? title.hashCode() : 0); + result = 31 * result + (username != null ? username.hashCode() : 0); + result = 31 * result + (password != null ? password.hashCode() : 0); + result = 31 * result + (deleteOnFailure ? 1 : 0); + result = 31 * result + (int) (feedfileId ^ (feedfileId >>> 32)); + result = 31 * result + feedfileType; + result = 31 * result + arguments.hashCode(); + result = 31 * result + progressPercent; + result = 31 * result + (int) (soFar ^ (soFar >>> 32)); + result = 31 * result + (int) (size ^ (size >>> 32)); + result = 31 * result + statusMsg; + return result; + } + + public String getDestination() { + return destination; + } + + public String getSource() { + return source; + } + + public String getTitle() { + return title; + } + + public long getFeedfileId() { + return feedfileId; + } + + public int getFeedfileType() { + return feedfileType; + } + + public int getProgressPercent() { + return progressPercent; + } + + public void setProgressPercent(int progressPercent) { + this.progressPercent = progressPercent; + } + + public long getSoFar() { + return soFar; + } + + public void setSoFar(long soFar) { + this.soFar = soFar; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public int getStatusMsg() { + return statusMsg; + } + + public void setStatusMsg(int statusMsg) { + this.statusMsg = statusMsg; + } + + public String getUsername() { + return username; + } + + public String getPassword() { + return password; + } + + public void setUsername(String username) { + this.username = username; + } + + public void setPassword(String password) { + this.password = password; + } + + public boolean isDeleteOnFailure() { + return deleteOnFailure; + } + + public Bundle getArguments() { + return arguments; + } +} 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 new file mode 100644 index 000000000..02a6aecbd --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -0,0 +1,1232 @@ +package de.danoeh.antennapod.core.service.download; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.NotificationManager; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.MediaMetadataRetriever; +import android.os.Binder; +import android.os.Handler; +import android.os.IBinder; +import android.support.v4.app.NotificationCompat; +import android.support.v4.util.Pair; +import android.util.Log; +import android.webkit.URLUtil; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.Validate; +import org.apache.http.HttpStatus; +import org.xml.sax.SAXException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import javax.xml.parsers.ParserConfigurationException; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +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.feed.FeedPreferences; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.storage.DBWriter; +import de.danoeh.antennapod.core.storage.DownloadRequestException; +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; + +/** + * Manages the download of feedfiles in the app. Downloads can be enqueued viathe startService intent. + * The argument of the intent is an instance of DownloadRequest in the EXTRA_REQUEST field of + * the intent. + * After the downloads have finished, the downloaded object will be passed on to a specific handler, depending on the + * type of the feedfile. + */ +public class DownloadService extends Service { + private static final String TAG = "DownloadService"; + + /** + * Cancels one download. The intent MUST have an EXTRA_DOWNLOAD_URL extra that contains the download URL of the + * object whose download should be cancelled. + */ + public static final String ACTION_CANCEL_DOWNLOAD = "action.de.danoeh.antennapod.core.service.cancelDownload"; + + /** + * Cancels all running downloads. + */ + public static final String ACTION_CANCEL_ALL_DOWNLOADS = "action.de.danoeh.antennapod.core.service.cancelAllDownloads"; + + /** + * Extra for ACTION_CANCEL_DOWNLOAD + */ + public static final String EXTRA_DOWNLOAD_URL = "downloadUrl"; + + /** + * Sent by the DownloadService when the content of the downloads list + * changes. + */ + public static final String ACTION_DOWNLOADS_CONTENT_CHANGED = "action.de.danoeh.antennapod.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; + + private ExecutorService syncExecutor; + private CompletionService<Downloader> downloadExecutor; + private FeedSyncThread feedSyncThread; + + /** + * Number of threads of downloadExecutor. + */ + private static final int NUM_PARALLEL_DOWNLOADS = 6; + + private DownloadRequester requester; + + + private NotificationCompat.Builder notificationCompatBuilder; + private Notification.BigTextStyle notificationBuilder; + private int NOTIFICATION_ID = 2; + private int REPORT_ID = 3; + + /** + * Currently running downloads. + */ + private List<Downloader> downloads; + + /** + * Number of running downloads. + */ + private AtomicInteger numberOfDownloads; + + /** + * True if service is running. + */ + public static boolean isRunning = false; + + private Handler handler; + + private NotificationUpdater notificationUpdater; + private ScheduledFuture notificationUpdaterFuture; + private static final int SCHED_EX_POOL_SIZE = 1; + private ScheduledThreadPoolExecutor schedExecutor; + + private final IBinder mBinder = new LocalBinder(); + + public class LocalBinder extends Binder { + public DownloadService getService() { + return DownloadService.this; + } + } + + private Thread downloadCompletionThread = new Thread() { + private static final String TAG = "downloadCompletionThread"; + + @Override + public void run() { + if (BuildConfig.DEBUG) Log.d(TAG, "downloadCompletionThread was started"); + while (!isInterrupted()) { + try { + Downloader downloader = downloadExecutor.take().get(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Received 'Download Complete' - message."); + removeDownload(downloader); + DownloadStatus status = downloader.getResult(); + boolean successful = status.isSuccessful(); + + final int type = status.getFeedfileType(); + if (successful) { + if (type == Feed.FEEDFILETYPE_FEED) { + handleCompletedFeedDownload(downloader + .getDownloadRequest()); + } else if (type == FeedImage.FEEDFILETYPE_FEEDIMAGE) { + handleCompletedImageDownload(status, downloader.getDownloadRequest()); + } else if (type == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + handleCompletedFeedMediaDownload(status, downloader.getDownloadRequest()); + } + } else { + numberOfDownloads.decrementAndGet(); + if (!status.isCancelled()) { + if (status.getReason() == DownloadError.ERROR_UNAUTHORIZED) { + postAuthenticationNotification(downloader.getDownloadRequest()); + } else if (status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR + && Integer.valueOf(status.getReasonDetailed()) == HttpStatus.SC_REQUESTED_RANGE_NOT_SATISFIABLE) { + + Log.d(TAG, "Requested invalid range, restarting download from the beginning"); + FileUtils.deleteQuietly(new File(downloader.getDownloadRequest().getDestination())); + DownloadRequester.getInstance().download(DownloadService.this, downloader.getDownloadRequest()); + } else { + Log.e(TAG, "Download failed"); + saveDownloadStatus(status); + handleFailedDownload(status, downloader.getDownloadRequest()); + } + } + sendDownloadHandledIntent(); + queryDownloadsAsync(); + } + } catch (InterruptedException e) { + if (BuildConfig.DEBUG) Log.d(TAG, "DownloadCompletionThread was interrupted"); + } catch (ExecutionException e) { + e.printStackTrace(); + numberOfDownloads.decrementAndGet(); + } + } + if (BuildConfig.DEBUG) Log.d(TAG, "End of downloadCompletionThread"); + } + }; + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + if (intent.getParcelableExtra(EXTRA_REQUEST) != null) { + onDownloadQueued(intent); + } else if (numberOfDownloads.get() == 0) { + stopSelf(); + } + return Service.START_NOT_STICKY; + } + + @SuppressLint("NewApi") + @Override + public void onCreate() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Service started"); + isRunning = true; + handler = new Handler(); + newMediaFiles = Collections.synchronizedList(new ArrayList<Long>()); + reportQueue = Collections.synchronizedList(new ArrayList<DownloadStatus>()); + downloads = new ArrayList<Downloader>(); + numberOfDownloads = new AtomicInteger(0); + + IntentFilter cancelDownloadReceiverFilter = new IntentFilter(); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_ALL_DOWNLOADS); + cancelDownloadReceiverFilter.addAction(ACTION_CANCEL_DOWNLOAD); + registerReceiver(cancelDownloadReceiver, cancelDownloadReceiverFilter); + syncExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + downloadExecutor = new ExecutorCompletionService<Downloader>( + Executors.newFixedThreadPool(NUM_PARALLEL_DOWNLOADS, + new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + } + ) + ); + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, + new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }, new RejectedExecutionHandler() { + + @Override + public void rejectedExecution(Runnable r, + ThreadPoolExecutor executor) { + Log.w(TAG, "SchedEx rejected submission of new task"); + } + } + ); + downloadCompletionThread.start(); + feedSyncThread = new FeedSyncThread(); + feedSyncThread.start(); + + setupNotificationBuilders(); + requester = DownloadRequester.getInstance(); + } + + @Override + public IBinder onBind(Intent intent) { + return mBinder; + } + + @Override + public void onDestroy() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Service shutting down"); + isRunning = false; + + if (ClientConfig.downloadServiceCallbacks.shouldCreateReport()) { + updateReport(); + } + + stopForeground(true); + NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); + nm.cancel(NOTIFICATION_ID); + + downloadCompletionThread.interrupt(); + syncExecutor.shutdown(); + schedExecutor.shutdown(); + feedSyncThread.shutdown(); + cancelNotificationUpdater(); + unregisterReceiver(cancelDownloadReceiver); + + if (!newMediaFiles.isEmpty()) { + DBTasks.autodownloadUndownloadedItems(getApplicationContext(), + ArrayUtils.toPrimitive(newMediaFiles.toArray(new Long[newMediaFiles.size()]))); + } + } + + @SuppressLint("NewApi") + private void setupNotificationBuilders() { + 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)) + .setLargeIcon(icon) + .setSmallIcon(R.drawable.stat_notify_sync); + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Notification set up"); + } + + /** + * Updates the contents of the service's notifications. Should be called + * before setupNotificationBuilders. + */ + @SuppressLint("NewApi") + private Notification updateNotifications() { + String contentTitle = getString(R.string.download_notification_title); + int numDownloads = requester.getNumberOfDownloads(); + String downloadsLeft; + if (numDownloads > 0) { + downloadsLeft = requester.getNumberOfDownloads() + + getString(R.string.downloads_left); + } else { + downloadsLeft = getString(R.string.downloads_processing); + } + if (android.os.Build.VERSION.SDK_INT >= 16) { + + if (notificationBuilder != null) { + + StringBuilder bigText = new StringBuilder(""); + for (int i = 0; i < downloads.size(); i++) { + Downloader downloader = downloads.get(i); + final DownloadRequest request = downloader + .getDownloadRequest(); + if (request.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + if (request.getTitle() != null) { + if (i > 0) { + bigText.append("\n"); + } + bigText.append("\u2022 " + request.getTitle()); + } + } else if (request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + if (request.getTitle() != null) { + if (i > 0) { + bigText.append("\n"); + } + bigText.append("\u2022 " + request.getTitle() + + " (" + request.getProgressPercent() + + "%)"); + } + } + + } + notificationBuilder.setSummaryText(downloadsLeft); + notificationBuilder.setBigContentTitle(contentTitle); + if (bigText != null) { + notificationBuilder.bigText(bigText.toString()); + } + return notificationBuilder.build(); + } + } else { + if (notificationCompatBuilder != null) { + notificationCompatBuilder.setContentTitle(contentTitle); + notificationCompatBuilder.setContentText(downloadsLeft); + return notificationCompatBuilder.build(); + } + } + return null; + } + + private Downloader getDownloader(String downloadUrl) { + for (Downloader downloader : downloads) { + if (downloader.getDownloadRequest().getSource().equals(downloadUrl)) { + return downloader; + } + } + return null; + } + + private BroadcastReceiver cancelDownloadReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_DOWNLOAD)) { + String url = intent.getStringExtra(EXTRA_DOWNLOAD_URL); + Validate.notNull(url, "ACTION_CANCEL_DOWNLOAD intent needs download url extra"); + + if (BuildConfig.DEBUG) + Log.d(TAG, "Cancelling download with url " + url); + Downloader d = getDownloader(url); + if (d != null) { + d.cancel(); + } else { + Log.e(TAG, "Could not cancel download with url " + url); + } + + } else if (StringUtils.equals(intent.getAction(), ACTION_CANCEL_ALL_DOWNLOADS)) { + for (Downloader d : downloads) { + d.cancel(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Cancelled all downloads"); + } + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + + } + queryDownloads(); + } + + }; + + private void onDownloadQueued(Intent intent) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received enqueue request"); + DownloadRequest request = intent.getParcelableExtra(EXTRA_REQUEST); + if (request == null) { + throw new IllegalArgumentException( + "ACTION_ENQUEUE_DOWNLOAD intent needs request extra"); + } + + Downloader downloader = getDownloader(request); + if (downloader != null) { + numberOfDownloads.incrementAndGet(); + downloads.add(downloader); + downloadExecutor.submit(downloader); + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + } + + queryDownloads(); + } + + private Downloader getDownloader(DownloadRequest request) { + if (URLUtil.isHttpUrl(request.getSource()) + || URLUtil.isHttpsUrl(request.getSource())) { + return new HttpDownloader(request); + } + Log.e(TAG, + "Could not find appropriate downloader for " + + request.getSource() + ); + return null; + } + + /** + * Remove download from the DownloadRequester list and from the + * DownloadService list. + */ + private void removeDownload(final Downloader d) { + handler.post(new Runnable() { + @Override + public void run() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Removing downloader: " + + d.getDownloadRequest().getSource()); + boolean rc = downloads.remove(d); + if (BuildConfig.DEBUG) + Log.d(TAG, "Result of downloads.remove: " + rc); + DownloadRequester.getInstance().removeDownload(d.getDownloadRequest()); + sendBroadcast(new Intent(ACTION_DOWNLOADS_CONTENT_CHANGED)); + } + }); + } + + /** + * Adds a new DownloadStatus object to the list of completed downloads and + * saves it in the database + * + * @param status the download that is going to be saved + */ + private void saveDownloadStatus(DownloadStatus status) { + reportQueue.add(status); + DBWriter.addDownloadStatus(this, status); + } + + private void sendDownloadHandledIntent() { + EventDistributor.getInstance().sendDownloadHandledBroadcast(); + } + + /** + * Creates a notification at the end of the service lifecycle to notify the + * user about the number of completed downloads. A report will only be + * created if the number of successfully downloaded feeds is bigger than 1 + * or if there is at least one failed download which is not an image or if + * there is at least one downloaded media file. + */ + private void updateReport() { + // check if report should be created + boolean createReport = false; + int successfulDownloads = 0; + int failedDownloads = 0; + + // a download report is created if at least one download has failed + // (excluding failed image downloads) + for (DownloadStatus status : reportQueue) { + if (status.isSuccessful()) { + successfulDownloads++; + } else if (!status.isCancelled()) { + if (status.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { + createReport = true; + } + failedDownloads++; + } + } + + if (createReport) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Creating report"); + // create notification object + Notification notification = new NotificationCompat.Builder(this) + .setTicker( + getString(R.string.download_report_title)) + .setContentTitle( + getString(R.string.download_report_title)) + .setContentText( + String.format( + getString(R.string.download_report_content), + successfulDownloads, failedDownloads) + ) + .setSmallIcon(R.drawable.stat_notify_sync) + .setLargeIcon( + BitmapFactory.decodeResource(getResources(), + R.drawable.stat_notify_sync) + ) + .setContentIntent( + ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(this) + ) + .setAutoCancel(true).build(); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(REPORT_ID, notification); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "No report is created"); + } + reportQueue.clear(); + } + + /** + * Calls query downloads on the services main thread. This method should be used instead of queryDownloads if it is + * used from a thread other than the main thread. + */ + void queryDownloadsAsync() { + handler.post(new Runnable() { + public void run() { + queryDownloads(); + ; + } + }); + } + + /** + * Check if there's something else to download, otherwise stop + */ + void queryDownloads() { + if (BuildConfig.DEBUG) { + Log.d(TAG, numberOfDownloads.get() + " downloads left"); + } + + if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); + stopSelf(); + } else { + setupNotificationUpdater(); + startForeground(NOTIFICATION_ID, updateNotifications()); + } + } + + private void postAuthenticationNotification(final DownloadRequest downloadRequest) { + handler.post(new Runnable() { + @Override + public void run() { + final String resourceTitle = (downloadRequest.getTitle() != null) + ? downloadRequest.getTitle() : downloadRequest.getSource(); + + NotificationCompat.Builder builder = new NotificationCompat.Builder(DownloadService.this); + builder.setTicker(getText(R.string.authentication_notification_title)) + .setContentTitle(getText(R.string.authentication_notification_title)) + .setContentText(getText(R.string.authentication_notification_msg)) + .setStyle(new NotificationCompat.BigTextStyle().bigText(getText(R.string.authentication_notification_msg) + + ": " + resourceTitle)) + .setSmallIcon(R.drawable.ic_stat_authentication) + .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.ic_stat_authentication)) + .setAutoCancel(true) + .setContentIntent(ClientConfig.downloadServiceCallbacks.getAuthentificationNotificationContentIntent(DownloadService.this, downloadRequest)); + Notification n = builder.build(); + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(downloadRequest.getSource().hashCode(), n); + } + }); + } + + /** + * Is called whenever a Feed is downloaded + */ + private void handleCompletedFeedDownload(DownloadRequest request) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Handling completed Feed Download"); + feedSyncThread.submitCompletedDownload(request); + + } + + /** + * Is called whenever a Feed-Image is downloaded + */ + private void handleCompletedImageDownload(DownloadStatus status, DownloadRequest request) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Handling completed Image Download"); + syncExecutor.execute(new ImageHandlerThread(status, request)); + } + + /** + * Is called whenever a FeedMedia is downloaded. + */ + private void handleCompletedFeedMediaDownload(DownloadStatus status, DownloadRequest request) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Handling completed FeedMedia Download"); + syncExecutor.execute(new MediaHandlerThread(status, request)); + } + + private void handleFailedDownload(DownloadStatus status, DownloadRequest request) { + if (BuildConfig.DEBUG) Log.d(TAG, "Handling failed download"); + syncExecutor.execute(new FailedDownloadHandler(status, request)); + } + + /** + * Takes a single Feed, parses the corresponding file and refreshes + * information in the manager + */ + class FeedSyncThread extends Thread { + private static final String TAG = "FeedSyncThread"; + + private BlockingQueue<DownloadRequest> completedRequests = new LinkedBlockingDeque<DownloadRequest>(); + private CompletionService<Pair<DownloadRequest, FeedHandlerResult>> parserService = new ExecutorCompletionService<Pair<DownloadRequest, FeedHandlerResult>>(Executors.newSingleThreadExecutor()); + private ExecutorService dbService = Executors.newSingleThreadExecutor(); + private Future<?> dbUpdateFuture; + private volatile boolean isActive = true; + private volatile boolean isCollectingRequests = false; + + private final long WAIT_TIMEOUT = 3000; + + + /** + * Waits for completed requests. Once the first request has been taken, the method will wait WAIT_TIMEOUT ms longer to + * collect more completed requests. + * + * @return Collected feeds or null if the method has been interrupted during the first waiting period. + */ + private List<Pair<DownloadRequest, FeedHandlerResult>> collectCompletedRequests() { + List<Pair<DownloadRequest, FeedHandlerResult>> results = new LinkedList<Pair<DownloadRequest, FeedHandlerResult>>(); + DownloadRequester requester = DownloadRequester.getInstance(); + int tasks = 0; + + try { + DownloadRequest request = completedRequests.take(); + parserService.submit(new FeedParserTask(request)); + tasks++; + } catch (InterruptedException e) { + return null; + } + + tasks += pollCompletedDownloads(); + + isCollectingRequests = true; + + if (requester.isDownloadingFeeds()) { + // wait for completion of more downloads + long startTime = System.currentTimeMillis(); + long currentTime = startTime; + while (requester.isDownloadingFeeds() && (currentTime - startTime) < WAIT_TIMEOUT) { + try { + if (BuildConfig.DEBUG) + Log.d(TAG, "Waiting for " + (startTime + WAIT_TIMEOUT - currentTime) + " ms"); + sleep(startTime + WAIT_TIMEOUT - currentTime); + } catch (InterruptedException e) { + if (BuildConfig.DEBUG) + Log.d(TAG, "interrupted while waiting for more downloads"); + tasks += pollCompletedDownloads(); + } finally { + currentTime = System.currentTimeMillis(); + } + } + + tasks += pollCompletedDownloads(); + + } + + isCollectingRequests = false; + + for (int i = 0; i < tasks; i++) { + try { + Pair<DownloadRequest, FeedHandlerResult> result = parserService.take().get(); + if (result != null) { + results.add(result); + } + } catch (InterruptedException e) { + e.printStackTrace(); + + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + return results; + } + + private int pollCompletedDownloads() { + int tasks = 0; + for (int i = 0; i < completedRequests.size(); i++) { + parserService.submit(new FeedParserTask(completedRequests.poll())); + tasks++; + } + return tasks; + } + + @Override + public void run() { + while (isActive) { + final List<Pair<DownloadRequest, FeedHandlerResult>> results = collectCompletedRequests(); + + if (results == null) { + continue; + } + + if (BuildConfig.DEBUG) Log.d(TAG, "Bundling " + results.size() + " feeds"); + + for (Pair<DownloadRequest, FeedHandlerResult> result : results) { + removeDuplicateImages(result.second.feed); // duplicate images have to removed because the DownloadRequester does not accept two downloads with the same download URL yet. + } + + // Save information of feed in DB + if (dbUpdateFuture != null) { + try { + dbUpdateFuture.get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + dbUpdateFuture = dbService.submit(new Runnable() { + @Override + public void run() { + Feed[] savedFeeds = DBTasks.updateFeed(DownloadService.this, getFeeds(results)); + + 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) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Feed has image; Downloading...."); + savedFeed.getImage().setOwner(savedFeed); + final Feed savedFeedRef = savedFeed; + try { + requester.downloadImage(DownloadService.this, + savedFeedRef.getImage()); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus( + DownloadService.this, + new DownloadStatus( + savedFeedRef.getImage(), + savedFeedRef + .getImage() + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, + false, e.getMessage() + ) + ); + } + } + + // queue new media files for automatic download + for (FeedItem item : savedFeed.getItems()) { + if (!item.isRead() && item.hasMedia() && !item.getMedia().isDownloaded()) { + newMediaFiles.add(item.getMedia().getId()); + } + } + + // 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) { + try { + feed.setId(savedFeed.getId()); + DBTasks.loadNextPageOfFeed(DownloadService.this, savedFeed, true); + } catch (DownloadRequestException e) { + Log.e(TAG, "Error trying to load next page", e); + } + } + + ClientConfig.downloadServiceCallbacks.onFeedParsed(DownloadService.this, + savedFeed); + + numberOfDownloads.decrementAndGet(); + } + + sendDownloadHandledIntent(); + + queryDownloadsAsync(); + } + }); + + } + + if (dbUpdateFuture != null) { + try { + dbUpdateFuture.get(); + } catch (InterruptedException e) { + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + + if (BuildConfig.DEBUG) Log.d(TAG, "Shutting down"); + + } + + /** + * Helper method + */ + private Feed[] getFeeds(List<Pair<DownloadRequest, FeedHandlerResult>> results) { + Feed[] feeds = new Feed[results.size()]; + for (int i = 0; i < results.size(); i++) { + feeds[i] = results.get(i).second.feed; + } + return feeds; + } + + private class FeedParserTask implements Callable<Pair<DownloadRequest, FeedHandlerResult>> { + + private DownloadRequest request; + + private FeedParserTask(DownloadRequest request) { + this.request = request; + } + + @Override + public Pair<DownloadRequest, FeedHandlerResult> call() throws Exception { + return parseFeed(request); + } + } + + private Pair<DownloadRequest, FeedHandlerResult> parseFeed(DownloadRequest request) { + Feed feed = new Feed(request.getSource(), new Date()); + feed.setFile_url(request.getDestination()); + feed.setId(request.getFeedfileId()); + feed.setDownloaded(true); + feed.setPreferences(new FeedPreferences(0, true, + request.getUsername(), request.getPassword())); + feed.setPageNr(request.getArguments().getInt(DownloadRequester.REQUEST_ARG_PAGE_NR, 0)); + + DownloadError reason = null; + String reasonDetailed = null; + boolean successful = true; + FeedHandler feedHandler = new FeedHandler(); + + FeedHandlerResult result = null; + try { + result = feedHandler.parseFeed(feed); + if (BuildConfig.DEBUG) + Log.d(TAG, feed.getTitle() + " parsed"); + if (checkFeedData(feed) == false) { + throw new InvalidFeedException(); + } + + } catch (SAXException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (IOException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (ParserConfigurationException e) { + successful = false; + e.printStackTrace(); + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } catch (UnsupportedFeedtypeException e) { + e.printStackTrace(); + successful = false; + reason = DownloadError.ERROR_UNSUPPORTED_TYPE; + reasonDetailed = e.getMessage(); + } catch (InvalidFeedException e) { + e.printStackTrace(); + successful = false; + reason = DownloadError.ERROR_PARSER_EXCEPTION; + reasonDetailed = e.getMessage(); + } + + // cleanup(); + + + if (successful) { + return Pair.create(request, result); + } else { + numberOfDownloads.decrementAndGet(); + saveDownloadStatus(new DownloadStatus(feed, + feed.getHumanReadableIdentifier(), reason, successful, + reasonDetailed)); + return null; + } + } + + + /** + * Checks if the feed was parsed correctly. + */ + private boolean checkFeedData(Feed feed) { + if (feed.getTitle() == null) { + Log.e(TAG, "Feed has no title."); + return false; + } + if (!hasValidFeedItems(feed)) { + Log.e(TAG, "Feed has invalid items"); + return false; + } + return true; + } + + /** + * Checks if the FeedItems of this feed have images that point + * to the same URL. If two FeedItems have an image that points to + * the same URL, the reference of the second item is removed, so that every image + * reference is unique. + */ + private void removeDuplicateImages(Feed feed) { + for (int x = 0; x < feed.getItems().size(); x++) { + for (int y = x + 1; y < feed.getItems().size(); y++) { + FeedItem item1 = feed.getItems().get(x); + FeedItem item2 = feed.getItems().get(y); + if (item1.hasItemImage() && item2.hasItemImage()) { + if (StringUtils.equals(item1.getImage().getDownload_url(), item2.getImage().getDownload_url())) { + item2.setImage(null); + } + } + } + } + } + + private boolean hasValidFeedItems(Feed feed) { + for (FeedItem item : feed.getItems()) { + if (item.getTitle() == null) { + Log.e(TAG, "Item has no title"); + return false; + } + if (item.getPubDate() == null) { + Log.e(TAG, + "Item has no pubDate. Using current time as pubDate"); + if (item.getTitle() != null) { + Log.e(TAG, "Title of invalid item: " + item.getTitle()); + } + item.setPubDate(new Date()); + } + } + return true; + } + + /** + * Delete files that aren't needed anymore + */ + private void cleanup(Feed feed) { + if (feed.getFile_url() != null) { + if (new File(feed.getFile_url()).delete()) + if (BuildConfig.DEBUG) + Log.d(TAG, "Successfully deleted cache file."); + else + Log.e(TAG, "Failed to delete cache file."); + feed.setFile_url(null); + } else if (BuildConfig.DEBUG) { + Log.d(TAG, "Didn't delete cache file: File url is not set."); + } + } + + public void shutdown() { + isActive = false; + if (isCollectingRequests) { + interrupt(); + } + } + + public void submitCompletedDownload(DownloadRequest request) { + completedRequests.offer(request); + if (isCollectingRequests) { + interrupt(); + } + } + + } + + /** + * Handles failed downloads. + * <p/> + * If the file has been partially downloaded, this handler will set the file_url of the FeedFile to the location + * of the downloaded file. + * <p/> + * Currently, this handler only handles FeedMedia objects, because Feeds and FeedImages are deleted if the download fails. + */ + class FailedDownloadHandler implements Runnable { + + private DownloadRequest request; + private DownloadStatus status; + + FailedDownloadHandler(DownloadStatus status, DownloadRequest request) { + this.request = request; + this.status = status; + } + + @Override + public void run() { + if (request.isDeleteOnFailure()) { + if (BuildConfig.DEBUG) Log.d(TAG, "Ignoring failed download, deleteOnFailure=true"); + } else { + File dest = new File(request.getDestination()); + if (dest.exists() && request.getFeedfileType() == FeedMedia.FEEDFILETYPE_FEEDMEDIA) { + Log.d(TAG, "File has been partially downloaded. Writing file url"); + FeedMedia media = DBReader.getFeedMedia(DownloadService.this, request.getFeedfileId()); + media.setFile_url(request.getDestination()); + try { + DBWriter.setFeedMedia(DownloadService.this, media).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + } + } + } + + /** + * Handles a completed image download. + */ + class ImageHandlerThread implements Runnable { + + private DownloadRequest request; + private DownloadStatus status; + + public ImageHandlerThread(DownloadStatus status, DownloadRequest request) { + Validate.notNull(status); + Validate.notNull(request); + + this.status = status; + this.request = request; + } + + @Override + public void run() { + FeedImage image = DBReader.getFeedImage(DownloadService.this, request.getFeedfileId()); + if (image == null) { + throw new IllegalStateException("Could not find downloaded image in database"); + } + + image.setFile_url(request.getDestination()); + image.setDownloaded(true); + + saveDownloadStatus(status); + sendDownloadHandledIntent(); + DBWriter.setFeedImage(DownloadService.this, image); + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + } + } + + /** + * Handles a completed media download. + */ + class MediaHandlerThread implements Runnable { + + private DownloadRequest request; + private DownloadStatus status; + + public MediaHandlerThread(DownloadStatus status, DownloadRequest request) { + Validate.notNull(status); + Validate.notNull(request); + + this.status = status; + this.request = request; + } + + @Override + public void run() { + FeedMedia media = DBReader.getFeedMedia(DownloadService.this, + request.getFeedfileId()); + if (media == null) { + throw new IllegalStateException( + "Could not find downloaded media object in database"); + } + boolean chaptersRead = false; + media.setDownloaded(true); + media.setFile_url(request.getDestination()); + + // Get duration + MediaMetadataRetriever mmr = null; + try { + mmr = new MediaMetadataRetriever(); + mmr.setDataSource(media.getFile_url()); + String durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + media.setDuration(Integer.parseInt(durationStr)); + if (BuildConfig.DEBUG) + Log.d(TAG, "Duration of file is " + media.getDuration()); + } catch (NumberFormatException e) { + e.printStackTrace(); + } catch (RuntimeException e) { + e.printStackTrace(); + } finally { + if (mmr != null) { + mmr.release(); + } + } + + if (media.getItem().getChapters() == null) { + ChapterUtils.loadChaptersFromFileUrl(media); + if (media.getItem().getChapters() != null) { + chaptersRead = true; + } + } + + try { + if (chaptersRead) { + DBWriter.setFeedItem(DownloadService.this, media.getItem()).get(); + } + DBWriter.setFeedMedia(DownloadService.this, media).get(); + if (!DBTasks.isInQueue(DownloadService.this, media.getItem().getId())) { + DBWriter.addQueueItem(DownloadService.this, media.getItem().getId()).get(); + } + } catch (ExecutionException e) { + e.printStackTrace(); + status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); + } catch (InterruptedException e) { + e.printStackTrace(); + status = new DownloadStatus(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.getMessage()); + } + + saveDownloadStatus(status); + sendDownloadHandledIntent(); + + numberOfDownloads.decrementAndGet(); + queryDownloadsAsync(); + } + } + + /** + * Schedules the notification updater task if it hasn't been scheduled yet. + */ + private void setupNotificationUpdater() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting up notification updater"); + if (notificationUpdater == null) { + notificationUpdater = new NotificationUpdater(); + notificationUpdaterFuture = schedExecutor.scheduleAtFixedRate( + notificationUpdater, 5L, 5L, TimeUnit.SECONDS); + } + } + + private void cancelNotificationUpdater() { + boolean result = false; + if (notificationUpdaterFuture != null) { + result = notificationUpdaterFuture.cancel(true); + } + notificationUpdater = null; + notificationUpdaterFuture = null; + Log.d(TAG, "NotificationUpdater cancelled. Result: " + result); + } + + private class NotificationUpdater implements Runnable { + public void run() { + handler.post(new Runnable() { + @Override + public void run() { + Notification n = updateNotifications(); + if (n != null) { + NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + nm.notify(NOTIFICATION_ID, n); + } + } + }); + } + } + + public List<Downloader> getDownloads() { + return downloads; + } + +} diff --git a/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 new file mode 100644 index 000000000..d05650d10 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadStatus.java @@ -0,0 +1,181 @@ +package de.danoeh.antennapod.core.service.download; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.core.feed.FeedFile; +import de.danoeh.antennapod.core.util.DownloadError; + +import java.util.Date; + +/** Contains status attributes for one download */ +public class DownloadStatus { + /** + * Downloaders should use this constant for the size attribute if necessary + * so that the listadapters etc. can react properly. + */ + public static final int SIZE_UNKNOWN = -1; + + // ----------------------------------- ATTRIBUTES STORED IN DB + /** Unique id for storing the object in database. */ + protected long id; + /** + * A human-readable string which is shown to the user so that he can + * identify the download. Should be the title of the item/feed/media or the + * URL if the download has no other title. + */ + protected String title; + protected DownloadError reason; + /** + * A message which can be presented to the user to give more information. + * Should be null if Download was successful. + */ + protected String reasonDetailed; + protected boolean successful; + protected Date completionDate; + protected long feedfileId; + /** + * Is used to determine the type of the feedfile even if the feedfile does + * not exist anymore. The value should be FEEDFILETYPE_FEED, + * FEEDFILETYPE_FEEDIMAGE or FEEDFILETYPE_FEEDMEDIA + */ + protected int feedfileType; + + // ------------------------------------ NOT STORED IN DB + protected boolean done; + protected boolean cancelled; + + /** Constructor for restoring Download status entries from DB. */ + public DownloadStatus(long id, String title, long feedfileId, + int feedfileType, boolean successful, DownloadError reason, + Date completionDate, String reasonDetailed) { + this.id = id; + this.title = title; + this.done = true; + this.feedfileId = feedfileId; + this.reason = reason; + this.successful = successful; + this.completionDate = (Date) completionDate.clone(); + this.reasonDetailed = reasonDetailed; + this.feedfileType = feedfileType; + } + + public DownloadStatus(DownloadRequest request, DownloadError reason, + boolean successful, boolean cancelled, String reasonDetailed) { + Validate.notNull(request); + + this.title = request.getTitle(); + this.feedfileId = request.getFeedfileId(); + this.feedfileType = request.getFeedfileType(); + this.reason = reason; + this.successful = successful; + this.cancelled = cancelled; + this.reasonDetailed = reasonDetailed; + this.completionDate = new Date(); + } + + /** Constructor for creating new completed downloads. */ + public DownloadStatus(FeedFile feedfile, String title, DownloadError reason, + boolean successful, String reasonDetailed) { + Validate.notNull(feedfile); + + this.title = title; + this.done = true; + this.feedfileId = feedfile.getId(); + this.feedfileType = feedfile.getTypeAsInt(); + this.reason = reason; + this.successful = successful; + this.completionDate = new Date(); + this.reasonDetailed = reasonDetailed; + } + + /** Constructor for creating new completed downloads. */ + public DownloadStatus(long feedfileId, int feedfileType, String title, + DownloadError reason, boolean successful, String reasonDetailed) { + this.title = title; + this.done = true; + this.feedfileId = feedfileId; + this.feedfileType = feedfileType; + this.reason = reason; + this.successful = successful; + this.completionDate = new Date(); + this.reasonDetailed = reasonDetailed; + } + + @Override + public String toString() { + return "DownloadStatus [id=" + id + ", title=" + title + ", reason=" + + reason + ", reasonDetailed=" + reasonDetailed + + ", successful=" + successful + ", completionDate=" + + completionDate + ", feedfileId=" + feedfileId + + ", feedfileType=" + feedfileType + ", done=" + done + + ", cancelled=" + cancelled + "]"; + } + + public long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public DownloadError getReason() { + return reason; + } + + public String getReasonDetailed() { + return reasonDetailed; + } + + public boolean isSuccessful() { + return successful; + } + + public Date getCompletionDate() { + return (Date) completionDate.clone(); + } + + public long getFeedfileId() { + return feedfileId; + } + + public int getFeedfileType() { + return feedfileType; + } + + public boolean isDone() { + return done; + } + + public boolean isCancelled() { + return cancelled; + } + + public void setSuccessful() { + this.successful = true; + this.reason = DownloadError.SUCCESS; + this.done = true; + } + + public void setFailed(DownloadError reason, String reasonDetailed) { + this.successful = false; + this.reason = reason; + this.reasonDetailed = reasonDetailed; + this.done = true; + } + + public void setCancelled() { + this.successful = false; + this.reason = DownloadError.ERROR_DOWNLOAD_CANCELLED; + this.done = true; + this.cancelled = true; + } + + public void setCompletionDate(Date completionDate) { + this.completionDate = (Date) completionDate.clone(); + } + + public void setId(long id) { + this.id = id; + } +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java new file mode 100644 index 000000000..d8042d202 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/Downloader.java @@ -0,0 +1,73 @@ +package de.danoeh.antennapod.core.service.download; + +import android.content.Context; +import android.net.wifi.WifiManager; + +import java.util.concurrent.Callable; + +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; + +/** + * Downloads files + */ +public abstract class Downloader implements Callable<Downloader> { + private static final String TAG = "Downloader"; + + protected volatile boolean finished; + + protected volatile boolean cancelled; + + protected DownloadRequest request; + protected DownloadStatus result; + + public Downloader(DownloadRequest request) { + super(); + this.request = request; + this.request.setStatusMsg(R.string.download_pending); + this.cancelled = false; + this.result = new DownloadStatus(request, null, false, false, null); + } + + protected abstract void download(); + + public final Downloader call() { + WifiManager wifiManager = (WifiManager) + ClientConfig.applicationCallbacks.getApplicationInstance().getSystemService(Context.WIFI_SERVICE); + WifiManager.WifiLock wifiLock = null; + if (wifiManager != null) { + wifiLock = wifiManager.createWifiLock(TAG); + wifiLock.acquire(); + } + + download(); + + if (wifiLock != null) { + wifiLock.release(); + } + + if (result == null) { + throw new IllegalStateException( + "Downloader hasn't created DownloadStatus object"); + } + finished = true; + return this; + } + + public DownloadRequest getDownloadRequest() { + return request; + } + + public DownloadStatus getResult() { + return result; + } + + public boolean isFinished() { + return finished; + } + + public void cancel() { + cancelled = true; + } + +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java new file mode 100644 index 000000000..2d9347b0a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloaderCallback.java @@ -0,0 +1,10 @@ +package de.danoeh.antennapod.core.service.download; + +/** + * Callback used by the Downloader-classes to notify the requester that the + * download has completed. + */ +public interface DownloaderCallback { + + public void onDownloadCompleted(Downloader downloader); +} diff --git a/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 new file mode 100644 index 000000000..6bfd9b4a0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -0,0 +1,252 @@ +package de.danoeh.antennapod.core.service.download; + +import android.util.Log; + +import com.squareup.okhttp.Credentials; +import com.squareup.okhttp.OkHttpClient; +import com.squareup.okhttp.Request; +import com.squareup.okhttp.Response; +import com.squareup.okhttp.ResponseBody; + +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; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.net.HttpURLConnection; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.UnknownHostException; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.StorageUtils; +import de.danoeh.antennapod.core.util.URIUtil; + +public class HttpDownloader extends Downloader { + private static final String TAG = "HttpDownloader"; + + private static final int BUFFER_SIZE = 8 * 1024; + + public HttpDownloader(DownloadRequest request) { + super(request); + } + + @Override + protected void download() { + File destination = new File(request.getDestination()); + final boolean fileExists = destination.exists(); + + if (request.isDeleteOnFailure() && fileExists) { + Log.w(TAG, "File already exists"); + if (request.getFeedfileType() != FeedImage.FEEDFILETYPE_FEEDIMAGE) { + onFail(DownloadError.ERROR_FILE_EXISTS, null); + return; + } else { + onSuccess(); + return; + } + } + + OkHttpClient httpClient = AntennapodHttpClient.getHttpClient(); + RandomAccessFile out = null; + InputStream connection; + ResponseBody responseBody = null; + + try { + final URI uri = URIUtil.getURIFromRequestUrl(request.getSource()); + Request.Builder httpReq = new Request.Builder().url(uri.toURL()) + .header("User-Agent", ClientConfig.USER_AGENT); + + // add authentication information + String userInfo = uri.getUserInfo(); + if (userInfo != null) { + String[] parts = userInfo.split(":"); + if (parts.length == 2) { + String credentials = Credentials.basic(parts[0], parts[1]); + httpReq.header("Authorization", credentials); + } + } else if (!StringUtils.isEmpty(request.getUsername()) && request.getPassword() != null) { + String credentials = Credentials.basic(request.getUsername(), request.getPassword()); + httpReq.header("Authorization", credentials); + } + + // add range header if necessary + if (fileExists) { + request.setSoFar(destination.length()); + httpReq.addHeader("Range", + "bytes=" + request.getSoFar() + "-"); + if (BuildConfig.DEBUG) Log.d(TAG, "Adding range header: " + request.getSoFar()); + } + + Response response = httpClient.newCall(httpReq.build()).execute(); + responseBody = response.body(); + + String contentEncodingHeader = response.header("Content-Encoding"); + + final boolean isGzip = StringUtils.equalsIgnoreCase(contentEncodingHeader, "gzip"); + + if (BuildConfig.DEBUG) + Log.d(TAG, "Response code is " + response.code()); + + if (!response.isSuccessful() || response.body() == null) { + final DownloadError error; + final String details; + if (response.code() == HttpURLConnection.HTTP_UNAUTHORIZED) { + error = DownloadError.ERROR_UNAUTHORIZED; + details = String.valueOf(response.code()); + } else { + error = DownloadError.ERROR_HTTP_DATA_ERROR; + details = String.valueOf(response.code()); + } + onFail(error, details); + return; + } + + if (!StorageUtils.storageAvailable(ClientConfig.applicationCallbacks.getApplicationInstance())) { + onFail(DownloadError.ERROR_DEVICE_NOT_FOUND, null); + return; + } + + connection = new BufferedInputStream(responseBody.byteStream()); + + String contentRangeHeader = (fileExists) ? response.header("Content-Range") : null; + + if (fileExists && response.code() == HttpStatus.SC_PARTIAL_CONTENT + && !StringUtils.isEmpty(contentRangeHeader)) { + String start = contentRangeHeader.substring("bytes ".length(), + contentRangeHeader.indexOf("-")); + request.setSoFar(Long.valueOf(start)); + Log.d(TAG, "Starting download at position " + request.getSoFar()); + + out = new RandomAccessFile(destination, "rw"); + out.seek(request.getSoFar()); + } else { + destination.delete(); + destination.createNewFile(); + out = new RandomAccessFile(destination, "rw"); + } + + + byte[] buffer = new byte[BUFFER_SIZE]; + int count = 0; + request.setStatusMsg(R.string.download_running); + if (BuildConfig.DEBUG) + Log.d(TAG, "Getting size of download"); + request.setSize(responseBody.contentLength() + request.getSoFar()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Size is " + request.getSize()); + if (request.getSize() < 0) { + request.setSize(DownloadStatus.SIZE_UNKNOWN); + } + + long freeSpace = StorageUtils.getFreeSpaceAvailable(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Free space is " + freeSpace); + + if (request.getSize() != DownloadStatus.SIZE_UNKNOWN + && request.getSize() > freeSpace) { + onFail(DownloadError.ERROR_NOT_ENOUGH_SPACE, null); + return; + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting download"); + while (!cancelled + && (count = connection.read(buffer)) != -1) { + out.write(buffer, 0, count); + request.setSoFar(request.getSoFar() + count); + request.setProgressPercent((int) (((double) request + .getSoFar() / (double) request + .getSize()) * 100)); + } + if (cancelled) { + onCancelled(); + } else { + // check if size specified in the response header is the same as the size of the + // written file. This check cannot be made if compression was used + if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN && + request.getSoFar() != request.getSize()) { + onFail(DownloadError.ERROR_IO_ERROR, + "Download completed but size: " + + request.getSoFar() + + " does not equal expected size " + + request.getSize() + ); + return; + } + onSuccess(); + } + + } catch (IllegalArgumentException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_MALFORMED_URL, e.getMessage()); + } catch (SocketTimeoutException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_CONNECTION_ERROR, e.getMessage()); + } catch (UnknownHostException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_UNKNOWN_HOST, e.getMessage()); + } catch (IOException e) { + e.printStackTrace(); + onFail(DownloadError.ERROR_IO_ERROR, e.getMessage()); + } catch (NullPointerException e) { + // might be thrown by connection.getInputStream() + e.printStackTrace(); + onFail(DownloadError.ERROR_CONNECTION_ERROR, request.getSource()); + } finally { + IOUtils.closeQuietly(out); + AntennapodHttpClient.cleanup(); + IOUtils.closeQuietly(responseBody); + } + } + + private void onSuccess() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Download was successful"); + result.setSuccessful(); + } + + private void onFail(DownloadError reason, String reasonDetailed) { + if (BuildConfig.DEBUG) { + Log.d(TAG, "Download failed"); + } + result.setFailed(reason, reasonDetailed); + if (request.isDeleteOnFailure()) { + cleanup(); + } + } + + private void onCancelled() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Download was cancelled"); + result.setCancelled(); + cleanup(); + } + + /** + * Deletes unfinished downloads. + */ + private void cleanup() { + if (request.getDestination() != null) { + File dest = new File(request.getDestination()); + if (dest.exists()) { + boolean rc = dest.delete(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleted file " + dest.getName() + "; Result: " + + rc); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "cleanup() didn't delete file: does not exist."); + } + } + } + +} diff --git a/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 new file mode 100644 index 000000000..a5560e3fb --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -0,0 +1,1138 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.annotation.SuppressLint; +import android.app.Notification; +import android.app.PendingIntent; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.media.AudioManager; +import android.media.MediaMetadataRetriever; +import android.media.MediaPlayer; +import android.media.RemoteControlClient; +import android.media.RemoteControlClient.MetadataEditor; +import android.os.AsyncTask; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.preference.PreferenceManager; +import android.support.v4.app.NotificationCompat; +import android.util.Log; +import android.util.Pair; +import android.view.KeyEvent; +import android.view.SurfaceHolder; +import android.widget.Toast; + +import com.squareup.picasso.Picasso; + +import org.apache.commons.lang3.StringUtils; + +import java.io.IOException; +import java.util.List; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +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.preferences.PlaybackPreferences; +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.QueueAccess; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; +import de.danoeh.antennapod.core.util.playback.Playable; + +/** + * Controls the MediaPlayer that plays a FeedMedia-file + */ +public class PlaybackService extends Service { + public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE"; + public static final String STOP_WIDGET_UPDATE = "de.danoeh.antennapod.STOP_WIDGET_UPDATE"; + /** + * Logging tag + */ + private static final String TAG = "PlaybackService"; + + /** + * Parcelable of type Playable. + */ + public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; + /** + * True if media should be streamed. + */ + public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream"; + /** + * True if playback should be started immediately after media has been + * prepared. + */ + public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared"; + + public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately"; + + public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.core.service.playerStatusChanged"; + public static final String EXTRA_NEW_PLAYER_STATUS = "extra.de.danoeh.antennapod.service.playerStatusChanged.newStatus"; + private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; + private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"; + + public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.core.service.playerNotification"; + public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.core.service.notificationCode"; + public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.core.service.notificationType"; + + /** + * If the PlaybackService receives this action, it will stop playback and + * try to shutdown. + */ + public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.core.service.actionShutdownPlaybackService"; + + /** + * If the PlaybackService receives this action, it will end playback of the + * current episode and load the next episode if there is one available. + */ + public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.skipCurrentEpisode"; + + /** + * Used in NOTIFICATION_TYPE_RELOAD. + */ + public static final int EXTRA_CODE_AUDIO = 1; + public static final int EXTRA_CODE_VIDEO = 2; + + public static final int NOTIFICATION_TYPE_ERROR = 0; + public static final int NOTIFICATION_TYPE_INFO = 1; + public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; + + /** + * Receivers of this intent should update their information about the curently playing media + */ + public static final int NOTIFICATION_TYPE_RELOAD = 3; + /** + * The state of the sleeptimer changed. + */ + public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; + public static final int NOTIFICATION_TYPE_BUFFER_START = 5; + public static final int NOTIFICATION_TYPE_BUFFER_END = 6; + /** + * No more episodes are going to be played. + */ + public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; + + /** + * Playback speed has changed + */ + public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; + + /** + * Returned by getPositionSafe() or getDurationSafe() if the playbackService + * is in an invalid state. + */ + public static final int INVALID_TIME = -1; + + /** + * Is true if service is running. + */ + public static boolean isRunning = false; + /** + * Is true if service has received a valid start command. + */ + public static boolean started = false; + + private static final int NOTIFICATION_ID = 1; + + private RemoteControlClient remoteControlClient; + private PlaybackServiceMediaPlayer mediaPlayer; + private PlaybackServiceTaskManager taskManager; + + private static volatile MediaType currentMediaType = MediaType.UNKNOWN; + + private final IBinder mBinder = new LocalBinder(); + + public class LocalBinder extends Binder { + public PlaybackService getService() { + return PlaybackService.this; + } + } + + @Override + public boolean onUnbind(Intent intent) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received onUnbind event"); + return super.onUnbind(intent); + } + + /** + * Returns an intent which starts an audio- or videoplayer, depending on the + * type of media that is being played. If the playbackservice is not + * running, the type of the last played media will be looked up. + */ + public static Intent getPlayerActivityIntent(Context context) { + if (isRunning) { + return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, currentMediaType); + } else { + if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { + return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.VIDEO); + } else { + return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.AUDIO); + } + } + } + + /** + * Same as getPlayerActivityIntent(context), but here the type of activity + * depends on the FeedMedia that is provided as an argument. + */ + public static Intent getPlayerActivityIntent(Context context, Playable media) { + MediaType mt = media.getMediaType(); + return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt); + } + + @SuppressLint("NewApi") + @Override + public void onCreate() { + super.onCreate(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Service created."); + isRunning = true; + + registerReceiver(headsetDisconnected, new IntentFilter( + Intent.ACTION_HEADSET_PLUG)); + registerReceiver(shutdownReceiver, new IntentFilter( + ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + registerReceiver(audioBecomingNoisy, new IntentFilter( + AudioManager.ACTION_AUDIO_BECOMING_NOISY)); + registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( + ACTION_SKIP_CURRENT_EPISODE)); + remoteControlClient = setupRemoteControlClient(); + taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); + mediaPlayer = new PlaybackServiceMediaPlayer(this, mediaPlayerCallback); + + } + + @SuppressLint("NewApi") + @Override + public void onDestroy() { + super.onDestroy(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Service is about to be destroyed"); + isRunning = false; + started = false; + currentMediaType = MediaType.UNKNOWN; + + unregisterReceiver(headsetDisconnected); + unregisterReceiver(shutdownReceiver); + unregisterReceiver(audioBecomingNoisy); + unregisterReceiver(skipCurrentEpisodeReceiver); + mediaPlayer.shutdown(); + taskManager.shutdown(); + } + + @Override + public IBinder onBind(Intent intent) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received onBind event"); + return mBinder; + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + super.onStartCommand(intent, flags, startId); + + if (BuildConfig.DEBUG) + Log.d(TAG, "OnStartCommand called"); + final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); + final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); + if (keycode == -1 && playable == null) { + Log.e(TAG, "PlaybackService was started with no arguments"); + stopSelf(); + } + + if ((flags & Service.START_FLAG_REDELIVERY) != 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); + stopForeground(true); + } else { + + if (keycode != -1) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received media button event"); + handleKeycode(keycode); + } else { + started = true; + boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, + true); + boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); + boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); + } + } + + return Service.START_REDELIVER_INTENT; + } + + /** + * Handles media button events + */ + private void handleKeycode(int keycode) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Handling keycode: " + keycode); + + final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); + final PlayerStatus status = info.playerStatus; + switch (keycode) { + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + if (status == PlayerStatus.PLAYING) { + if (UserPreferences.isPersistNotify()) { + mediaPlayer.pause(false, true); + } else { + mediaPlayer.pause(true, true); + } + } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { + mediaPlayer.resume(); + } else if (status == PlayerStatus.PREPARING) { + mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared()); + } else if (status == PlayerStatus.INITIALIZED) { + mediaPlayer.setStartWhenPrepared(true); + mediaPlayer.prepare(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PLAY: + if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { + mediaPlayer.resume(); + } else if (status == PlayerStatus.INITIALIZED) { + mediaPlayer.setStartWhenPrepared(true); + mediaPlayer.prepare(); + } + break; + case KeyEvent.KEYCODE_MEDIA_PAUSE: + if (status == PlayerStatus.PLAYING) { + if (UserPreferences.isPersistNotify()) { + mediaPlayer.pause(false, true); + } else { + mediaPlayer.pause(true, true); + } + } + break; + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + mediaPlayer.seekDelta(UserPreferences.getSeekDeltaMs()); + break; + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + mediaPlayer.seekDelta(-UserPreferences.getSeekDeltaMs()); + break; + case KeyEvent.KEYCODE_MEDIA_STOP: + if (status == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, true); + } + stopForeground(true); // gets rid of persistent notification + break; + default: + if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something + String message = String.format(getResources().getString(R.string.unknown_media_key), keycode); + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + break; + } + } + + /** + * Called by a mediaplayer Activity as soon as it has prepared its + * mediaplayer. + */ + public void setVideoSurface(SurfaceHolder sh) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting display"); + mediaPlayer.setVideoSurface(sh); + } + + /** + * Called when the surface holder of the mediaplayer has to be changed. + */ + private void resetVideoSurface() { + taskManager.cancelPositionSaver(); + mediaPlayer.resetVideoSurface(); + } + + public void notifyVideoSurfaceAbandoned() { + stopForeground(true); + mediaPlayer.resetVideoSurface(); + } + + private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { + @Override + public void positionSaverTick() { + saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL); + } + + @Override + public void onSleepTimerExpired() { + mediaPlayer.pause(true, true); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + + @Override + public void onWidgetUpdaterTick() { + updateWidget(); + } + + @Override + public void onChapterLoaded(Playable media) { + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); + } + }; + + private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { + @Override + public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { + currentMediaType = mediaPlayer.getCurrentMediaType(); + switch (newInfo.playerStatus) { + case INITIALIZED: + writePlaybackPreferences(); + break; + + case PREPARED: + taskManager.startChapterLoader(newInfo.playable); + break; + + case PAUSED: + taskManager.cancelPositionSaver(); + saveCurrentPosition(false, 0); + taskManager.cancelWidgetUpdater(); + if (UserPreferences.isPersistNotify() && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // do not remove notification on pause based on user pref and whether android version supports expanded notifications + } else { + // remove notifcation on pause + stopForeground(true); + } + break; + + case STOPPED: + //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); + //stopSelf(); + break; + + case PLAYING: + if (BuildConfig.DEBUG) + Log.d(TAG, "Audiofocus successfully requested"); + if (BuildConfig.DEBUG) + Log.d(TAG, "Resuming/Starting playback"); + + taskManager.startPositionSaver(); + taskManager.startWidgetUpdater(); + setupNotification(newInfo); + break; + case ERROR: + writePlaybackPreferencesNoMediaPlaying(); + break; + + } + + Intent statusUpdate = new Intent(ACTION_PLAYER_STATUS_CHANGED); + statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal()); + sendBroadcast(statusUpdate); + sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); + updateWidget(); + refreshRemoteControlClientState(newInfo); + bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); + bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); + } + + @Override + public void shouldStop() { + stopSelf(); + } + + @Override + public void playbackSpeedChanged(float s) { + sendNotificationBroadcast( + NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0); + } + + @Override + public void onBufferingUpdate(int percent) { + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent); + } + + @Override + public boolean onMediaPlayerInfo(int code) { + switch (code) { + case MediaPlayer.MEDIA_INFO_BUFFERING_START: + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0); + return true; + case MediaPlayer.MEDIA_INFO_BUFFERING_END: + sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0); + return true; + default: + return false; + } + } + + @Override + public boolean onMediaPlayerError(Object inObj, int what, int extra) { + final String TAG = "PlaybackService.onErrorListener"; + Log.w(TAG, "An error has occured: " + what + " " + extra); + if (mediaPlayer.getPSMPInfo().playerStatus == PlayerStatus.PLAYING) { + mediaPlayer.pause(true, false); + } + sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); + writePlaybackPreferencesNoMediaPlaying(); + stopSelf(); + return true; + } + + @Override + public boolean endPlayback(boolean playNextEpisode) { + PlaybackService.this.endPlayback(true); + return true; + } + + @Override + public RemoteControlClient getRemoteControlClient() { + return remoteControlClient; + } + }; + + private void endPlayback(boolean playNextEpisode) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Playback ended"); + + final Playable media = mediaPlayer.getPSMPInfo().playable; + if (media == null) { + Log.e(TAG, "Cannot end playback: media was null"); + return; + } + + taskManager.cancelPositionSaver(); + + boolean isInQueue = false; + FeedItem nextItem = null; + + if (media instanceof FeedMedia) { + FeedItem item = ((FeedMedia) media).getItem(); + DBWriter.markItemRead(PlaybackService.this, item, true, true); + + try { + final List<FeedItem> queue = taskManager.getQueue(); + isInQueue = QueueAccess.ItemListAccess(queue).contains(((FeedMedia) media).getItem().getId()); + nextItem = DBTasks.getQueueSuccessorOfItem(this, item.getId(), queue); + } catch (InterruptedException e) { + e.printStackTrace(); + // isInQueue remains false + } + if (isInQueue) { + DBWriter.removeQueueItem(PlaybackService.this, item.getId(), true); + } + DBWriter.addItemToPlaybackHistory(PlaybackService.this, (FeedMedia) media); + + // auto-flattr if enabled + if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { + DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); + } + } + + // Load next episode if previous episode was in the queue and if there + // is an episode in the queue left. + // Start playback immediately if continuous playback is enabled + Playable nextMedia = null; + boolean loadNextItem = ClientConfig.playbackServiceCallbacks.useQueue() && + isInQueue && + nextItem != null; + + playNextEpisode = playNextEpisode && + loadNextItem && + UserPreferences.isFollowQueue(); + + if (loadNextItem) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading next item in queue"); + nextMedia = nextItem.getMedia(); + } + final boolean prepareImmediately; + final boolean startWhenPrepared; + final boolean stream; + + if (playNextEpisode) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Playback of next episode will start immediately."); + prepareImmediately = startWhenPrepared = true; + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "No more episodes available to play"); + + prepareImmediately = startWhenPrepared = false; + stopForeground(true); + stopWidgetUpdater(); + } + + writePlaybackPreferencesNoMediaPlaying(); + if (nextMedia != null) { + stream = !media.localFileAvailable(); + mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); + sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, + (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); + } else { + sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); + mediaPlayer.stop(); + //stopSelf(); + } + } + + public void setSleepTimer(long waitingTime) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + + " milliseconds"); + taskManager.setSleepTimer(waitingTime); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + public void disableSleepTimer() { + taskManager.disableSleepTimer(); + sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); + } + + private void writePlaybackPreferencesNoMediaPlaying() { + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()).edit(); + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.commit(); + } + + + private void writePlaybackPreferences() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Writing playback preferences"); + + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()).edit(); + PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); + MediaType mediaType = mediaPlayer.getCurrentMediaType(); + boolean stream = mediaPlayer.isStreaming(); + + if (info.playable != null) { + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, + info.playable.getPlayableType()); + editor.putBoolean( + PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, + stream); + editor.putBoolean( + PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO, + mediaType == MediaType.VIDEO); + if (info.playable instanceof FeedMedia) { + FeedMedia fMedia = (FeedMedia) info.playable; + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + fMedia.getItem().getFeed().getId()); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + fMedia.getId()); + } else { + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + } + info.playable.writeToPreferences(editor); + } else { + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, + PlaybackPreferences.NO_MEDIA_PLAYING); + } + + editor.commit(); + } + + /** + * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. + */ + private void postStatusUpdateIntent() { + sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); + } + + private void sendNotificationBroadcast(int type, int code) { + Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION); + intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); + intent.putExtra(EXTRA_NOTIFICATION_CODE, code); + sendBroadcast(intent); + } + + /** + * Used by setupNotification to load notification data in another thread. + */ + private AsyncTask<Void, Void, Void> notificationSetupTask; + + /** + * Prepares notification and starts the service in the foreground. + */ + @SuppressLint("NewApi") + private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) { + final PendingIntent pIntent = PendingIntent.getActivity(this, 0, + PlaybackService.getPlayerActivityIntent(this), + PendingIntent.FLAG_UPDATE_CURRENT); + + if (notificationSetupTask != null) { + notificationSetupTask.cancel(true); + } + notificationSetupTask = new AsyncTask<Void, Void, Void>() { + Bitmap icon = null; + + @Override + protected Void doInBackground(Void... params) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting background work"); + if (android.os.Build.VERSION.SDK_INT >= 11) { + if (info.playable != null) { + try { + int iconSize = getResources().getDimensionPixelSize( + android.R.dimen.notification_large_icon_width); + icon = Picasso.with(PlaybackService.this) + .load(info.playable.getImageUri()) + .resize(iconSize, iconSize) + .get(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + } + 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(); + final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext()); + + if (!isCancelled() && info.playerStatus == PlayerStatus.PLAYING + && info.playable != null) { + String contentText = info.playable.getFeedTitle(); + String contentTitle = info.playable.getEpisodeTitle(); + Notification notification = null; + if (android.os.Build.VERSION.SDK_INT >= 16) { + Intent pauseButtonIntent = new Intent( // pause button intent + PlaybackService.this, PlaybackService.class); + pauseButtonIntent.putExtra( + MediaButtonReceiver.EXTRA_KEYCODE, + KeyEvent.KEYCODE_MEDIA_PAUSE); + PendingIntent pauseButtonPendingIntent = PendingIntent + .getService(PlaybackService.this, 0, + pauseButtonIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + Intent playButtonIntent = new Intent( // play button intent + PlaybackService.this, PlaybackService.class); + playButtonIntent.putExtra( + MediaButtonReceiver.EXTRA_KEYCODE, + KeyEvent.KEYCODE_MEDIA_PLAY); + PendingIntent playButtonPendingIntent = PendingIntent + .getService(PlaybackService.this, 1, + playButtonIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + Intent stopButtonIntent = new Intent( // stop button intent + PlaybackService.this, PlaybackService.class); + stopButtonIntent.putExtra( + MediaButtonReceiver.EXTRA_KEYCODE, + KeyEvent.KEYCODE_MEDIA_STOP); + PendingIntent stopButtonPendingIntent = PendingIntent + .getService(PlaybackService.this, 2, + stopButtonIntent, + PendingIntent.FLAG_UPDATE_CURRENT); + Notification.Builder notificationBuilder = new Notification.Builder( + PlaybackService.this) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setOngoing(true) + .setContentIntent(pIntent) + .setLargeIcon(icon) + .setSmallIcon(smallIcon) + .setPriority(UserPreferences.getNotifyPriority()) // set notification priority + .addAction(android.R.drawable.ic_media_play, //play action + getString(R.string.play_label), + playButtonPendingIntent) + .addAction(android.R.drawable.ic_media_pause, //pause action + getString(R.string.pause_label), + pauseButtonPendingIntent) + .addAction(android.R.drawable.ic_menu_close_clear_cancel, // stop action + getString(R.string.stop_label), + stopButtonPendingIntent); + notification = notificationBuilder.build(); + } else { + NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder( + PlaybackService.this) + .setContentTitle(contentTitle) + .setContentText(contentText).setOngoing(true) + .setContentIntent(pIntent).setLargeIcon(icon) + .setSmallIcon(smallIcon); + notification = notificationBuilder.getNotification(); + } + if (newInfo.playerStatus == PlayerStatus.PLAYING) { + startForeground(NOTIFICATION_ID, notification); + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Notification set up"); + } + } + + }; + if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.GINGERBREAD_MR1) { + notificationSetupTask + .executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } else { + notificationSetupTask.execute(); + } + + } + + /** + * Saves the current position of the media file to the DB + * + * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects + * @param deltaPlayedDuration value by which played_duration should be increased. + */ + private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { + int position = getCurrentPosition(); + int duration = getDuration(); + float playbackSpeed = getCurrentPlaybackSpeed(); + final Playable playable = mediaPlayer.getPSMPInfo().playable; + if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Saving current position to " + position); + if (updatePlayedDuration && playable instanceof FeedMedia) { + FeedMedia m = (FeedMedia) playable; + FeedItem item = m.getItem(); + m.setPlayedDuration(m.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); + // Auto flattr + if (isAutoFlattrable(m) && + (m.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { + + if (BuildConfig.DEBUG) + Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(m.getPlayedDuration()) + + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration)); + DBTasks.flattrItemIfLoggedIn(this, item); + } + } + playable.saveCurrentPosition(PreferenceManager + .getDefaultSharedPreferences(getApplicationContext()), + position + ); + } + } + + private void stopWidgetUpdater() { + taskManager.cancelWidgetUpdater(); + sendBroadcast(new Intent(STOP_WIDGET_UPDATE)); + } + + private void updateWidget() { + PlaybackService.this.sendBroadcast(new Intent( + FORCE_WIDGET_UPDATE)); + } + + public boolean sleepTimerActive() { + return taskManager.isSleepTimerActive(); + } + + public long getSleepTimerTimeLeft() { + return taskManager.getSleepTimerTimeLeft(); + } + + @SuppressLint("NewApi") + private RemoteControlClient setupRemoteControlClient() { + if (Build.VERSION.SDK_INT < 14) { + return null; + } + + Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + mediaButtonIntent.setComponent(new ComponentName(getPackageName(), + MediaButtonReceiver.class.getName())); + PendingIntent mediaPendingIntent = PendingIntent.getBroadcast( + getApplicationContext(), 0, mediaButtonIntent, 0); + remoteControlClient = new RemoteControlClient(mediaPendingIntent); + int controlFlags; + if (android.os.Build.VERSION.SDK_INT < 16) { + controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE + | RemoteControlClient.FLAG_KEY_MEDIA_NEXT; + } else { + controlFlags = RemoteControlClient.FLAG_KEY_MEDIA_PLAY_PAUSE; + } + remoteControlClient.setTransportControlFlags(controlFlags); + return remoteControlClient; + } + + /** + * Refresh player status and metadata. + */ + @SuppressLint("NewApi") + private void refreshRemoteControlClientState(PlaybackServiceMediaPlayer.PSMPInfo info) { + if (android.os.Build.VERSION.SDK_INT >= 14) { + if (remoteControlClient != null) { + switch (info.playerStatus) { + case PLAYING: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_PLAYING); + break; + case PAUSED: + case INITIALIZED: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_PAUSED); + break; + case STOPPED: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_STOPPED); + break; + case ERROR: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_ERROR); + break; + default: + remoteControlClient + .setPlaybackState(RemoteControlClient.PLAYSTATE_BUFFERING); + } + if (info.playable != null) { + MetadataEditor editor = remoteControlClient + .editMetadata(false); + editor.putString(MediaMetadataRetriever.METADATA_KEY_TITLE, + info.playable.getEpisodeTitle()); + + editor.putString(MediaMetadataRetriever.METADATA_KEY_ALBUM, + info.playable.getFeedTitle()); + + editor.apply(); + } + if (BuildConfig.DEBUG) + Log.d(TAG, "RemoteControlClient state was refreshed"); + } + } + } + + private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) { + boolean isPlaying = false; + + if (info.playerStatus == PlayerStatus.PLAYING) { + isPlaying = true; + } + + if (info.playable != null) { + Intent i = new Intent(whatChanged); + i.putExtra("id", 1); + i.putExtra("artist", ""); + i.putExtra("album", info.playable.getFeedTitle()); + i.putExtra("track", info.playable.getEpisodeTitle()); + i.putExtra("playing", isPlaying); + final List<FeedItem> queue = taskManager.getQueueIfLoaded(); + if (queue != null) { + i.putExtra("ListSize", queue.size()); + } + i.putExtra("duration", info.playable.getDuration()); + i.putExtra("position", info.playable.getPosition()); + sendBroadcast(i); + } + } + + /** + * Pauses playback when the headset is disconnected and the preference is + * set + */ + private BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { + private static final String TAG = "headsetDisconnected"; + private static final int UNPLUGGED = 0; + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { + int state = intent.getIntExtra("state", -1); + if (state != -1) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Headset plug event. State is " + state); + if (state == UNPLUGGED) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Headset was unplugged during playback."); + pauseIfPauseOnDisconnect(); + } + } else { + Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); + } + } + } + }; + + private BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + // sound is about to change, eg. bluetooth -> speaker + if (BuildConfig.DEBUG) + Log.d(TAG, "Pausing playback because audio is becoming noisy"); + pauseIfPauseOnDisconnect(); + } + // android.media.AUDIO_BECOMING_NOISY + }; + + /** + * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. + */ + private void pauseIfPauseOnDisconnect() { + if (UserPreferences.isPauseOnHeadsetDisconnect()) { + if (UserPreferences.isPersistNotify()) { + mediaPlayer.pause(false, true); + } else { + mediaPlayer.pause(true, true); + } + } + } + + private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { + stopSelf(); + } + } + + }; + + private BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (StringUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); + mediaPlayer.endPlayback(); + } + } + }; + + public static MediaType getCurrentMediaType() { + return currentMediaType; + } + + public void resume() { + mediaPlayer.resume(); + } + + public void prepare() { + mediaPlayer.prepare(); + } + + public void pause(boolean abandonAudioFocus, boolean reinit) { + mediaPlayer.pause(abandonAudioFocus, reinit); + } + + public void reinit() { + mediaPlayer.reinit(); + } + + public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() { + return mediaPlayer.getPSMPInfo(); + } + + public PlayerStatus getStatus() { + return mediaPlayer.getPSMPInfo().playerStatus; + } + + public Playable getPlayable() { + return mediaPlayer.getPSMPInfo().playable; + } + + public void setSpeed(float speed) { + mediaPlayer.setSpeed(speed); + } + + public boolean canSetSpeed() { + return mediaPlayer.canSetSpeed(); + } + + public float getCurrentPlaybackSpeed() { + return mediaPlayer.getPlaybackSpeed(); + } + + public boolean isStartWhenPrepared() { + return mediaPlayer.isStartWhenPrepared(); + } + + public void setStartWhenPrepared(boolean s) { + mediaPlayer.setStartWhenPrepared(s); + } + + + public void seekTo(final int t) { + mediaPlayer.seekTo(t); + } + + + public void seekDelta(final int d) { + mediaPlayer.seekDelta(d); + } + + /** + * @see de.danoeh.antennapod.core.service.playback.PlaybackServiceMediaPlayer#seekToChapter(de.danoeh.antennapod.core.feed.Chapter) + */ + public void seekToChapter(Chapter c) { + mediaPlayer.seekToChapter(c); + } + + /** + * call getDuration() on mediaplayer or return INVALID_TIME if player is in + * an invalid state. + */ + public int getDuration() { + return mediaPlayer.getDuration(); + } + + /** + * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player + * is in an invalid state. + */ + public int getCurrentPosition() { + return mediaPlayer.getPosition(); + } + + public boolean isStreaming() { + return mediaPlayer.isStreaming(); + } + + public Pair<Integer, Integer> getVideoSize() { + return mediaPlayer.getVideoSize(); + } + + private boolean isAutoFlattrable(Playable p) { + if (p != null && p instanceof FeedMedia) { + FeedMedia media = (FeedMedia) p; + FeedItem item = ((FeedMedia) p).getItem(); + return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred(); + } else { + return false; + } + } +} diff --git a/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 new file mode 100644 index 000000000..dbf870eac --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceMediaPlayer.java @@ -0,0 +1,979 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.content.ComponentName; +import android.content.Context; +import android.media.AudioManager; +import android.media.RemoteControlClient; +import android.net.wifi.WifiManager; +import android.os.PowerManager; +import android.telephony.TelephonyManager; +import android.util.Log; +import android.util.Pair; +import android.view.SurfaceHolder; + +import org.apache.commons.lang3.Validate; + +import java.io.IOException; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.ReentrantLock; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; +import de.danoeh.antennapod.core.util.playback.AudioPlayer; +import de.danoeh.antennapod.core.util.playback.IPlayer; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.playback.VideoPlayer; + +/** + * Manages the MediaPlayer object of the PlaybackService. + */ +public class PlaybackServiceMediaPlayer { + public static final String TAG = "PlaybackServiceMediaPlayer"; + + /** + * Return value of some PSMP methods if the method call failed. + */ + public static final int INVALID_TIME = -1; + + private final AudioManager audioManager; + + private volatile PlayerStatus playerStatus; + private volatile PlayerStatus statusBeforeSeeking; + private volatile IPlayer mediaPlayer; + private volatile Playable media; + + private volatile boolean stream; + private volatile MediaType mediaType; + private volatile AtomicBoolean startWhenPrepared; + private volatile boolean pausedBecauseOfTransientAudiofocusLoss; + private volatile Pair<Integer, Integer> videoSize; + + /** + * Some asynchronous calls might change the state of the MediaPlayer object. Therefore calls in other threads + * have to wait until these operations have finished. + */ + private final ReentrantLock playerLock; + + private final PSMPCallback callback; + private final Context context; + + private final ThreadPoolExecutor executor; + + /** + * A wifi-lock that is acquired if the media file is being streamed. + */ + private WifiManager.WifiLock wifiLock; + + public PlaybackServiceMediaPlayer(Context context, PSMPCallback callback) { + Validate.notNull(context); + Validate.notNull(callback); + + this.context = context; + this.callback = callback; + this.audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + this.playerLock = new ReentrantLock(); + this.startWhenPrepared = new AtomicBoolean(false); + executor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.MINUTES, new LinkedBlockingDeque<Runnable>(), + new RejectedExecutionHandler() { + @Override + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + if (BuildConfig.DEBUG) Log.d(TAG, "Rejected execution of runnable"); + } + } + ); + + mediaPlayer = null; + statusBeforeSeeking = null; + pausedBecauseOfTransientAudiofocusLoss = false; + mediaType = MediaType.UNKNOWN; + playerStatus = PlayerStatus.STOPPED; + videoSize = null; + } + + /** + * Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing + * episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will + * not do anything. + * Whether playback starts immediately depends on the given parameters. See below for more details. + * <p/> + * States: + * During execution of the method, the object will be in the INITIALIZING state. The end state depends on the given parameters. + * <p/> + * If 'prepareImmediately' is set to true, the method will go into PREPARING state and after that into PREPARED state. If + * 'startWhenPrepared' is set to true, the method will additionally go into PLAYING state. + * <p/> + * If an unexpected error occurs while loading the Playable's metadata or while setting the MediaPlayers data source, the object + * will enter the ERROR state. + * <p/> + * This method is executed on an internal executor service. + * + * @param playable The Playable object that is supposed to be played. This parameter must not be null. + * @param stream The type of playback. If false, the Playable object MUST provide access to a locally available file via + * getLocalMediaUrl. If true, the Playable object MUST provide access to a resource that can be streamed by + * the Android MediaPlayer via getStreamUrl. + * @param startWhenPrepared Sets the 'startWhenPrepared' flag. This flag determines whether playback will start immediately after the + * episode has been prepared for playback. Setting this flag to true does NOT mean that the episode will be prepared + * for playback immediately (see 'prepareImmediately' parameter for more details) + * @param prepareImmediately Set to true if the method should also prepare the episode for playback. + */ + public void playMediaObject(final Playable playable, final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) { + Validate.notNull(playable); + + if (BuildConfig.DEBUG) Log.d(TAG, "Play media object."); + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + try { + playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately); + } catch (RuntimeException e) { + e.printStackTrace(); + throw e; + } finally { + playerLock.unlock(); + } + } + }); + } + + /** + * Internal implementation of playMediaObject. This method has an additional parameter that allows the caller to force a media player reset even if + * the given playable parameter is the same object as the currently playing media. + * <p/> + * This method requires the playerLock and is executed on the caller's thread. + * + * @see #playMediaObject(de.danoeh.antennapod.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); + if (!playerLock.isHeldByCurrentThread()) + throw new IllegalStateException("method requires playerLock"); + + + if (media != null) { + if (!forceReset && media.getIdentifier().equals(playable.getIdentifier())) { + // episode is already playing -> ignore method call + if (BuildConfig.DEBUG) + Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing."); + return; + } else { + // stop playback of this episode + if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED) { + mediaPlayer.stop(); + } + setPlayerStatus(PlayerStatus.INDETERMINATE, null); + } + } + + this.media = playable; + this.stream = stream; + this.mediaType = media.getMediaType(); + this.videoSize = null; + createMediaPlayer(); + PlaybackServiceMediaPlayer.this.startWhenPrepared.set(startWhenPrepared); + setPlayerStatus(PlayerStatus.INITIALIZING, media); + try { + media.loadMetadata(); + if (stream) { + mediaPlayer.setDataSource(media.getStreamUrl()); + } else { + mediaPlayer.setDataSource(media.getLocalMediaUrl()); + } + setPlayerStatus(PlayerStatus.INITIALIZED, media); + + if (mediaType == MediaType.VIDEO) { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + // vp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT); + } + + if (prepareImmediately) { + setPlayerStatus(PlayerStatus.PREPARING, media); + mediaPlayer.prepare(); + onPrepared(startWhenPrepared); + } + + } catch (Playable.PlayableException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } catch (IOException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } catch (IllegalStateException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } + } + + + /** + * Resumes playback if the PSMP object is in PREPARED or PAUSED state. If the PSMP object is in an invalid state. + * nothing will happen. + * <p/> + * This method is executed on an internal executor service. + */ + public void resume() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + resumeSync(); + playerLock.unlock(); + } + }); + } + + private void resumeSync() { + if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) { + int focusGained = audioManager.requestAudioFocus( + audioFocusChangeListener, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN); + if (focusGained == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { + acquireWifiLockIfNecessary(); + setSpeed(Float.parseFloat(UserPreferences.getPlaybackSpeed())); + mediaPlayer.start(); + if (playerStatus == PlayerStatus.PREPARED && media.getPosition() > 0) { + mediaPlayer.seekTo(media.getPosition()); + } + + setPlayerStatus(PlayerStatus.PLAYING, media); + pausedBecauseOfTransientAudiofocusLoss = false; + if (android.os.Build.VERSION.SDK_INT >= 14) { + RemoteControlClient remoteControlClient = callback.getRemoteControlClient(); + if (remoteControlClient != null) { + audioManager + .registerRemoteControlClient(remoteControlClient); + } + } + audioManager + .registerMediaButtonEventReceiver(new ComponentName(context.getPackageName(), + MediaButtonReceiver.class.getName())); + media.onPlaybackStart(); + + } else { + if (BuildConfig.DEBUG) Log.e(TAG, "Failed to request audio focus"); + } + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is " + playerStatus); + } + } + + + /** + * Saves the current position and pauses playback. Note that, if audiofocus + * is abandoned, the lockscreen controls will also disapear. + * <p/> + * This method is executed on an internal executor service. + * + * @param abandonFocus is true if the service should release audio focus + * @param reinit is true if service should reinit after pausing if the media + * file is being streamed + */ + public void pause(final boolean abandonFocus, final boolean reinit) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + releaseWifiLockIfNecessary(); + if (playerStatus == PlayerStatus.PLAYING) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Pausing playback."); + mediaPlayer.pause(); + setPlayerStatus(PlayerStatus.PAUSED, media); + + if (abandonFocus) { + audioManager.abandonAudioFocus(audioFocusChangeListener); + pausedBecauseOfTransientAudiofocusLoss = false; + } + if (stream && reinit) { + reinit(); + } + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Ignoring call to pause: Player is in " + playerStatus + " state"); + } + + playerLock.unlock(); + } + }); + } + + /** + * Prepared media player for playback if the service is in the INITALIZED + * state. + * <p/> + * This method is executed on an internal executor service. + */ + public void prepare() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + if (playerStatus == PlayerStatus.INITIALIZED) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Preparing media player"); + setPlayerStatus(PlayerStatus.PREPARING, media); + try { + mediaPlayer.prepare(); + onPrepared(startWhenPrepared.get()); + } catch (IOException e) { + e.printStackTrace(); + setPlayerStatus(PlayerStatus.ERROR, null); + } + } + playerLock.unlock(); + + } + }); + } + + /** + * Called after media player has been prepared. This method is executed on the caller's thread. + */ + void onPrepared(final boolean startWhenPrepared) { + playerLock.lock(); + + if (playerStatus != PlayerStatus.PREPARING) { + playerLock.unlock(); + throw new IllegalStateException("Player is not in PREPARING state"); + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "Resource prepared"); + + if (mediaType == MediaType.VIDEO) { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight()); + } + + if (media.getPosition() > 0) { + mediaPlayer.seekTo(media.getPosition()); + } + + if (media.getDuration() == 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting duration of media"); + media.setDuration(mediaPlayer.getDuration()); + } + setPlayerStatus(PlayerStatus.PREPARED, media); + + if (startWhenPrepared) { + resumeSync(); + } + + playerLock.unlock(); + } + + /** + * Resets the media player and moves it into INITIALIZED state. + * <p/> + * This method is executed on an internal executor service. + */ + public void reinit() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + releaseWifiLockIfNecessary(); + if (media != null) { + playMediaObject(media, true, stream, startWhenPrepared.get(), false); + } else if (mediaPlayer != null) { + mediaPlayer.reset(); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null"); + } + playerLock.unlock(); + } + }); + } + + + /** + * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. + * + * @param t The position to seek to in milliseconds. t < 0 will be interpreted as t = 0 + * <p/> + * This method is executed on the caller's thread. + */ + private void seekToSync(int t) { + if (t < 0) { + t = 0; + } + playerLock.lock(); + + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + if (stream) { + // statusBeforeSeeking = playerStatus; + // setPlayerStatus(PlayerStatus.SEEKING, media); + } + mediaPlayer.seekTo(t); + + } else if (playerStatus == PlayerStatus.INITIALIZED) { + media.setPosition(t); + startWhenPrepared.set(true); + prepare(); + } + playerLock.unlock(); + } + + /** + * Seeks to the specified position. If the PSMP object is in an invalid state, this method will do nothing. + * Invalid time values (< 0) will be ignored. + * <p/> + * This method is executed on an internal executor service. + */ + public void seekTo(final int t) { + executor.submit(new Runnable() { + @Override + public void run() { + seekToSync(t); + } + }); + } + + /** + * Seek a specific position from the current position + * + * @param d offset from current position (positive or negative) + */ + public void seekDelta(final int d) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + int currentPosition = getPosition(); + if (currentPosition != INVALID_TIME) { + seekToSync(currentPosition + d); + } else { + Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta"); + } + + playerLock.unlock(); + } + }); + } + + /** + * Seek to the start of the specified chapter. + */ + public void seekToChapter(Chapter c) { + Validate.notNull(c); + + seekTo((int) c.getStart()); + } + + /** + * Returns the duration of the current media object or INVALID_TIME if the duration could not be retrieved. + */ + public int getDuration() { + if (!playerLock.tryLock()) { + return INVALID_TIME; + } + + int retVal = INVALID_TIME; + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + retVal = mediaPlayer.getDuration(); + } else if (media != null && media.getDuration() > 0) { + retVal = media.getDuration(); + } + + playerLock.unlock(); + return retVal; + } + + /** + * Returns the position of the current media object or INVALID_TIME if the position could not be retrieved. + */ + public int getPosition() { + if (!playerLock.tryLock()) { + return INVALID_TIME; + } + + int retVal = INVALID_TIME; + if (playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) { + retVal = mediaPlayer.getCurrentPosition(); + } else if (media != null && media.getPosition() > 0) { + retVal = media.getPosition(); + } + + playerLock.unlock(); + return retVal; + } + + public boolean isStartWhenPrepared() { + return startWhenPrepared.get(); + } + + public void setStartWhenPrepared(boolean startWhenPrepared) { + this.startWhenPrepared.set(startWhenPrepared); + } + + /** + * Returns true if the playback speed can be adjusted. + */ + public boolean canSetSpeed() { + boolean retVal = false; + if (mediaPlayer != null && media != null && media.getMediaType() == MediaType.AUDIO) { + retVal = (mediaPlayer).canSetSpeed(); + } + return retVal; + } + + /** + * Sets the playback speed. + * This method is executed on the caller's thread. + */ + private void setSpeedSync(float speed) { + playerLock.lock(); + if (media != null && media.getMediaType() == MediaType.AUDIO) { + if (mediaPlayer.canSetSpeed()) { + mediaPlayer.setPlaybackSpeed((float) speed); + if (BuildConfig.DEBUG) + Log.d(TAG, "Playback speed was set to " + speed); + callback.playbackSpeedChanged(speed); + } + } + playerLock.unlock(); + } + + /** + * Sets the playback speed. + * This method is executed on an internal executor service. + */ + public void setSpeed(final float speed) { + executor.submit(new Runnable() { + @Override + public void run() { + setSpeedSync(speed); + } + }); + } + + /** + * Returns the current playback speed. If the playback speed could not be retrieved, 1 is returned. + */ + public float getPlaybackSpeed() { + if (!playerLock.tryLock()) { + return 1; + } + + float retVal = 1; + if ((playerStatus == PlayerStatus.PLAYING + || playerStatus == PlayerStatus.PAUSED + || playerStatus == PlayerStatus.PREPARED) && mediaPlayer.canSetSpeed()) { + retVal = mediaPlayer.getCurrentSpeedMultiplier(); + } + playerLock.unlock(); + return retVal; + } + + public MediaType getCurrentMediaType() { + return mediaType; + } + + public boolean isStreaming() { + return stream; + } + + + /** + * Releases internally used resources. This method should only be called when the object is not used anymore. + */ + public void shutdown() { + executor.shutdown(); + if (mediaPlayer != null) { + mediaPlayer.release(); + } + releaseWifiLockIfNecessary(); + } + + public void setVideoSurface(final SurfaceHolder surface) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (mediaPlayer != null) { + mediaPlayer.setDisplay(surface); + } + playerLock.unlock(); + } + }); + } + + public void resetVideoSurface() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Resetting video surface"); + mediaPlayer.setDisplay(null); + reinit(); + playerLock.unlock(); + } + }); + } + + /** + * Return width and height of the currently playing video as a pair. + * + * @return Width and height as a Pair or null if the video size could not be determined. The method might still + * return an invalid non-null value if the getVideoWidth() and getVideoHeight() methods of the media player return + * invalid values. + */ + public Pair<Integer, Integer> getVideoSize() { + if (!playerLock.tryLock()) { + // use cached value if lock can't be aquired + return videoSize; + } + Pair<Integer, Integer> res; + if (mediaPlayer == null || playerStatus == PlayerStatus.ERROR || mediaType != MediaType.VIDEO) { + res = null; + } else { + VideoPlayer vp = (VideoPlayer) mediaPlayer; + videoSize = new Pair<Integer, Integer>(vp.getVideoWidth(), vp.getVideoHeight()); + res = videoSize; + } + playerLock.unlock(); + return res; + } + + /** + * Returns a PSMInfo object that contains information about the current state of the PSMP object. + * + * @return The PSMPInfo object. + */ + public synchronized PSMPInfo getPSMPInfo() { + return new PSMPInfo(playerStatus, media); + } + + /** + * Sets the player status of the PSMP object. PlayerStatus and media attributes have to be set at the same time + * so that getPSMPInfo can't return an invalid state (e.g. status is PLAYING, but media is null). + * <p/> + * This method will notify the callback about the change of the player status (even if the new status is the same + * as the old one). + * + * @param newStatus The new PlayerStatus. This must not be null. + * @param newMedia The new playable object of the PSMP object. This can be null. + */ + private synchronized void setPlayerStatus(PlayerStatus newStatus, Playable newMedia) { + Validate.notNull(newStatus); + + if (BuildConfig.DEBUG) Log.d(TAG, "Setting player status to " + newStatus); + + this.playerStatus = newStatus; + this.media = newMedia; + callback.statusChanged(new PSMPInfo(playerStatus, media)); + } + + private IPlayer createMediaPlayer() { + if (mediaPlayer != null) { + mediaPlayer.release(); + } + if (media == null || media.getMediaType() == MediaType.VIDEO) { + mediaPlayer = new VideoPlayer(); + } else { + mediaPlayer = new AudioPlayer(context); + } + mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); + mediaPlayer.setWakeMode(context, PowerManager.PARTIAL_WAKE_LOCK); + return setMediaPlayerListeners(mediaPlayer); + } + + private final AudioManager.OnAudioFocusChangeListener audioFocusChangeListener = new AudioManager.OnAudioFocusChangeListener() { + + @Override + public void onAudioFocusChange(final int focusChange) { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + + // If there is an incoming call, playback should be paused permanently + TelephonyManager tm = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + final int callState = (tm != null) ? tm.getCallState() : 0; + if (BuildConfig.DEBUG) Log.d(TAG, "Call state: " + callState); + Log.i(TAG, "Call state:" + callState); + + if (focusChange == AudioManager.AUDIOFOCUS_LOSS || callState != TelephonyManager.CALL_STATE_IDLE) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Lost audio focus"); + pause(true, false); + callback.shouldStop(); + } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Gained audio focus"); + if (pausedBecauseOfTransientAudiofocusLoss) { // we paused => play now + resume(); + } else { // we ducked => raise audio level back + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_RAISE, 0); + } + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) { + if (playerStatus == PlayerStatus.PLAYING) { + if (!UserPreferences.shouldPauseForFocusLoss()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Ducking..."); + audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC, + AudioManager.ADJUST_LOWER, 0); + pausedBecauseOfTransientAudiofocusLoss = false; + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Could duck, but won't, pausing..."); + pause(false, false); + pausedBecauseOfTransientAudiofocusLoss = true; + } + } + } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) { + if (playerStatus == PlayerStatus.PLAYING) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Lost audio focus temporarily. Pausing..."); + pause(false, false); + pausedBecauseOfTransientAudiofocusLoss = true; + } + } + playerLock.unlock(); + } + }); + } + }; + + + public void endPlayback() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + releaseWifiLockIfNecessary(); + + if (playerStatus != PlayerStatus.INDETERMINATE) { + setPlayerStatus(PlayerStatus.INDETERMINATE, media); + } + if (mediaPlayer != null) { + mediaPlayer.reset(); + + } + audioManager.abandonAudioFocus(audioFocusChangeListener); + callback.endPlayback(true); + + playerLock.unlock(); + } + }); + } + + /** + * Moves the PlaybackServiceMediaPlayer into STOPPED state. This call is only valid if the player is currently in + * INDETERMINATE state, for example after a call to endPlayback. + * This method will only take care of changing the PlayerStatus of this object! Other tasks like + * abandoning audio focus have to be done with other methods. + */ + public void stop() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + releaseWifiLockIfNecessary(); + + if (playerStatus == PlayerStatus.INDETERMINATE) { + setPlayerStatus(PlayerStatus.STOPPED, null); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Ignored call to stop: Current player state is: " + playerStatus); + } + playerLock.unlock(); + + } + }); + } + + private synchronized void acquireWifiLockIfNecessary() { + if (stream) { + if (wifiLock == null) { + wifiLock = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE)) + .createWifiLock(WifiManager.WIFI_MODE_FULL, TAG); + wifiLock.setReferenceCounted(false); + } + wifiLock.acquire(); + } + } + + private synchronized void releaseWifiLockIfNecessary() { + if (wifiLock != null && wifiLock.isHeld()) { + wifiLock.release(); + } + } + + /** + * Holds information about a PSMP object. + */ + public class PSMPInfo { + public PlayerStatus playerStatus; + public Playable playable; + + public PSMPInfo(PlayerStatus playerStatus, Playable playable) { + this.playerStatus = playerStatus; + this.playable = playable; + } + } + + public static interface PSMPCallback { + public void statusChanged(PSMPInfo newInfo); + + public void shouldStop(); + + public void playbackSpeedChanged(float s); + + public void onBufferingUpdate(int percent); + + public boolean onMediaPlayerInfo(int code); + + public boolean onMediaPlayerError(Object inObj, int what, int extra); + + public boolean endPlayback(boolean playNextEpisode); + + public RemoteControlClient getRemoteControlClient(); + } + + private IPlayer setMediaPlayerListeners(IPlayer mp) { + if (mp != null && media != null) { + if (media.getMediaType() == MediaType.AUDIO) { + ((AudioPlayer) mp) + .setOnCompletionListener(audioCompletionListener); + ((AudioPlayer) mp) + .setOnSeekCompleteListener(audioSeekCompleteListener); + ((AudioPlayer) mp).setOnErrorListener(audioErrorListener); + ((AudioPlayer) mp) + .setOnBufferingUpdateListener(audioBufferingUpdateListener); + ((AudioPlayer) mp).setOnInfoListener(audioInfoListener); + } else { + ((VideoPlayer) mp) + .setOnCompletionListener(videoCompletionListener); + ((VideoPlayer) mp) + .setOnSeekCompleteListener(videoSeekCompleteListener); + ((VideoPlayer) mp).setOnErrorListener(videoErrorListener); + ((VideoPlayer) mp) + .setOnBufferingUpdateListener(videoBufferingUpdateListener); + ((VideoPlayer) mp).setOnInfoListener(videoInfoListener); + } + } + return mp; + } + + private final com.aocate.media.MediaPlayer.OnCompletionListener audioCompletionListener = new com.aocate.media.MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(com.aocate.media.MediaPlayer mp) { + genericOnCompletion(); + } + }; + + private final android.media.MediaPlayer.OnCompletionListener videoCompletionListener = new android.media.MediaPlayer.OnCompletionListener() { + @Override + public void onCompletion(android.media.MediaPlayer mp) { + genericOnCompletion(); + } + }; + + private void genericOnCompletion() { + endPlayback(); + } + + private final com.aocate.media.MediaPlayer.OnBufferingUpdateListener audioBufferingUpdateListener = new com.aocate.media.MediaPlayer.OnBufferingUpdateListener() { + @Override + public void onBufferingUpdate(com.aocate.media.MediaPlayer mp, + int percent) { + genericOnBufferingUpdate(percent); + } + }; + + private final android.media.MediaPlayer.OnBufferingUpdateListener videoBufferingUpdateListener = new android.media.MediaPlayer.OnBufferingUpdateListener() { + @Override + public void onBufferingUpdate(android.media.MediaPlayer mp, int percent) { + genericOnBufferingUpdate(percent); + } + }; + + private void genericOnBufferingUpdate(int percent) { + callback.onBufferingUpdate(percent); + } + + private final com.aocate.media.MediaPlayer.OnInfoListener audioInfoListener = new com.aocate.media.MediaPlayer.OnInfoListener() { + @Override + public boolean onInfo(com.aocate.media.MediaPlayer mp, int what, + int extra) { + return genericInfoListener(what); + } + }; + + private final android.media.MediaPlayer.OnInfoListener videoInfoListener = new android.media.MediaPlayer.OnInfoListener() { + @Override + public boolean onInfo(android.media.MediaPlayer mp, int what, int extra) { + return genericInfoListener(what); + } + }; + + private boolean genericInfoListener(int what) { + return callback.onMediaPlayerInfo(what); + } + + private final com.aocate.media.MediaPlayer.OnErrorListener audioErrorListener = new com.aocate.media.MediaPlayer.OnErrorListener() { + @Override + public boolean onError(com.aocate.media.MediaPlayer mp, int what, + int extra) { + return genericOnError(mp, what, extra); + } + }; + + private final android.media.MediaPlayer.OnErrorListener videoErrorListener = new android.media.MediaPlayer.OnErrorListener() { + @Override + public boolean onError(android.media.MediaPlayer mp, int what, int extra) { + return genericOnError(mp, what, extra); + } + }; + + private boolean genericOnError(Object inObj, int what, int extra) { + return callback.onMediaPlayerError(inObj, what, extra); + } + + private final com.aocate.media.MediaPlayer.OnSeekCompleteListener audioSeekCompleteListener = new com.aocate.media.MediaPlayer.OnSeekCompleteListener() { + @Override + public void onSeekComplete(com.aocate.media.MediaPlayer mp) { + genericSeekCompleteListener(); + } + }; + + private final android.media.MediaPlayer.OnSeekCompleteListener videoSeekCompleteListener = new android.media.MediaPlayer.OnSeekCompleteListener() { + @Override + public void onSeekComplete(android.media.MediaPlayer mp) { + genericSeekCompleteListener(); + } + }; + + private final void genericSeekCompleteListener() { + executor.submit(new Runnable() { + @Override + public void run() { + playerLock.lock(); + if (playerStatus == PlayerStatus.SEEKING) { + setPlayerStatus(statusBeforeSeeking, media); + } + playerLock.unlock(); + } + }); + } +} diff --git a/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 new file mode 100644 index 000000000..1865afa6f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceTaskManager.java @@ -0,0 +1,384 @@ +package de.danoeh.antennapod.core.service.playback; + +import android.content.Context; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.EventDistributor; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.storage.DBReader; +import de.danoeh.antennapod.core.util.playback.Playable; + +import java.util.List; +import java.util.concurrent.*; + +/** + * Manages the background tasks of PlaybackSerivce, i.e. + * the sleep timer, the position saver, the widget updater and + * the queue loader. + * <p/> + * The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback) + * to notify the PlaybackService about updates from the running tasks. + */ +public class PlaybackServiceTaskManager { + private static final String TAG = "PlaybackServiceTaskManager"; + + /** + * Update interval of position saver in milliseconds. + */ + public static final int POSITION_SAVER_WAITING_INTERVAL = 5000; + /** + * Notification interval of widget updater in milliseconds. + */ + public static final int WIDGET_UPDATER_NOTIFICATION_INTERVAL = 1500; + + private static final int SCHED_EX_POOL_SIZE = 2; + private final ScheduledThreadPoolExecutor schedExecutor; + + private ScheduledFuture positionSaverFuture; + private ScheduledFuture widgetUpdaterFuture; + private ScheduledFuture sleepTimerFuture; + private volatile Future<List<FeedItem>> queueFuture; + private volatile Future chapterLoaderFuture; + + private SleepTimer sleepTimer; + + private final Context context; + private final PSTMCallback callback; + + /** + * Sets up a new PSTM. This method will also start the queue loader task. + * + * @param context + * @param callback A PSTMCallback object for notifying the user about updates. Must not be null. + */ + public PlaybackServiceTaskManager(Context context, PSTMCallback callback) { + Validate.notNull(context); + Validate.notNull(callback); + + this.context = context; + this.callback = callback; + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOL_SIZE, new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + loadQueue(); + EventDistributor.getInstance().register(eventDistributorListener); + } + + private final EventDistributor.EventListener eventDistributorListener = new EventDistributor.EventListener() { + @Override + public void update(EventDistributor eventDistributor, Integer arg) { + if ((EventDistributor.QUEUE_UPDATE & arg) != 0) { + cancelQueueLoader(); + loadQueue(); + } + } + }; + + private synchronized boolean isQueueLoaderActive() { + return queueFuture != null && !queueFuture.isDone(); + } + + private synchronized void cancelQueueLoader() { + if (isQueueLoaderActive()) { + queueFuture.cancel(true); + } + } + + private synchronized void loadQueue() { + if (!isQueueLoaderActive()) { + queueFuture = schedExecutor.submit(new Callable<List<FeedItem>>() { + @Override + public List<FeedItem> call() throws Exception { + return DBReader.getQueue(context); + } + }); + } + } + + /** + * Returns the queue if it is already loaded or null if it hasn't been loaded yet. + * In order to wait until the queue has been loaded, use getQueue() + */ + public synchronized List<FeedItem> getQueueIfLoaded() { + if (queueFuture.isDone()) { + try { + return queueFuture.get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + return null; + } + + /** + * Returns the queue or waits until the PSTM has loaded the queue from the database. + */ + public synchronized List<FeedItem> getQueue() throws InterruptedException { + try { + return queueFuture.get(); + } catch (ExecutionException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Starts the position saver task. If the position saver is already active, nothing will happen. + */ + public synchronized void startPositionSaver() { + if (!isPositionSaverActive()) { + Runnable positionSaver = new Runnable() { + @Override + public void run() { + callback.positionSaverTick(); + } + }; + positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL, + POSITION_SAVER_WAITING_INTERVAL, TimeUnit.MILLISECONDS); + + if (BuildConfig.DEBUG) Log.d(TAG, "Started PositionSaver"); + } else { + if (BuildConfig.DEBUG) Log.d(TAG, "Call to startPositionSaver was ignored."); + } + } + + /** + * Returns true if the position saver is currently running. + */ + public synchronized boolean isPositionSaverActive() { + return positionSaverFuture != null && !positionSaverFuture.isCancelled() && !positionSaverFuture.isDone(); + } + + /** + * Cancels the position saver. If the position saver is not running, nothing will happen. + */ + public synchronized void cancelPositionSaver() { + if (isPositionSaverActive()) { + positionSaverFuture.cancel(false); + if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled PositionSaver"); + } + } + + /** + * Starts the widget updater task. If the widget updater is already active, nothing will happen. + */ + public synchronized void startWidgetUpdater() { + if (!isWidgetUpdaterActive()) { + Runnable widgetUpdater = new Runnable() { + @Override + public void run() { + callback.onWidgetUpdaterTick(); + } + }; + widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL, + WIDGET_UPDATER_NOTIFICATION_INTERVAL, TimeUnit.MILLISECONDS); + + if (BuildConfig.DEBUG) Log.d(TAG, "Started WidgetUpdater"); + } else { + if (BuildConfig.DEBUG) Log.d(TAG, "Call to startWidgetUpdater was ignored."); + } + } + + /** + * Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be + * cancelled first. + * After waitingTime has elapsed, onSleepTimerExpired() will be called. + * + * @throws java.lang.IllegalArgumentException if waitingTime <= 0 + */ + public synchronized void setSleepTimer(long waitingTime) { + Validate.isTrue(waitingTime > 0, "Waiting time <= 0"); + + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + + " milliseconds"); + if (isSleepTimerActive()) { + sleepTimerFuture.cancel(true); + } + sleepTimer = new SleepTimer(waitingTime); + sleepTimerFuture = schedExecutor.schedule(sleepTimer, 0, TimeUnit.MILLISECONDS); + } + + /** + * Returns true if the sleep timer is currently active. + */ + public synchronized boolean isSleepTimerActive() { + return sleepTimer != null && sleepTimerFuture != null && !sleepTimerFuture.isCancelled() && !sleepTimerFuture.isDone() && sleepTimer.isWaiting; + } + + /** + * Disables the sleep timer. If the sleep timer is not active, nothing will happen. + */ + public synchronized void disableSleepTimer() { + if (isSleepTimerActive()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Disabling sleep timer"); + sleepTimerFuture.cancel(true); + } + } + + /** + * Returns the current sleep timer time or 0 if the sleep timer is not active. + */ + public synchronized long getSleepTimerTimeLeft() { + if (isSleepTimerActive()) { + return sleepTimer.getWaitingTime(); + } else { + return 0; + } + } + + + /** + * Returns true if the widget updater is currently running. + */ + public synchronized boolean isWidgetUpdaterActive() { + return widgetUpdaterFuture != null && !widgetUpdaterFuture.isCancelled() && !widgetUpdaterFuture.isDone(); + } + + /** + * Cancels the widget updater. If the widget updater is not running, nothing will happen. + */ + public synchronized void cancelWidgetUpdater() { + if (isWidgetUpdaterActive()) { + widgetUpdaterFuture.cancel(false); + if (BuildConfig.DEBUG) Log.d(TAG, "Cancelled WidgetUpdater"); + } + } + + private synchronized void cancelChapterLoader() { + if (isChapterLoaderActive()) { + chapterLoaderFuture.cancel(true); + } + } + + private synchronized boolean isChapterLoaderActive() { + return chapterLoaderFuture != null && !chapterLoaderFuture.isDone(); + } + + /** + * Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active, + * it will be cancelled first. + * On completion, the callback's onChapterLoaded method will be called. + */ + public synchronized void startChapterLoader(final Playable media) { + Validate.notNull(media); + + if (isChapterLoaderActive()) { + cancelChapterLoader(); + } + + Runnable chapterLoader = new Runnable() { + @Override + public void run() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Chapter loader started"); + if (media.getChapters() == null) { + media.loadChapterMarks(); + if (!Thread.currentThread().isInterrupted() && media.getChapters() != null) { + callback.onChapterLoaded(media); + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Chapter loader stopped"); + } + }; + chapterLoaderFuture = schedExecutor.submit(chapterLoader); + } + + + /** + * Cancels all tasks. The PSTM will be in the initial state after execution of this method. + */ + public synchronized void cancelAllTasks() { + cancelPositionSaver(); + cancelWidgetUpdater(); + disableSleepTimer(); + cancelQueueLoader(); + cancelChapterLoader(); + } + + /** + * Cancels all tasks and shuts down the internal executor service of the PSTM. The object should not be used after + * execution of this method. + */ + public synchronized void shutdown() { + EventDistributor.getInstance().unregister(eventDistributorListener); + cancelAllTasks(); + schedExecutor.shutdown(); + } + + /** + * Sleeps for a given time and then pauses playback. + */ + private class SleepTimer implements Runnable { + private static final String TAG = "SleepTimer"; + private static final long UPDATE_INTERVALL = 1000L; + private volatile long waitingTime; + private volatile boolean isWaiting; + + public SleepTimer(long waitingTime) { + super(); + this.waitingTime = waitingTime; + isWaiting = true; + } + + @Override + public void run() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting"); + while (waitingTime > 0) { + try { + Thread.sleep(UPDATE_INTERVALL); + waitingTime -= UPDATE_INTERVALL; + + if (waitingTime <= 0) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Waiting completed"); + postExecute(); + if (!Thread.currentThread().isInterrupted()) { + callback.onSleepTimerExpired(); + } + + } + } catch (InterruptedException e) { + Log.d(TAG, "Thread was interrupted while waiting"); + break; + } + } + postExecute(); + } + + protected void postExecute() { + isWaiting = false; + } + + public long getWaitingTime() { + return waitingTime; + } + + public boolean isWaiting() { + return isWaiting; + } + + } + + public static interface PSTMCallback { + void positionSaverTick(); + + void onSleepTimerExpired(); + + void onWidgetUpdaterTick(); + + void onChapterLoaded(Playable media); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java new file mode 100644 index 000000000..7c666abd5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlayerStatus.java @@ -0,0 +1,24 @@ +package de.danoeh.antennapod.core.service.playback; + +public enum PlayerStatus { + INDETERMINATE, // player is currently changing its state, listeners should wait until the player has left this state. + ERROR, + PREPARING, + PAUSED, + PLAYING, + STOPPED, + PREPARED, + SEEKING, + INITIALIZING, // playback service is loading the Playable's metadata + INITIALIZED; // playback service was started, data source of media player was set. + + private static final PlayerStatus[] fromOrdinalLookup; + + static { + fromOrdinalLookup = PlayerStatus.values(); + } + + public static PlayerStatus fromOrdinal(int o) { + return fromOrdinalLookup[o]; + } +} 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 new file mode 100644 index 000000000..6c0b6df74 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -0,0 +1,909 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.database.Cursor; +import android.database.SQLException; +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.util.DownloadError; +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; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +/** + * Provides methods for reading data from the AntennaPod database. + * In general, all database calls in DBReader-methods are executed on the caller's thread. + * This means that the caller should make sure that DBReader-methods are not executed on the GUI-thread. + * This class will use the {@link de.danoeh.antennapod.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)}. + */ + public static final int PLAYBACK_HISTORY_SIZE = 50; + + /** + * Maximum size of the list returned by {@link #getDownloadLog(android.content.Context)}. + */ + public static final int DOWNLOAD_LOG_SIZE = 200; + + + private DBReader() { + } + + /** + * Returns a list of Feeds, sorted alphabetically by their title. + * + * @param context A context that is used for opening a database connection. + * @return A list of Feeds, sorted alphabetically by their title. A Feed-object + * of the returned list does NOT have its list of FeedItems yet. The FeedItem-list + * can be loaded separately with {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)}. + */ + public static List<Feed> getFeedList(final Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting Feedlist"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List<Feed> result = getFeedList(adapter); + adapter.close(); + return result; + } + + 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()); + + if (feedlistCursor.moveToFirst()) { + do { + Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor); + feeds.add(feed); + } while (feedlistCursor.moveToNext()); + } + feedlistCursor.close(); + return feeds; + } + + /** + * 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>(); + adapter.open(); + Cursor feeds = adapter.getFeedCursorDownloadUrls(); + if (feeds.moveToFirst()) { + do { + result.add(feeds.getString(1)); + } while (feeds.moveToNext()); + } + feeds.close(); + adapter.close(); + + 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)}. + */ + 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(); + + Cursor feedlistCursor = adapter.getExpiredFeedsCursor(expirationTime); + List<Feed> feeds = new ArrayList<Feed>(feedlistCursor.getCount()); + + if (feedlistCursor.moveToFirst()) { + do { + Feed feed = extractFeedFromCursorRow(adapter, feedlistCursor); + feeds.add(feed); + } while (feedlistCursor.moveToNext()); + } + feedlistCursor.close(); + return feeds; + } + + /** + * Takes a list of FeedItems and loads their corresponding Feed-objects from the database. + * 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); + for (FeedItem item : items) { + for (Feed feed : feeds) { + if (feed.getId() == item.getFeedId()) { + item.setFeed(feed); + break; + } + } + if (item.getFeed() == null) { + Log.w(TAG, "No match found for item with ID " + item.getId() + ". Feed ID was " + item.getFeedId()); + } + } + } + + /** + * Loads the list of FeedItems for a certain Feed-object. This method should NOT be used if the FeedItems are not + * used. In order to get information ABOUT the list of FeedItems, consider using {@link #getFeedStatisticsList(android.content.Context)} instead. + * + * @param context A context that is used for opening a database connection. + * @param feed The Feed whose items should be loaded + * @return A list with the FeedItems of the Feed. The Feed-attribute of the FeedItems will already be set correctly. + * The method does NOT change the items-attribute of the feed. + */ + public static List<FeedItem> getFeedItemList(Context context, + final Feed feed) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting Feeditems of feed " + feed.getTitle()); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getAllItemsOfFeedCursor(feed); + List<FeedItem> items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + + Collections.sort(items, new FeedItemPubdateComparator()); + + adapter.close(); + + for (FeedItem item : items) { + item.setFeed(feed); + } + + return items; + } + + static List<FeedItem> extractItemlistFromCursor(Context context, Cursor itemlistCursor) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List<FeedItem> result = extractItemlistFromCursor(adapter, itemlistCursor); + adapter.close(); + return result; + } + + private static List<FeedItem> extractItemlistFromCursor( + PodDBAdapter adapter, Cursor itemlistCursor) { + ArrayList<String> itemIds = new ArrayList<String>(); + List<FeedItem> items = new ArrayList<FeedItem>( + itemlistCursor.getCount()); + + if (itemlistCursor.moveToFirst()) { + do { + FeedItem item = new FeedItem(); + + item.setId(itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_ID)); + item.setTitle(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_TITLE)); + item.setLink(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_LINK)); + item.setPubDate(new Date(itemlistCursor + .getLong(PodDBAdapter.IDX_FI_SMALL_PUBDATE))); + item.setPaymentLink(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_PAYMENT_LINK)); + item.setFeedId(itemlistCursor + .getLong(PodDBAdapter.IDX_FI_SMALL_FEED)); + itemIds.add(String.valueOf(item.getId())); + + item.setRead((itemlistCursor + .getInt(PodDBAdapter.IDX_FI_SMALL_READ) > 0)); + item.setItemIdentifier(itemlistCursor + .getString(PodDBAdapter.IDX_FI_SMALL_ITEM_IDENTIFIER)); + item.setFlattrStatus(new FlattrStatus(itemlistCursor + .getLong(PodDBAdapter.IDX_FI_SMALL_FLATTR_STATUS))); + + long imageIndex = itemlistCursor.getLong(PodDBAdapter.IDX_FI_SMALL_IMAGE); + if (imageIndex != 0) { + item.setImage(getFeedImage(adapter, imageIndex)); + } + + // extract chapters + boolean hasSimpleChapters = itemlistCursor + .getInt(PodDBAdapter.IDX_FI_SMALL_HAS_CHAPTERS) > 0; + if (hasSimpleChapters) { + Cursor chapterCursor = adapter + .getSimpleChaptersOfFeedItemCursor(item); + if (chapterCursor.moveToFirst()) { + item.setChapters(new ArrayList<Chapter>()); + do { + int chapterType = chapterCursor + .getInt(PodDBAdapter.KEY_CHAPTER_TYPE_INDEX); + Chapter chapter = null; + long start = chapterCursor + .getLong(PodDBAdapter.KEY_CHAPTER_START_INDEX); + String title = chapterCursor + .getString(PodDBAdapter.KEY_TITLE_INDEX); + String link = chapterCursor + .getString(PodDBAdapter.KEY_CHAPTER_LINK_INDEX); + + switch (chapterType) { + case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER: + chapter = new SimpleChapter(start, title, item, + link); + break; + case ID3Chapter.CHAPTERTYPE_ID3CHAPTER: + chapter = new ID3Chapter(start, title, item, + link); + break; + case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER: + chapter = new VorbisCommentChapter(start, + title, item, link); + break; + } + if (chapter != null) { + chapter.setId(chapterCursor + .getLong(PodDBAdapter.KEY_ID_INDEX)); + item.getChapters().add(chapter); + } + } while (chapterCursor.moveToNext()); + } + chapterCursor.close(); + } + items.add(item); + } while (itemlistCursor.moveToNext()); + } + + extractMediafromItemlist(adapter, items, itemIds); + return items; + } + + private static void extractMediafromItemlist(PodDBAdapter adapter, + List<FeedItem> items, ArrayList<String> itemIds) { + + List<FeedItem> itemsCopy = new ArrayList<FeedItem>(items); + Cursor cursor = adapter.getFeedMediaCursorByItemID(itemIds + .toArray(new String[itemIds.size()])); + if (cursor.moveToFirst()) { + do { + long itemId = cursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); + // find matching feed item + FeedItem item = getMatchingItemForMedia(itemId, itemsCopy); + if (item != null) { + item.setMedia(extractFeedMediaFromCursorRow(cursor)); + item.getMedia().setItem(item); + } + } while (cursor.moveToNext()); + cursor.close(); + } + } + + private static FeedMedia extractFeedMediaFromCursorRow(final Cursor cursor) { + long mediaId = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); + Date playbackCompletionDate = null; + long playbackCompletionTime = cursor + .getLong(PodDBAdapter.KEY_PLAYBACK_COMPLETION_DATE_INDEX); + if (playbackCompletionTime > 0) { + playbackCompletionDate = new Date( + playbackCompletionTime); + } + + return new FeedMedia( + mediaId, + null, + cursor.getInt(PodDBAdapter.KEY_DURATION_INDEX), + cursor.getInt(PodDBAdapter.KEY_POSITION_INDEX), + cursor.getLong(PodDBAdapter.KEY_SIZE_INDEX), + cursor.getString(PodDBAdapter.KEY_MIME_TYPE_INDEX), + cursor.getString(PodDBAdapter.KEY_FILE_URL_INDEX), + cursor.getString(PodDBAdapter.KEY_DOWNLOAD_URL_INDEX), + cursor.getInt(PodDBAdapter.KEY_DOWNLOADED_INDEX) > 0, + playbackCompletionDate, + cursor.getInt(PodDBAdapter.KEY_PLAYED_DURATION_INDEX)); + } + + 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); + } 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)); + + if (image != null) { + 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)); + + feed.setPreferences(preferences); + return feed; + } + + private static FeedItem getMatchingItemForMedia(long itemId, + List<FeedItem> items) { + for (FeedItem item : items) { + if (item.getId() == itemId) { + return item; + } + } + return null; + } + + static List<FeedItem> getQueue(Context context, PodDBAdapter adapter) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting queue"); + + Cursor itemlistCursor = adapter.getQueueCursor(); + List<FeedItem> items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + loadFeedDataOfFeedItemlist(context, items); + + return items; + } + + /** + * Loads the IDs of the FeedItems in the queue. This method should be preferred over + * {@link #getQueue(android.content.Context)} if the FeedItems of the queue are not needed. + * + * @param context A context that is used for opening a database connection. + * @return A list of IDs sorted by the same order as the queue. The caller can wrap the returned + * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties. + */ + public static List<Long> getQueueIDList(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + + adapter.open(); + List<Long> result = getQueueIDList(adapter); + adapter.close(); + + return result; + } + + static List<Long> getQueueIDList(PodDBAdapter adapter) { + adapter.open(); + Cursor queueCursor = adapter.getQueueIDCursor(); + + List<Long> queueIds = new ArrayList<Long>(queueCursor.getCount()); + if (queueCursor.moveToFirst()) { + do { + queueIds.add(queueCursor.getLong(0)); + } while (queueCursor.moveToNext()); + } + return queueIds; + } + + + /** + * Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using + * {@link #getQueueIDList(android.content.Context)} instead. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItems sorted by the same order as the queue. The caller can wrap the returned + * list in a {@link de.danoeh.antennapod.core.util.QueueAccess} object for easier access to the queue's properties. + */ + public static List<FeedItem> getQueue(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting queue"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List<FeedItem> items = getQueue(context, adapter); + adapter.close(); + return items; + } + + /** + * Loads a list of FeedItems whose episode has been downloaded. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItems whose episdoe has been downloaded. + */ + public static List<FeedItem> getDownloadedItems(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting downloaded items"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getDownloadedItemsCursor(); + List<FeedItem> items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + loadFeedDataOfFeedItemlist(context, items); + Collections.sort(items, new FeedItemPubdateComparator()); + + adapter.close(); + return items; + + } + + /** + * Loads a list of FeedItems whose 'read'-attribute is set to false. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItems whose 'read'-attribute it set to false. If the FeedItems in the list are not used, + * consider using {@link #getUnreadItemIds(android.content.Context)} instead. + */ + public static List<FeedItem> getUnreadItemsList(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting unread items list"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getUnreadItemsCursor(); + List<FeedItem> items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + + loadFeedDataOfFeedItemlist(context, items); + + adapter.close(); + + return items; + } + + /** + * Loads the IDs of the FeedItems whose 'read'-attribute is set to false. + * + * @param context A context that is used for opening a database connection. + * @return A list of IDs of the FeedItems whose 'read'-attribute is set to false. This method should be preferred + * over {@link #getUnreadItemsList(android.content.Context)} if the FeedItems in the UnreadItems list are not used. + */ + public static long[] getUnreadItemIds(Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getUnreadItemIdsCursor(); + long[] itemIds = new long[cursor.getCount()]; + int i = 0; + if (cursor.moveToFirst()) { + do { + itemIds[i] = cursor.getLong(PodDBAdapter.KEY_ID_INDEX); + i++; + } while (cursor.moveToNext()); + } + return itemIds; + } + + + /** + * Loads 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"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor itemlistCursor = adapter.getRecentlyPublishedItemsCursor(limit); + List<FeedItem> items = extractItemlistFromCursor(adapter, + itemlistCursor); + itemlistCursor.close(); + + loadFeedDataOfFeedItemlist(context, items); + + adapter.close(); + + return items; + } + + /** + * 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"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + Cursor mediaCursor = adapter.getCompletedMediaCursor(PLAYBACK_HISTORY_SIZE); + String[] itemIds = new String[mediaCursor.getCount()]; + for (int i = 0; i < itemIds.length && mediaCursor.moveToPosition(i); i++) { + itemIds[i] = Long.toString(mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX)); + } + mediaCursor.close(); + Cursor itemCursor = adapter.getFeedItemCursor(itemIds); + List<FeedItem> items = extractItemlistFromCursor(adapter, itemCursor); + loadFeedDataOfFeedItemlist(context, items); + itemCursor.close(); + adapter.close(); + + Collections.sort(items, new PlaybackCompletionDateComparator()); + return items; + } + + /** + * Loads the download log from the database. + * + * @param context A context that is used for opening a database connection. + * @return A list with DownloadStatus objects that represent the download log. + * The size of the returned list is limited by {@link #DOWNLOAD_LOG_SIZE}. + */ + public static List<DownloadStatus> getDownloadLog(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Extracting DownloadLog"); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor logCursor = adapter.getDownloadLogCursor(DOWNLOAD_LOG_SIZE); + List<DownloadStatus> downloadLog = new ArrayList<DownloadStatus>( + logCursor.getCount()); + + if (logCursor.moveToFirst()) { + do { + long id = logCursor.getLong(PodDBAdapter.KEY_ID_INDEX); + + long feedfileId = logCursor + .getLong(PodDBAdapter.KEY_FEEDFILE_INDEX); + int feedfileType = logCursor + .getInt(PodDBAdapter.KEY_FEEDFILETYPE_INDEX); + boolean successful = logCursor + .getInt(PodDBAdapter.KEY_SUCCESSFUL_INDEX) > 0; + int reason = logCursor.getInt(PodDBAdapter.KEY_REASON_INDEX); + String reasonDetailed = logCursor + .getString(PodDBAdapter.KEY_REASON_DETAILED_INDEX); + String title = logCursor + .getString(PodDBAdapter.KEY_DOWNLOADSTATUS_TITLE_INDEX); + Date completionDate = new Date( + logCursor + .getLong(PodDBAdapter.KEY_COMPLETION_DATE_INDEX) + ); + downloadLog.add(new DownloadStatus(id, title, feedfileId, + feedfileType, successful, DownloadError.fromCode(reason), completionDate, + reasonDetailed)); + + } while (logCursor.moveToNext()); + } + logCursor.close(); + Collections.sort(downloadLog, new DownloadStatusComparator()); + return downloadLog; + } + + /** + * Loads the FeedItemStatistics objects of all Feeds in the database. This method should be preferred over + * {@link #getFeedItemList(android.content.Context, de.danoeh.antennapod.core.feed.Feed)} if only metadata about + * the FeedItems is needed. + * + * @param context A context that is used for opening a database connection. + * @return A list of FeedItemStatistics objects sorted alphabetically by their Feed's title. + */ + public static List<FeedItemStatistics> getFeedStatisticsList(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + List<FeedItemStatistics> result = new ArrayList<FeedItemStatistics>(); + Cursor cursor = adapter.getFeedStatisticsCursor(); + if (cursor.moveToFirst()) { + do { + result.add(new FeedItemStatistics(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_FEED), + cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NUM_ITEMS), + cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_NEW_ITEMS), + cursor.getInt(PodDBAdapter.IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES), + new Date(cursor.getLong(PodDBAdapter.IDX_FEEDSTATISTICS_LATEST_EPISODE)))); + } while (cursor.moveToNext()); + } + + cursor.close(); + adapter.close(); + return result; + } + + /** + * Loads a specific Feed from the database. + * + * @param context A context that is used for opening a database connection. + * @param feedId The ID of the Feed + * @return The Feed or null if the Feed could not be found. The Feeds FeedItems will also be loaded from the + * database and the items-attribute will be set correctly. + */ + public static Feed getFeed(final Context context, final long feedId) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Feed result = getFeed(context, 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); + Feed feed = null; + + Cursor feedCursor = adapter.getFeedCursor(feedId); + if (feedCursor.moveToFirst()) { + feed = extractFeedFromCursorRow(adapter, feedCursor); + feed.setItems(getFeedItemList(context, feed)); + } else { + Log.e(TAG, "getFeed could not find feed with id " + feedId); + } + feedCursor.close(); + return feed; + } + + static FeedItem getFeedItem(final Context context, final long itemId, PodDBAdapter adapter) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading feeditem with id " + itemId); + FeedItem item = null; + + Cursor itemCursor = adapter.getFeedItemCursor(Long.toString(itemId)); + if (itemCursor.moveToFirst()) { + List<FeedItem> list = extractItemlistFromCursor(adapter, itemCursor); + if (list.size() > 0) { + item = list.get(0); + loadFeedDataOfFeedItemlist(context, list); + } + } + return item; + + } + + /** + * Loads a specific FeedItem from the database. + * + * @param context A context that is used for opening a database connection. + * @param itemId The ID of the FeedItem + * @return The FeedItem or null if the FeedItem could not be found. All FeedComponent-attributes of the FeedItem will + * also be loaded from the database. + */ + public static FeedItem getFeedItem(final Context context, final long itemId) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Loading feeditem with id " + itemId); + + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + FeedItem item = getFeedItem(context, itemId, adapter); + adapter.close(); + return item; + + } + + /** + * Loads additional information about a FeedItem, e.g. shownotes + * + * @param context A context that is used for opening a database connection. + * @param item The FeedItem + */ + public static void loadExtraInformationOfFeedItem(final Context context, final FeedItem item) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor extraCursor = adapter.getExtraInformationOfItem(item); + if (extraCursor.moveToFirst()) { + String description = extraCursor + .getString(PodDBAdapter.IDX_FI_EXTRA_DESCRIPTION); + String contentEncoded = extraCursor + .getString(PodDBAdapter.IDX_FI_EXTRA_CONTENT_ENCODED); + item.setDescription(description); + item.setContentEncoded(contentEncoded); + } + adapter.close(); + } + + /** + * Returns the number of downloaded episodes. + * + * @param context A context that is used for opening a database connection. + * @return The number of downloaded episodes. + */ + public static int getNumberOfDownloadedEpisodes(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final int result = adapter.getNumberOfDownloadedEpisodes(); + adapter.close(); + return result; + } + + /** + * Returns the number of unread items. + * + * @param context A context that is used for opening a database connection. + * @return The number of unread items. + */ + public static int getNumberOfUnreadItems(final Context context) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final int result = adapter.getNumberOfUnreadItems(); + adapter.close(); + return result; + } + + /** + * Searches the DB for a FeedImage of the given id. + * + * @param context A context that is used for opening a database connection. + * @param imageId The id of the object + * @return The found object + */ + public static FeedImage getFeedImage(final Context context, final long imageId) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + FeedImage result = getFeedImage(adapter, imageId); + adapter.close(); + return result; + } + + /** + * Searches the DB for a FeedImage of the given id. + * + * @param id The id of the object + * @return The found object + */ + static FeedImage getFeedImage(PodDBAdapter adapter, final long id) { + Cursor cursor = adapter.getImageCursor(id); + if ((cursor.getCount() == 0) || !cursor.moveToFirst()) { + throw new SQLException("No FeedImage found at index: " + id); + } + FeedImage image = new FeedImage(id, cursor.getString(cursor + .getColumnIndex(PodDBAdapter.KEY_TITLE)), + cursor.getString(cursor + .getColumnIndex(PodDBAdapter.KEY_FILE_URL)), + cursor.getString(cursor + .getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL)), + cursor.getInt(cursor + .getColumnIndex(PodDBAdapter.KEY_DOWNLOADED)) > 0 + ); + cursor.close(); + return image; + } + + /** + * Searches the DB for a FeedMedia of the given id. + * + * @param context A context that is used for opening a database connection. + * @param mediaId The id of the object + * @return The found object + */ + public static FeedMedia getFeedMedia(final Context context, final long mediaId) { + PodDBAdapter adapter = new PodDBAdapter(context); + + adapter.open(); + Cursor mediaCursor = adapter.getSingleFeedMediaCursor(mediaId); + + FeedMedia media = null; + if (mediaCursor.moveToFirst()) { + final long itemId = mediaCursor.getLong(PodDBAdapter.KEY_MEDIA_FEEDITEM_INDEX); + media = extractFeedMediaFromCursorRow(mediaCursor); + FeedItem item = getFeedItem(context, itemId); + if (media != null && item != null) { + media.setItem(item); + item.setMedia(media); + } + } + + mediaCursor.close(); + adapter.close(); + + return media; + } + + /** + * 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); + adapter.open(); + List<FlattrThing> result = new ArrayList<FlattrThing>(); + + // load feeds + Cursor feedCursor = adapter.getFeedsInFlattrQueueCursor(); + if (feedCursor.moveToFirst()) { + do { + result.add(extractFeedFromCursorRow(adapter, feedCursor)); + } while (feedCursor.moveToNext()); + } + feedCursor.close(); + + //load feed items + Cursor feedItemCursor = adapter.getFeedItemsInFlattrQueueCursor(); + result.addAll(extractItemlistFromCursor(adapter, feedItemCursor)); + feedItemCursor.close(); + + adapter.close(); + Log.d(TAG, "Returning flattrQueueIterator for queue with " + result.size() + " items."); + 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); + adapter.open(); + List<Feed> feeds = getFeedList(adapter); + int queueSize = adapter.getQueueSize(); + int numUnreadItems = adapter.getNumberOfUnreadItems(); + NavDrawerData result = new NavDrawerData(feeds, queueSize, numUnreadItems); + adapter.close(); + return result; + } + + public static class NavDrawerData { + public List<Feed> feeds; + public int queueSize; + public int numUnreadItems; + + public NavDrawerData(List<Feed> feeds, int queueSize, int numUnreadItems) { + this.feeds = feeds; + this.queueSize = queueSize; + this.numUnreadItems = numUnreadItems; + } + } +} 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 new file mode 100644 index 000000000..b1aff5594 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -0,0 +1,974 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicBoolean; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +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.service.GpodnetSyncService; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.DownloadError; +import de.danoeh.antennapod.core.util.NetworkUtils; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.comparator.FeedItemPubdateComparator; +import de.danoeh.antennapod.core.util.exception.MediaFileNotFoundException; +import de.danoeh.antennapod.core.util.flattr.FlattrUtils; + +/** + * Provides methods for doing common tasks that use DBReader and DBWriter. + */ +public final class DBTasks { + private static final String TAG = "DBTasks"; + + /** + * Executor service used by the autodownloadUndownloadedEpisodes method. + */ + private static ExecutorService autodownloadExec; + + static { + autodownloadExec = Executors.newSingleThreadExecutor(new ThreadFactory() { + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + } + + private DBTasks() { + } + + /** + * Removes the feed with the given download url. This method should NOT be executed on the GUI thread. + * + * @param context Used for accessing the db + * @param downloadUrl URL of the feed. + */ + public static void removeFeedWithDownloadUrl(Context context, String downloadUrl) { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor cursor = adapter.getFeedCursorDownloadUrls(); + long feedID = 0; + if (cursor.moveToFirst()) { + do { + if (cursor.getString(1).equals(downloadUrl)) { + feedID = cursor.getLong(0); + } + } while (cursor.moveToNext()); + } + cursor.close(); + adapter.close(); + + if (feedID != 0) { + try { + DBWriter.deleteFeed(context, feedID).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } else { + Log.w(TAG, "removeFeedWithDownloadUrl: Could not find feed with url: " + downloadUrl); + } + } + + /** + * Starts playback of a FeedMedia object's file. This method will build an Intent based on the given parameters to + * start the {@link PlaybackService}. + * + * @param context Used for sending starting Services and Activities. + * @param media The FeedMedia object. + * @param showPlayer If true, starts the appropriate player activity ({@link de.danoeh.antennapod.activity.AudioplayerActivity} + * or {@link de.danoeh.antennapod.activity.VideoplayerActivity} + * @param startWhenPrepared Parameter for the {@link PlaybackService} start intent. If true, playback will start as + * soon as the PlaybackService has finished loading the FeedMedia object's file. + * @param shouldStream Parameter for the {@link PlaybackService} start intent. If true, the FeedMedia object's file + * will be streamed, otherwise the downloaded file will be used. If the downloaded file cannot be + * found, the PlaybackService will shutdown and the database entry of the FeedMedia object will be + * corrected. + */ + public static void playMedia(final Context context, final FeedMedia media, + boolean showPlayer, boolean startWhenPrepared, boolean shouldStream) { + try { + if (!shouldStream) { + if (media.fileExists() == false) { + throw new MediaFileNotFoundException( + "No episode was found at " + media.getFile_url(), + media); + } + } + // Start playback Service + Intent launchIntent = new Intent(context, PlaybackService.class); + launchIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + launchIntent.putExtra(PlaybackService.EXTRA_START_WHEN_PREPARED, + startWhenPrepared); + launchIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, + shouldStream); + launchIntent.putExtra(PlaybackService.EXTRA_PREPARE_IMMEDIATELY, + true); + context.startService(launchIntent); + if (showPlayer) { + // Launch media player + context.startActivity(PlaybackService.getPlayerActivityIntent( + context, media)); + } + DBWriter.addQueueItemAt(context, media.getItem().getId(), 0, false); + } catch (MediaFileNotFoundException e) { + e.printStackTrace(); + if (media.isPlaying()) { + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + } + notifyMissingFeedMediaFile(context, media); + } + } + + private static AtomicBoolean isRefreshing = new AtomicBoolean(false); + + /** + * Refreshes a given list of Feeds in a separate Thread. This method might ignore subsequent calls if it is still + * enqueuing Feeds for download from a previous call + * + * @param context Might be used for accessing the database + * @param feeds List of Feeds that should be refreshed. + */ + public static void refreshAllFeeds(final Context context, + final List<Feed> feeds) { + if (isRefreshing.compareAndSet(false, true)) { + new Thread() { + public void run() { + if (feeds != null) { + refreshFeeds(context, feeds); + } else { + refreshFeeds(context, DBReader.getFeedList(context)); + } + isRefreshing.set(false); + + if (FlattrUtils.hasToken()) { + if (BuildConfig.DEBUG) Log.d(TAG, "Flattring all pending things."); + new FlattrClickWorker(context).executeAsync(); // flattr pending things + + if (BuildConfig.DEBUG) Log.d(TAG, "Fetching flattr status."); + new FlattrStatusFetcher(context).start(); + + } + if (ClientConfig.gpodnetCallbacks.gpodnetEnabled()) { + GpodnetSyncService.sendSyncIntent(context); + } + autodownloadUndownloadedItems(context); + } + }.start(); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Ignoring request to refresh all feeds: Refresh lock is locked"); + } + } + + /** + * 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. + */ + public static void refreshExpiredFeeds(final Context context) { + if (BuildConfig.DEBUG) + 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() + ) + ); + } + } + + } + + /** + * Downloads all pages of the given feed. + * + * @param context Used for requesting the download. + * @param feed The Feed object. + */ + public static void refreshCompleteFeed(final Context context, final Feed feed) { + try { + refreshFeed(context, feed, true); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus( + context, + new DownloadStatus(feed, feed + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, false, e + .getMessage() + ) + ); + } + } + + /** + * Queues the next page of this Feed for download. The given Feed has to be a paged + * Feed (isPaged()=true) and must contain a nextPageLink. + * + * @param context Used for requesting the download. + * @param feed The feed whose next page should be loaded. + * @param loadAllPages True if any subsequent pages should also be loaded, false otherwise. + */ + public static void loadNextPageOfFeed(final Context context, Feed feed, boolean loadAllPages) throws DownloadRequestException { + if (feed.isPaged() && feed.getNextPageLink() != null) { + int pageNr = feed.getPageNr() + 1; + Feed nextFeed = new Feed(feed.getNextPageLink(), new Date(), feed.getTitle() + "(" + pageNr + ")"); + nextFeed.setPageNr(pageNr); + nextFeed.setPaged(true); + nextFeed.setId(feed.getId()); + DownloadRequester.getInstance().downloadFeed(context, nextFeed, loadAllPages); + } else { + Log.e(TAG, "loadNextPageOfFeed: Feed was either not paged or contained no nextPageLink"); + } + } + + /** + * Updates a specific Feed. + * + * @param context Used for requesting the download. + * @param feed The Feed object. + */ + public static void refreshFeed(Context context, Feed feed) + throws DownloadRequestException { + refreshFeed(context, feed, false); + } + + private static void refreshFeed(Context context, Feed feed, boolean loadAllPages) throws DownloadRequestException { + Feed f; + if (feed.getPreferences() == null) { + f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle()); + } else { + f = new Feed(feed.getDownload_url(), new Date(), feed.getTitle(), + feed.getPreferences().getUsername(), feed.getPreferences().getPassword()); + } + f.setId(feed.getId()); + DownloadRequester.getInstance().downloadFeed(context, f, loadAllPages); + } + + /** + * Notifies the database about a missing FeedImage file. This method will attempt to re-download the file. + * + * @param context Used for requesting the download. + * @param image The FeedImage object. + */ + public static void notifyInvalidImageFile(final Context context, + final FeedImage image) { + Log.i(TAG, + "The DB was notified about an invalid image download. It will now try to re-download the image file"); + try { + DownloadRequester.getInstance().downloadImage(context, image); + } catch (DownloadRequestException e) { + e.printStackTrace(); + Log.w(TAG, "Failed to download invalid feed image"); + } + } + + /** + * Notifies the database about a missing FeedMedia file. This method will correct the FeedMedia object's values in the + * DB and send a FeedUpdateBroadcast. + */ + public static void notifyMissingFeedMediaFile(final Context context, + final FeedMedia media) { + Log.i(TAG, + "The feedmanager was notified about a missing episode. It will update its database now."); + media.setDownloaded(false); + media.setFile_url(null); + DBWriter.setFeedMedia(context, media); + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + } + + /** + * Request the download of all objects in the queue. from a separate Thread. + * + * @param context Used for requesting the download an accessing the database. + */ + public static void downloadAllItemsInQueue(final Context context) { + new Thread() { + public void run() { + List<FeedItem> queue = DBReader.getQueue(context); + if (!queue.isEmpty()) { + try { + downloadFeedItems(context, + queue.toArray(new FeedItem[queue.size()])); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + } + } + }.start(); + } + + /** + * Requests the download of a list of FeedItem objects. + * + * @param context Used for requesting the download and accessing the DB. + * @param items The FeedItem objects. + */ + public static void downloadFeedItems(final Context context, + FeedItem... items) throws DownloadRequestException { + downloadFeedItems(true, context, items); + } + + private static void downloadFeedItems(boolean performAutoCleanup, + final Context context, final FeedItem... items) + throws DownloadRequestException { + final DownloadRequester requester = DownloadRequester.getInstance(); + + if (performAutoCleanup) { + new Thread() { + + @Override + public void run() { + performAutoCleanup(context, + getPerformAutoCleanupArgs(context, items.length)); + } + + }.start(); + } + for (FeedItem item : items) { + if (item.getMedia() != null + && !requester.isDownloadingFile(item.getMedia()) + && !item.getMedia().isDownloaded()) { + if (items.length > 1) { + try { + requester.downloadMedia(context, item.getMedia()); + } catch (DownloadRequestException e) { + e.printStackTrace(); + DBWriter.addDownloadStatus(context, + new DownloadStatus(item.getMedia(), item + .getMedia() + .getHumanReadableIdentifier(), + DownloadError.ERROR_REQUEST_ERROR, + false, e.getMessage() + ) + ); + } + } else { + requester.downloadMedia(context, item.getMedia()); + } + } + } + } + + private static int getNumberOfUndownloadedEpisodes( + final List<FeedItem> queue, final List<FeedItem> unreadItems) { + int counter = 0; + for (FeedItem item : queue) { + if (item.hasMedia() && !item.getMedia().isDownloaded() + && !item.getMedia().isPlaying() + && item.getFeed().getPreferences().getAutoDownload()) { + counter++; + } + } + for (FeedItem item : unreadItems) { + if (item.hasMedia() && !item.getMedia().isDownloaded() + && item.getFeed().getPreferences().getAutoDownload()) { + counter++; + } + } + return counter; + } + + /** + * Looks for undownloaded episodes in the queue or list of unread items and request a download if + * 1. Network is available + * 2. There is free space in the episode cache + * This method 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) { + return autodownloadExec.submit(new Runnable() { + @Override + public void run() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Performing auto-dl of undownloaded episodes"); + if (NetworkUtils.autodownloadNetworkAvailable(context) + && UserPreferences.isEnableAutodownload()) { + final List<FeedItem> queue = DBReader.getQueue(context); + final List<FeedItem> unreadItems = DBReader + .getUnreadItemsList(context); + + int undownloadedEpisodes = getNumberOfUndownloadedEpisodes(queue, + unreadItems); + int downloadedEpisodes = DBReader + .getNumberOfDownloadedEpisodes(context); + int deletedEpisodes = performAutoCleanup(context, + getPerformAutoCleanupArgs(context, undownloadedEpisodes)); + int episodeSpaceLeft = undownloadedEpisodes; + boolean cacheIsUnlimited = UserPreferences.getEpisodeCacheSize() == UserPreferences + .getEpisodeCacheSizeUnlimited(); + + if (!cacheIsUnlimited + && UserPreferences.getEpisodeCacheSize() < downloadedEpisodes + + undownloadedEpisodes) { + episodeSpaceLeft = UserPreferences.getEpisodeCacheSize() + - (downloadedEpisodes - deletedEpisodes); + } + + Arrays.sort(mediaIds); // sort for binary search + final boolean ignoreMediaIds = mediaIds.length == 0; + List<FeedItem> itemsToDownload = new ArrayList<FeedItem>(); + + if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { + for (int i = 0; i < queue.size(); i++) { // ignore playing item + FeedItem item = queue.get(i); + long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; + if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) + && item.hasMedia() + && !item.getMedia().isDownloaded() + && !item.getMedia().isPlaying() + && item.getFeed().getPreferences().getAutoDownload()) { + itemsToDownload.add(item); + episodeSpaceLeft--; + undownloadedEpisodes--; + if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { + break; + } + } + } + } + + if (episodeSpaceLeft > 0 && undownloadedEpisodes > 0) { + for (FeedItem item : unreadItems) { + long mediaId = (item.hasMedia()) ? item.getMedia().getId() : -1; + if ((ignoreMediaIds || Arrays.binarySearch(mediaIds, mediaId) >= 0) + && item.hasMedia() + && !item.getMedia().isDownloaded() + && item.getFeed().getPreferences().getAutoDownload()) { + itemsToDownload.add(item); + episodeSpaceLeft--; + undownloadedEpisodes--; + if (episodeSpaceLeft == 0 || undownloadedEpisodes == 0) { + break; + } + } + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Enqueueing " + itemsToDownload.size() + + " items for download"); + + try { + downloadFeedItems(false, context, + itemsToDownload.toArray(new FeedItem[itemsToDownload + .size()]) + ); + } catch (DownloadRequestException e) { + e.printStackTrace(); + } + + } + } + }); + + } + + private static int getPerformAutoCleanupArgs(Context context, + final int episodeNumber) { + if (episodeNumber >= 0 + && UserPreferences.getEpisodeCacheSize() != UserPreferences + .getEpisodeCacheSizeUnlimited()) { + int downloadedEpisodes = DBReader + .getNumberOfDownloadedEpisodes(context); + if (downloadedEpisodes + episodeNumber >= UserPreferences + .getEpisodeCacheSize()) { + + return downloadedEpisodes + episodeNumber + - UserPreferences.getEpisodeCacheSize(); + } + } + return 0; + } + + /** + * Removed downloaded episodes outside of the queue if the episode cache is full. Episodes with a smaller + * 'playbackCompletionDate'-value will be deleted first. + * <p/> + * This method should NOT be executed on the GUI thread. + * + * @param context Used for accessing the DB. + */ + public static void performAutoCleanup(final Context context) { + performAutoCleanup(context, getPerformAutoCleanupArgs(context, 0)); + } + + private static int performAutoCleanup(final Context context, + final int episodeNumber) { + List<FeedItem> candidates = new ArrayList<FeedItem>(); + List<FeedItem> downloadedItems = DBReader.getDownloadedItems(context); + QueueAccess queue = QueueAccess.IDListAccess(DBReader.getQueueIDList(context)); + List<FeedItem> delete; + for (FeedItem item : downloadedItems) { + if (item.hasMedia() && item.getMedia().isDownloaded() + && !queue.contains(item.getId()) && item.isRead()) { + candidates.add(item); + } + + } + + Collections.sort(candidates, new Comparator<FeedItem>() { + @Override + public int compare(FeedItem lhs, FeedItem rhs) { + Date l = lhs.getMedia().getPlaybackCompletionDate(); + Date r = rhs.getMedia().getPlaybackCompletionDate(); + + if (l == null) { + l = new Date(0); + } + if (r == null) { + r = new Date(0); + } + return l.compareTo(r); + } + }); + + if (candidates.size() > episodeNumber) { + delete = candidates.subList(0, episodeNumber); + } else { + delete = candidates; + } + + for (FeedItem item : delete) { + try { + DBWriter.deleteFeedMediaOfItem(context, item.getMedia().getId()).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + } + + int counter = delete.size(); + + if (BuildConfig.DEBUG) + Log.d(TAG, String.format( + "Auto-delete deleted %d episodes (%d requested)", counter, + episodeNumber)); + + return counter; + } + + /** + * Adds all FeedItem objects whose 'read'-attribute is false to the queue in a separate thread. + */ + public static void enqueueAllNewItems(final Context context) { + long[] unreadItems = DBReader.getUnreadItemIds(context); + DBWriter.addQueueItem(context, unreadItems); + } + + /** + * Returns the successor of a FeedItem in the queue. + * + * @param context Used for accessing the DB. + * @param itemId ID of the FeedItem + * @param queue Used for determining the successor of the item. If this parameter is null, the method will load + * the queue from the database in the same thread. + * @return Successor of the FeedItem or null if the FeedItem is not in the queue or has no successor. + */ + public static FeedItem getQueueSuccessorOfItem(Context context, + final long itemId, List<FeedItem> queue) { + FeedItem result = null; + if (queue == null) { + queue = DBReader.getQueue(context); + } + if (queue != null) { + Iterator<FeedItem> iterator = queue.iterator(); + while (iterator.hasNext()) { + FeedItem item = iterator.next(); + if (item.getId() == itemId) { + if (iterator.hasNext()) { + result = iterator.next(); + } + break; + } + } + } + return result; + } + + /** + * Loads the queue from the database and checks if the specified FeedItem is in the queue. + * This method should NOT be executed in the GUI thread. + * + * @param context Used for accessing the DB. + * @param feedItemId ID of the FeedItem + */ + public static boolean isInQueue(Context context, final long feedItemId) { + List<Long> queue = DBReader.getQueueIDList(context); + return QueueAccess.IDListAccess(queue).contains(feedItemId); + } + + private static Feed searchFeedByIdentifyingValueOrID(Context context, PodDBAdapter adapter, + Feed feed) { + if (feed.getId() != 0) { + return DBReader.getFeed(context, feed.getId(), adapter); + } else { + List<Feed> feeds = DBReader.getFeedList(context); + for (Feed f : feeds) { + if (f.getIdentifyingValue().equals(feed.getIdentifyingValue())) { + f.setItems(DBReader.getFeedItemList(context, f)); + return f; + } + } + } + return null; + } + + /** + * Get a FeedItem by its identifying value. + */ + private static FeedItem searchFeedItemByIdentifyingValue(Feed feed, + String identifier) { + for (FeedItem item : feed.getItems()) { + if (item.getIdentifyingValue().equals(identifier)) { + return item; + } + } + return null; + } + + /** + * Adds new Feeds to the database or updates the old versions if they already exists. If another Feed with the same + * identifying value already exists, this method will add new FeedItems from the new Feed to the existing Feed. + * These FeedItems will be marked as unread. + * <p/> + * This method can update multiple feeds at once. Submitting a feed twice in the same method call can result in undefined behavior. + * <p/> + * This method should NOT be executed on the GUI thread. + * + * @param context Used for accessing the DB. + * @param newFeeds The new Feed objects. + * @return The updated Feeds from the database if it already existed, or the new Feed from the parameters otherwise. + */ + public static synchronized Feed[] updateFeed(final Context context, + final Feed... newFeeds) { + List<Feed> newFeedsList = new ArrayList<Feed>(); + List<Feed> updatedFeedsList = new ArrayList<Feed>(); + Feed[] resultFeeds = new Feed[newFeeds.length]; + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + + for (int feedIdx = 0; feedIdx < newFeeds.length; feedIdx++) { + + final Feed newFeed = newFeeds[feedIdx]; + + // Look up feed in the feedslist + final Feed savedFeed = searchFeedByIdentifyingValueOrID(context, adapter, + newFeed); + if (savedFeed == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Found no existing Feed with title " + + newFeed.getTitle() + ". Adding as new one." + ); + // Add a new Feed + newFeedsList.add(newFeed); + resultFeeds[feedIdx] = newFeed; + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Feed with title " + newFeed.getTitle() + + " already exists. Syncing new with existing one."); + + Collections.sort(newFeed.getItems(), new FeedItemPubdateComparator()); + + final boolean markNewItemsAsUnread; + if (newFeed.getPageNr() == savedFeed.getPageNr()) { + if (savedFeed.compareWithOther(newFeed)) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Feed has updated attribute values. Updating old feed's attributes"); + savedFeed.updateFromOther(newFeed); + } + markNewItemsAsUnread = true; + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "New feed has a higher page number. Merging without marking as unread"); + markNewItemsAsUnread = false; + savedFeed.setNextPageLink(newFeed.getNextPageLink()); + } + if (savedFeed.getPreferences().compareWithOther(newFeed.getPreferences())) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Feed has updated preferences. Updating old feed's preferences"); + savedFeed.getPreferences().updateFromOther(newFeed.getPreferences()); + } + // Look for new or updated Items + for (int idx = 0; idx < newFeed.getItems().size(); idx++) { + final FeedItem item = newFeed.getItems().get(idx); + FeedItem oldItem = searchFeedItemByIdentifyingValue(savedFeed, + item.getIdentifyingValue()); + if (oldItem == null) { + // item is new + final int i = idx; + item.setFeed(savedFeed); + savedFeed.getItems().add(i, item); + if (markNewItemsAsUnread) { + item.setRead(false); + } + } else { + oldItem.updateFromOther(item); + } + } + // update attributes + savedFeed.setLastUpdate(newFeed.getLastUpdate()); + savedFeed.setType(newFeed.getType()); + + updatedFeedsList.add(savedFeed); + resultFeeds[feedIdx] = savedFeed; + } + } + + adapter.close(); + + try { + DBWriter.addNewFeed(context, newFeedsList.toArray(new Feed[newFeedsList.size()])).get(); + DBWriter.setCompleteFeed(context, updatedFeedsList.toArray(new Feed[updatedFeedsList.size()])).get(); + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + + return resultFeeds; + } + + /** + * Searches the titles of FeedItems of a specific Feed for a given + * string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string. + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask<List<FeedItem>> searchFeedItemTitle(final Context context, + final long feedID, final String query) { + return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemTitles(feedID, + query); + List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * Searches the descriptions of FeedItems of a specific Feed for a given + * string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask<List<FeedItem>> searchFeedItemDescription(final Context context, + final long feedID, final String query) { + return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemDescriptions(feedID, + query); + List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * Searches the contentEncoded-value of FeedItems of a specific Feed for a given + * string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask<List<FeedItem>> searchFeedItemContentEncoded(final Context context, + final long feedID, final String query) { + return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemContentEncoded(feedID, + query); + List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * Searches chapters of the FeedItems of a specific Feed for a given string. + * + * @param context Used for accessing the DB. + * @param feedID The id of the feed whose items should be searched. + * @param query The search string + * @return A FutureTask object that executes the search request and returns the search result as a List of FeedItems. + */ + public static FutureTask<List<FeedItem>> searchFeedItemChapters(final Context context, + final long feedID, final String query) { + return new FutureTask<List<FeedItem>>(new QueryTask<List<FeedItem>>(context) { + @Override + public void execute(PodDBAdapter adapter) { + Cursor searchResult = adapter.searchItemChapters(feedID, + query); + List<FeedItem> items = DBReader.extractItemlistFromCursor(context, searchResult); + DBReader.loadFeedDataOfFeedItemlist(context, items); + setResult(items); + searchResult.close(); + } + }); + } + + /** + * A runnable which should be used for database queries. The onCompletion + * method is executed on the database executor to handle Cursors correctly. + * This class automatically creates a PodDBAdapter object and closes it when + * it is no longer in use. + */ + static abstract class QueryTask<T> implements Callable<T> { + private T result; + private Context context; + + public QueryTask(Context context) { + this.context = context; + } + + @Override + public T call() throws Exception { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + execute(adapter); + adapter.close(); + return result; + } + + public abstract void execute(PodDBAdapter adapter); + + protected void setResult(T result) { + this.result = result; + } + } + + /** + * Adds the given FeedItem to the flattr queue if the user is logged in. Otherwise, a dialog + * will be opened that lets the user go either to the login screen or the website of the flattr thing. + * + * @param context + * @param item + */ + public static void flattrItemIfLoggedIn(Context context, FeedItem item) { + if (FlattrUtils.hasToken()) { + item.getFlattrStatus().setFlattrQueue(); + DBWriter.setFlattredStatus(context, item, true); + } else { + FlattrUtils.showNoTokenDialogOrRedirect(context, item.getPaymentLink()); + } + } + + /** + * Adds the given Feed to the flattr queue if the user is logged in. Otherwise, a dialog + * will be opened that lets the user go either to the login screen or the website of the flattr thing. + * + * @param context + * @param feed + */ + public static void flattrFeedIfLoggedIn(Context context, Feed feed) { + if (FlattrUtils.hasToken()) { + feed.getFlattrStatus().setFlattrQueue(); + DBWriter.setFlattredStatus(context, feed, true); + } else { + FlattrUtils.showNoTokenDialogOrRedirect(context, feed.getPaymentLink()); + } + } + +} 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 new file mode 100644 index 000000000..d71d19433 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -0,0 +1,979 @@ +package de.danoeh.antennapod.core.storage; + +import android.app.backup.BackupManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.database.Cursor; +import android.preference.PreferenceManager; +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.asynctask.FlattrClickWorker; +import de.danoeh.antennapod.core.feed.*; +import de.danoeh.antennapod.core.preferences.GpodnetPreferences; +import de.danoeh.antennapod.core.preferences.PlaybackPreferences; +import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.service.playback.PlaybackService; +import de.danoeh.antennapod.core.util.QueueAccess; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrThing; +import de.danoeh.antennapod.core.util.flattr.SimpleFlattrThing; +import org.shredzone.flattr4j.model.Flattr; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ThreadFactory; + +/** + * Provides methods for writing data to AntennaPod's database. + * In general, DBWriter-methods will be executed on an internal ExecutorService. + * Some methods return a Future-object which the caller can use for waiting for the method's completion. The returned Future's + * will NOT contain any results. + * The caller can also use the {@link EventDistributor} in order to be notified about the method's completion asynchronously. + * This class will use the {@link EventDistributor} to notify listeners about changes in the database. + */ +public class DBWriter { + private static final String TAG = "DBWriter"; + + private static final ExecutorService dbExec; + + static { + dbExec = Executors.newSingleThreadExecutor(new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }); + } + + private DBWriter() { + } + + /** + * Deletes a downloaded FeedMedia file from the storage device. + * + * @param context A context that is used for opening a database connection. + * @param mediaId ID of the FeedMedia object whose downloaded file should be deleted. + */ + public static Future<?> deleteFeedMediaOfItem(final Context context, + final long mediaId) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + + final FeedMedia media = DBReader.getFeedMedia(context, mediaId); + if (media != null) { + 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)); + } + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleting File. Result: " + result); + EventDistributor.getInstance().sendQueueUpdateBroadcast(); + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + } + } + }); + } + + /** + * Deletes a Feed and all downloaded files of its components like images and downloaded episodes. + * + * @param context A context that is used for opening a database connection. + * @param feedId ID of the Feed that should be deleted. + */ + public static Future<?> deleteFeed(final Context context, final long feedId) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + DownloadRequester requester = DownloadRequester.getInstance(); + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(context + .getApplicationContext()); + final Feed feed = DBReader.getFeed(context, feedId); + if (feed != null) { + if (PlaybackPreferences.getCurrentlyPlayingMedia() == FeedMedia.PLAYABLE_TYPE_FEEDMEDIA + && PlaybackPreferences.getLastPlayedFeedId() == feed + .getId()) { + context.sendBroadcast(new Intent( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + SharedPreferences.Editor editor = prefs.edit(); + editor.putLong( + PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, + -1); + editor.commit(); + } + + // delete image file + if (feed.getImage() != null) { + if (feed.getImage().isDownloaded() + && feed.getImage().getFile_url() != null) { + File imageFile = new File(feed.getImage() + .getFile_url()); + imageFile.delete(); + } else if (requester.isDownloadingFile(feed.getImage())) { + requester.cancelDownload(context, feed.getImage()); + } + } + // delete stored media files and mark them as read + List<FeedItem> queue = DBReader.getQueue(context); + boolean queueWasModified = false; + if (feed.getItems() == null) { + DBReader.getFeedItemList(context, feed); + } + + for (FeedItem item : feed.getItems()) { + queueWasModified |= queue.remove(item); + if (item.getMedia() != null + && item.getMedia().isDownloaded()) { + File mediaFile = new File(item.getMedia() + .getFile_url()); + mediaFile.delete(); + } else if (item.getMedia() != null + && requester.isDownloadingFile(item.getMedia())) { + requester.cancelDownload(context, item.getMedia()); + } + + 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()); + } + EventDistributor.getInstance().sendFeedUpdateBroadcast(); + + BackupManager backupManager = new BackupManager(context); + backupManager.dataChanged(); + } + } + }); + } + + /** + * Deletes the entire playback history. + * + * @param context A context that is used for opening a database connection. + */ + public static Future<?> clearPlaybackHistory(final Context context) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.clearPlaybackHistory(); + adapter.close(); + EventDistributor.getInstance() + .sendPlaybackHistoryUpdateBroadcast(); + } + }); + } + + /** + * Adds a FeedMedia object to the playback history. A FeedMedia object is in the playback history if + * its playback completion date is set to a non-null value. This method will set the playback completion date to the + * current date regardless of the current value. + * + * @param context A context that is used for opening a database connection. + * @param media FeedMedia that should be added to the playback history. + */ + public static Future<?> addItemToPlaybackHistory(final Context context, + final FeedMedia media) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + if (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(); + + } + }); + } + + 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(); + } + }); + + } + + /** + * Inserts a FeedItem in the queue at the specified index. The 'read'-attribute of the FeedItem will be set to + * true. If the FeedItem is already in the queue, the queue will not be modified. + * + * @param context A context that is used for opening a database connection. + * @param itemId ID of the FeedItem that should be added to the queue. + * @param index Destination index. Must be in range 0..queue.size() + * @param performAutoDownload True if an auto-download process should be started after the operation + * @throws IndexOutOfBoundsException if index < 0 || index >= queue.size() + */ + public static Future<?> addQueueItemAt(final Context context, final long itemId, + final int index, final boolean performAutoDownload) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List<FeedItem> queue = DBReader + .getQueue(context, adapter); + FeedItem item = null; + + if (queue != null) { + boolean queueModified = false; + boolean unreadItemsModified = false; + + if (!itemListContains(queue, itemId)) { + item = DBReader.getFeedItem(context, itemId); + if (item != null) { + queue.add(index, item); + queueModified = true; + if (!item.isRead()) { + item.setRead(true); + unreadItemsModified = true; + } + } + } + if (queueModified) { + adapter.setQueue(queue); + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } + if (unreadItemsModified && item != null) { + adapter.setSingleFeedItem(item); + EventDistributor.getInstance() + .sendUnreadItemsUpdateBroadcast(); + } + } + adapter.close(); + if (performAutoDownload) { + DBTasks.autodownloadUndownloadedItems(context); + } + + } + }); + + } + + /** + * Appends FeedItem objects to the end of the queue. The 'read'-attribute of all items will be set to true. + * If a FeedItem is already in the queue, the FeedItem will not change its position in the queue. + * + * @param context A context that is used for opening a database connection. + * @param itemIds IDs of the FeedItem objects that should be added to the queue. + */ + public static Future<?> addQueueItem(final Context context, + final long... itemIds) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + if (itemIds.length > 0) { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List<FeedItem> queue = DBReader.getQueue(context, + adapter); + + if (queue != null) { + boolean queueModified = false; + boolean unreadItemsModified = false; + List<FeedItem> itemsToSave = new LinkedList<FeedItem>(); + for (int i = 0; i < itemIds.length; i++) { + if (!itemListContains(queue, itemIds[i])) { + final FeedItem item = DBReader.getFeedItem( + context, itemIds[i]); + + if (item != null) { + queue.add(item); + queueModified = true; + if (!item.isRead()) { + item.setRead(true); + itemsToSave.add(item); + unreadItemsModified = true; + } + } + } + } + if (queueModified) { + adapter.setQueue(queue); + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } + if (unreadItemsModified) { + adapter.setFeedItemlist(itemsToSave); + EventDistributor.getInstance() + .sendUnreadItemsUpdateBroadcast(); + } + } + adapter.close(); + 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(); + + EventDistributor.getInstance().sendQueueUpdateBroadcast(); + } + }); + } + + /** + * Removes a FeedItem object from the queue. + * + * @param context A context that is used for opening a database connection. + * @param itemId ID of the FeedItem that should be removed. + * @param performAutoDownload true if an auto-download process should be started after the operation. + */ + public static Future<?> removeQueueItem(final Context context, + final long itemId, final boolean performAutoDownload) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List<FeedItem> queue = DBReader + .getQueue(context, adapter); + FeedItem item = null; + + if (queue != null) { + boolean queueModified = false; + QueueAccess queueAccess = QueueAccess.ItemListAccess(queue); + if (queueAccess.contains(itemId)) { + item = DBReader.getFeedItem(context, itemId); + if (item != null) { + queueModified = queueAccess.remove(itemId); + } + } + if (queueModified) { + adapter.setQueue(queue); + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } else { + Log.w(TAG, "Queue was not modified by call to removeQueueItem"); + } + } else { + Log.e(TAG, "removeQueueItem: Could not load queue"); + } + adapter.close(); + if (performAutoDownload) { + DBTasks.autodownloadUndownloadedItems(context); + } + } + }); + + } + + /** + * 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 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() { + List<Long> queueIdList = DBReader.getQueueIDList(context); + int currentLocation = 0; + for (long id : queueIdList) { + if (id == itemId) { + moveQueueItemHelper(context, currentLocation, 0, broadcastUpdate); + return; + } + currentLocation++; + } + 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 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, + final boolean broadcastUpdate) { + return dbExec.submit(new Runnable() { + @Override + public void run() { + List<Long> queueIdList = DBReader.getQueueIDList(context); + int currentLocation = 0; + for (long id : queueIdList) { + if (id == itemId) { + moveQueueItemHelper(context, currentLocation, queueIdList.size() - 1, + broadcastUpdate); + return; + } + currentLocation++; + } + Log.e(TAG, "moveQueueItemToBottom: item not found"); + } + }); + } + + /** + * Changes the position of a FeedItem in the queue. + * + * @param context A context that is used for opening a database connection. + * @param from Source index. Must be in range 0..queue.size()-1. + * @param to Destination index. Must be in range 0..queue.size()-1. + * @param broadcastUpdate true if this operation should trigger a QueueUpdateBroadcast. This option should be set to + * false if the caller wants to avoid unexpected updates of the GUI. + * @throws IndexOutOfBoundsException if (to < 0 || to >= queue.size()) || (from < 0 || from >= queue.size()) + */ + public static Future<?> moveQueueItem(final Context context, final int from, + final int to, final boolean broadcastUpdate) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + moveQueueItemHelper(context, from, to, broadcastUpdate); + } + }); + } + + /** + * Changes the position of a FeedItem in the queue. + * <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, + final int to, final boolean broadcastUpdate) { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + final List<FeedItem> queue = DBReader + .getQueue(context, adapter); + + if (queue != null) { + if (from >= 0 && from < queue.size() && to >= 0 + && to < queue.size()) { + + final FeedItem item = queue.remove(from); + queue.add(to, item); + + adapter.setQueue(queue); + if (broadcastUpdate) { + EventDistributor.getInstance() + .sendQueueUpdateBroadcast(); + } + + } + } else { + Log.e(TAG, "moveQueueItemHelper: Could not load queue"); + } + adapter.close(); + } + + /** + * Sets the 'read'-attribute of a FeedItem to the specified value. + * + * @param context A context that is used for opening a database connection. + * @param item The FeedItem object + * @param read New value of the 'read'-attribute + * @param resetMediaPosition true if this method should also reset the position of the FeedItem's FeedMedia object. + * If the FeedItem has no FeedMedia object, this parameter will be ignored. + */ + public static Future<?> markItemRead(Context context, FeedItem item, boolean read, boolean resetMediaPosition) { + long mediaId = (item.hasMedia()) ? item.getMedia().getId() : 0; + return markItemRead(context, item.getId(), read, mediaId, resetMediaPosition); + } + + /** + * Sets the 'read'-attribute of a FeedItem to the specified value. + * + * @param context A context that is used for opening a database connection. + * @param itemId ID of the FeedItem + * @param read New value of the 'read'-attribute + */ + public static Future<?> markItemRead(final Context context, final long itemId, + final boolean read) { + return markItemRead(context, itemId, read, 0, false); + } + + private static Future<?> markItemRead(final Context context, final long itemId, + final boolean read, final long mediaId, + final boolean resetMediaPosition) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setFeedItemRead(read, itemId, mediaId, + resetMediaPosition); + adapter.close(); + + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + } + }); + } + + /** + * Sets the 'read'-attribute of all FeedItems of a specific Feed to true. + * + * @param context A context that is used for opening a database connection. + * @param feedId ID of the Feed. + */ + public static Future<?> markFeedRead(final Context context, final long feedId) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor itemCursor = adapter.getAllItemsOfFeedCursor(feedId); + long[] itemIds = new long[itemCursor.getCount()]; + itemCursor.moveToFirst(); + for (int i = 0; i < itemIds.length; i++) { + itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX); + itemCursor.moveToNext(); + } + itemCursor.close(); + adapter.setFeedItemRead(true, itemIds); + adapter.close(); + + EventDistributor.getInstance().sendUnreadItemsUpdateBroadcast(); + } + }); + + } + + /** + * Sets the 'read'-attribute of all FeedItems to true. + * + * @param context A context that is used for opening a database connection. + */ + public static Future<?> markAllItemsRead(final Context context) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + final PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + Cursor itemCursor = adapter.getUnreadItemsCursor(); + long[] itemIds = new long[itemCursor.getCount()]; + itemCursor.moveToFirst(); + for (int i = 0; i < itemIds.length; i++) { + itemIds[i] = itemCursor.getLong(PodDBAdapter.KEY_ID_INDEX); + itemCursor.moveToNext(); + } + itemCursor.close(); + adapter.setFeedItemRead(true, 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()); + } + } + + 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(); + + } + }); + + } + + /** + * Saves a FeedMedia object in the database. This method will save all attributes of the FeedMedia object. The + * contents of FeedComponent-attributes (e.g. the FeedMedia's 'item'-attribute) will not be saved. + * + * @param context A context that is used for opening a database connection. + * @param media The FeedMedia object. + */ + public static Future<?> setFeedMedia(final Context context, + final FeedMedia media) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setMedia(media); + adapter.close(); + } + }); + } + + /** + * Saves the 'position' and 'duration' 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(); + } + }); + } + + /** + * Saves a FeedItem object in the database. This method will save all attributes of the FeedItem object including + * the content of FeedComponent-attributes. + * + * @param context A context that is used for opening a database connection. + * @param item The FeedItem object. + */ + public static Future<?> setFeedItem(final Context context, + final FeedItem item) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setSingleFeedItem(item); + adapter.close(); + } + }); + } + + /** + * Saves a FeedImage object in the database. This method will save all attributes of the FeedImage object. The + * contents of FeedComponent-attributes (e.g. the FeedImages's 'feed'-attribute) will not be saved. + * + * @param context A context that is used for opening a database connection. + * @param image The FeedImage object. + */ + public static Future<?> setFeedImage(final Context context, + final FeedImage image) { + return dbExec.submit(new Runnable() { + + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.setImage(image); + adapter.close(); + } + }); + } + + /** + * 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 + */ + 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(); + } + }); + } + + /** + * 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(); + } + }); + } + + private static boolean itemListContains(List<FeedItem> items, long itemId) { + for (FeedItem item : items) { + if (item.getId() == itemId) { + return true; + } + } + return false; + } + + /** + * Saves the FlattrStatus of a FeedItem object in the database. + * + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + */ + 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(); + } + } + }); + } + + /** + * Saves the FlattrStatus of a Feed object in the database. + * + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + */ + 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(); + } + } + }); + } + + /** + * format an url for querying the database + * (postfix a / and apply percent-encoding) + */ + private static String formatURIForQuery(String uri) { + try { + return URLEncoder.encode(uri.endsWith("/") ? uri.substring(0, uri.length() - 1) : uri, "UTF-8"); + } catch (UnsupportedEncodingException e) { + Log.e(TAG, e.getMessage()); + return ""; + } + } + + + /** + * Set flattr status of the passed thing (either a FeedItem or a Feed) + * + * @param context + * @param thing + * @param startFlattrClickWorker true if FlattrClickWorker should be started after the FlattrStatus has been saved + * @return + */ + public static Future<?> setFlattredStatus(Context context, FlattrThing thing, boolean startFlattrClickWorker) { + // must propagate this to back db + if (thing instanceof FeedItem) + return setFeedItemFlattrStatus(context, (FeedItem) thing, startFlattrClickWorker); + 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 + Log.e(TAG, "flattrQueue processing - thing is neither FeedItem nor Feed nor SimpleFlattrThing"); + + return null; + } + + /** + * Reset flattr status to unflattrd for all items + */ + public static Future<?> clearAllFlattrStatus(final Context context) { + Log.d(TAG, "clearAllFlattrStatus()"); + return dbExec.submit(new Runnable() { + @Override + public void run() { + PodDBAdapter adapter = new PodDBAdapter(context); + adapter.open(); + adapter.clearAllFlattrStatus(); + adapter.close(); + } + }); + } + + /** + * 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) { + Log.d(TAG, "setFlattredStatus to status retrieved from flattr api running with " + flattrList.size() + " items"); + // clear flattr status in db + clearAllFlattrStatus(context); + + // 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(); + } + }); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java new file mode 100644 index 000000000..c85559e20 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequestException.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.storage; + +/** + * Thrown by the DownloadRequester if a download request contains invalid data + * or something went wrong while processing the request. + */ +public class DownloadRequestException extends Exception { + + public DownloadRequestException() { + super(); + } + + public DownloadRequestException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public DownloadRequestException(String detailMessage) { + super(detailMessage); + } + + public DownloadRequestException(Throwable throwable) { + super(throwable); + } + +} 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 new file mode 100644 index 000000000..148d886ae --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DownloadRequester.java @@ -0,0 +1,390 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +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; +import de.danoeh.antennapod.core.service.download.DownloadService; +import de.danoeh.antennapod.core.util.FileNameGenerator; +import de.danoeh.antennapod.core.util.URLChecker; + + +/** + * Sends download requests to the DownloadService. This class should always be used for starting downloads, + * otherwise they won't work correctly. + */ +public class DownloadRequester { + private static final String TAG = "DownloadRequester"; + + public static final String IMAGE_DOWNLOADPATH = "images/"; + public static final String FEED_DOWNLOADPATH = "cache/"; + public static final String MEDIA_DOWNLOADPATH = "media/"; + + /** + * Denotes the page of the feed that is contained in the DownloadRequest sent by the DownloadRequester. + */ + public static final String REQUEST_ARG_PAGE_NR = "page"; + + /** + * True if all pages after the feed that is contained in this DownloadRequest should be downloaded. + */ + public static final String REQUEST_ARG_LOAD_ALL_PAGES = "loadAllPages"; + + private static DownloadRequester downloader; + + private Map<String, DownloadRequest> downloads; + + private DownloadRequester() { + downloads = new ConcurrentHashMap<String, DownloadRequest>(); + } + + public static synchronized DownloadRequester getInstance() { + if (downloader == null) { + downloader = new DownloadRequester(); + } + return downloader; + } + + /** + * Starts a new download with the given DownloadRequest. This method should only + * be used from outside classes if the DownloadRequest was created by the DownloadService to + * ensure that the data is valid. Use downloadFeed(), downloadImage() or downloadMedia() instead. + * + * @param context Context object for starting the DownloadService + * @param request The DownloadRequest. If another DownloadRequest with the same source URL is already stored, this method + * 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); + + if (downloads.containsKey(request.getSource())) { + if (BuildConfig.DEBUG) Log.i(TAG, "DownloadRequest is already stored."); + return false; + } + downloads.put(request.getSource(), request); + + Intent launchIntent = new Intent(context, DownloadService.class); + launchIntent.putExtra(DownloadService.EXTRA_REQUEST, request); + context.startService(launchIntent); + EventDistributor.getInstance().sendDownloadQueuedBroadcast(); + return true; + } + + private void download(Context context, FeedFile item, File dest, + boolean overwriteIfExists, String username, String password, boolean deleteOnFailure, Bundle arguments) { + if (!isDownloadingFile(item)) { + if (!isFilenameAvailable(dest.toString()) || (deleteOnFailure && dest.exists())) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Filename already used."); + if (isFilenameAvailable(dest.toString()) && overwriteIfExists) { + boolean result = dest.delete(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleting file. Result: " + result); + } else { + // find different name + File newDest = null; + for (int i = 1; i < Integer.MAX_VALUE; i++) { + String newName = FilenameUtils.getBaseName(dest + .getName()) + + "-" + + i + + FilenameUtils.EXTENSION_SEPARATOR + + FilenameUtils.getExtension(dest.getName()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Testing filename " + newName); + newDest = new File(dest.getParent(), newName); + if (!newDest.exists() + && isFilenameAvailable(newDest.toString())) { + if (BuildConfig.DEBUG) + Log.d(TAG, "File doesn't exist yet. Using " + + newName); + break; + } + } + if (newDest != null) { + dest = newDest; + } + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, + "Requesting download of url " + item.getDownload_url()); + item.setDownload_url(URLChecker.prepareURL(item.getDownload_url())); + + DownloadRequest request = new DownloadRequest(dest.toString(), + URLChecker.prepareURL(item.getDownload_url()), item.getHumanReadableIdentifier(), + item.getId(), item.getTypeAsInt(), username, password, deleteOnFailure, arguments); + + download(context, request); + } else { + Log.e(TAG, "URL " + item.getDownload_url() + + " is already being downloaded"); + } + } + + /** + * Returns true if a filename is available and false if it has already been + * taken by another requested download. + */ + private boolean isFilenameAvailable(String path) { + for (String key : downloads.keySet()) { + DownloadRequest r = downloads.get(key); + if (StringUtils.equals(r.getDestination(), path)) { + if (BuildConfig.DEBUG) + Log.d(TAG, path + + " is already used by another requested download"); + return false; + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, path + " is available as a download destination"); + return true; + } + + public synchronized void downloadFeed(Context context, Feed feed, boolean loadAllPages) + throws DownloadRequestException { + if (feedFileValid(feed)) { + String username = (feed.getPreferences() != null) ? feed.getPreferences().getUsername() : null; + String password = (feed.getPreferences() != null) ? feed.getPreferences().getPassword() : null; + + Bundle args = new Bundle(); + args.putInt(REQUEST_ARG_PAGE_NR, feed.getPageNr()); + args.putBoolean(REQUEST_ARG_LOAD_ALL_PAGES, loadAllPages); + + download(context, feed, new File(getFeedfilePath(context), + getFeedfileName(feed)), true, username, password, true, args); + } + } + + public synchronized void downloadFeed(Context context, Feed feed) throws DownloadRequestException { + downloadFeed(context, feed, false); + } + + public synchronized void downloadImage(Context context, FeedImage image) + throws DownloadRequestException { + if (feedFileValid(image)) { + download(context, image, new File(getImagefilePath(context), + getImagefileName(image)), false, null, null, false, null); + } + } + + public synchronized void downloadMedia(Context context, FeedMedia feedmedia) + throws DownloadRequestException { + if (feedFileValid(feedmedia)) { + Feed feed = feedmedia.getItem().getFeed(); + String username; + String password; + if (feed != null && feed.getPreferences() != null) { + username = feed.getPreferences().getUsername(); + password = feed.getPreferences().getPassword(); + } else { + username = null; + password = null; + } + + File dest; + if (feedmedia.getFile_url() != null) { + dest = new File(feedmedia.getFile_url()); + } else { + dest = new File(getMediafilePath(context, feedmedia), + getMediafilename(feedmedia)); + } + download(context, feedmedia, + dest, false, username, password, false, null); + } + } + + /** + * Throws a DownloadRequestException if the feedfile or the download url of + * the feedfile is null. + * + * @throws DownloadRequestException + */ + private boolean feedFileValid(FeedFile f) throws DownloadRequestException { + if (f == null) { + throw new DownloadRequestException("Feedfile was null"); + } else if (f.getDownload_url() == null) { + throw new DownloadRequestException("File has no download URL"); + } else { + return true; + } + } + + /** + * Cancels a running download. + */ + public synchronized void cancelDownload(final Context context, final FeedFile f) { + cancelDownload(context, f.getDownload_url()); + } + + /** + * Cancels a running download. + */ + public synchronized void cancelDownload(final Context context, final String downloadUrl) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Cancelling download with url " + downloadUrl); + Intent cancelIntent = new Intent(DownloadService.ACTION_CANCEL_DOWNLOAD); + cancelIntent.putExtra(DownloadService.EXTRA_DOWNLOAD_URL, downloadUrl); + context.sendBroadcast(cancelIntent); + } + + /** + * Cancels all running downloads + */ + public synchronized void cancelAllDownloads(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Cancelling all running downloads"); + context.sendBroadcast(new Intent( + DownloadService.ACTION_CANCEL_ALL_DOWNLOADS)); + } + + /** + * Returns true if there is at least one Feed in the downloads queue. + */ + public synchronized boolean isDownloadingFeeds() { + for (DownloadRequest r : downloads.values()) { + if (r.getFeedfileType() == Feed.FEEDFILETYPE_FEED) { + return true; + } + } + return false; + } + + /** + * Checks if feedfile is in the downloads list + */ + public synchronized boolean isDownloadingFile(FeedFile item) { + if (item.getDownload_url() != null) { + return downloads.containsKey(item.getDownload_url()); + } + return false; + } + + public synchronized DownloadRequest getDownload(String downloadUrl) { + return downloads.get(downloadUrl); + } + + /** + * Checks if feedfile with the given download url is in the downloads list + */ + public synchronized boolean isDownloadingFile(String downloadUrl) { + return downloads.get(downloadUrl) != null; + } + + public synchronized boolean hasNoDownloads() { + return downloads.isEmpty(); + } + + /** + * Remove an object from the downloads-list of the requester. + */ + public synchronized void removeDownload(DownloadRequest r) { + if (downloads.remove(r.getSource()) == null) { + Log.e(TAG, + "Could not remove object with url " + r.getSource()); + } + } + + /** + * Get the number of uncompleted Downloads + */ + public synchronized int getNumberOfDownloads() { + return downloads.size(); + } + + public synchronized String getFeedfilePath(Context context) + throws DownloadRequestException { + return getExternalFilesDirOrThrowException(context, FEED_DOWNLOADPATH) + .toString() + "/"; + } + + public synchronized String getFeedfileName(Feed feed) { + String filename = feed.getDownload_url(); + if (feed.getTitle() != null && !feed.getTitle().isEmpty()) { + filename = feed.getTitle(); + } + return "feed-" + FileNameGenerator.generateFileName(filename); + } + + public 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( + context, + MEDIA_DOWNLOADPATH + + FileNameGenerator.generateFileName(media.getItem() + .getFeed().getTitle()) + "/" + ); + return externalStorage.toString(); + } + + private File getExternalFilesDirOrThrowException(Context context, + String type) throws DownloadRequestException { + File result = UserPreferences.getDataFolder(context, type); + if (result == null) { + throw new DownloadRequestException( + "Failed to access external storage"); + } + return result; + } + + private String getMediafilename(FeedMedia media) { + String filename; + String titleBaseFilename = ""; + + // Try to generate the filename by the item title + if (media.getItem() != null && media.getItem().getTitle() != null) { + String title = media.getItem().getTitle(); + // Delete reserved characters + titleBaseFilename = title.replaceAll("[\\\\/%\\?\\*:|<>\"\\p{Cntrl}]", ""); + titleBaseFilename = titleBaseFilename.trim(); + } + + String URLBaseFilename = URLUtil.guessFileName(media.getDownload_url(), + null, media.getMime_type()); + ; + + if (titleBaseFilename != "") { + // Append extension + filename = titleBaseFilename + FilenameUtils.EXTENSION_SEPARATOR + + FilenameUtils.getExtension(URLBaseFilename); + } else { + // Fall back on URL file name + filename = URLBaseFilename; + } + return filename; + } +} 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 new file mode 100644 index 000000000..f6a59836b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedItemStatistics.java @@ -0,0 +1,70 @@ +package de.danoeh.antennapod.core.storage; + +import java.util.Date; + +/** + * Contains information about a feed's items. + */ +public class FeedItemStatistics { + private long feedID; + private int numberOfItems; + private int numberOfNewItems; + private int numberOfInProgressItems; + private Date lastUpdate; + private static final Date UNKNOWN_DATE = new Date(0); + + + /** + * Creates new FeedItemStatistics object. + * + * @param feedID ID of the feed. + * @param numberOfItems Number of items that this feed has. + * @param numberOfNewItems Number of unread items this feed has. + * @param numberOfInProgressItems Number of items that the user has started listening to. + * @param lastUpdate pubDate of the latest episode. A lastUpdate value of 0 will be interpreted as DATE_UNKOWN if + * numberOfItems is 0. + */ + public FeedItemStatistics(long feedID, int numberOfItems, int numberOfNewItems, int numberOfInProgressItems, Date lastUpdate) { + this.feedID = feedID; + this.numberOfItems = numberOfItems; + this.numberOfNewItems = numberOfNewItems; + this.numberOfInProgressItems = numberOfInProgressItems; + if (numberOfItems > 0) { + this.lastUpdate = (lastUpdate != null) ? (Date) lastUpdate.clone() : null; + } else { + this.lastUpdate = UNKNOWN_DATE; + } + } + + public long getFeedID() { + return feedID; + } + + public int getNumberOfItems() { + return numberOfItems; + } + + public int getNumberOfNewItems() { + return numberOfNewItems; + } + + public int getNumberOfInProgressItems() { + return numberOfInProgressItems; + } + + /** + * Returns the pubDate of the latest item in the feed. Users of this method + * should check if this value is unkown or not by calling lastUpdateKnown() first. + */ + public Date getLastUpdate() { + return (lastUpdate != null) ? (Date) lastUpdate.clone() : null; + } + + /** + * Returns true if the lastUpdate value is known. The lastUpdate value is unkown if the + * feed has no items. + */ + public boolean lastUpdateKnown() { + return lastUpdate != UNKNOWN_DATE; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java new file mode 100644 index 000000000..3a63685ba --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java @@ -0,0 +1,57 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.Context; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.feed.SearchResult; +import de.danoeh.antennapod.core.util.comparator.SearchResultValueComparator; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +/** + * Performs search on Feeds and FeedItems + */ +public class FeedSearcher { + private static final String TAG = "FeedSearcher"; + + + /** + * Performs a search in all feeds or one specific feed. + */ + public static List<SearchResult> performSearch(final Context context, + final String query, final long selectedFeed) { + final int values[] = {0, 0, 1, 2}; + final String[] subtitles = {context.getString(R.string.found_in_shownotes_label), + context.getString(R.string.found_in_shownotes_label), + context.getString(R.string.found_in_chapters_label), + context.getString(R.string.found_in_title_label)}; + + List<SearchResult> result = new ArrayList<SearchResult>(); + + FutureTask<List<FeedItem>>[] tasks = new FutureTask[4]; + (tasks[0] = DBTasks.searchFeedItemContentEncoded(context, selectedFeed, query)).run(); + (tasks[1] = DBTasks.searchFeedItemDescription(context, selectedFeed, query)).run(); + (tasks[2] = DBTasks.searchFeedItemChapters(context, selectedFeed, query)).run(); + (tasks[3] = DBTasks.searchFeedItemTitle(context, selectedFeed, query)).run(); + try { + for (int i = 0; i < tasks.length; i++) { + FutureTask task = tasks[i]; + List<FeedItem> items = (List<FeedItem>) task.get(); + for (FeedItem item : items) { + result.add(new SearchResult(item, values[i], subtitles[i])); + } + + } + } catch (InterruptedException e) { + e.printStackTrace(); + } catch (ExecutionException e) { + e.printStackTrace(); + } + Collections.sort(result, new SearchResultValueComparator()); + return result; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java new file mode 100644 index 000000000..79124521f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -0,0 +1,1324 @@ +package de.danoeh.antennapod.core.storage; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.DatabaseUtils; +import android.database.MergeCursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteDatabase.CursorFactory; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.Log; + +import org.apache.commons.lang3.Validate; + +import java.util.Arrays; +import java.util.List; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedComponent; +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.service.download.DownloadStatus; +import de.danoeh.antennapod.core.util.flattr.FlattrStatus; + +// TODO Remove media column from feeditem table + +/** + * Implements methods for accessing the database + */ +public class PodDBAdapter { + private static final String TAG = "PodDBAdapter"; + public static final String DATABASE_NAME = "Antennapod.db"; + + /** + * Maximum number of arguments for IN-operator. + */ + public static final int IN_OPERATOR_MAXIMUM = 800; + + /** + * Maximum number of entries per search request. + */ + public static final int SEARCH_LIMIT = 30; + + // ----------- Column indices + // ----------- General indices + public static final int KEY_ID_INDEX = 0; + public static final int KEY_TITLE_INDEX = 1; + public static final int KEY_FILE_URL_INDEX = 2; + public static final int KEY_DOWNLOAD_URL_INDEX = 3; + public static final int KEY_DOWNLOADED_INDEX = 4; + public static final int KEY_LINK_INDEX = 5; + public static final int KEY_DESCRIPTION_INDEX = 6; + public static final int KEY_PAYMENT_LINK_INDEX = 7; + // ----------- Feed indices + public static final int KEY_LAST_UPDATE_INDEX = 8; + public static final int KEY_LANGUAGE_INDEX = 9; + public static final int KEY_AUTHOR_INDEX = 10; + public static final int KEY_IMAGE_INDEX = 11; + public static final int KEY_TYPE_INDEX = 12; + public static final int KEY_FEED_IDENTIFIER_INDEX = 13; + 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"; + public static final String KEY_NAME = "name"; + public static final String KEY_LINK = "link"; + public static final String KEY_DESCRIPTION = "description"; + public static final String KEY_FILE_URL = "file_url"; + public static final String KEY_DOWNLOAD_URL = "download_url"; + public static final String KEY_PUBDATE = "pubDate"; + public static final String KEY_READ = "read"; + public static final String KEY_DURATION = "duration"; + public static final String KEY_POSITION = "position"; + public static final String KEY_SIZE = "filesize"; + public static final String KEY_MIME_TYPE = "mime_type"; + public static final String KEY_IMAGE = "image"; + public static final String KEY_FEED = "feed"; + public static final String KEY_MEDIA = "media"; + public static final String KEY_DOWNLOADED = "downloaded"; + public static final String KEY_LASTUPDATE = "last_update"; + public static final String KEY_FEEDFILE = "feedfile"; + public static final String KEY_REASON = "reason"; + public static final String KEY_SUCCESSFUL = "successful"; + public static final String KEY_FEEDFILETYPE = "feedfile_type"; + public static final String KEY_COMPLETION_DATE = "completion_date"; + public static final String KEY_FEEDITEM = "feeditem"; + public static final String KEY_CONTENT_ENCODED = "content_encoded"; + public static final String KEY_PAYMENT_LINK = "payment_link"; + public static final String KEY_START = "start"; + public static final String KEY_LANGUAGE = "language"; + public static final String KEY_AUTHOR = "author"; + public static final String KEY_HAS_CHAPTERS = "has_simple_chapters"; + public static final String KEY_TYPE = "type"; + public static final String KEY_ITEM_IDENTIFIER = "item_identifier"; + public static final String KEY_FLATTR_STATUS = "flattr_status"; + public static final String KEY_FEED_IDENTIFIER = "feed_identifier"; + public static final String KEY_REASON_DETAILED = "reason_detailed"; + public static final String KEY_DOWNLOADSTATUS_TITLE = "title"; + public static final String KEY_CHAPTER_TYPE = "type"; + public static final String KEY_PLAYBACK_COMPLETION_DATE = "playback_completion_date"; + public static final String KEY_AUTO_DOWNLOAD = "auto_download"; + public static final String KEY_PLAYED_DURATION = "played_duration"; + public static final String KEY_USERNAME = "username"; + public static final String KEY_PASSWORD = "password"; + public static final String KEY_IS_PAGED = "is_paged"; + public static final String KEY_NEXT_PAGE_LINK = "next_page_link"; + + // Table names + public static final String TABLE_NAME_FEEDS = "Feeds"; + public static final String TABLE_NAME_FEED_ITEMS = "FeedItems"; + public static final String TABLE_NAME_FEED_IMAGES = "FeedImages"; + public static final String TABLE_NAME_FEED_MEDIA = "FeedMedia"; + public static final String TABLE_NAME_DOWNLOAD_LOG = "DownloadLog"; + public static final String TABLE_NAME_QUEUE = "Queue"; + public static final String TABLE_NAME_SIMPLECHAPTERS = "SimpleChapters"; + + // SQL Statements for creating new tables + private static final String TABLE_PRIMARY_KEY = KEY_ID + + " INTEGER PRIMARY KEY AUTOINCREMENT ,"; + + public static final String CREATE_TABLE_FEEDS = "CREATE TABLE " + + TABLE_NAME_FEEDS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT," + + KEY_DOWNLOADED + " INTEGER," + KEY_LINK + " TEXT," + + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + + KEY_LASTUPDATE + " TEXT," + KEY_LANGUAGE + " TEXT," + KEY_AUTHOR + + " TEXT," + KEY_IMAGE + " INTEGER," + KEY_TYPE + " TEXT," + + KEY_FEED_IDENTIFIER + " TEXT," + KEY_AUTO_DOWNLOAD + " INTEGER DEFAULT 1," + + KEY_FLATTR_STATUS + " INTEGER," + + KEY_USERNAME + " TEXT," + + KEY_PASSWORD + " TEXT," + + KEY_IS_PAGED + " INTEGER DEFAULT 0," + + KEY_NEXT_PAGE_LINK + " TEXT)"; + + + public static final String CREATE_TABLE_FEED_ITEMS = "CREATE TABLE " + + TABLE_NAME_FEED_ITEMS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + + " TEXT," + KEY_CONTENT_ENCODED + " TEXT," + KEY_PUBDATE + + " INTEGER," + KEY_READ + " INTEGER," + KEY_LINK + " TEXT," + + KEY_DESCRIPTION + " TEXT," + KEY_PAYMENT_LINK + " TEXT," + + KEY_MEDIA + " INTEGER," + KEY_FEED + " INTEGER," + + KEY_HAS_CHAPTERS + " INTEGER," + KEY_ITEM_IDENTIFIER + " TEXT," + + KEY_FLATTR_STATUS + " INTEGER," + + KEY_IMAGE + " INTEGER)"; + + public static final String CREATE_TABLE_FEED_IMAGES = "CREATE TABLE " + + TABLE_NAME_FEED_IMAGES + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + + " TEXT," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + " TEXT," + + KEY_DOWNLOADED + " INTEGER)"; + + public static final String CREATE_TABLE_FEED_MEDIA = "CREATE TABLE " + + TABLE_NAME_FEED_MEDIA + " (" + TABLE_PRIMARY_KEY + KEY_DURATION + + " INTEGER," + KEY_FILE_URL + " TEXT," + KEY_DOWNLOAD_URL + + " TEXT," + KEY_DOWNLOADED + " INTEGER," + KEY_POSITION + + " INTEGER," + KEY_SIZE + " INTEGER," + KEY_MIME_TYPE + " TEXT," + + KEY_PLAYBACK_COMPLETION_DATE + " INTEGER," + + KEY_FEEDITEM + " INTEGER," + + KEY_PLAYED_DURATION + " INTEGER)"; + + public static final String CREATE_TABLE_DOWNLOAD_LOG = "CREATE TABLE " + + TABLE_NAME_DOWNLOAD_LOG + " (" + TABLE_PRIMARY_KEY + KEY_FEEDFILE + + " INTEGER," + KEY_FEEDFILETYPE + " INTEGER," + KEY_REASON + + " INTEGER," + KEY_SUCCESSFUL + " INTEGER," + KEY_COMPLETION_DATE + + " INTEGER," + KEY_REASON_DETAILED + " TEXT," + + KEY_DOWNLOADSTATUS_TITLE + " TEXT)"; + + public static final String CREATE_TABLE_QUEUE = "CREATE TABLE " + + TABLE_NAME_QUEUE + "(" + KEY_ID + " INTEGER PRIMARY KEY," + + KEY_FEEDITEM + " INTEGER," + KEY_FEED + " INTEGER)"; + + public static final String CREATE_TABLE_SIMPLECHAPTERS = "CREATE TABLE " + + TABLE_NAME_SIMPLECHAPTERS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE + + " TEXT," + KEY_START + " INTEGER," + KEY_FEEDITEM + " INTEGER," + + KEY_LINK + " TEXT," + KEY_CHAPTER_TYPE + " INTEGER)"; + + private SQLiteDatabase db; + private final Context context; + private PodDBHelper helper; + + /** + * Select all columns from the feed-table + */ + private static final String[] FEED_SEL_STD = { + TABLE_NAME_FEEDS + "." + KEY_ID, + TABLE_NAME_FEEDS + "." + KEY_TITLE, + TABLE_NAME_FEEDS + "." + KEY_FILE_URL, + TABLE_NAME_FEEDS + "." + KEY_DOWNLOAD_URL, + TABLE_NAME_FEEDS + "." + KEY_DOWNLOADED, + TABLE_NAME_FEEDS + "." + KEY_LINK, + TABLE_NAME_FEEDS + "." + KEY_DESCRIPTION, + TABLE_NAME_FEEDS + "." + KEY_PAYMENT_LINK, + TABLE_NAME_FEEDS + "." + KEY_LASTUPDATE, + TABLE_NAME_FEEDS + "." + KEY_LANGUAGE, + TABLE_NAME_FEEDS + "." + KEY_AUTHOR, + TABLE_NAME_FEEDS + "." + KEY_IMAGE, + TABLE_NAME_FEEDS + "." + KEY_TYPE, + TABLE_NAME_FEEDS + "." + KEY_FEED_IDENTIFIER, + TABLE_NAME_FEEDS + "." + KEY_AUTO_DOWNLOAD, + TABLE_NAME_FEEDS + "." + KEY_FLATTR_STATUS, + TABLE_NAME_FEEDS + "." + KEY_IS_PAGED, + TABLE_NAME_FEEDS + "." + KEY_NEXT_PAGE_LINK, + TABLE_NAME_FEEDS + "." + KEY_USERNAME, + TABLE_NAME_FEEDS + "." + KEY_PASSWORD, + }; + + // 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. + */ + private static final String[] FEEDITEM_SEL_FI_SMALL = { + TABLE_NAME_FEED_ITEMS + "." + KEY_ID, + TABLE_NAME_FEED_ITEMS + "." + KEY_TITLE, + TABLE_NAME_FEED_ITEMS + "." + KEY_PUBDATE, + TABLE_NAME_FEED_ITEMS + "." + KEY_READ, + TABLE_NAME_FEED_ITEMS + "." + KEY_LINK, + TABLE_NAME_FEED_ITEMS + "." + KEY_PAYMENT_LINK, KEY_MEDIA, + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED, + TABLE_NAME_FEED_ITEMS + "." + KEY_HAS_CHAPTERS, + TABLE_NAME_FEED_ITEMS + "." + KEY_ITEM_IDENTIFIER, + TABLE_NAME_FEED_ITEMS + "." + KEY_FLATTR_STATUS, + TABLE_NAME_FEED_ITEMS + "." + KEY_IMAGE}; + + /** + * Contains FEEDITEM_SEL_FI_SMALL as comma-separated list. Useful for raw queries. + */ + private static final String SEL_FI_SMALL_STR; + + static { + String selFiSmall = Arrays.toString(FEEDITEM_SEL_FI_SMALL); + 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; + + static PodDBHelper dbHelperSingleton; + + private static synchronized PodDBHelper getDbHelperSingleton(Context appContext) { + if (dbHelperSingleton == null) { + dbHelperSingleton = new PodDBHelper(appContext, DATABASE_NAME, null, + ClientConfig.storageCallbacks.getDatabaseVersion()); + } + return dbHelperSingleton; + } + + public PodDBAdapter(Context c) { + this.context = c; + helper = getDbHelperSingleton(c.getApplicationContext()); + } + + public PodDBAdapter open() { + if (db == null || !db.isOpen() || db.isReadOnly()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Opening DB"); + try { + db = helper.getWritableDatabase(); + } catch (SQLException ex) { + ex.printStackTrace(); + db = helper.getReadableDatabase(); + } + } + return this; + } + + public void close() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Closing DB"); + //db.close(); + } + + public static boolean deleteDatabase(Context context) { + Log.w(TAG, "Deleting database"); + dbHelperSingleton.close(); + dbHelperSingleton = null; + return context.deleteDatabase(DATABASE_NAME); + } + + /** + * Inserts or updates a feed entry + * + * @return the id of the entry + */ + public long setFeed(Feed feed) { + ContentValues values = new ContentValues(); + values.put(KEY_TITLE, feed.getTitle()); + values.put(KEY_LINK, feed.getLink()); + values.put(KEY_DESCRIPTION, feed.getDescription()); + values.put(KEY_PAYMENT_LINK, feed.getPaymentLink()); + values.put(KEY_AUTHOR, feed.getAuthor()); + values.put(KEY_LANGUAGE, feed.getLanguage()); + if (feed.getImage() != null) { + if (feed.getImage().getId() == 0) { + setImage(feed.getImage()); + } + values.put(KEY_IMAGE, feed.getImage().getId()); + } + + values.put(KEY_FILE_URL, feed.getFile_url()); + values.put(KEY_DOWNLOAD_URL, feed.getDownload_url()); + values.put(KEY_DOWNLOADED, feed.isDownloaded()); + values.put(KEY_LASTUPDATE, feed.getLastUpdate().getTime()); + values.put(KEY_TYPE, feed.getType()); + values.put(KEY_FEED_IDENTIFIER, feed.getFeedIdentifier()); + + Log.d(TAG, "Setting feed with flattr status " + feed.getTitle() + ": " + feed.getFlattrStatus().toLong()); + + values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); + values.put(KEY_IS_PAGED, feed.isPaged()); + values.put(KEY_NEXT_PAGE_LINK, feed.getNextPageLink()); + if (feed.getId() == 0) { + // Create new entry + if (BuildConfig.DEBUG) + Log.d(this.toString(), "Inserting new Feed into db"); + feed.setId(db.insert(TABLE_NAME_FEEDS, null, values)); + } else { + if (BuildConfig.DEBUG) + Log.d(this.toString(), "Updating existing Feed in db"); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", + new String[]{String.valueOf(feed.getId())}); + + } + return feed.getId(); + } + + public void setFeedPreferences(FeedPreferences prefs) { + if (prefs.getFeedID() == 0) { + throw new IllegalArgumentException("Feed ID of preference must not be null"); + } + ContentValues values = new ContentValues(); + values.put(KEY_AUTO_DOWNLOAD, prefs.getAutoDownload()); + values.put(KEY_USERNAME, prefs.getUsername()); + values.put(KEY_PASSWORD, prefs.getPassword()); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(prefs.getFeedID())}); + } + + /** + * Inserts or updates an image entry + * + * @return the id of the entry + */ + public long setImage(FeedImage image) { + db.beginTransaction(); + ContentValues values = new ContentValues(); + values.put(KEY_TITLE, image.getTitle()); + values.put(KEY_DOWNLOAD_URL, image.getDownload_url()); + values.put(KEY_DOWNLOADED, image.isDownloaded()); + values.put(KEY_FILE_URL, image.getFile_url()); + if (image.getId() == 0) { + image.setId(db.insert(TABLE_NAME_FEED_IMAGES, null, values)); + } else { + db.update(TABLE_NAME_FEED_IMAGES, values, KEY_ID + "=?", + new String[]{String.valueOf(image.getId())}); + } + + final FeedComponent owner = image.getOwner(); + if (owner != null && owner.getId() != 0) { + values.clear(); + values.put(KEY_IMAGE, image.getId()); + if (owner instanceof Feed) { + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(image.getOwner().getId())}); + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + return image.getId(); + } + + /** + * Inserts or updates an image entry + * + * @return the id of the entry + */ + public long setMedia(FeedMedia media) { + ContentValues values = new ContentValues(); + values.put(KEY_DURATION, media.getDuration()); + values.put(KEY_POSITION, media.getPosition()); + values.put(KEY_SIZE, media.getSize()); + values.put(KEY_MIME_TYPE, media.getMime_type()); + values.put(KEY_DOWNLOAD_URL, media.getDownload_url()); + values.put(KEY_DOWNLOADED, media.isDownloaded()); + values.put(KEY_FILE_URL, media.getFile_url()); + + if (media.getPlaybackCompletionDate() != null) { + values.put(KEY_PLAYBACK_COMPLETION_DATE, media + .getPlaybackCompletionDate().getTime()); + } else { + values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); + } + if (media.getItem() != null) { + values.put(KEY_FEEDITEM, media.getItem().getId()); + } + if (media.getId() == 0) { + media.setId(db.insert(TABLE_NAME_FEED_MEDIA, null, values)); + } else { + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } + return media.getId(); + } + + public void setFeedMediaPlaybackInformation(FeedMedia media) { + if (media.getId() != 0) { + ContentValues values = new ContentValues(); + values.put(KEY_POSITION, media.getPosition()); + values.put(KEY_DURATION, media.getDuration()); + values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } else { + Log.e(TAG, "setFeedMediaPlaybackInformation: ID of media was 0"); + } + } + + public void setFeedMediaPlaybackCompletionDate(FeedMedia media) { + if (media.getId() != 0) { + ContentValues values = new ContentValues(); + values.put(KEY_PLAYBACK_COMPLETION_DATE, media.getPlaybackCompletionDate().getTime()); + values.put(KEY_PLAYED_DURATION, media.getPlayedDuration()); + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } else { + Log.e(TAG, "setFeedMediaPlaybackCompletionDate: ID of media was 0"); + } + } + + /** + * Insert all FeedItems of a feed and the feed object itself in a single + * transaction + */ + public void setCompleteFeed(Feed... feeds) { + db.beginTransaction(); + for (Feed feed : feeds) { + setFeed(feed); + if (feed.getItems() != null) { + for (FeedItem item : feed.getItems()) { + setFeedItem(item, false); + } + } + if (feed.getPreferences() != null) { + setFeedPreferences(feed.getPreferences()); + } + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + /** + * Update the flattr status of a feed + */ + public void setFeedFlattrStatus(Feed feed) { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, feed.getFlattrStatus().toLong()); + db.update(TABLE_NAME_FEEDS, values, KEY_ID + "=?", new String[]{String.valueOf(feed.getId())}); + } + + /** + * Get all feeds in the flattr queue. + */ + public Cursor getFeedsInFlattrQueueCursor() { + return db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_FLATTR_STATUS + "=?", + new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null); + } + + /** + * Get all feed items in the flattr queue. + */ + public Cursor getFeedItemsInFlattrQueueCursor() { + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FLATTR_STATUS + "=?", + new String[]{String.valueOf(FlattrStatus.STATUS_QUEUE)}, null, null, null); + } + + /** + * Counts feeds and feed items in the flattr queue + */ + public int getFlattrQueueSize() { + int res = 0; + Cursor c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", + TABLE_NAME_FEEDS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); + if (c.moveToFirst()) { + res = c.getInt(0); + c.close(); + } else { + Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feeds"); + } + c = db.rawQuery(String.format("SELECT count(*) FROM %s WHERE %s=%s", + TABLE_NAME_FEED_ITEMS, KEY_FLATTR_STATUS, String.valueOf(FlattrStatus.STATUS_QUEUE)), null); + if (c.moveToFirst()) { + res += c.getInt(0); + c.close(); + } else { + Log.e(TAG, "Unable to determine size of flattr queue: Could not count number of feed items"); + } + + return res; + } + + /** + * Updates the download URL of a Feed. + */ + public void setFeedDownloadUrl(String original, String updated) { + ContentValues values = new ContentValues(); + values.put(KEY_DOWNLOAD_URL, updated); + db.update(TABLE_NAME_FEEDS, values, KEY_DOWNLOAD_URL + "=?", new String[]{original}); + } + + public void setFeedItemlist(List<FeedItem> items) { + db.beginTransaction(); + for (FeedItem item : items) { + setFeedItem(item, true); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public long setSingleFeedItem(FeedItem item) { + db.beginTransaction(); + long result = setFeedItem(item, true); + db.setTransactionSuccessful(); + db.endTransaction(); + return result; + } + + /** + * Update the flattr status of a FeedItem + */ + public void setFeedItemFlattrStatus(FeedItem feedItem) { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, feedItem.getFlattrStatus().toLong()); + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(feedItem.getId())}); + } + + /** + * Update the flattr status of a feed or feed item specified by its payment link + * and the new flattr status to use + */ + public void setItemFlattrStatus(String url, FlattrStatus status) { + //Log.d(TAG, "setItemFlattrStatus(" + url + ") = " + status.toString()); + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, status.toLong()); + + // regexps in sqlite would be neat! + String[] query_urls = new String[]{ + "*" + url + "&*", + "*" + url + "%2F&*", + "*" + url + "", + "*" + url + "%2F" + }; + + if (db.update(TABLE_NAME_FEEDS, values, + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls + ) > 0) { + Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in Feeds table"); + return; + } + if (db.update(TABLE_NAME_FEED_ITEMS, values, + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?" + + " OR " + KEY_PAYMENT_LINK + " GLOB ?", query_urls + ) > 0) { + Log.i(TAG, "setItemFlattrStatus found match for " + url + " = " + status.toLong() + " in FeedsItems table"); + } + } + + /** + * Reset flattr status to unflattrd for all items + */ + public void clearAllFlattrStatus() { + ContentValues values = new ContentValues(); + values.put(KEY_FLATTR_STATUS, 0); + db.update(TABLE_NAME_FEEDS, values, null, null); + db.update(TABLE_NAME_FEED_ITEMS, values, null, null); + } + + /** + * Inserts or updates a feeditem entry + * + * @param item The FeedItem + * @param saveFeed true if the Feed of the item should also be saved. This should be set to + * false if the method is executed on a list of FeedItems of the same Feed. + * @return the id of the entry + */ + private long setFeedItem(FeedItem item, boolean saveFeed) { + ContentValues values = new ContentValues(); + values.put(KEY_TITLE, item.getTitle()); + values.put(KEY_LINK, item.getLink()); + if (item.getDescription() != null) { + values.put(KEY_DESCRIPTION, item.getDescription()); + } + if (item.getContentEncoded() != null) { + values.put(KEY_CONTENT_ENCODED, item.getContentEncoded()); + } + values.put(KEY_PUBDATE, item.getPubDate().getTime()); + values.put(KEY_PAYMENT_LINK, item.getPaymentLink()); + if (saveFeed && item.getFeed() != null) { + setFeed(item.getFeed()); + } + values.put(KEY_FEED, item.getFeed().getId()); + values.put(KEY_READ, item.isRead()); + values.put(KEY_HAS_CHAPTERS, item.getChapters() != null); + values.put(KEY_ITEM_IDENTIFIER, item.getItemIdentifier()); + values.put(KEY_FLATTR_STATUS, item.getFlattrStatus().toLong()); + if (item.hasItemImage()) { + if (item.getImage().getId() == 0) { + setImage(item.getImage()); + } + values.put(KEY_IMAGE, item.getImage().getId()); + } + + if (item.getId() == 0) { + item.setId(db.insert(TABLE_NAME_FEED_ITEMS, null, values)); + } else { + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", + new String[]{String.valueOf(item.getId())}); + } + if (item.getMedia() != null) { + setMedia(item.getMedia()); + } + if (item.getChapters() != null) { + setChapters(item); + } + return item.getId(); + } + + public void setFeedItemRead(boolean read, long itemId, long mediaId, + boolean resetMediaPosition) { + db.beginTransaction(); + ContentValues values = new ContentValues(); + + values.put(KEY_READ, read); + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(itemId)}); + + if (resetMediaPosition) { + values.clear(); + values.put(KEY_POSITION, 0); + db.update(TABLE_NAME_FEED_MEDIA, values, KEY_ID + "=?", new String[]{String.valueOf(mediaId)}); + } + + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void setFeedItemRead(boolean read, long... itemIds) { + db.beginTransaction(); + ContentValues values = new ContentValues(); + for (long id : itemIds) { + values.clear(); + values.put(KEY_READ, read); + db.update(TABLE_NAME_FEED_ITEMS, values, KEY_ID + "=?", new String[]{String.valueOf(id)}); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void setChapters(FeedItem item) { + ContentValues values = new ContentValues(); + for (Chapter chapter : item.getChapters()) { + values.put(KEY_TITLE, chapter.getTitle()); + values.put(KEY_START, chapter.getStart()); + values.put(KEY_FEEDITEM, item.getId()); + values.put(KEY_LINK, chapter.getLink()); + values.put(KEY_CHAPTER_TYPE, chapter.getChapterType()); + if (chapter.getId() == 0) { + chapter.setId(db + .insert(TABLE_NAME_SIMPLECHAPTERS, null, values)); + } else { + db.update(TABLE_NAME_SIMPLECHAPTERS, values, KEY_ID + "=?", + new String[]{String.valueOf(chapter.getId())}); + } + } + } + + /** + * Inserts or updates a download status. + */ + public long setDownloadStatus(DownloadStatus status) { + ContentValues values = new ContentValues(); + values.put(KEY_FEEDFILE, status.getFeedfileId()); + values.put(KEY_FEEDFILETYPE, status.getFeedfileType()); + values.put(KEY_REASON, status.getReason().getCode()); + values.put(KEY_SUCCESSFUL, status.isSuccessful()); + values.put(KEY_COMPLETION_DATE, status.getCompletionDate().getTime()); + values.put(KEY_REASON_DETAILED, status.getReasonDetailed()); + values.put(KEY_DOWNLOADSTATUS_TITLE, status.getTitle()); + if (status.getId() == 0) { + status.setId(db.insert(TABLE_NAME_DOWNLOAD_LOG, null, values)); + } else { + db.update(TABLE_NAME_DOWNLOAD_LOG, values, KEY_ID + "=?", + new String[]{String.valueOf(status.getId())}); + } + return status.getId(); + } + + public long getDownloadLogSize() { + final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_DOWNLOAD_LOG); + Cursor result = db.rawQuery(query, null); + long count = 0; + if (result.moveToFirst()) { + count = result.getLong(0); + } + result.close(); + 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(); + db.delete(TABLE_NAME_QUEUE, null, null); + for (int i = 0; i < queue.size(); i++) { + FeedItem item = queue.get(i); + values.put(KEY_ID, i); + values.put(KEY_FEEDITEM, item.getId()); + values.put(KEY_FEED, item.getFeed().getId()); + db.insertWithOnConflict(TABLE_NAME_QUEUE, null, values, + SQLiteDatabase.CONFLICT_REPLACE); + } + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void clearQueue() { + db.delete(TABLE_NAME_QUEUE, null, null); + } + + public void removeFeedMedia(FeedMedia media) { + db.delete(TABLE_NAME_FEED_MEDIA, KEY_ID + "=?", + new String[]{String.valueOf(media.getId())}); + } + + public void removeChaptersOfItem(FeedItem item) { + db.delete(TABLE_NAME_SIMPLECHAPTERS, KEY_FEEDITEM + "=?", + new String[]{String.valueOf(item.getId())}); + } + + public void removeFeedImage(FeedImage image) { + db.delete(TABLE_NAME_FEED_IMAGES, KEY_ID + "=?", + new String[]{String.valueOf(image.getId())}); + } + + /** + * Remove a FeedItem and its FeedMedia entry. + */ + public void removeFeedItem(FeedItem item) { + if (item.getMedia() != null) { + removeFeedMedia(item.getMedia()); + } + if (item.getChapters() != null) { + removeChaptersOfItem(item); + } + if (item.hasItemImage()) { + removeFeedImage(item.getImage()); + } + db.delete(TABLE_NAME_FEED_ITEMS, KEY_ID + "=?", + new String[]{String.valueOf(item.getId())}); + } + + /** + * Remove a feed with all its FeedItems and Media entries. + */ + public void removeFeed(Feed feed) { + db.beginTransaction(); + if (feed.getImage() != null) { + removeFeedImage(feed.getImage()); + } + if (feed.getItems() != null) { + for (FeedItem item : feed.getItems()) { + removeFeedItem(item); + } + } + + db.delete(TABLE_NAME_FEEDS, KEY_ID + "=?", + new String[]{String.valueOf(feed.getId())}); + db.setTransactionSuccessful(); + db.endTransaction(); + } + + public void removeDownloadStatus(DownloadStatus remove) { + db.delete(TABLE_NAME_DOWNLOAD_LOG, KEY_ID + "=?", + new String[]{String.valueOf(remove.getId())}); + } + + public void clearPlaybackHistory() { + ContentValues values = new ContentValues(); + values.put(KEY_PLAYBACK_COMPLETION_DATE, 0); + db.update(TABLE_NAME_FEED_MEDIA, values, null, null); + } + + /** + * Get all Feeds from the Feed Table. + * + * @return The cursor of the query + */ + public final Cursor getAllFeedsCursor() { + Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, null, null, null, null, + KEY_TITLE + " COLLATE NOCASE ASC"); + return c; + } + + public final Cursor getFeedCursorDownloadUrls() { + 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 + * + * @param feed The feed you want to get the FeedItems from. + * @return The cursor of the query + */ + public final Cursor getAllItemsOfFeedCursor(final Feed feed) { + return getAllItemsOfFeedCursor(feed.getId()); + } + + public final Cursor getAllItemsOfFeedCursor(final long feedId) { + Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED + + "=?", new String[]{String.valueOf(feedId)}, null, null, + null + ); + return c; + } + + /** + * Return a cursor with the SEL_FI_EXTRA selection of a single feeditem. + */ + public final Cursor getExtraInformationOfItem(final FeedItem item) { + Cursor c = db + .query(TABLE_NAME_FEED_ITEMS, SEL_FI_EXTRA, KEY_ID + "=?", + new String[]{String.valueOf(item.getId())}, null, + null, null); + return c; + } + + /** + * Returns a cursor for a DB query in the FeedMedia table for a given ID. + * + * @param item The item you want to get the FeedMedia from + * @return The cursor of the query + */ + public final Cursor getFeedMediaOfItemCursor(final FeedItem item) { + Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", + new String[]{String.valueOf(item.getMedia().getId())}, null, + null, null); + return c; + } + + /** + * Returns a cursor for a DB query in the FeedImages table for a given ID. + * + * @param id ID of the FeedImage + * @return The cursor of the query + */ + public final Cursor 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 getSimpleChaptersOfFeedItemCursor(final FeedItem item) { + Cursor c = db.query(TABLE_NAME_SIMPLECHAPTERS, null, KEY_FEEDITEM + + "=?", new String[]{String.valueOf(item.getId())}, null, + null, null + ); + return c; + } + + public final Cursor getDownloadLogCursor(final int limit) { + Cursor c = db.query(TABLE_NAME_DOWNLOAD_LOG, null, null, null, null, + null, KEY_COMPLETION_DATE + " DESC LIMIT " + limit); + return c; + } + + /** + * Returns a cursor which contains all feed items in the queue. The returned + * cursor uses the FEEDITEM_SEL_FI_SMALL selection. + */ + public final Cursor getQueueCursor() { + Object[] args = (Object[]) new String[]{ + SEL_FI_SMALL_STR + "," + TABLE_NAME_QUEUE + "." + KEY_ID, + TABLE_NAME_FEED_ITEMS, TABLE_NAME_QUEUE, + TABLE_NAME_FEED_ITEMS + "." + KEY_ID, + TABLE_NAME_QUEUE + "." + KEY_FEEDITEM, + TABLE_NAME_QUEUE + "." + KEY_ID}; + String query = String.format( + "SELECT %s FROM %s INNER JOIN %s ON %s=%s ORDER BY %s", args); + Cursor c = db.rawQuery(query, null); + /* + * Cursor c = db.query(TABLE_NAME_FEED_ITEMS, 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; + } + + public Cursor getQueueIDCursor() { + Cursor c = db.query(TABLE_NAME_QUEUE, new String[]{KEY_FEEDITEM}, null, null, null, null, KEY_ID + " ASC", null); + return c; + } + + /** + * Returns a cursor which contains all feed items in the unread items list. + * The returned cursor uses the 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"); + return c; + } + + public final Cursor getUnreadItemIdsCursor() { + Cursor c = db.query(TABLE_NAME_FEED_ITEMS, new String[]{KEY_ID}, + KEY_READ + "=0", null, null, null, KEY_PUBDATE + " DESC"); + return c; + + } + + public final Cursor getRecentlyPublishedItemsCursor(int limit) { + Cursor c = db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, null, null, null, null, KEY_PUBDATE + " DESC LIMIT " + limit); + return c; + } + + public Cursor getDownloadedItemsCursor() { + final String query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + + " INNER JOIN " + TABLE_NAME_FEED_MEDIA + " ON " + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + "=" + + TABLE_NAME_FEED_MEDIA + "." + KEY_FEEDITEM + " WHERE " + + TABLE_NAME_FEED_MEDIA + "." + KEY_DOWNLOADED + ">0"; + Cursor c = db.rawQuery(query, null); + return c; + } + + /** + * Returns a cursor which contains feed media objects with a playback + * completion date in ascending order. + * + * @param limit The maximum row count of the returned cursor. Must be an + * integer >= 0. + * @throws IllegalArgumentException if limit < 0 + */ + public final Cursor getCompletedMediaCursor(int limit) { + Validate.isTrue(limit >= 0, "Limit must be >= 0"); + + Cursor c = db.query(TABLE_NAME_FEED_MEDIA, null, + KEY_PLAYBACK_COMPLETION_DATE + " > 0", null, null, + null, String.format("%s DESC LIMIT %d", KEY_PLAYBACK_COMPLETION_DATE, limit)); + return c; + } + + public final Cursor getSingleFeedMediaCursor(long id) { + return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_ID + "=?", new String[]{String.valueOf(id)}, null, null, null); + } + + public final Cursor getFeedMediaCursorByItemID(String... mediaIds) { + int length = mediaIds.length; + if (length > IN_OPERATOR_MAXIMUM) { + Log.w(TAG, "Length of id array is larger than " + + IN_OPERATOR_MAXIMUM + ". Creating multiple cursors"); + int numCursors = (int) (((double) length) / (IN_OPERATOR_MAXIMUM)) + 1; + Cursor[] cursors = new Cursor[numCursors]; + for (int i = 0; i < numCursors; i++) { + int neededLength = 0; + String[] parts = null; + final int elementsLeft = length - i * IN_OPERATOR_MAXIMUM; + + if (elementsLeft >= IN_OPERATOR_MAXIMUM) { + neededLength = IN_OPERATOR_MAXIMUM; + parts = Arrays.copyOfRange(mediaIds, i + * IN_OPERATOR_MAXIMUM, (i + 1) + * IN_OPERATOR_MAXIMUM); + } else { + neededLength = elementsLeft; + parts = Arrays.copyOfRange(mediaIds, i + * IN_OPERATOR_MAXIMUM, (i * IN_OPERATOR_MAXIMUM) + + neededLength); + } + + cursors[i] = db.rawQuery("SELECT * FROM " + + TABLE_NAME_FEED_MEDIA + " WHERE " + KEY_FEEDITEM + " IN " + + buildInOperator(neededLength), parts); + } + return new MergeCursor(cursors); + } else { + return db.query(TABLE_NAME_FEED_MEDIA, null, KEY_FEEDITEM + " IN " + + buildInOperator(length), mediaIds, null, null, null); + } + } + + /** + * Builds an IN-operator argument depending on the number of items. + */ + private String buildInOperator(int size) { + if (size == 1) { + return "(?)"; + } + StringBuffer buffer = new StringBuffer("("); + for (int i = 0; i < size - 1; i++) { + buffer.append("?,"); + } + buffer.append("?)"); + return buffer.toString(); + } + + public final Cursor getFeedCursor(final long id) { + Cursor c = db.query(TABLE_NAME_FEEDS, FEED_SEL_STD, KEY_ID + "=" + id, null, + null, null, null); + return c; + } + + public final Cursor getFeedItemCursor(final String... ids) { + if (ids.length > IN_OPERATOR_MAXIMUM) { + throw new IllegalArgumentException( + "number of IDs must not be larger than " + + IN_OPERATOR_MAXIMUM + ); + } + + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_ID + " IN " + + buildInOperator(ids.length), ids, null, null, null); + + } + + public int getQueueSize() { + final String query = String.format("SELECT COUNT(%s) FROM %s", KEY_ID, TABLE_NAME_QUEUE); + Cursor c = db.rawQuery(query, null); + int result = 0; + if (c.moveToFirst()) { + result = c.getInt(0); + } + c.close(); + return result; + } + + public final int getNumberOfUnreadItems() { + final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_ITEMS + + " WHERE " + KEY_READ + " = 0"; + Cursor c = db.rawQuery(query, null); + int result = 0; + if (c.moveToFirst()) { + result = c.getInt(0); + } + c.close(); + return result; + } + + public final int getNumberOfDownloadedEpisodes() { + final String query = "SELECT COUNT(DISTINCT " + KEY_ID + ") AS count FROM " + TABLE_NAME_FEED_MEDIA + + " WHERE " + KEY_DOWNLOADED + " > 0"; + + Cursor c = db.rawQuery(query, null); + int result = 0; + if (c.moveToFirst()) { + result = c.getInt(0); + } + c.close(); + return result; + } + + /** + * Uses DatabaseUtils to escape a search query and removes ' at the + * beginning and the end of the string returned by the escape method. + */ + private String prepareSearchQuery(String query) { + StringBuilder builder = new StringBuilder(); + DatabaseUtils.appendEscapedSQLString(builder, query); + builder.deleteCharAt(0); + builder.deleteCharAt(builder.length() - 1); + return builder.toString(); + } + + /** + * Searches for the given query in the description of all items or the items + * of a specified feed. + * + * @return A cursor with all search results in SEL_FI_EXTRA selection. + */ + public Cursor searchItemDescriptions(long feedID, String query) { + if (feedID != 0) { + // search items in specific feed + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED + + "=? AND " + KEY_DESCRIPTION + " LIKE '%" + + prepareSearchQuery(query) + "%'", + new String[]{String.valueOf(feedID)}, null, null, + null + ); + } else { + // search through all items + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, + KEY_DESCRIPTION + " LIKE '%" + prepareSearchQuery(query) + + "%'", null, null, null, null + ); + } + } + + /** + * Searches for the given query in the content-encoded field of all items or + * the items of a specified feed. + * + * @return A cursor with all search results in SEL_FI_EXTRA selection. + */ + public Cursor searchItemContentEncoded(long feedID, String query) { + if (feedID != 0) { + // search items in specific feed + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED + + "=? AND " + KEY_CONTENT_ENCODED + " LIKE '%" + + prepareSearchQuery(query) + "%'", + new String[]{String.valueOf(feedID)}, null, null, + null + ); + } else { + // search through all items + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, + KEY_CONTENT_ENCODED + " LIKE '%" + + prepareSearchQuery(query) + "%'", null, null, + null, null + ); + } + } + + public Cursor searchItemTitles(long feedID, String query) { + if (feedID != 0) { + // search items in specific feed + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, KEY_FEED + + "=? AND " + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(query) + "%'", + new String[]{String.valueOf(feedID)}, null, null, + null + ); + } else { + // search through all items + return db.query(TABLE_NAME_FEED_ITEMS, FEEDITEM_SEL_FI_SMALL, + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(query) + "%'", null, null, + null, null + ); + } + } + + public Cursor searchItemChapters(long feedID, String searchQuery) { + final String query; + if (feedID != 0) { + query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " + + TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_FEED_ITEMS + "." + KEY_FEED + "=" + + feedID + " AND " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(searchQuery) + "%'"; + } else { + query = "SELECT " + SEL_FI_SMALL_STR + " FROM " + TABLE_NAME_FEED_ITEMS + " INNER JOIN " + + TABLE_NAME_SIMPLECHAPTERS + " ON " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_FEEDITEM + "=" + + TABLE_NAME_FEED_ITEMS + "." + KEY_ID + " WHERE " + TABLE_NAME_SIMPLECHAPTERS + "." + KEY_TITLE + " LIKE '%" + + prepareSearchQuery(searchQuery) + "%'"; + } + return db.rawQuery(query, null); + } + + + public static final int IDX_FEEDSTATISTICS_FEED = 0; + public static final int IDX_FEEDSTATISTICS_NUM_ITEMS = 1; + public static final int IDX_FEEDSTATISTICS_NEW_ITEMS = 2; + public static final int IDX_FEEDSTATISTICS_LATEST_EPISODE = 3; + public static final int IDX_FEEDSTATISTICS_IN_PROGRESS_EPISODES = 4; + + /** + * Select number of items, new items, the date of the latest episode and the number of episodes in progress. The result + * is sorted by the title of the feed. + */ + private static final String FEED_STATISTICS_QUERY = "SELECT Feeds.id, num_items, new_items, latest_episode, in_progress FROM " + + " Feeds LEFT JOIN " + + "(SELECT feed,count(*) AS num_items," + + " COUNT(CASE WHEN read=0 THEN 1 END) AS new_items," + + " MAX(pubDate) AS latest_episode," + + " COUNT(CASE WHEN position>0 THEN 1 END) AS in_progress," + + " COUNT(CASE WHEN downloaded=1 THEN 1 END) AS episodes_downloaded " + + " FROM FeedItems LEFT JOIN FeedMedia ON FeedItems.id=FeedMedia.feeditem GROUP BY FeedItems.feed)" + + " ON Feeds.id = feed ORDER BY Feeds.title COLLATE NOCASE ASC;"; + + public Cursor getFeedStatisticsCursor() { + return db.rawQuery(FEED_STATISTICS_QUERY, null); + } + + /** + * Helper class for opening the Antennapod database. + */ + private static class PodDBHelper extends SQLiteOpenHelper { + /** + * Constructor. + * + * @param context Context to use + * @param name Name of the database + * @param factory to use for creating cursor objects + * @param version number of the database + */ + public PodDBHelper(final Context context, final String name, + final CursorFactory factory, final int version) { + super(context, name, factory, version); + } + + @Override + public void onCreate(final SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_FEEDS); + db.execSQL(CREATE_TABLE_FEED_ITEMS); + db.execSQL(CREATE_TABLE_FEED_IMAGES); + db.execSQL(CREATE_TABLE_FEED_MEDIA); + db.execSQL(CREATE_TABLE_DOWNLOAD_LOG); + db.execSQL(CREATE_TABLE_QUEUE); + db.execSQL(CREATE_TABLE_SIMPLECHAPTERS); + } + + @Override + public void onUpgrade(final SQLiteDatabase db, final int oldVersion, + final int newVersion) { + ClientConfig.storageCallbacks.onUpgrade(db, oldVersion, newVersion); + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java new file mode 100644 index 000000000..9efc5888f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandler.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import de.danoeh.antennapod.core.feed.Feed; +import org.apache.commons.io.input.XmlStreamReader; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; +import java.io.File; +import java.io.IOException; +import java.io.Reader; + +public class FeedHandler { + + public FeedHandlerResult parseFeed(Feed feed) throws SAXException, IOException, + ParserConfigurationException, UnsupportedFeedtypeException { + TypeGetter tg = new TypeGetter(); + TypeGetter.Type type = tg.getType(feed); + SyndHandler handler = new SyndHandler(feed, type); + + SAXParserFactory factory = SAXParserFactory.newInstance(); + factory.setNamespaceAware(true); + SAXParser saxParser = factory.newSAXParser(); + File file = new File(feed.getFile_url()); + Reader inputStreamReader = new XmlStreamReader(file); + InputSource inputSource = new InputSource(inputStreamReader); + + saxParser.parse(inputSource, handler); + inputStreamReader.close(); + return new FeedHandlerResult(handler.state.feed, handler.state.alternateUrls); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java new file mode 100644 index 000000000..f67721a6e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/FeedHandlerResult.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import java.util.Map; + +import de.danoeh.antennapod.core.feed.Feed; + +/** + * Container for results returned by the Feed parser + */ +public class FeedHandlerResult { + + public Feed feed; + public Map<String, String> alternateFeedUrls; + + public FeedHandlerResult(Feed feed, Map<String, String> alternateFeedUrls) { + this.feed = feed; + this.alternateFeedUrls = alternateFeedUrls; + } +} 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 new file mode 100644 index 000000000..413a11f8e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/HandlerState.java @@ -0,0 +1,111 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Stack; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; +import de.danoeh.antennapod.core.syndication.namespace.Namespace; +import de.danoeh.antennapod.core.syndication.namespace.SyndElement; + +/** + * Contains all relevant information to describe the current state of a + * SyndHandler. + */ +public class HandlerState { + + /** + * Feed that the Handler is currently processing. + */ + protected Feed feed; + /** + * Contains links to related feeds, e.g. feeds with enclosures in other formats. The key of the map is the + * URL of the feed, the value is the title + */ + protected Map<String, String> alternateUrls; + protected ArrayList<FeedItem> items; + protected FeedItem currentItem; + protected Stack<SyndElement> tagstack; + /** + * Namespaces that have been defined so far. + */ + protected HashMap<String, Namespace> namespaces; + protected Stack<Namespace> defaultNamespaces; + /** + * Buffer for saving characters. + */ + protected StringBuffer contentBuf; + + /** + * Temporarily saved objects. + */ + protected HashMap<String, Object> tempObjects; + + public HandlerState(Feed feed) { + this.feed = feed; + alternateUrls = new LinkedHashMap<String, String>(); + items = new ArrayList<FeedItem>(); + tagstack = new Stack<SyndElement>(); + namespaces = new HashMap<String, Namespace>(); + defaultNamespaces = new Stack<Namespace>(); + tempObjects = new HashMap<String, Object>(); + } + + public Feed getFeed() { + return feed; + } + + public ArrayList<FeedItem> getItems() { + return items; + } + + public FeedItem getCurrentItem() { + return currentItem; + } + + public Stack<SyndElement> getTagstack() { + return tagstack; + } + + public void setFeed(Feed feed) { + this.feed = feed; + } + + public void setCurrentItem(FeedItem currentItem) { + this.currentItem = currentItem; + } + + /** + * Returns the SyndElement that comes after the top element of the tagstack. + */ + public SyndElement getSecondTag() { + SyndElement top = tagstack.pop(); + SyndElement second = tagstack.peek(); + tagstack.push(top); + return second; + } + + public SyndElement getThirdTag() { + SyndElement top = tagstack.pop(); + SyndElement second = tagstack.pop(); + SyndElement third = tagstack.peek(); + tagstack.push(second); + tagstack.push(top); + return third; + } + + public StringBuffer getContentBuf() { + return contentBuf; + } + + public void addAlternateFeedUrl(String title, String url) { + alternateUrls.put(url, title); + } + + public HashMap<String, Object> getTempObjects() { + return tempObjects; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java new file mode 100644 index 000000000..1dda24944 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/SyndHandler.java @@ -0,0 +1,126 @@ +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 de.danoeh.antennapod.core.syndication.namespace.*; +import de.danoeh.antennapod.core.syndication.namespace.atom.NSAtom; +import org.xml.sax.Attributes; +import org.xml.sax.SAXException; +import org.xml.sax.helpers.DefaultHandler; + +/** Superclass for all SAX Handlers which process Syndication formats */ +public class SyndHandler extends DefaultHandler { + private static final String TAG = "SyndHandler"; + private static final String DEFAULT_PREFIX = ""; + protected HandlerState state; + + public SyndHandler(Feed feed, TypeGetter.Type type) { + state = new HandlerState(feed); + if (type == TypeGetter.Type.RSS20 || type == TypeGetter.Type.RSS091) { + state.defaultNamespaces.push(new NSRSS20()); + } + } + + @Override + public void startElement(String uri, String localName, String qName, + Attributes attributes) throws SAXException { + state.contentBuf = new StringBuffer(); + Namespace handler = getHandlingNamespace(uri, qName); + if (handler != null) { + SyndElement element = handler.handleElementStart(localName, state, + attributes); + state.tagstack.push(element); + + } + } + + @Override + public void characters(char[] ch, int start, int length) + throws SAXException { + if (!state.tagstack.empty()) { + if (state.getTagstack().size() >= 2) { + if (state.contentBuf != null) { + state.contentBuf.append(ch, start, length); + } + } + } + } + + @Override + public void endElement(String uri, String localName, String qName) + throws SAXException { + Namespace handler = getHandlingNamespace(uri, qName); + if (handler != null) { + handler.handleElementEnd(localName, state); + state.tagstack.pop(); + + } + state.contentBuf = null; + + } + + @Override + public void endPrefixMapping(String prefix) throws SAXException { + if (state.defaultNamespaces.size() > 1 && prefix.equals(DEFAULT_PREFIX)) { + state.defaultNamespaces.pop(); + } + } + + @Override + public void startPrefixMapping(String prefix, String uri) + throws SAXException { + // Find the right namespace + if (!state.namespaces.containsKey(uri)) { + if (uri.equals(NSAtom.NSURI)) { + if (prefix.equals(DEFAULT_PREFIX)) { + state.defaultNamespaces.push(new NSAtom()); + } else if (prefix.equals(NSAtom.NSTAG)) { + state.namespaces.put(uri, new NSAtom()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized Atom namespace"); + } + } else if (uri.equals(NSContent.NSURI) + && prefix.equals(NSContent.NSTAG)) { + state.namespaces.put(uri, new NSContent()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized Content namespace"); + } else if (uri.equals(NSITunes.NSURI) + && prefix.equals(NSITunes.NSTAG)) { + state.namespaces.put(uri, new NSITunes()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized ITunes namespace"); + } else if (uri.equals(NSSimpleChapters.NSURI) + && prefix.matches(NSSimpleChapters.NSTAG)) { + state.namespaces.put(uri, new NSSimpleChapters()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized SimpleChapters namespace"); + } else if (uri.equals(NSMedia.NSURI) + && prefix.equals(NSMedia.NSTAG)) { + state.namespaces.put(uri, new NSMedia()); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized media namespace"); + } + } + } + + private Namespace getHandlingNamespace(String uri, String qName) { + Namespace handler = state.namespaces.get(uri); + if (handler == null && !state.defaultNamespaces.empty() + && !qName.contains(":")) { + handler = state.defaultNamespaces.peek(); + } + return handler; + } + + @Override + public void endDocument() throws SAXException { + super.endDocument(); + state.getFeed().setItems(state.getItems()); + } + + public HandlerState getState() { + return state; + } + +} 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 new file mode 100644 index 000000000..32cd538d5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/TypeGetter.java @@ -0,0 +1,111 @@ +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; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.Reader; + +/** Gets the type of a specific feed by reading the root element. */ +public class TypeGetter { + private static final String TAG = "TypeGetter"; + + public enum Type { + RSS20, RSS091, ATOM, INVALID + } + + private static final String ATOM_ROOT = "feed"; + private static final String RSS_ROOT = "rss"; + + public Type getType(Feed feed) throws UnsupportedFeedtypeException { + XmlPullParserFactory factory; + if (feed.getFile_url() != null) { + try { + factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + XmlPullParser xpp = factory.newPullParser(); + xpp.setInput(createReader(feed)); + int eventType = xpp.getEventType(); + + while (eventType != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.START_TAG) { + String tag = xpp.getName(); + if (tag.equals(ATOM_ROOT)) { + feed.setType(Feed.TYPE_ATOM1); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized type Atom"); + return Type.ATOM; + } else if (tag.equals(RSS_ROOT)) { + 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"); + 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"); + return Type.RSS091; + } + } + throw new UnsupportedFeedtypeException(Type.INVALID); + } else { + if (BuildConfig.DEBUG) + 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 + String rootElement = null; + try { + if (Jsoup.parse(new File(feed.getFile_url()), null) != null) { + rootElement = "html"; + } + } catch (IOException e1) { + e1.printStackTrace(); + } finally { + throw new UnsupportedFeedtypeException(Type.INVALID, rootElement); + } + + } catch (IOException e) { + e.printStackTrace(); + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Type is invalid"); + throw new UnsupportedFeedtypeException(Type.INVALID); + } + + private Reader createReader(Feed feed) { + Reader reader; + try { + reader = new XmlStreamReader(new File(feed.getFile_url())); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } catch (IOException e) { + e.printStackTrace(); + return null; + } + return reader; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java new file mode 100644 index 000000000..3da9251d9 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/handler/UnsupportedFeedtypeException.java @@ -0,0 +1,38 @@ +package de.danoeh.antennapod.core.syndication.handler; + +import de.danoeh.antennapod.core.syndication.handler.TypeGetter.Type; + +public class UnsupportedFeedtypeException extends Exception { + private static final long serialVersionUID = 9105878964928170669L; + private TypeGetter.Type type; + private String rootElement; + + public UnsupportedFeedtypeException(Type type) { + super(); + this.type = type; + } + + public UnsupportedFeedtypeException(Type type, String rootElement) { + this.type = type; + this.rootElement = rootElement; + } + + public TypeGetter.Type getType() { + return type; + } + + public String getRootElement() { + return rootElement; + } + + @Override + public String getMessage() { + if (type == TypeGetter.Type.INVALID) { + return "Invalid type"; + } else { + return "Type " + type + " not supported"; + } + } + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java new file mode 100644 index 000000000..71bf69ffa --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSContent.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import org.xml.sax.Attributes; + +public class NSContent extends Namespace { + public static final String NSTAG = "content"; + public static final String NSURI = "http://purl.org/rss/1.0/modules/content/"; + + private static final String ENCODED = "encoded"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (localName.equals(ENCODED)) { + state.getCurrentItem().setContentEncoded(state.getContentBuf().toString()); + } + } + +} diff --git a/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 new file mode 100644 index 000000000..70e1126b9 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSITunes.java @@ -0,0 +1,75 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import de.danoeh.antennapod.core.feed.FeedImage; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import org.xml.sax.Attributes; + +import java.util.concurrent.TimeUnit; + +public class NSITunes extends Namespace { + public static final String NSTAG = "itunes"; + public static final String NSURI = "http://www.itunes.com/dtds/podcast-1.0.dtd"; + + private static final String IMAGE = "image"; + private static final String IMAGE_TITLE = "image"; + private static final String IMAGE_HREF = "href"; + + private static final String AUTHOR = "author"; + public static final String DURATION = "duration"; + + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(IMAGE)) { + FeedImage image = new FeedImage(); + image.setTitle(IMAGE_TITLE); + image.setDownload_url(attributes.getValue(IMAGE_HREF)); + + if (state.getCurrentItem() != null) { + // this is an items image + image.setTitle(state.getCurrentItem().getTitle()+IMAGE_TITLE); + image.setOwner(state.getCurrentItem()); + state.getCurrentItem().setImage(image); + + } else { + // this is the feed image + if (state.getFeed().getImage() == null) { + image.setOwner(state.getFeed()); + state.getFeed().setImage(image); + } + } + + } + + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (localName.equals(AUTHOR)) { + state.getFeed().setAuthor(state.getContentBuf().toString()); + } else if (localName.equals(DURATION)) { + String[] parts = state.getContentBuf().toString().split(":"); + try { + int duration = 0; + if (parts.length == 2) { + duration += TimeUnit.MINUTES.toMillis(Long.valueOf(parts[0])) + + TimeUnit.SECONDS.toMillis(Long.valueOf(parts[1])); + } else if (parts.length >= 3) { + duration += TimeUnit.HOURS.toMillis(Long.valueOf(parts[0])) + + TimeUnit.MINUTES.toMillis(Long.valueOf(parts[1])) + + TimeUnit.SECONDS.toMillis(Long.valueOf(parts[2])); + } else { + return; + } + + state.getTempObjects().put(DURATION, duration); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java new file mode 100644 index 000000000..7f03f1139 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSMedia.java @@ -0,0 +1,68 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.FeedMedia; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; +import org.xml.sax.Attributes; + +import java.util.concurrent.TimeUnit; + +/** Processes tags from the http://search.yahoo.com/mrss/ namespace. */ +public class NSMedia extends Namespace { + private static final String TAG = "NSMedia"; + + public static final String NSTAG = "media"; + public static final String NSURI = "http://search.yahoo.com/mrss/"; + + private static final String CONTENT = "content"; + private static final String DOWNLOAD_URL = "url"; + private static final String SIZE = "fileSize"; + private static final String MIME_TYPE = "type"; + private static final String DURATION = "duration"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(CONTENT)) { + String url = attributes.getValue(DOWNLOAD_URL); + String type = attributes.getValue(MIME_TYPE); + if (state.getCurrentItem().getMedia() == null + && url != null + && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils + .getValidMimeTypeFromUrl(url)) != null))) { + + long size = 0; + try { + size = Long.parseLong(attributes.getValue(SIZE)); + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Length attribute could not be parsed."); + } + + int duration = 0; + try { + String durationStr = attributes.getValue(DURATION); + if (durationStr != null) { + duration = (int) TimeUnit.MILLISECONDS.convert( + Long.parseLong(durationStr), TimeUnit.SECONDS); + } + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Duration attribute could not be parsed"); + } + + state.getCurrentItem().setMedia( + new FeedMedia(state.getCurrentItem(), url, size, type)); + } + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + + } + +} 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 new file mode 100644 index 000000000..0ca261a0e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSRSS20.java @@ -0,0 +1,149 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import android.util.Log; +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; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; +import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; +import org.xml.sax.Attributes; + +/** + * SAX-Parser for reading RSS-Feeds + * + * @author daniel + * + */ +public class NSRSS20 extends Namespace { + private static final String TAG = "NSRSS20"; + public static final String NSTAG = "rss"; + public static final String NSURI = ""; + + public final static String CHANNEL = "channel"; + public final static String ITEM = "item"; + public final static String GUID = "guid"; + public final static String TITLE = "title"; + public final static String LINK = "link"; + public final static String DESCR = "description"; + public final static String PUBDATE = "pubDate"; + public final static String ENCLOSURE = "enclosure"; + public final static String IMAGE = "image"; + public final static String URL = "url"; + public final static String LANGUAGE = "language"; + + public final static String ENC_URL = "url"; + public final static String ENC_LEN = "length"; + public final static String ENC_TYPE = "type"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(ITEM)) { + state.setCurrentItem(new FeedItem()); + state.getItems().add(state.getCurrentItem()); + state.getCurrentItem().setFeed(state.getFeed()); + + } else if (localName.equals(ENCLOSURE)) { + String type = attributes.getValue(ENC_TYPE); + String url = attributes.getValue(ENC_URL); + if (state.getCurrentItem().getMedia() == null + && (SyndTypeUtils.enclosureTypeValid(type) || ((type = SyndTypeUtils + .getValidMimeTypeFromUrl(url)) != null))) { + + long size = 0; + try { + size = Long.parseLong(attributes.getValue(ENC_LEN)); + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Length attribute could not be parsed."); + } + state.getCurrentItem().setMedia( + new FeedMedia(state.getCurrentItem(), url, size, type)); + } + + } else if (localName.equals(IMAGE)) { + 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()); + } + } + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (localName.equals(ITEM)) { + if (state.getCurrentItem() != null) { + // the title tag is optional in RSS 2.0. The description is used + // as a + // title if the item has no title-tag. + if (state.getCurrentItem().getTitle() == null) { + state.getCurrentItem().setTitle( + state.getCurrentItem().getDescription()); + } + + if (state.getTempObjects().containsKey(NSITunes.DURATION)) { + if (state.getCurrentItem().hasMedia()) { + state.getCurrentItem().getMedia().setDuration((Integer) state.getTempObjects().get(NSITunes.DURATION)); + } + state.getTempObjects().remove(NSITunes.DURATION); + } + } + state.setCurrentItem(null); + } else if (state.getTagstack().size() >= 2 + && state.getContentBuf() != null) { + String content = state.getContentBuf().toString(); + SyndElement topElement = state.getTagstack().peek(); + String top = topElement.getName(); + SyndElement secondElement = state.getSecondTag(); + String second = secondElement.getName(); + String third = null; + if (state.getTagstack().size() >= 3) { + third = state.getThirdTag().getName(); + } + + if (top.equals(GUID) && second.equals(ITEM)) { + // some feed creators include an empty or non-standard guid-element in their feed, which should be ignored + if (!content.isEmpty()) { + state.getCurrentItem().setItemIdentifier(content); + } + } else if (top.equals(TITLE)) { + if (second.equals(ITEM)) { + state.getCurrentItem().setTitle(content); + } else if (second.equals(CHANNEL)) { + state.getFeed().setTitle(content); + } else if (second.equals(IMAGE) && third != null + && third.equals(CHANNEL)) { + state.getFeed().getImage().setTitle(content); + } + } else if (top.equals(LINK)) { + if (second.equals(CHANNEL)) { + state.getFeed().setLink(content); + } else if (second.equals(ITEM)) { + state.getCurrentItem().setLink(content); + } + } else if (top.equals(PUBDATE) && second.equals(ITEM)) { + state.getCurrentItem().setPubDate( + SyndDateUtils.parseRFC822Date(content)); + } else if (top.equals(URL) && second.equals(IMAGE) && third != null + && third.equals(CHANNEL)) { + state.getFeed().getImage().setDownload_url(content); + } else if (localName.equals(DESCR)) { + if (second.equals(CHANNEL)) { + state.getFeed().setDescription(content); + } else if (second.equals(ITEM)) { + state.getCurrentItem().setDescription(content); + } + + } else if (localName.equals(LANGUAGE)) { + state.getFeed().setLanguage(content.toLowerCase()); + } + } + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java new file mode 100644 index 000000000..a2e5d0187 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/NSSimpleChapters.java @@ -0,0 +1,52 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import android.util.Log; + +import org.xml.sax.Attributes; + +import java.util.ArrayList; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.SimpleChapter; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; + +public class NSSimpleChapters extends Namespace { + private static final String TAG = "NSSimpleChapters"; + + public static final String NSTAG = "psc|sc"; + public static final String NSURI = "http://podlove.org/simple-chapters"; + + public static final String CHAPTERS = "chapters"; + public static final String CHAPTER = "chapter"; + public static final String START = "start"; + public static final String TITLE = "title"; + public static final String HREF = "href"; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(CHAPTERS)) { + state.getCurrentItem().setChapters(new ArrayList<Chapter>()); + } else if (localName.equals(CHAPTER)) { + try { + state.getCurrentItem() + .getChapters() + .add(new SimpleChapter(SyndDateUtils + .parseTimeString(attributes.getValue(START)), + attributes.getValue(TITLE), state.getCurrentItem(), + attributes.getValue(HREF))); + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) Log.w(TAG, "Unable to read chapter", e); + } + } + + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java new file mode 100644 index 000000000..cf118d202 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/Namespace.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import org.xml.sax.Attributes; + + +public abstract class Namespace { + public static final String NSTAG = null; + public static final String NSURI = null; + + /** Called by a Feedhandler when in startElement and it detects a namespace element + * @return The SyndElement to push onto the stack + * */ + public abstract SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes); + + /** Called by a Feedhandler when in endElement and it detects a namespace element + * @return true if namespace handled the element, false if it ignored it + * */ + public abstract void handleElementEnd(String localName, HandlerState state); + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java new file mode 100644 index 000000000..8adcd2086 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/SyndElement.java @@ -0,0 +1,22 @@ +package de.danoeh.antennapod.core.syndication.namespace; + +/** Defines a XML Element that is pushed on the tagstack */ +public class SyndElement { + protected String name; + protected Namespace namespace; + + public SyndElement(String name, Namespace namespace) { + this.name = name; + this.namespace = namespace; + } + + public Namespace getNamespace() { + return namespace; + } + + public String getName() { + return name; + } + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java new file mode 100644 index 000000000..43fe0edb7 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/AtomText.java @@ -0,0 +1,46 @@ +package de.danoeh.antennapod.core.syndication.namespace.atom; + +import de.danoeh.antennapod.core.syndication.namespace.Namespace; +import de.danoeh.antennapod.core.syndication.namespace.SyndElement; +import org.apache.commons.lang3.StringEscapeUtils; + +/** Represents Atom Element which contains text (content, title, summary). */ +public class AtomText extends SyndElement { + public static final String TYPE_TEXT = "text"; + public static final String TYPE_HTML = "html"; + public static final String TYPE_XHTML = "xhtml"; + + private String type; + private String content; + + public AtomText(String name, Namespace namespace, String type) { + super(name, namespace); + this.type = type; + } + + /** Processes the content according to the type and returns it. */ + public String getProcessedContent() { + if (type == null) { + return content; + } else if (type.equals(TYPE_HTML)) { + return StringEscapeUtils.unescapeHtml4(content); + } else if (type.equals(TYPE_XHTML)) { + return content; + } else { // Handle as text by default + return content; + } + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public String getType() { + return type; + } + +} diff --git a/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 new file mode 100644 index 000000000..3928c65b3 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/namespace/atom/NSAtom.java @@ -0,0 +1,208 @@ +package de.danoeh.antennapod.core.syndication.namespace.atom; + +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; +import de.danoeh.antennapod.core.syndication.handler.HandlerState; +import de.danoeh.antennapod.core.syndication.namespace.NSITunes; +import de.danoeh.antennapod.core.syndication.namespace.NSRSS20; +import de.danoeh.antennapod.core.syndication.namespace.Namespace; +import de.danoeh.antennapod.core.syndication.namespace.SyndElement; +import de.danoeh.antennapod.core.syndication.util.SyndDateUtils; +import de.danoeh.antennapod.core.syndication.util.SyndTypeUtils; + +public class NSAtom extends Namespace { + private static final String TAG = "NSAtom"; + public static final String NSTAG = "atom"; + public static final String NSURI = "http://www.w3.org/2005/Atom"; + + private static final String FEED = "feed"; + private static final String ID = "id"; + private static final String TITLE = "title"; + private static final String ENTRY = "entry"; + private static final String LINK = "link"; + private static final String UPDATED = "updated"; + private static final String AUTHOR = "author"; + private static final String CONTENT = "content"; + private static final String IMAGE = "logo"; + private static final String SUBTITLE = "subtitle"; + private static final String PUBLISHED = "published"; + + private static final String TEXT_TYPE = "type"; + // Link + private static final String LINK_HREF = "href"; + private static final String LINK_REL = "rel"; + private static final String LINK_TYPE = "type"; + private static final String LINK_TITLE = "title"; + private static final String LINK_LENGTH = "length"; + // rel-values + private static final String LINK_REL_ALTERNATE = "alternate"; + private static final String LINK_REL_ENCLOSURE = "enclosure"; + private static final String LINK_REL_PAYMENT = "payment"; + private static final String LINK_REL_RELATED = "related"; + private static final String LINK_REL_SELF = "self"; + private static final String LINK_REL_NEXT = "next"; + // type-values + private static final String LINK_TYPE_ATOM = "application/atom+xml"; + private static final String LINK_TYPE_HTML = "text/html"; + private static final String LINK_TYPE_XHTML = "application/xml+xhtml"; + + private static final String LINK_TYPE_RSS = "application/rss+xml"; + + /** + * Regexp to test whether an Element is a Text Element. + */ + private static final String isText = TITLE + "|" + CONTENT + "|" + "|" + + SUBTITLE; + + public static final String isFeed = FEED + "|" + NSRSS20.CHANNEL; + public static final String isFeedItem = ENTRY + "|" + NSRSS20.ITEM; + + @Override + public SyndElement handleElementStart(String localName, HandlerState state, + Attributes attributes) { + if (localName.equals(ENTRY)) { + state.setCurrentItem(new FeedItem()); + state.getItems().add(state.getCurrentItem()); + state.getCurrentItem().setFeed(state.getFeed()); + } else if (localName.matches(isText)) { + String type = attributes.getValue(TEXT_TYPE); + return new AtomText(localName, this, type); + } else if (localName.equals(LINK)) { + String href = attributes.getValue(LINK_HREF); + String rel = attributes.getValue(LINK_REL); + SyndElement parent = state.getTagstack().peek(); + if (parent.getName().matches(isFeedItem)) { + if (rel == null || rel.equals(LINK_REL_ALTERNATE)) { + state.getCurrentItem().setLink(href); + } else if (rel.equals(LINK_REL_ENCLOSURE)) { + String strSize = attributes.getValue(LINK_LENGTH); + long size = 0; + try { + if (strSize != null) { + size = Long.parseLong(strSize); + } + } catch (NumberFormatException e) { + if (BuildConfig.DEBUG) 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) + ); + } + } else if (rel.equals(LINK_REL_PAYMENT)) { + state.getCurrentItem().setPaymentLink(href); + } + } else if (parent.getName().matches(isFeed)) { + if (rel == null || rel.equals(LINK_REL_ALTERNATE)) { + String type = attributes.getValue(LINK_TYPE); + /* + * Use as link if a) no type-attribute is given and + * feed-object has no link yet b) type of link is + * 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)))) { + state.getFeed().setLink(href); + } 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) { + title = href; + } + state.addAlternateFeedUrl(title, href); + } + } else if (rel.equals(LINK_REL_PAYMENT)) { + state.getFeed().setPaymentLink(href); + } else if (rel.equals(LINK_REL_NEXT)) { + state.getFeed().setPaged(true); + state.getFeed().setNextPageLink(href); + } + } + } + return new SyndElement(localName, this); + } + + @Override + public void handleElementEnd(String localName, HandlerState state) { + if (localName.equals(ENTRY)) { + if (state.getCurrentItem() != null && + state.getTempObjects().containsKey(NSITunes.DURATION)) { + if (state.getCurrentItem().hasMedia()) { + state.getCurrentItem().getMedia().setDuration((Integer) state.getTempObjects().get(NSITunes.DURATION)); + } + state.getTempObjects().remove(NSITunes.DURATION); + } + state.setCurrentItem(null); + } + + if (state.getTagstack().size() >= 2) { + AtomText textElement = null; + String content; + if (state.getContentBuf() != null) { + content = state.getContentBuf().toString(); + } else { + content = ""; + } + SyndElement topElement = state.getTagstack().peek(); + String top = topElement.getName(); + SyndElement secondElement = state.getSecondTag(); + String second = secondElement.getName(); + + if (top.matches(isText)) { + textElement = (AtomText) topElement; + textElement.setContent(content); + } + + if (top.equals(ID)) { + if (second.equals(FEED)) { + state.getFeed().setFeedIdentifier(content); + } else if (second.equals(ENTRY)) { + state.getCurrentItem().setItemIdentifier(content); + } + } else if (top.equals(TITLE)) { + + if (second.equals(FEED)) { + state.getFeed().setTitle(textElement.getProcessedContent()); + } else if (second.equals(ENTRY)) { + state.getCurrentItem().setTitle( + textElement.getProcessedContent()); + } + } else if (top.equals(SUBTITLE)) { + if (second.equals(FEED)) { + state.getFeed().setDescription( + textElement.getProcessedContent()); + } + } else if (top.equals(CONTENT)) { + if (second.equals(ENTRY)) { + state.getCurrentItem().setDescription( + textElement.getProcessedContent()); + } + } else if (top.equals(UPDATED)) { + if (second.equals(ENTRY) + && state.getCurrentItem().getPubDate() == null) { + state.getCurrentItem().setPubDate( + SyndDateUtils.parseRFC3339Date(content)); + } + } else if (top.equals(PUBLISHED)) { + if (second.equals(ENTRY)) { + state.getCurrentItem().setPubDate( + SyndDateUtils.parseRFC3339Date(content)); + } + } else if (top.equals(IMAGE)) { + state.getFeed().setImage(new FeedImage(state.getFeed(), content, null)); + } + + } + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java new file mode 100644 index 000000000..d493286ac --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndDateUtils.java @@ -0,0 +1,161 @@ +package de.danoeh.antennapod.core.syndication.util; + +import android.util.Log; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Parses several date formats. + */ +public class SyndDateUtils { + private static final String TAG = "DateUtils"; + + private static final String[] RFC822DATES = {"dd MMM yy HH:mm:ss Z",}; + + /** + * RFC 3339 date format for UTC dates. + */ + public static final String RFC3339UTC = "yyyy-MM-dd'T'HH:mm:ss'Z'"; + + /** + * RFC 3339 date format for localtime dates with offset. + */ + public static final String RFC3339LOCAL = "yyyy-MM-dd'T'HH:mm:ssZ"; + + private static ThreadLocal<SimpleDateFormat> RFC822Formatter = new ThreadLocal<SimpleDateFormat>() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat(RFC822DATES[0], Locale.US); + } + + }; + + private static ThreadLocal<SimpleDateFormat> RFC3339Formatter = new ThreadLocal<SimpleDateFormat>() { + @Override + protected SimpleDateFormat initialValue() { + return new SimpleDateFormat(RFC3339UTC, Locale.US); + } + + }; + + public static Date parseRFC822Date(String date) { + Date result = null; + if (date.contains("PDT")) { + date = date.replace("PDT", "PST8PDT"); + } + if (date.contains(",")) { + // Remove day of the week + date = date.substring(date.indexOf(",") + 1).trim(); + } + SimpleDateFormat format = RFC822Formatter.get(); + for (int i = 0; i < RFC822DATES.length; i++) { + try { + format.applyPattern(RFC822DATES[i]); + result = format.parse(date); + break; + } catch (ParseException e) { + e.printStackTrace(); + } + } + if (result == null) { + Log.e(TAG, "Unable to parse feed date correctly"); + } + + return result; + } + + public static Date parseRFC3339Date(String date) { + Date result = null; + SimpleDateFormat format = RFC3339Formatter.get(); + boolean isLocal = date.endsWith("Z"); + if (date.contains(".")) { + // remove secfrac + int fracIndex = date.indexOf("."); + String first = date.substring(0, fracIndex); + String second = null; + if (isLocal) { + second = date.substring(date.length() - 1); + } else { + if (date.contains("+")) { + second = date.substring(date.indexOf("+")); + } else { + second = date.substring(date.indexOf("-")); + } + } + + date = first + second; + } + if (isLocal) { + try { + result = format.parse(date); + } catch (ParseException e) { + e.printStackTrace(); + } + } else { + format.applyPattern(RFC3339LOCAL); + // remove last colon + StringBuffer buf = new StringBuffer(date.length() - 1); + int colonIdx = date.lastIndexOf(':'); + for (int x = 0; x < date.length(); x++) { + if (x != colonIdx) + buf.append(date.charAt(x)); + } + String bufStr = buf.toString(); + try { + result = format.parse(bufStr); + } catch (ParseException e) { + e.printStackTrace(); + Log.e(TAG, "Unable to parse date"); + } finally { + format.applyPattern(RFC3339UTC); + } + + } + + return result; + + } + + /** + * Takes a string of the form [HH:]MM:SS[.mmm] and converts it to + * milliseconds. + * + * @throws java.lang.NumberFormatException if the number segments contain invalid numbers. + */ + public static long parseTimeString(final String time) { + String[] parts = time.split(":"); + long result = 0; + int idx = 0; + if (parts.length == 3) { + // string has hours + result += Integer.valueOf(parts[idx]) * 3600000L; + idx++; + } + result += Integer.valueOf(parts[idx]) * 60000L; + idx++; + result += (Float.valueOf(parts[idx])) * 1000L; + return result; + } + + public static String formatRFC822Date(Date date) { + SimpleDateFormat format = RFC822Formatter.get(); + return format.format(date); + } + + public static String formatRFC3339Local(Date date) { + SimpleDateFormat format = RFC3339Formatter.get(); + format.applyPattern(RFC3339LOCAL); + String result = format.format(date); + format.applyPattern(RFC3339UTC); + return result; + } + + public static String formatRFC3339UTC(Date date) { + SimpleDateFormat format = RFC3339Formatter.get(); + format.applyPattern(RFC3339UTC); + return format.format(date); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java new file mode 100644 index 000000000..8d1d8ffde --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/syndication/util/SyndTypeUtils.java @@ -0,0 +1,42 @@ +package de.danoeh.antennapod.core.syndication.util; + +import android.webkit.MimeTypeMap; +import org.apache.commons.io.FilenameUtils; + +/** Utility class for handling MIME-Types of enclosures */ +public class SyndTypeUtils { + + private final static String VALID_MIMETYPE = "audio/.*" + "|" + "video/.*" + + "|" + "application/ogg"; + + private SyndTypeUtils() { + + } + + public static boolean enclosureTypeValid(String type) { + if (type == null) { + return false; + } else { + return type.matches(VALID_MIMETYPE); + } + } + + /** + * Should be used if mime-type of enclosure tag is not supported. This + * method will check if the mime-type of the file extension is supported. If + * the type is not supported, this method will return null. + */ + public static String getValidMimeTypeFromUrl(String url) { + if (url != null) { + String extension = FilenameUtils.getExtension(url); + if (extension != null) { + String type = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension(extension); + if (type != null && enclosureTypeValid(type)) { + return type; + } + } + } + return null; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java new file mode 100644 index 000000000..8dd9ffe4b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ChapterUtils.java @@ -0,0 +1,274 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; + +import org.apache.commons.io.IOUtils; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Collections; +import java.util.List; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.util.comparator.ChapterStartTimeComparator; +import de.danoeh.antennapod.core.util.id3reader.ChapterReader; +import de.danoeh.antennapod.core.util.id3reader.ID3ReaderException; +import de.danoeh.antennapod.core.util.playback.Playable; +import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentChapterReader; +import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException; + +/** + * Utility class for getting chapter data from media files. + */ +public class ChapterUtils { + private static final String TAG = "ChapterUtils"; + + private ChapterUtils() { + } + + /** + * Uses the download URL of a media object of a feeditem to read its ID3 + * chapters. + */ + public static void readID3ChaptersFromPlayableStreamUrl(Playable p) { + if (p != null && p.getStreamUrl() != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); + InputStream in = null; + try { + URL url = new URL(p.getStreamUrl()); + ChapterReader reader = new ChapterReader(); + + in = url.openStream(); + reader.readInputStream(in); + List<Chapter> chapters = reader.getChapters(); + + if (chapters != null) { + Collections + .sort(chapters, new ChapterStartTimeComparator()); + processChapters(chapters, p); + if (chaptersValid(chapters)) { + p.setChapters(chapters); + Log.i(TAG, "Chapters loaded"); + } else { + Log.e(TAG, "Chapter data was invalid"); + } + } else { + Log.i(TAG, "ChapterReader could not find any ID3 chapters"); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } catch (ID3ReaderException e) { + e.printStackTrace(); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } else { + Log.e(TAG, + "Unable to read ID3 chapters: media or download URL was null"); + } + } + + /** + * Uses the file URL of a media object of a feeditem to read its ID3 + * chapters. + */ + public static void readID3ChaptersFromPlayableFileUrl(Playable p) { + if (p != null && p.localFileAvailable() && p.getLocalMediaUrl() != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle()); + File source = new File(p.getLocalMediaUrl()); + if (source.exists()) { + ChapterReader reader = new ChapterReader(); + InputStream in = null; + + try { + in = new BufferedInputStream(new FileInputStream(source)); + reader.readInputStream(in); + List<Chapter> chapters = reader.getChapters(); + + if (chapters != null) { + Collections.sort(chapters, + new ChapterStartTimeComparator()); + processChapters(chapters, p); + if (chaptersValid(chapters)) { + p.setChapters(chapters); + Log.i(TAG, "Chapters loaded"); + } else { + Log.e(TAG, "Chapter data was invalid"); + } + } else { + Log.i(TAG, + "ChapterReader could not find any ID3 chapters"); + } + } catch (IOException e) { + e.printStackTrace(); + } catch (ID3ReaderException e) { + e.printStackTrace(); + } finally { + if (in != null) { + try { + in.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } else { + Log.e(TAG, "Unable to read id3 chapters: Source doesn't exist"); + } + } + } + + public static void readOggChaptersFromPlayableStreamUrl(Playable media) { + if (media != null && media.streamAvailable()) { + InputStream input = null; + try { + URL url = new URL(media.getStreamUrl()); + input = url.openStream(); + if (input != null) { + readOggChaptersFromInputStream(media, input); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(input); + } + } + } + + public static void readOggChaptersFromPlayableFileUrl(Playable media) { + if (media != null && media.getLocalMediaUrl() != null) { + File source = new File(media.getLocalMediaUrl()); + if (source.exists()) { + InputStream input = null; + try { + input = new BufferedInputStream(new FileInputStream(source)); + readOggChaptersFromInputStream(media, input); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } finally { + IOUtils.closeQuietly(input); + } + } + } + } + + private static void readOggChaptersFromInputStream(Playable p, + InputStream input) { + if (BuildConfig.DEBUG) + Log.d(TAG, + "Trying to read chapters from item with title " + + p.getEpisodeTitle()); + try { + VorbisCommentChapterReader reader = new VorbisCommentChapterReader(); + reader.readInputStream(input); + List<Chapter> chapters = reader.getChapters(); + if (chapters != null) { + Collections.sort(chapters, new ChapterStartTimeComparator()); + processChapters(chapters, p); + if (chaptersValid(chapters)) { + p.setChapters(chapters); + Log.i(TAG, "Chapters loaded"); + } else { + Log.e(TAG, "Chapter data was invalid"); + } + } else { + Log.i(TAG, + "ChapterReader could not find any Ogg vorbis chapters"); + } + } catch (VorbisCommentReaderException e) { + e.printStackTrace(); + } + } + + /** + * Makes sure that chapter does a title and an item attribute. + */ + private static void processChapters(List<Chapter> chapters, Playable p) { + for (int i = 0; i < chapters.size(); i++) { + Chapter c = chapters.get(i); + if (c.getTitle() == null) { + c.setTitle(Integer.toString(i)); + } + } + } + + private static boolean chaptersValid(List<Chapter> chapters) { + if (chapters.isEmpty()) { + return false; + } + for (Chapter c : chapters) { + if (c.getTitle() == null) { + return false; + } + if (c.getStart() < 0) { + return false; + } + } + return true; + } + + /** + * Calls getCurrentChapter with current position. + */ + public static Chapter getCurrentChapter(Playable media) { + if (media.getChapters() != null) { + List<Chapter> chapters = media.getChapters(); + Chapter current = null; + if (chapters != null) { + current = chapters.get(0); + for (Chapter sc : chapters) { + if (sc.getStart() > media.getPosition()) { + break; + } else { + current = sc; + } + } + } + return current; + } else { + return null; + } + } + + public static void loadChaptersFromStreamUrl(Playable media) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Starting chapterLoader thread"); + ChapterUtils.readID3ChaptersFromPlayableStreamUrl(media); + if (media.getChapters() == null) { + ChapterUtils.readOggChaptersFromPlayableStreamUrl(media); + } + + if (BuildConfig.DEBUG) + Log.d(TAG, "ChapterLoaderThread has finished"); + } + + public static void loadChaptersFromFileUrl(Playable media) { + if (media.localFileAvailable()) { + ChapterUtils.readID3ChaptersFromPlayableFileUrl(media); + if (media.getChapters() == null) { + ChapterUtils.readOggChaptersFromPlayableFileUrl(media); + } + } else { + Log.e(TAG, "Could not load chapters from file url: local file not available"); + } + } +} 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 new file mode 100644 index 000000000..a0b514bd6 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/Converter.java @@ -0,0 +1,103 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; + +/** Provides methods for converting various units. */ +public final class Converter { + /** Class shall not be instantiated. */ + private Converter() { + } + + /** Logging tag. */ + private static final String TAG = "Converter"; + + + /** Indicates that the value is in the Byte range.*/ + private static final int B_RANGE = 0; + /** Indicates that the value is in the Kilobyte range.*/ + private static final int KB_RANGE = 1; + /** Indicates that the value is in the Megabyte range.*/ + private static final int MB_RANGE = 2; + /** Indicates that the value is in the Gigabyte range.*/ + private static final int GB_RANGE = 3; + /** Determines the length of the number for best readability.*/ + private static final int NUM_LENGTH = 1024; + + + private static final int HOURS_MIL = 3600000; + private static final int MINUTES_MIL = 60000; + private static final int SECONDS_MIL = 1000; + + /** Takes a byte-value and converts it into a more readable + * String. + * @param input The value to convert + * @return The converted String with a unit + * */ + public static String byteToString(final long input) { + int i = 0; + int result = 0; + + for (i = 0; i < GB_RANGE + 1; i++) { + result = (int) (input / Math.pow(1024, i)); + if (result < NUM_LENGTH) { + break; + } + } + + switch (i) { + case B_RANGE: + return result + " B"; + case KB_RANGE: + return result + " KB"; + case MB_RANGE: + return result + " MB"; + case GB_RANGE: + return result + " GB"; + default: + Log.e(TAG, "Error happened in byteToString"); + return "ERROR"; + } + } + + /** Converts milliseconds to a string containing hours, minutes and seconds */ + public static String getDurationStringLong(int duration) { + int h = duration / HOURS_MIL; + int rest = duration - h * HOURS_MIL; + int m = rest / MINUTES_MIL; + rest -= m * MINUTES_MIL; + int s = rest / SECONDS_MIL; + + return String.format("%02d:%02d:%02d", h, m, s); + } + + /** Converts milliseconds to a string containing hours and minutes */ + public static String getDurationStringShort(int duration) { + int h = duration / HOURS_MIL; + int rest = duration - h * HOURS_MIL; + int m = rest / MINUTES_MIL; + + return String.format("%02d:%02d", h, m); + } + + /** Converts long duration string (HH:MM:SS) to milliseconds. */ + public static int durationStringLongToMs(String input) { + String[] parts = input.split(":"); + if (parts.length != 3) { + return 0; + } + return Integer.valueOf(parts[0]) * 3600 * 1000 + + Integer.valueOf(parts[1]) * 60 * 1000 + + Integer.valueOf(parts[2]) * 1000; + } + + /** Converts short duration string (HH:MM) to milliseconds. */ + public static int durationStringShortToMs(String input) { + String[] parts = input.split(":"); + if (parts.length != 2) { + return 0; + } + return Integer.valueOf(parts[0]) * 3600 * 1000 + + Integer.valueOf(parts[1]) * 1000 * 60; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java new file mode 100644 index 000000000..602c221bf --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java @@ -0,0 +1,52 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import de.danoeh.antennapod.core.R; + +/** Utility class for Download Errors. */ +public enum DownloadError { + SUCCESS(0, R.string.download_successful), + ERROR_PARSER_EXCEPTION(1, R.string.download_error_parser_exception), + ERROR_UNSUPPORTED_TYPE(2, R.string.download_error_unsupported_type), + ERROR_CONNECTION_ERROR(3, R.string.download_error_connection_error), + ERROR_MALFORMED_URL(4, R.string.download_error_error_unknown), + ERROR_IO_ERROR(5, R.string.download_error_io_error), + ERROR_FILE_EXISTS(6, R.string.download_error_error_unknown), + ERROR_DOWNLOAD_CANCELLED(7, R.string.download_error_error_unknown), + ERROR_DEVICE_NOT_FOUND(8, R.string.download_error_device_not_found), + ERROR_HTTP_DATA_ERROR(9, R.string.download_error_http_data_error), + ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space), + ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host), + ERROR_REQUEST_ERROR(12, R.string.download_error_request_error), + ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access), + ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized); + + private final int code; + private final int resId; + + private DownloadError(int code, int resId) { + this.code = code; + this.resId = resId; + } + + /** Return DownloadError from its associated code. */ + public static DownloadError fromCode(int code) { + for (DownloadError reason : values()) { + if (reason.getCode() == code) { + return reason; + } + } + throw new IllegalArgumentException("unknown code: " + code); + } + + /** Get machine-readable code. */ + public int getCode() { + return code; + } + + /** Get a human-readable string. */ + public String getErrorString(Context context) { + return context.getString(resId); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java b/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java new file mode 100644 index 000000000..f432424f8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DuckType.java @@ -0,0 +1,117 @@ +/* Adapted from: http://thinking-in-code.blogspot.com/2008/11/duck-typing-in-java-using-dynamic.html */ + +package de.danoeh.antennapod.core.util; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import de.danoeh.antennapod.core.BuildConfig; + +/** + * Allows "duck typing" or dynamic invocation based on method signature rather + * than type hierarchy. In other words, rather than checking whether something + * IS-a duck, check whether it WALKS-like-a duck or QUACKS-like a duck. + * + * To use first use the coerce static method to indicate the object you want to + * do Duck Typing for, then specify an interface to the to method which you want + * to coerce the type to, e.g: + * + * public interface Foo { void aMethod(); } class Bar { ... public void + * aMethod() { ... } ... } Bar bar = ...; Foo foo = + * DuckType.coerce(bar).to(Foo.class); foo.aMethod(); + * + * + */ +public class DuckType { + + private final Object objectToCoerce; + + private DuckType(Object objectToCoerce) { + this.objectToCoerce = objectToCoerce; + } + + private class CoercedProxy implements InvocationHandler { + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Method delegateMethod = findMethodBySignature(method); + assert delegateMethod != null; + return delegateMethod.invoke(DuckType.this.objectToCoerce, args); + } + } + + /** + * Specify the duck typed object to coerce. + * + * @param object + * the object to coerce + * @return + */ + public static DuckType coerce(Object object) { + return new DuckType(object); + } + + /** + * Coerce the Duck Typed object to the given interface providing it + * implements all the necessary methods. + * + * @param + * @param iface + * @return an instance of the given interface that wraps the duck typed + * class + * @throws ClassCastException + * if the object being coerced does not implement all the + * methods in the given interface. + */ + @SuppressWarnings({ "rawtypes", "unchecked" }) + public <T> T to(Class iface) { + if (BuildConfig.DEBUG && !iface.isInterface()) throw new AssertionError("cannot coerce object to a class, must be an interface"); + if (isA(iface)) { + return (T) iface.cast(objectToCoerce); + } + if (quacksLikeA(iface)) { + return generateProxy(iface); + } + throw new ClassCastException("Could not coerce object of type " + objectToCoerce.getClass() + " to " + iface); + } + + @SuppressWarnings("rawtypes") + private boolean isA(Class iface) { + return objectToCoerce.getClass().isInstance(iface); + } + + /** + * Determine whether the duck typed object can be used with the given + * interface. + * + * @param Type + * of the interface to check. + * @param iface + * Interface class to check + * @return true if the object will support all the methods in the interface, + * false otherwise. + */ + @SuppressWarnings("rawtypes") + public boolean quacksLikeA(Class iface) { + for (Method method : iface.getMethods()) { + if (findMethodBySignature(method) == null) { + return false; + } + } + return true; + } + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private <T> T generateProxy(Class iface) { + return (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[] { iface }, new CoercedProxy()); + } + + private Method findMethodBySignature(Method method) { + try { + return objectToCoerce.getClass().getMethod(method.getName(), method.getParameterTypes()); + } catch (NoSuchMethodException e) { + return null; + } + } + +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java b/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java new file mode 100644 index 000000000..4c23b161b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/EpisodeFilter.java @@ -0,0 +1,49 @@ +package de.danoeh.antennapod.core.util; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.ArrayList; +import java.util.List; + +public class EpisodeFilter { + private EpisodeFilter() { + + } + + /** Return a copy of the itemlist without items which have no media. */ + public static ArrayList<FeedItem> getEpisodeList(List<FeedItem> items) { + ArrayList<FeedItem> episodes = new ArrayList<FeedItem>(items); + for (FeedItem item : items) { + if (item.getMedia() == null) { + episodes.remove(item); + } + } + return episodes; + } + + public static int countItemsWithEpisodes(List<FeedItem> items) { + int count = 0; + for (FeedItem item : items) { + if (item.getMedia() != null) { + count++; + } + } + return count; + } + + public static FeedItem accessEpisodeByIndex(List<FeedItem> items, + int position) { + int count = 0; + for (FeedItem item : items) { + + if (item.getMedia() != null) { + if (count == position) { + return item; + } else { + count++; + } + } + } + return null; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java new file mode 100644 index 000000000..bf14cd23e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FeedtitleComparator.java @@ -0,0 +1,15 @@ +package de.danoeh.antennapod.core.util; + +import de.danoeh.antennapod.core.feed.Feed; + +import java.util.Comparator; + +/** Compares the title of two feeds for sorting. */ +public class FeedtitleComparator implements Comparator<Feed> { + + @Override + public int compare(Feed lhs, Feed rhs) { + return lhs.getTitle().compareToIgnoreCase(rhs.getTitle()); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java new file mode 100644 index 000000000..30c2e62f2 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java @@ -0,0 +1,36 @@ +package de.danoeh.antennapod.core.util; + +import java.util.Arrays; + +/** Generates valid filenames for a given string. */ +public class FileNameGenerator { + + private static final char[] ILLEGAL_CHARACTERS = { '/', '\\', '?', '%', + '*', ':', '|', '"', '<', '>', '\n' }; + static { + Arrays.sort(ILLEGAL_CHARACTERS); + } + + private FileNameGenerator() { + + } + + /** + * This method will return a new string that doesn't contain any illegal + * characters of the given string. + */ + public static String generateFileName(String string) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < string.length(); i++) { + char c = string.charAt(i); + if (Arrays.binarySearch(ILLEGAL_CHARACTERS, c) < 0) { + builder.append(c); + } + } + return builder.toString().replaceFirst(" *$",""); + } + + public static long generateLong(final String str) { + return str.hashCode(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java b/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java new file mode 100644 index 000000000..c98c2d82a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/InvalidFeedException.java @@ -0,0 +1,21 @@ +package de.danoeh.antennapod.core.util; + +/** Thrown if a feed has invalid attribute values. */ +public class InvalidFeedException extends Exception { + + public InvalidFeedException() { + } + + public InvalidFeedException(String detailMessage) { + super(detailMessage); + } + + public InvalidFeedException(Throwable throwable) { + super(throwable); + } + + public InvalidFeedException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + +} 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 new file mode 100644 index 000000000..07432d28a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java @@ -0,0 +1,120 @@ +package de.danoeh.antennapod.core.util; + +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; + static { + languages = new HashMap<String, String>(); + languages.put("af", "Afrikaans"); + languages.put("sq", "Albanian"); + languages.put("sq", "Albanian"); + languages.put("eu", "Basque"); + languages.put("be", "Belarusian"); + languages.put("bg", "Bulgarian"); + languages.put("ca", "Catalan"); + languages.put("Chinese (Simplified)", "zh-cn"); + languages.put("Chinese (Traditional)", "zh-tw"); + languages.put("hr", "Croatian"); + languages.put("cs", "Czech"); + languages.put("da", "Danish"); + languages.put("nl", "Dutch"); + languages.put("nl-be", "Dutch (Belgium)"); + languages.put("nl-nl", "Dutch (Netherlands)"); + languages.put("en", "English"); + languages.put("en-au", "English (Australia)"); + languages.put("en-bz", "English (Belize)"); + languages.put("en-ca", "English (Canada)"); + languages.put("en-ie", "English (Ireland)"); + languages.put("en-jm", "English (Jamaica)"); + languages.put("en-nz", "English (New Zealand)"); + languages.put("en-ph", "English (Phillipines)"); + languages.put("en-za", "English (South Africa)"); + languages.put("en-tt", "English (Trinidad)"); + languages.put("en-gb", "English (United Kingdom)"); + languages.put("en-us", "English (United States)"); + languages.put("en-zw", "English (Zimbabwe)"); + languages.put("et", "Estonian"); + languages.put("fo", "Faeroese"); + languages.put("fi", "Finnish"); + languages.put("fr", "French"); + languages.put("fr-be", "French (Belgium)"); + languages.put("fr-ca", "French (Canada)"); + languages.put("fr-fr", "French (France)"); + languages.put("fr-lu", "French (Luxembourg)"); + languages.put("fr-mc", "French (Monaco)"); + languages.put("fr-ch", "French (Switzerland)"); + languages.put("gl", "Galician"); + languages.put("gd", "Gaelic"); + languages.put("de", "German"); + languages.put("de-at", "German (Austria)"); + languages.put("de-de", "German (Germany)"); + languages.put("de-li", "German (Liechtenstein)"); + languages.put("de-lu", "German (Luxembourg)"); + languages.put("de-ch", "German (Switzerland)"); + languages.put("el", "Greek"); + languages.put("haw", "Hawaiian"); + languages.put("hu", "Hungarian"); + languages.put("is", "Icelandic"); + languages.put("in", "Indonesian"); + languages.put("ga", "Irish"); + languages.put("it", "Italian"); + languages.put("it-it", "Italian (Italy)"); + languages.put("it-ch", "Italian (Switzerland)"); + languages.put("ja", "Japanese"); + languages.put("ko", "Korean"); + languages.put("mk", "Macedonian"); + languages.put("no", "Norwegian"); + languages.put("pl", "Polish"); + languages.put("pt", "Portugese"); + languages.put("pt-br", "Portugese (Brazil)"); + languages.put("pt-pt", "Portugese (Portugal"); + languages.put("ro", "Romanian"); + languages.put("ro-mo", "Romanian (Moldova)"); + languages.put("ro-ro", "Romanian (Romania"); + languages.put("ru", "Russian"); + languages.put("ru-mo", "Russian (Moldova)"); + languages.put("ru-ru", "Russian (Russia)"); + languages.put("sr", "Serbian"); + languages.put("sk", "Slovak"); + languages.put("sl", "Slovenian"); + languages.put("es", "Spanish"); + languages.put("es-ar", "Spanish (Argentinia)"); + languages.put("es=bo", "Spanish (Bolivia)"); + languages.put("es-cl", "Spanish (Chile)"); + languages.put("es-co", "Spanish (Colombia)"); + languages.put("es-cr", "Spanish (Costa Rica)"); + languages.put("es-do", "Spanish (Dominican Republic)"); + languages.put("es-ec", "Spanish (Ecuador)"); + languages.put("es-sv", "Spanish (El Salvador)"); + languages.put("es-gt", "Spanish (Guatemala)"); + languages.put("es-hn", "Spanish (Honduras)"); + languages.put("es-mx", "Spanish (Mexico)"); + languages.put("es-ni", "Spanish (Nicaragua)"); + languages.put("es-pa", "Spanish (Panama)"); + languages.put("es-py", "Spanish (Paraguay)"); + languages.put("es-pe", "Spanish (Peru)"); + languages.put("es-pr", "Spanish (Puerto Rico)"); + languages.put("es-es", "Spanish (Spain)"); + languages.put("es-uy", "Spanish (Uruguay)"); + languages.put("es-ve", "Spanish (Venezuela)"); + languages.put("sv", "Swedish"); + languages.put("sv-fi", "Swedish (Finland)"); + languages.put("sv-se", "Swedish (Sweden)"); + languages.put("tr", "Turkish"); + languages.put("uk", "Ukranian"); + } + + /** Finds language string for key or returns the language key if it can't be found. */ + public static String getLanguageString(String key) { + String language = languages.get(key); + if (language != null) { + return language; + } else { + return key; + } + } +} 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 new file mode 100644 index 000000000..b321536a3 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/NetworkUtils.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.NetworkInfo; +import android.net.wifi.WifiInfo; +import android.net.wifi.WifiManager; +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +import java.util.Arrays; +import java.util.List; + +public class NetworkUtils { + private static final String TAG = "NetworkUtils"; + + private NetworkUtils() { + + } + + /** + * Returns true if the device is connected to Wi-Fi and the Wi-Fi filter for + * automatic downloads is disabled or the device is connected to a Wi-Fi + * 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) { + 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"); + if (networkInfo.isConnected()) { + if (!UserPreferences.isEnableAutodownloadWifiFilter()) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Auto-dl filter is disabled"); + return true; + } else { + WifiManager wm = (WifiManager) context + .getSystemService(Context.WIFI_SERVICE); + WifiInfo wifiInfo = wm.getConnectionInfo(); + List<String> selectedNetworks = Arrays + .asList(UserPreferences + .getAutodownloadSelectedNetworks()); + if (selectedNetworks.contains(Integer.toString(wifiInfo + .getNetworkId()))) { + if (BuildConfig.DEBUG) + 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"); + return false; + } + + public static boolean networkAvailable(Context context) { + ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo info = cm.getActiveNetworkInfo(); + return info != null && info.isConnected(); + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java b/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java new file mode 100644 index 000000000..8e40ae184 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/QueueAccess.java @@ -0,0 +1,93 @@ +package de.danoeh.antennapod.core.util; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.Iterator; +import java.util.List; + +/** + * Provides methods for accessing the queue. It is possible to load only a part of the information about the queue that + * is stored in the database (e.g. sometimes the user just has to test if a specific item is contained in the List. + * QueueAccess provides an interface for accessing the queue without having to care about the type of the queue + * representation. + */ +public abstract class QueueAccess { + /** + * Returns true if the item is in the queue, false otherwise. + */ + public abstract boolean contains(long id); + + /** + * Removes the item from the queue. + * + * @return true if the queue was modified by this operation. + */ + public abstract boolean remove(long id); + + private QueueAccess() { + + } + + public static QueueAccess IDListAccess(final List<Long> ids) { + return new QueueAccess() { + @Override + public boolean contains(long id) { + return (ids != null) && ids.contains(id); + } + + @Override + public boolean remove(long id) { + return ids.remove(id); + } + + + }; + } + + public static QueueAccess ItemListAccess(final List<FeedItem> items) { + return new QueueAccess() { + @Override + public boolean contains(long id) { + if (items == null) { + return false; + } + for (FeedItem item : items) { + if (item.getId() == id) { + return true; + } + } + return false; + } + + @Override + public boolean remove(long id) { + Iterator<FeedItem> it = items.iterator(); + FeedItem item; + while (it.hasNext()) { + item = it.next(); + if (item.getId() == id) { + it.remove(); + return true; + } + } + return false; + } + }; + } + + public static QueueAccess NotInQueueAccess() { + return new QueueAccess() { + @Override + public boolean contains(long id) { + return false; + } + + @Override + public boolean remove(long id) { + return false; + } + }; + + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java new file mode 100644 index 000000000..85f32ed50 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.util; + +import android.content.Context; +import android.content.Intent; +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedItem; + +/** Utility methods for sharing data */ +public class ShareUtils { + private static final String TAG = "ShareUtils"; + + private ShareUtils() {} + + public static void shareLink(Context context, String link) { + Intent i = new Intent(Intent.ACTION_SEND); + i.setType("text/plain"); + i.putExtra(Intent.EXTRA_SUBJECT, "Sharing URL"); + i.putExtra(Intent.EXTRA_TEXT, link); + context.startActivity(Intent.createChooser(i, "Share URL")); + } + + public static void shareFeedItemLink(Context context, FeedItem item) { + shareLink(context, item.getLink()); + } + + public static void shareFeedDownloadLink(Context context, Feed feed) { + shareLink(context, feed.getDownload_url()); + } + + public static void shareFeedlink(Context context, Feed feed) { + shareLink(context, feed.getLink()); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java new file mode 100644 index 000000000..7e7c6c08b --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ShownotesProvider.java @@ -0,0 +1,16 @@ +package de.danoeh.antennapod.core.util; + +import java.util.concurrent.Callable; + +/** + * Created by daniel on 04.08.13. + */ +public interface ShownotesProvider { + /** + * Loads shownotes. If the shownotes have to be loaded from a file or from a + * database, it should be done in a separate thread. After the shownotes + * have been loaded, callback.onShownotesLoaded should be called. + */ + public Callable<String> loadShownotes(); + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java new file mode 100644 index 000000000..dea380937 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/StorageUtils.java @@ -0,0 +1,67 @@ +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; + +/** + * Utility functions for handling storage errors + */ +public class StorageUtils { + private static final String TAG = "StorageUtils"; + + public static boolean storageAvailable(Context context) { + File dir = UserPreferences.getDataFolder(context, 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"); + return false; + } + } + + /** + * Checks if external storage is available. If external storage isn't + * available, the current activity is finsished an an error activity is + * launched. + * + * @param activity the activity which would be finished if no storage is + * available + * @return true if external storage is available + */ + public static boolean checkStorageAvailability(Activity activity) { + boolean storageAvailable = storageAvailable(activity); + if (!storageAvailable) { + activity.finish(); + activity.startActivity(ClientConfig.applicationCallbacks.getStorageErrorActivity(activity)); + } + return storageAvailable; + } + + /** + * 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()); + long availableBlocks; + long blockSize; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + availableBlocks = stat.getAvailableBlocksLong(); + blockSize = stat.getBlockSizeLong(); + } else { + availableBlocks = stat.getAvailableBlocks(); + blockSize = stat.getBlockSize(); + } + return availableBlocks * blockSize; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java new file mode 100644 index 000000000..f67367643 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ThemeUtils.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; + +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; + +public class ThemeUtils { + private static final String TAG = "ThemeUtils"; + + public static int getSelectionBackgroundColor() { + int theme = UserPreferences.getTheme(); + if (theme == R.style.Theme_AntennaPod_Dark) { + return R.color.selection_background_color_dark; + } else if (theme == R.style.Theme_AntennaPod_Light) { + return R.color.selection_background_color_light; + } else { + Log.e(TAG, + "getSelectionBackgroundColor could not match the current theme to any color!"); + return R.color.selection_background_color_light; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java new file mode 100644 index 000000000..092c06b4a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/URIUtil.java @@ -0,0 +1,35 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +/** + * Utility methods for dealing with URL encoding. + */ +public class URIUtil { + private static final String TAG = "URIUtil"; + + private URIUtil() {} + + public static URI getURIFromRequestUrl(String source) { + // try without encoding the URI + try { + return new URI(source); + } catch (URISyntaxException e) { + if (BuildConfig.DEBUG) Log.d(TAG, "Source is not encoded, encoding now"); + } + try { + URL url = new URL(source); + return new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(), url.getPath(), url.getQuery(), url.getRef()); + } catch (MalformedURLException e) { + throw new IllegalArgumentException(e); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } +} 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 new file mode 100644 index 000000000..4bd18c8bc --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java @@ -0,0 +1,54 @@ +package de.danoeh.antennapod.core.util; + +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; + +import de.danoeh.antennapod.core.BuildConfig; + +/** + * Provides methods for checking and editing a URL. + */ +public final class URLChecker { + + /** + * Class shall not be instantiated. + */ + private URLChecker() { + } + + /** + * Logging tag. + */ + private static final String TAG = "URLChecker"; + + private static final String AP_SUBSCRIBE = "antennapod-subscribe://"; + + /** + * Checks if URL is valid and modifies it if necessary. + * + * @param url The url which is going to be prepared + * @return The prepared url + */ + public static String prepareURL(String url) { + url = StringUtils.trim(url); + 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://")); + } 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)); + } 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; + } else { + return url; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java b/core/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java new file mode 100644 index 000000000..5843c5f8f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/UndoBarController.java @@ -0,0 +1,137 @@ +/* + * Copyright 2012 Roman Nurik + * + * 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 de.danoeh.antennapod.core.util; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.text.TextUtils; +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 { + private View mBarView; + private TextView mMessageView; + private ViewPropertyAnimator mBarAnimator; + private Handler mHideHandler = new Handler(); + + private UndoListener mUndoListener; + + // State objects + private Parcelable mUndoToken; + private CharSequence mUndoMessage; + + public interface UndoListener { + void onUndo(Parcelable token); + } + + public UndoBarController(View undoBarView, UndoListener 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, Parcelable 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 void hideUndoBar(boolean immediate) { + mHideHandler.removeCallbacks(mHideRunnable); + if (immediate) { + mBarView.setVisibility(View.GONE); + ViewHelper.setAlpha(mBarView, 0); + mUndoMessage = null; + mUndoToken = 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; + } + }); + } + } + + public void onSaveInstanceState(Bundle outState) { + outState.putCharSequence("undo_message", mUndoMessage); + outState.putParcelable("undo_token", mUndoToken); + } + + public void onRestoreInstanceState(Bundle savedInstanceState) { + if (savedInstanceState != null) { + mUndoMessage = savedInstanceState.getCharSequence("undo_message"); + mUndoToken = savedInstanceState.getParcelable("undo_token"); + + if (mUndoToken != null || !TextUtils.isEmpty(mUndoMessage)) { + showUndoBar(true, mUndoMessage, mUndoToken); + } + } + } + + private Runnable mHideRunnable = new Runnable() { + @Override + public void run() { + hideUndoBar(false); + } + }; +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java new file mode 100644 index 000000000..5274ffc9e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/ChapterStartTimeComparator.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.Chapter; + +import java.util.Comparator; + +public class ChapterStartTimeComparator implements Comparator<Chapter> { + + @Override + public int compare(Chapter lhs, Chapter rhs) { + if (lhs.getStart() == rhs.getStart()) { + return 0; + } else if (lhs.getStart() < rhs.getStart()) { + return -1; + } else { + return 1; + } + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java new file mode 100644 index 000000000..ebdbfe2a5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/DownloadStatusComparator.java @@ -0,0 +1,15 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.service.download.DownloadStatus; + +import java.util.Comparator; + +/** Compares the completion date of two Downloadstatus objects. */ +public class DownloadStatusComparator implements Comparator<DownloadStatus> { + + @Override + public int compare(DownloadStatus lhs, DownloadStatus rhs) { + return rhs.getCompletionDate().compareTo(lhs.getCompletionDate()); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java new file mode 100644 index 000000000..a1f3ec699 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.Comparator; + +/** Compares the pubDate of two FeedItems for sorting*/ +public class FeedItemPubdateComparator implements Comparator<FeedItem> { + + /** Returns a new instance of this comparator in reverse order. + public static FeedItemPubdateComparator newInstance() { + FeedItemPubdateComparator + }*/ + @Override + public int compare(FeedItem lhs, FeedItem rhs) { + return rhs.getPubDate().compareTo(lhs.getPubDate()); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java new file mode 100644 index 000000000..84d244660 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/PlaybackCompletionDateComparator.java @@ -0,0 +1,19 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.FeedItem; + +import java.util.Comparator; + +public class PlaybackCompletionDateComparator implements Comparator<FeedItem> { + + public int compare(FeedItem lhs, FeedItem rhs) { + if (lhs.getMedia() != null + && lhs.getMedia().getPlaybackCompletionDate() != null + && rhs.getMedia() != null + && rhs.getMedia().getPlaybackCompletionDate() != null) { + return rhs.getMedia().getPlaybackCompletionDate() + .compareTo(lhs.getMedia().getPlaybackCompletionDate()); + } + return 0; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java new file mode 100644 index 000000000..b16e0949d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/SearchResultValueComparator.java @@ -0,0 +1,14 @@ +package de.danoeh.antennapod.core.util.comparator; + +import de.danoeh.antennapod.core.feed.SearchResult; + +import java.util.Comparator; + +public class SearchResultValueComparator implements Comparator<SearchResult> { + + @Override + public int compare(SearchResult lhs, SearchResult rhs) { + return rhs.getValue() - lhs.getValue(); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java b/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java new file mode 100644 index 000000000..287fe1100 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/exception/MediaFileNotFoundException.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.core.util.exception; + +import de.danoeh.antennapod.core.feed.FeedMedia; + +public class MediaFileNotFoundException extends Exception { + private static final long serialVersionUID = 1L; + + private FeedMedia media; + + public MediaFileNotFoundException(String msg, FeedMedia media) { + super(msg); + this.media = media; + } + + public MediaFileNotFoundException(FeedMedia media) { + super(); + this.media = media; + } + + public FeedMedia getMedia() { + return media; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java new file mode 100644 index 000000000..e4818214e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrServiceCreator.java @@ -0,0 +1,25 @@ +package de.danoeh.antennapod.core.util.flattr; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import org.shredzone.flattr4j.FlattrFactory; +import org.shredzone.flattr4j.FlattrService; +import org.shredzone.flattr4j.oauth.AccessToken; + +/** Ensures that only one instance of the FlattrService class exists at a time */ + +public class FlattrServiceCreator { + public static final String TAG = "FlattrServiceCreator"; + + private static volatile FlattrService flattrService; + + public static FlattrService getService(AccessToken token) { + return FlattrFactory.getInstance().createFlattrService(token); + } + + public static void deleteFlattrService() { + if (BuildConfig.DEBUG) Log.d(TAG, "Deleting service instance"); + flattrService = null; + } +} + diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java new file mode 100644 index 000000000..d82171d1a --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrStatus.java @@ -0,0 +1,68 @@ +package de.danoeh.antennapod.core.util.flattr; + +import java.util.Calendar; + +public class FlattrStatus { + public static final int STATUS_UNFLATTERED = 0; + public static final int STATUS_QUEUE = 1; + public static final int STATUS_FLATTRED = 2; + + private int status = STATUS_UNFLATTERED; + private Calendar lastFlattred; + + public FlattrStatus() { + status = STATUS_UNFLATTERED; + lastFlattred = Calendar.getInstance(); + } + + public FlattrStatus(long status) { + lastFlattred = Calendar.getInstance(); + fromLong(status); + } + + public void setFlattred() { + status = STATUS_FLATTRED; + lastFlattred = Calendar.getInstance(); + } + + public void setUnflattred() { + status = STATUS_UNFLATTERED; + } + + public boolean getUnflattred() { + return status == STATUS_UNFLATTERED; + } + + public void setFlattrQueue() { + if (flattrable()) + status = STATUS_QUEUE; + } + + public void fromLong(long status) { + if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) + this.status = (int) status; + else { + this.status = STATUS_FLATTRED; + lastFlattred.setTimeInMillis(status); + } + } + + public long toLong() { + if (status == STATUS_UNFLATTERED || status == STATUS_QUEUE) + return status; + else { + return lastFlattred.getTimeInMillis(); + } + } + + public boolean flattrable() { + Calendar firstOfMonth = Calendar.getInstance(); + firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH)); + + return (status == STATUS_UNFLATTERED) || (status == STATUS_FLATTRED && firstOfMonth.after(lastFlattred) ); + } + + public boolean getFlattrQueue() { + return status == STATUS_QUEUE; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java new file mode 100644 index 000000000..515028ab6 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrThing.java @@ -0,0 +1,7 @@ +package de.danoeh.antennapod.core.util.flattr; + +public interface FlattrThing { + public String getTitle(); + public String getPaymentLink(); + public FlattrStatus getFlattrStatus(); +} 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 new file mode 100644 index 000000000..90caad946 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/FlattrUtils.java @@ -0,0 +1,304 @@ +package de.danoeh.antennapod.core.util.flattr; + +import android.app.AlertDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.preference.PreferenceManager; +import android.util.Log; + +import org.apache.commons.lang3.StringUtils; +import org.shredzone.flattr4j.FlattrService; +import org.shredzone.flattr4j.exception.FlattrException; +import org.shredzone.flattr4j.model.Flattr; +import org.shredzone.flattr4j.model.Thing; +import org.shredzone.flattr4j.oauth.AccessToken; +import org.shredzone.flattr4j.oauth.AndroidAuthenticator; +import org.shredzone.flattr4j.oauth.Scope; + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.EnumSet; +import java.util.List; +import java.util.TimeZone; + +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.ClientConfig; +import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.asynctask.FlattrTokenFetcher; +import de.danoeh.antennapod.core.storage.DBWriter; + +/** + * Utility methods for doing something with flattr. + */ + +public class FlattrUtils { + private static final String TAG = "FlattrUtils"; + + private static final String HOST_NAME = "de.danoeh.antennapod"; + + private static final String 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() { + return new AndroidAuthenticator(HOST_NAME, ClientConfig.flattrCallbacks.getFlattrAppKey(), + ClientConfig.flattrCallbacks.getFlattrAppSecret()); + } + + public static void startAuthProcess(Context context) throws FlattrException { + AndroidAuthenticator auth = createAuthenticator(); + auth.setScope(EnumSet.of(Scope.FLATTR)); + Intent intent = auth.createAuthenticateIntent(); + context.startActivity(intent); + } + + private static AccessToken retrieveToken() { + if (cachedToken == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Retrieving access token"); + String token = PreferenceManager.getDefaultSharedPreferences( + ClientConfig.applicationCallbacks.getApplicationInstance()) + .getString(PREF_ACCESS_TOKEN, null); + if (token != null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Found access token. Caching."); + cachedToken = new AccessToken(token); + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "No access token found"); + return null; + } + } + return cachedToken; + + } + + /** + * 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()); + } + + public static boolean hasToken() { + return retrieveToken() != null; + } + + public static void storeToken(AccessToken token) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Storing token"); + SharedPreferences.Editor editor = PreferenceManager + .getDefaultSharedPreferences(ClientConfig.applicationCallbacks.getApplicationInstance()).edit(); + if (token != null) { + editor.putString(PREF_ACCESS_TOKEN, token.getToken()); + } else { + editor.putString(PREF_ACCESS_TOKEN, null); + } + editor.commit(); + cachedToken = token; + } + + public static void deleteToken() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Deleting flattr token"); + 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()) { + FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); + fs.flattr(url); + } else { + Log.e(TAG, "clickUrl was called with null access token"); + } + } + + public static List<Flattr> retrieveFlattredThings() + throws FlattrException { + ArrayList<Flattr> myFlattrs = new ArrayList<Flattr>(); + + if (hasToken()) { + FlattrService fs = FlattrServiceCreator.getService(retrieveToken()); + + Calendar firstOfMonth = Calendar.getInstance(TimeZone.getTimeZone("UTC")); + firstOfMonth.set(Calendar.MILLISECOND, 0); + firstOfMonth.set(Calendar.SECOND, 0); + firstOfMonth.set(Calendar.MINUTE, 0); + firstOfMonth.set(Calendar.HOUR_OF_DAY, 0); + firstOfMonth.set(Calendar.DAY_OF_MONTH, Calendar.getInstance().getActualMinimum(Calendar.DAY_OF_MONTH)); + + Date firstOfMonthDate = firstOfMonth.getTime(); + + // subscriptions some times get flattrd slightly before midnight - give it an hour leeway + firstOfMonthDate = new Date(firstOfMonthDate.getTime() - 60 * 60 * 1000); + + final int FLATTR_COUNT = 30; + final int FLATTR_MAXPAGE = 5; + + for (int page = 0; page < FLATTR_MAXPAGE; page++) { + for (Flattr fl : fs.getMyFlattrs(FLATTR_COUNT, page)) { + if (fl.getCreated().after(firstOfMonthDate)) + myFlattrs.add(fl); + else + break; + } + } + + if (BuildConfig.DEBUG) { + Log.d(TAG, "Got my flattrs list of length " + Integer.toString(myFlattrs.size()) + " comparison date" + firstOfMonthDate); + + for (Flattr fl : myFlattrs) { + Thing thing = fl.getThing(); + Log.d(TAG, "Flattr thing: " + fl.getThingId() + " name: " + thing.getTitle() + " url: " + thing.getUrl() + " on: " + fl.getCreated()); + } + } + + } else { + Log.e(TAG, "retrieveFlattrdThings was called with null access token"); + } + + return myFlattrs; + } + + public static void handleCallback(Context context, Uri uri) { + AndroidAuthenticator auth = createAuthenticator(); + new FlattrTokenFetcher(context, auth, uri).executeAsync(); + } + + public static void revokeAccessToken(Context context) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Revoking access token"); + deleteToken(); + FlattrServiceCreator.deleteFlattrService(); + showRevokeDialog(context); + DBWriter.clearAllFlattrStatus(context); + } + + // ------------------------------------------------ DIALOGS + + public static void showRevokeDialog(final Context context) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.access_revoked_title); + builder.setMessage(R.string.access_revoked_info); + builder.setNeutralButton(android.R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.create().show(); + } + + /** + * Opens a dialog that ask the user to either connect the app with flattr or to be redirected to + * the thing's website. + * If no API credentials are available, the user will immediately be redirected to the thing's website. + */ + public static void showNoTokenDialogOrRedirect(final Context context, final String url) { + if (hasAPICredentials()) { + AlertDialog.Builder builder = new AlertDialog.Builder(context); + builder.setTitle(R.string.no_flattr_token_title); + builder.setMessage(R.string.no_flattr_token_msg); + builder.setPositiveButton(R.string.authenticate_now_label, + new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + context.startActivity( + 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(); + } else { + Uri uri = Uri.parse(url); + context.startActivity(new Intent(Intent.ACTION_VIEW, uri)); + } + } + + 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); + builder.setMessage(msg); + builder.setNeutralButton(android.R.string.ok, new OnClickListener() { + + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + builder.create().show(); + } + +}
\ No newline at end of file diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java new file mode 100644 index 000000000..2c178496e --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/flattr/SimpleFlattrThing.java @@ -0,0 +1,30 @@ +package de.danoeh.antennapod.core.util.flattr; + +/* SimpleFlattrThing is a trivial implementation of the FlattrThing interface */ +public class SimpleFlattrThing implements FlattrThing { + public SimpleFlattrThing(String title, String url, FlattrStatus status) + { + this.title = title; + this.url = url; + this.status = status; + } + + public String getTitle() + { + return this.title; + } + + public String getPaymentLink() + { + return this.url; + } + + public FlattrStatus getFlattrStatus() + { + return this.status; + } + + private String title; + private String url; + private FlattrStatus status; +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java new file mode 100644 index 000000000..17581d3e9 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/FeedItemUndoToken.java @@ -0,0 +1,55 @@ +package de.danoeh.antennapod.core.util.gui; + +import android.os.Parcel; +import android.os.Parcelable; +import de.danoeh.antennapod.core.feed.FeedItem; + +/** + * Used by an UndoBarController for saving a removed FeedItem + */ +public class FeedItemUndoToken implements Parcelable { + private long itemId; + private long feedId; + private int position; + + public FeedItemUndoToken(FeedItem item, int position) { + this.itemId = item.getId(); + this.feedId = item.getFeed().getId(); + this.position = position; + } + + private FeedItemUndoToken(Parcel in) { + itemId = in.readLong(); + feedId = in.readLong(); + position = in.readInt(); + } + + public static final Parcelable.Creator<FeedItemUndoToken> CREATOR = new Parcelable.Creator<FeedItemUndoToken>() { + public FeedItemUndoToken createFromParcel(Parcel in) { + return new FeedItemUndoToken(in); + } + + public FeedItemUndoToken[] newArray(int size) { + return new FeedItemUndoToken[size]; + } + }; + + public int describeContents() { + return 0; + } + + public void writeToParcel(Parcel out, int flags) { + out.writeLong(itemId); + out.writeLong(feedId); + out.writeInt(position); + } + + public long getFeedItemId() { + return itemId; + } + + public int getPosition() { + return position; + } +} + diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/MoreContentListFooterUtil.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/MoreContentListFooterUtil.java new file mode 100644 index 000000000..d56871fd1 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/MoreContentListFooterUtil.java @@ -0,0 +1,56 @@ +package de.danoeh.antennapod.core.util.gui; + +import android.view.View; +import android.widget.ImageView; +import android.widget.ProgressBar; + +import de.danoeh.antennapod.core.R; + +/** + * Utility methods for the more_content_list_footer layout. + */ +public class MoreContentListFooterUtil { + + private final View root; + + private boolean loading; + + private Listener listener; + + public MoreContentListFooterUtil(View root) { + this.root = root; + root.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null && !loading) { + listener.onClick(); + } + } + }); + } + + public void setLoadingState(boolean newState) { + final ImageView imageView = (ImageView) root.findViewById(R.id.imgExpand); + final ProgressBar progressBar = (ProgressBar) root.findViewById(R.id.progBar); + if (newState) { + imageView.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + } else { + imageView.setVisibility(View.VISIBLE); + progressBar.setVisibility(View.GONE); + } + loading = newState; + } + + public void setClickListener(Listener l) { + listener = l; + } + + public static interface Listener { + public void onClick(); + } + + public View getRoot() { + return root; + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java new file mode 100644 index 000000000..9f3c4c6d5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ChapterReader.java @@ -0,0 +1,118 @@ +package de.danoeh.antennapod.core.util.id3reader; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.ID3Chapter; +import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; +import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.List; + +public class ChapterReader extends ID3Reader { + private static final String TAG = "ID3ChapterReader"; + + private static final String FRAME_ID_CHAPTER = "CHAP"; + private static final String FRAME_ID_TITLE = "TIT2"; + private static final String FRAME_ID_LINK = "WXXX"; + + private List<Chapter> chapters; + private ID3Chapter currentChapter; + + @Override + public int onStartTagHeader(TagHeader header) { + chapters = new ArrayList<Chapter>(); + System.out.println(header.toString()); + return ID3Reader.ACTION_DONT_SKIP; + } + + @Override + public int onStartFrameHeader(FrameHeader header, InputStream input) + throws IOException, ID3ReaderException { + System.out.println(header.toString()); + if (header.getId().equals(FRAME_ID_CHAPTER)) { + if (currentChapter != null) { + if (!hasId3Chapter(currentChapter)) { + chapters.add(currentChapter); + if (BuildConfig.DEBUG) Log.d(TAG, "Found chapter: " + currentChapter); + currentChapter = null; + } + } + StringBuffer elementId = new StringBuffer(); + readISOString(elementId, input, Integer.MAX_VALUE); + char[] startTimeSource = readBytes(input, 4); + long startTime = ((int) startTimeSource[0] << 24) + | ((int) startTimeSource[1] << 16) + | ((int) startTimeSource[2] << 8) | startTimeSource[3]; + currentChapter = new ID3Chapter(elementId.toString(), startTime); + skipBytes(input, 12); + return ID3Reader.ACTION_DONT_SKIP; + } else if (header.getId().equals(FRAME_ID_TITLE)) { + if (currentChapter != null && currentChapter.getTitle() == null) { + StringBuffer title = new StringBuffer(); + readString(title, input, header.getSize()); + currentChapter + .setTitle(title.toString()); + if (BuildConfig.DEBUG) Log.d(TAG, "Found title: " + currentChapter.getTitle()); + + return ID3Reader.ACTION_DONT_SKIP; + } + } else if (header.getId().equals(FRAME_ID_LINK)) { + if (currentChapter != null) { + // skip description + int descriptionLength = readString(null, input, header.getSize()); + StringBuffer link = new StringBuffer(); + readISOString(link, input, header.getSize() - descriptionLength); + String decodedLink = URLDecoder.decode(link.toString(), "UTF-8"); + + currentChapter.setLink(decodedLink); + + if (BuildConfig.DEBUG) Log.d(TAG, "Found link: " + currentChapter.getLink()); + return ID3Reader.ACTION_DONT_SKIP; + } + } else if (header.getId().equals("APIC")) { + Log.d(TAG, header.toString()); + } + + return super.onStartFrameHeader(header, input); + } + + private boolean hasId3Chapter(ID3Chapter chapter) { + for (Chapter c : chapters) { + if (((ID3Chapter) c).getId3ID().equals(chapter.getId3ID())) { + return true; + } + } + return false; + } + + @Override + public void onEndTag() { + if (currentChapter != null) { + if (!hasId3Chapter(currentChapter)) { + chapters.add(currentChapter); + } + } + System.out.println("Reached end of tag"); + if (chapters != null) { + for (Chapter c : chapters) { + System.out.println(c.toString()); + } + } + } + + @Override + public void onNoTagHeaderFound() { + System.out.println("No tag header found"); + super.onNoTagHeaderFound(); + } + + public List<Chapter> getChapters() { + return chapters; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java new file mode 100644 index 000000000..a238c11e9 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3Reader.java @@ -0,0 +1,250 @@ +package de.danoeh.antennapod.core.util.id3reader; + +import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader; +import de.danoeh.antennapod.core.util.id3reader.model.TagHeader; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +/** + * Reads the ID3 Tag of a given file. In order to use this class, you should + * create a subclass of it and overwrite the onStart* - or onEnd* - methods. + */ +public class ID3Reader { + private static final int HEADER_LENGTH = 10; + private static final int ID3_LENGTH = 3; + private static final int FRAME_ID_LENGTH = 4; + + protected static final int ACTION_SKIP = 1; + protected static final int ACTION_DONT_SKIP = 2; + + protected int readerPosition; + + private static final byte ENCODING_UTF16_WITH_BOM = 1; + private static final byte ENCODING_UTF16_WITHOUT_BOM = 2; + private static final byte ENCODING_UTF8 = 3; + + private TagHeader tagHeader; + + public ID3Reader() { + } + + public final void readInputStream(InputStream input) throws IOException, + ID3ReaderException { + int rc; + readerPosition = 0; + char[] tagHeaderSource = readBytes(input, HEADER_LENGTH); + tagHeader = createTagHeader(tagHeaderSource); + if (tagHeader == null) { + onNoTagHeaderFound(); + } else { + rc = onStartTagHeader(tagHeader); + if (rc == ACTION_SKIP) { + onEndTag(); + } else { + while (readerPosition < tagHeader.getSize()) { + FrameHeader frameHeader = createFrameHeader(readBytes( + input, HEADER_LENGTH)); + if (checkForNullString(frameHeader.getId())) { + break; + } else { + rc = onStartFrameHeader(frameHeader, input); + if (rc == ACTION_SKIP) { + + if (frameHeader.getSize() + readerPosition > tagHeader + .getSize()) { + break; + } else { + skipBytes(input, frameHeader.getSize()); + } + } + } + } + onEndTag(); + } + } + } + + /** Returns true if string only contains null-bytes. */ + private boolean checkForNullString(String s) { + if (!s.isEmpty()) { + int i = 0; + if (s.charAt(i) == 0) { + for (i = 1; i < s.length(); i++) { + if (s.charAt(i) != 0) { + return false; + } + } + return true; + } + return false; + } else { + return true; + } + + } + + /** + * Read a certain number of bytes from the given input stream. This method + * changes the readerPosition-attribute. + */ + protected char[] readBytes(InputStream input, int number) + throws IOException, ID3ReaderException { + char[] header = new char[number]; + for (int i = 0; i < number; i++) { + int b = input.read(); + readerPosition++; + if (b != -1) { + header[i] = (char) b; + } else { + throw new ID3ReaderException("Unexpected end of stream"); + } + } + return header; + } + + /** + * Skip a certain number of bytes on the given input stream. This method + * changes the readerPosition-attribute. + */ + protected void skipBytes(InputStream input, int number) throws IOException { + if (number <= 0) { + number = 1; + } + IOUtils.skipFully(input, number); + + readerPosition += number; + } + + private TagHeader createTagHeader(char[] source) throws ID3ReaderException { + boolean hasTag = (source[0] == 0x49) && (source[1] == 0x44) + && (source[2] == 0x33); + if (source.length != HEADER_LENGTH) { + throw new ID3ReaderException("Length of header must be " + + HEADER_LENGTH); + } + if (hasTag) { + String id = new String(source, 0, ID3_LENGTH); + char version = (char) ((source[3] << 8) | source[4]); + byte flags = (byte) source[5]; + int size = (source[6] << 24) | (source[7] << 16) | (source[8] << 8) + | source[9]; + size = unsynchsafe(size); + return new TagHeader(id, size, version, flags); + } else { + return null; + } + } + + private FrameHeader createFrameHeader(char[] source) + throws ID3ReaderException { + if (source.length != HEADER_LENGTH) { + throw new ID3ReaderException("Length of header must be " + + HEADER_LENGTH); + } + String id = new String(source, 0, FRAME_ID_LENGTH); + + int size = (((int) source[4]) << 24) | (((int) source[5]) << 16) + | (((int) source[6]) << 8) | source[7]; + if (tagHeader != null && tagHeader.getVersion() >= 0x0400) { + size = unsynchsafe(size); + } + char flags = (char) ((source[8] << 8) | source[9]); + return new FrameHeader(id, size, flags); + } + + private int unsynchsafe(int in) { + int out = 0; + int mask = 0x7F000000; + + while (mask != 0) { + out >>= 1; + out |= in & mask; + mask >>= 8; + } + + return out; + } + + protected int readString(StringBuffer buffer, InputStream input, int max) throws IOException, + ID3ReaderException { + if (max > 0) { + char[] encoding = readBytes(input, 1); + max--; + + if (encoding[0] == ENCODING_UTF16_WITH_BOM || encoding[0] == ENCODING_UTF16_WITHOUT_BOM) { + return readUnicodeString(buffer, input, max, Charset.forName("UTF-16")) + 1; // take encoding byte into account + } else if (encoding[0] == ENCODING_UTF8) { + return readUnicodeString(buffer, input, max, Charset.forName("UTF-8")) + 1; // take encoding byte into account + } else { + return readISOString(buffer, input, max) + 1; // take encoding byte into account + } + } else { + if (buffer != null) { + buffer.append(""); + } + return 0; + } + } + + protected int readISOString(StringBuffer buffer, InputStream input, int max) + throws IOException, ID3ReaderException { + + int bytesRead = 0; + char c; + while (++bytesRead <= max && (c = (char) input.read()) > 0) { + if (buffer != null) { + buffer.append(c); + } + } + return bytesRead; + } + + private int readUnicodeString(StringBuffer strBuffer, InputStream input, int max, Charset charset) + throws IOException, ID3ReaderException { + byte[] buffer = new byte[max]; + int c, cZero = -1; + int i = 0; + for (; i < max; i++) { + c = input.read(); + if (c == -1) { + break; + } else if (c == 0) { + if (cZero == 0) { + // termination character found + break; + } else { + cZero = 0; + } + } else { + buffer[i] = (byte) c; + cZero = -1; + } + } + if (strBuffer != null) { + strBuffer.append(charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString()); + } + return i; + } + + public int onStartTagHeader(TagHeader header) { + return ACTION_SKIP; + } + + public int onStartFrameHeader(FrameHeader header, InputStream input) + throws IOException, ID3ReaderException { + return ACTION_SKIP; + } + + public void onEndTag() { + + } + + public void onNoTagHeaderFound() { + + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java new file mode 100644 index 000000000..0c746d7e5 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/ID3ReaderException.java @@ -0,0 +1,20 @@ +package de.danoeh.antennapod.core.util.id3reader; + +public class ID3ReaderException extends Exception { + + public ID3ReaderException() { + } + + public ID3ReaderException(String arg0) { + super(arg0); + } + + public ID3ReaderException(Throwable arg0) { + super(arg0); + } + + public ID3ReaderException(String arg0, Throwable arg1) { + super(arg0, arg1); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java new file mode 100644 index 000000000..89eab1398 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/FrameHeader.java @@ -0,0 +1,17 @@ +package de.danoeh.antennapod.core.util.id3reader.model; + +public class FrameHeader extends Header { + + protected char flags; + + public FrameHeader(String id, int size, char flags) { + super(id, size); + this.flags = flags; + } + + @Override + public String toString() { + return String.format("FrameHeader [flags=%s, id=%s, size=%s]", Integer.toBinaryString(flags), id, Integer.toBinaryString(size)); + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java new file mode 100644 index 000000000..346e2893f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/Header.java @@ -0,0 +1,29 @@ +package de.danoeh.antennapod.core.util.id3reader.model; + +public abstract class Header { + + protected String id; + protected int size; + + public Header(String id, int size) { + super(); + this.id = id; + this.size = size; + } + + public String getId() { + return id; + } + + public int getSize() { + return size; + } + + @Override + public String toString() { + return "Header [id=" + id + ", size=" + size + "]"; + } + + + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java new file mode 100644 index 000000000..0a6b8357f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/id3reader/model/TagHeader.java @@ -0,0 +1,26 @@ +package de.danoeh.antennapod.core.util.id3reader.model; + +public class TagHeader extends Header { + + protected char version; + protected byte flags; + + public TagHeader(String id, int size, char version, byte flags) { + super(id, size); + this.version = version; + this.flags = flags; + } + + @Override + public String toString() { + return "TagHeader [version=" + version + ", flags=" + flags + ", id=" + + id + ", size=" + size + "]"; + } + + public char getVersion() { + return version; + } + + + +} 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 new file mode 100644 index 000000000..aafcea307 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java @@ -0,0 +1,34 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.util.Log; +import android.view.SurfaceHolder; +import com.aocate.media.MediaPlayer; + +public class AudioPlayer extends MediaPlayer implements IPlayer { + private static final String TAG = "AudioPlayer"; + + public AudioPlayer(Context context) { + super(context); + } + + @Override + 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 + public void setDisplay(SurfaceHolder sh) { + if (sh != null) { + Log.e(TAG, "Setting display not supported in Audio Player"); + throw new UnsupportedOperationException("Setting display not supported in Audio Player"); + } + } + + @Override + public void setVideoScalingMode(int mode) { + throw new UnsupportedOperationException("Setting scaling mode is not supported in Audio Player"); + } +} 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 new file mode 100644 index 000000000..49769f4f0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/ExternalMedia.java @@ -0,0 +1,235 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.SharedPreferences; +import android.content.SharedPreferences.Editor; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.MediaType; +import de.danoeh.antennapod.core.util.ChapterUtils; + +import java.util.List; +import java.util.concurrent.Callable; + +/** Represents a media file that is stored on the local storage device. */ +public class ExternalMedia implements Playable { + + public static final int PLAYABLE_TYPE_EXTERNAL_MEDIA = 2; + 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"; + + private String source; + + private String episodeTitle; + private String feedTitle; + private MediaType mediaType = MediaType.AUDIO; + private List<Chapter> chapters; + private int duration; + private int position; + + public ExternalMedia(String source, MediaType mediaType) { + super(); + this.source = source; + this.mediaType = mediaType; + } + + public ExternalMedia(String source, MediaType mediaType, int position) { + this(source, mediaType); + this.position = position; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(source); + dest.writeString(mediaType.toString()); + dest.writeInt(position); + } + + @Override + public void writeToPreferences(Editor prefEditor) { + prefEditor.putString(PREF_SOURCE_URL, source); + prefEditor.putString(PREF_MEDIA_TYPE, mediaType.toString()); + prefEditor.putInt(PREF_POSITION, position); + } + + @Override + public void loadMetadata() throws PlayableException { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + try { + mmr.setDataSource(source); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + throw new PlayableException( + "IllegalArgumentException when setting up MediaMetadataReceiver"); + } catch (RuntimeException e) { + // http://code.google.com/p/android/issues/detail?id=39770 + e.printStackTrace(); + throw new PlayableException( + "RuntimeException when setting up MediaMetadataRetriever"); + } + episodeTitle = mmr + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); + feedTitle = mmr + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM); + try { + duration = Integer.parseInt(mmr + .extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); + } catch (NumberFormatException e) { + e.printStackTrace(); + throw new PlayableException("NumberFormatException when reading duration of media file"); + } + ChapterUtils.loadChaptersFromFileUrl(this); + } + + @Override + public void loadChapterMarks() { + + } + + @Override + public String getEpisodeTitle() { + return episodeTitle; + } + + @Override + public Callable<String> loadShownotes() { + return new Callable<String>() { + @Override + public String call() throws Exception { + return ""; + } + }; + } + + @Override + public List<Chapter> getChapters() { + return chapters; + } + + @Override + public String getWebsiteLink() { + return null; + } + + @Override + public String getPaymentLink() { + return null; + } + + @Override + public String getFeedTitle() { + return feedTitle; + } + + @Override + public Object getIdentifier() { + return source; + } + + @Override + public int getDuration() { + return duration; + } + + @Override + public int getPosition() { + return position; + } + + @Override + public MediaType getMediaType() { + return mediaType; + } + + @Override + public String getLocalMediaUrl() { + return source; + } + + @Override + public String getStreamUrl() { + return null; + } + + @Override + public boolean localFileAvailable() { + return true; + } + + @Override + public boolean streamAvailable() { + return false; + } + + @Override + public void saveCurrentPosition(SharedPreferences pref, int newPosition) { + SharedPreferences.Editor editor = pref.edit(); + editor.putInt(PREF_POSITION, newPosition); + position = newPosition; + editor.commit(); + } + + @Override + public void setPosition(int newPosition) { + position = newPosition; + } + + @Override + public void setDuration(int newDuration) { + duration = newDuration; + } + + @Override + public void onPlaybackStart() { + + } + + @Override + public void onPlaybackCompleted() { + + } + + @Override + public int getPlayableType() { + return PLAYABLE_TYPE_EXTERNAL_MEDIA; + } + + @Override + public void setChapters(List<Chapter> chapters) { + this.chapters = chapters; + } + + public static final Parcelable.Creator<ExternalMedia> CREATOR = new Parcelable.Creator<ExternalMedia>() { + public ExternalMedia createFromParcel(Parcel in) { + String source = in.readString(); + MediaType type = MediaType.valueOf(in.readString()); + int position = 0; + if (in.dataAvail() > 0) { + position = in.readInt(); + } + ExternalMedia extMedia = new ExternalMedia(source, type, position); + return extMedia; + } + + public ExternalMedia[] newArray(int size) { + return new ExternalMedia[size]; + } + }; + + @Override + public Uri getImageUri() { + if (localFileAvailable()) { + return new Uri.Builder().scheme(SCHEME_MEDIA).encodedPath(getLocalMediaUrl()).build(); + } else { + return null; + } + } +} 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 new file mode 100644 index 000000000..147c7848d --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java @@ -0,0 +1,69 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.view.SurfaceHolder; + +import java.io.IOException; + +public interface IPlayer { + boolean canSetPitch(); + + boolean canSetSpeed(); + + float getCurrentPitchStepsAdjustment(); + + int getCurrentPosition(); + + float getCurrentSpeedMultiplier(); + + int getDuration(); + + float getMaxSpeedMultiplier(); + + float getMinSpeedMultiplier(); + + boolean isLooping(); + + boolean isPlaying(); + + void pause(); + + void prepare() throws IllegalStateException, IOException; + + void prepareAsync(); + + void release(); + + void reset(); + + void seekTo(int msec); + + void setAudioStreamType(int streamtype); + + void setScreenOnWhilePlaying(boolean screenOn); + + void setDataSource(String path) throws IllegalStateException, IOException, + IllegalArgumentException, SecurityException; + + void setDisplay(SurfaceHolder sh); + + void setEnableSpeedAdjustment(boolean enableSpeedAdjustment); + + void setLooping(boolean looping); + + void setPitchStepsAdjustment(float pitchSteps); + + void setPlaybackPitch(float f); + + void setPlaybackSpeed(float f); + + void setVolume(float left, float right); + + void start(); + + void stop(); + + public void setVideoScalingMode(int mode); + + public void setWakeMode(Context context, int mode); +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java new file mode 100644 index 000000000..0650225f0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/MediaPlayerError.java @@ -0,0 +1,23 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.media.MediaPlayer; +import de.danoeh.antennapod.core.R; + +/** Utility class for MediaPlayer errors. */ +public class MediaPlayerError { + + /** Get a human-readable string for a specific error code. */ + public static String getErrorString(Context context, int code) { + int resId; + switch(code) { + case MediaPlayer.MEDIA_ERROR_SERVER_DIED: + resId = R.string.playback_error_server_died; + break; + default: + resId = R.string.playback_error_unknown; + break; + } + return context.getString(resId); + } +} diff --git a/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 new file mode 100644 index 000000000..7ebd580f7 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Playable.java @@ -0,0 +1,207 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Parcelable; +import android.util.Log; + +import java.util.List; + +import de.danoeh.antennapod.core.asynctask.PicassoImageResource; +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.storage.DBReader; +import de.danoeh.antennapod.core.util.ShownotesProvider; + +/** + * Interface for objects that can be played by the PlaybackService. + */ +public interface Playable extends Parcelable, + ShownotesProvider, PicassoImageResource { + + /** + * Save information about the playable in a preference so that it can be + * restored later via PlayableUtils.createInstanceFromPreferences. + * Implementations must NOT call commit() after they have written the values + * to the preferences file. + */ + public void writeToPreferences(SharedPreferences.Editor prefEditor); + + /** + * This method is called from a separate thread by the PlaybackService. + * Playable objects should load their metadata in this method. This method + * should execute as quickly as possible and NOT load chapter marks if no + * local file is available. + */ + public void loadMetadata() throws PlayableException; + + /** + * This method is called from a separate thread by the PlaybackService. + * Playable objects should load their chapter marks in this method if no + * local file was available when loadMetadata() was called. + */ + public void loadChapterMarks(); + + /** + * Returns the title of the episode that this playable represents + */ + public String getEpisodeTitle(); + + /** + * Returns a list of chapter marks or null if this Playable has no chapters. + */ + public List<Chapter> getChapters(); + + /** + * Returns a link to a website that is meant to be shown in a browser + */ + public String getWebsiteLink(); + + public String getPaymentLink(); + + /** + * Returns the title of the feed this Playable belongs to. + */ + public String getFeedTitle(); + + /** + * Returns a unique identifier, for example a file url or an ID from a + * database. + */ + public Object getIdentifier(); + + /** + * Return duration of object or 0 if duration is unknown. + */ + public int getDuration(); + + /** + * Return position of object or 0 if position is unknown. + */ + public int getPosition(); + + /** + * Returns the type of media. This method should return the correct value + * BEFORE loadMetadata() is called. + */ + public MediaType getMediaType(); + + /** + * Returns an url to a local file that can be played or null if this file + * does not exist. + */ + public String getLocalMediaUrl(); + + /** + * Returns an url to a file that can be streamed by the player or null if + * this url is not known. + */ + public String getStreamUrl(); + + /** + * Returns true if a local file that can be played is available. getFileUrl + * MUST return a non-null string if this method returns true. + */ + public boolean localFileAvailable(); + + /** + * Returns true if a streamable file is available. getStreamUrl MUST return + * a non-null string if this method returns true. + */ + public boolean streamAvailable(); + + /** + * Saves the current position of this object. Implementations can use the + * provided SharedPreference to save this information and retrieve it later + * via PlayableUtils.createInstanceFromPreferences. + */ + public void saveCurrentPosition(SharedPreferences pref, int newPosition); + + public void setPosition(int newPosition); + + public void setDuration(int newDuration); + + /** + * Is called by the PlaybackService when playback starts. + */ + public void onPlaybackStart(); + + /** + * Is called by the PlaybackService when playback is completed. + */ + public void onPlaybackCompleted(); + + /** + * Returns an integer that must be unique among all Playable classes. The + * return value is later used by PlayableUtils to determine the type of the + * Playable object that is restored. + */ + public int getPlayableType(); + + public void setChapters(List<Chapter> chapters); + + /** + * Provides utility methods for Playable objects. + */ + public static class PlayableUtils { + private static final String TAG = "PlayableUtils"; + + /** + * Restores a playable object from a sharedPreferences file. This method might load data from the database, + * depending on the type of playable that was restored. + * + * @param type An integer that represents the type of the Playable object + * that is restored. + * @param pref The SharedPreferences file from which the Playable object + * is restored + * @return The restored Playable object + */ + public static Playable createInstanceFromPreferences(Context context, int type, + SharedPreferences pref) { + // ADD new Playable types here: + switch (type) { + case FeedMedia.PLAYABLE_TYPE_FEEDMEDIA: + long mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1); + if (mediaId != -1) { + return DBReader.getFeedMedia(context, mediaId); + } + break; + case ExternalMedia.PLAYABLE_TYPE_EXTERNAL_MEDIA: + String source = pref.getString(ExternalMedia.PREF_SOURCE_URL, + null); + String mediaType = pref.getString( + ExternalMedia.PREF_MEDIA_TYPE, null); + if (source != null && mediaType != null) { + int position = pref.getInt(ExternalMedia.PREF_POSITION, 0); + return new ExternalMedia(source, + MediaType.valueOf(mediaType), position); + } + break; + } + Log.e(TAG, "Could not restore Playable object from preferences"); + return null; + } + } + + public static class PlayableException extends Exception { + private static final long serialVersionUID = 1L; + + public PlayableException() { + super(); + } + + public PlayableException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public PlayableException(String detailMessage) { + super(detailMessage); + } + + public PlayableException(Throwable throwable) { + super(throwable); + } + + } +} 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 new file mode 100644 index 000000000..3b791f444 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -0,0 +1,790 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.app.Activity; +import android.content.*; +import android.content.res.TypedArray; +import android.media.MediaPlayer; +import android.os.AsyncTask; +import android.os.IBinder; +import android.preference.PreferenceManager; +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 de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.R; +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; +import de.danoeh.antennapod.core.storage.DBTasks; +import de.danoeh.antennapod.core.util.Converter; +import de.danoeh.antennapod.core.util.playback.Playable.PlayableUtils; + +import java.util.concurrent.*; + +/** + * Communicates with the playback service. GUI classes should use this class to + * 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; + + private final Activity activity; + + private PlaybackService playbackService; + private Playable media; + private PlayerStatus status; + + private ScheduledThreadPoolExecutor schedExecutor; + private static final int SCHED_EX_POOLSIZE = 1; + + protected MediaPositionObserver positionObserver; + protected ScheduledFuture positionObserverFuture; + + private boolean mediaInfoLoaded = false; + private boolean released = false; + + /** + * True if controller should reinit playback service if 'pause' button is + * pressed. + */ + private boolean reinitOnPause; + + public PlaybackController(Activity activity, boolean reinitOnPause) { + Validate.notNull(activity); + + this.activity = activity; + this.reinitOnPause = reinitOnPause; + schedExecutor = new ScheduledThreadPoolExecutor(SCHED_EX_POOLSIZE, + new ThreadFactory() { + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setPriority(Thread.MIN_PRIORITY); + return t; + } + }, new RejectedExecutionHandler() { + + @Override + public void rejectedExecution(Runnable r, + ThreadPoolExecutor executor) { + Log.w(TAG, + "Rejected execution of runnable in schedExecutor"); + } + } + ); + } + + /** + * Creates a new connection to the playbackService. Should be called in the + * activity's onResume() method. + */ + public void init() { + activity.registerReceiver(statusUpdate, new IntentFilter( + PlaybackService.ACTION_PLAYER_STATUS_CHANGED)); + + activity.registerReceiver(notificationReceiver, new IntentFilter( + PlaybackService.ACTION_PLAYER_NOTIFICATION)); + + activity.registerReceiver(shutdownReceiver, new IntentFilter( + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)); + + if (!released) { + bindToService(); + } else { + throw new IllegalStateException( + "Can't call init() after release() has been called"); + } + } + + /** + * Should be called if the PlaybackController is no longer needed, for + * example in the activity's onStop() method. + */ + public void release() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Releasing PlaybackController"); + + try { + activity.unregisterReceiver(statusUpdate); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unregisterReceiver(notificationReceiver); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unbindService(mConnection); + } catch (IllegalArgumentException e) { + // ignore + } + + try { + activity.unregisterReceiver(shutdownReceiver); + } catch (IllegalArgumentException e) { + // ignore + } + cancelPositionObserver(); + schedExecutor.shutdownNow(); + media = null; + released = true; + + } + + /** + * Should be called in the activity's onPause() method. + */ + public void pause() { + mediaInfoLoaded = false; + } + + /** + * Tries to establish a connection to the PlaybackService. If it isn't + * running, the PlaybackService will be started with the last played media + * as the arguments of the launch intent. + */ + private void bindToService() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Trying to connect to service"); + AsyncTask<Void, Void, Intent> intentLoader = new AsyncTask<Void, Void, Intent>() { + @Override + protected Intent doInBackground(Void... voids) { + return getPlayLastPlayedMediaIntent(); + } + + @Override + protected void onPostExecute(Intent serviceIntent) { + boolean bound = false; + if (!PlaybackService.started) { + if (serviceIntent != null) { + if (BuildConfig.DEBUG) Log.d(TAG, "Calling start service"); + activity.startService(serviceIntent); + bound = activity.bindService(serviceIntent, mConnection, 0); + } else { + status = PlayerStatus.STOPPED; + setupGUI(); + handleStatus(); + } + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, + "PlaybackService is running, trying to connect without start command."); + bound = activity.bindService(new Intent(activity, + PlaybackService.class), mConnection, 0); + } + if (BuildConfig.DEBUG) + Log.d(TAG, "Result for service binding: " + bound); + } + }; + intentLoader.execute(); + } + + /** + * Returns an intent that starts the PlaybackService and plays the last + * played media or null if no last played media could be found. + */ + private Intent getPlayLastPlayedMediaIntent() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Trying to restore last played media"); + SharedPreferences prefs = PreferenceManager + .getDefaultSharedPreferences(activity.getApplicationContext()); + long currentlyPlayingMedia = PlaybackPreferences + .getCurrentlyPlayingMedia(); + if (currentlyPlayingMedia != PlaybackPreferences.NO_MEDIA_PLAYING) { + Playable media = PlayableUtils.createInstanceFromPreferences(activity, + (int) currentlyPlayingMedia, prefs); + if (media != null) { + Intent serviceIntent = new Intent(activity, + PlaybackService.class); + serviceIntent.putExtra(PlaybackService.EXTRA_PLAYABLE, media); + serviceIntent.putExtra( + PlaybackService.EXTRA_START_WHEN_PREPARED, false); + serviceIntent.putExtra( + PlaybackService.EXTRA_PREPARE_IMMEDIATELY, false); + boolean fileExists = media.localFileAvailable(); + boolean lastIsStream = PlaybackPreferences + .getCurrentEpisodeIsStream(); + if (!fileExists && !lastIsStream && media instanceof FeedMedia) { + DBTasks.notifyMissingFeedMediaFile( + activity, (FeedMedia) media); + } + serviceIntent.putExtra(PlaybackService.EXTRA_SHOULD_STREAM, + lastIsStream || !fileExists); + return serviceIntent; + } + } + if (BuildConfig.DEBUG) + Log.d(TAG, "No last played media found"); + return null; + } + + public abstract void setupGUI(); + + private void setupPositionObserver() { + if ((positionObserverFuture != null && positionObserverFuture + .isCancelled()) + || (positionObserverFuture != null && positionObserverFuture + .isDone()) || positionObserverFuture == null) { + + if (BuildConfig.DEBUG) + Log.d(TAG, "Setting up position observer"); + positionObserver = new MediaPositionObserver(); + positionObserverFuture = schedExecutor.scheduleWithFixedDelay( + positionObserver, MediaPositionObserver.WAITING_INTERVALL, + MediaPositionObserver.WAITING_INTERVALL, + TimeUnit.MILLISECONDS); + } + } + + private void cancelPositionObserver() { + if (positionObserverFuture != null) { + boolean result = positionObserverFuture.cancel(true); + if (BuildConfig.DEBUG) + Log.d(TAG, "PositionObserver cancelled. Result: " + result); + } + } + + public abstract void onPositionObserverUpdate(); + + private ServiceConnection mConnection = new ServiceConnection() { + public void onServiceConnected(ComponentName className, IBinder service) { + playbackService = ((PlaybackService.LocalBinder) service) + .getService(); + if (!released) { + queryService(); + if (BuildConfig.DEBUG) + Log.d(TAG, "Connection to Service established"); + } else { + Log.i(TAG, "Connection to playback service has been established, but controller has already been released"); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + playbackService = null; + if (BuildConfig.DEBUG) + Log.d(TAG, "Disconnected from Service"); + + } + }; + + protected BroadcastReceiver statusUpdate = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (BuildConfig.DEBUG) + Log.d(TAG, "Received statusUpdate Intent."); + if (isConnectedToPlaybackService()) { + PlaybackServiceMediaPlayer.PSMPInfo info = playbackService.getPSMPInfo(); + status = info.playerStatus; + media = info.playable; + handleStatus(); + } else { + Log.w(TAG, + "Couldn't receive status update: playbackService was null"); + bindToService(); + } + } + }; + + protected BroadcastReceiver notificationReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (isConnectedToPlaybackService()) { + int type = intent.getIntExtra( + PlaybackService.EXTRA_NOTIFICATION_TYPE, -1); + int code = intent.getIntExtra( + PlaybackService.EXTRA_NOTIFICATION_CODE, -1); + if (code != -1 && type != -1) { + switch (type) { + case PlaybackService.NOTIFICATION_TYPE_ERROR: + handleError(code); + break; + case PlaybackService.NOTIFICATION_TYPE_BUFFER_UPDATE: + float progress = ((float) code) / 100; + onBufferUpdate(progress); + break; + case PlaybackService.NOTIFICATION_TYPE_RELOAD: + cancelPositionObserver(); + mediaInfoLoaded = false; + queryService(); + onReloadNotification(intent.getIntExtra( + PlaybackService.EXTRA_NOTIFICATION_CODE, -1)); + break; + case PlaybackService.NOTIFICATION_TYPE_SLEEPTIMER_UPDATE: + onSleepTimerUpdate(); + break; + case PlaybackService.NOTIFICATION_TYPE_BUFFER_START: + onBufferStart(); + break; + case PlaybackService.NOTIFICATION_TYPE_BUFFER_END: + onBufferEnd(); + break; + case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_END: + onPlaybackEnd(); + break; + case PlaybackService.NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE: + onPlaybackSpeedChange(); + break; + } + + } else { + if (BuildConfig.DEBUG) + Log.d(TAG, "Bad arguments. Won't handle intent"); + } + } else { + bindToService(); + } + } + + }; + + private BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (isConnectedToPlaybackService()) { + if (StringUtils.equals(intent.getAction(), + PlaybackService.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { + release(); + onShutdownNotification(); + } + } + } + }; + + public abstract void onPlaybackSpeedChange(); + + public abstract void onShutdownNotification(); + + /** + * Called when the currently displayed information should be refreshed. + */ + public abstract void onReloadNotification(int code); + + public abstract void onBufferStart(); + + public abstract void onBufferEnd(); + + public abstract void onBufferUpdate(float progress); + + public abstract void onSleepTimerUpdate(); + + public abstract void handleError(int code); + + public abstract void onPlaybackEnd(); + + public void repeatHandleStatus() { + if (status != null && playbackService != null) { + handleStatus(); + } + } + + /** + * Is called whenever the PlaybackService changes it's status. This method + * should be used to update the GUI or start/cancel background threads. + */ + private void handleStatus() { + final int playResource; + final int pauseResource; + final CharSequence playText = activity.getString(R.string.play_label); + final CharSequence pauseText = activity.getString(R.string.pause_label); + + if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO) { + TypedArray res = activity.obtainStyledAttributes(new int[]{ + R.attr.av_play_big, R.attr.av_pause_big}); + playResource = res.getResourceId(0, R.drawable.ic_play_arrow_grey600_36dp); + pauseResource = res.getResourceId(1, R.drawable.ic_pause_grey600_36dp); + res.recycle(); + } else { + playResource = R.drawable.ic_action_play_over_video; + pauseResource = R.drawable.ic_action_pause_over_video; + } + + switch (status) { + + case ERROR: + postStatusMsg(R.string.player_error_msg); + handleError(MediaPlayer.MEDIA_ERROR_UNKNOWN); + break; + case PAUSED: + clearStatusMsg(); + checkMediaInfoLoaded(); + cancelPositionObserver(); + updatePlayButtonAppearance(playResource, playText); + if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { + setScreenOn(false); + } + break; + case PLAYING: + clearStatusMsg(); + checkMediaInfoLoaded(); + if (PlaybackService.getCurrentMediaType() == MediaType.VIDEO) { + onAwaitingVideoSurface(); + setScreenOn(true); + } + setupPositionObserver(); + updatePlayButtonAppearance(pauseResource, pauseText); + break; + case PREPARING: + postStatusMsg(R.string.player_preparing_msg); + checkMediaInfoLoaded(); + if (playbackService != null) { + if (playbackService.isStartWhenPrepared()) { + updatePlayButtonAppearance(pauseResource, pauseText); + } else { + updatePlayButtonAppearance(playResource, playText); + } + } + break; + case STOPPED: + postStatusMsg(R.string.player_stopped_msg); + break; + case PREPARED: + checkMediaInfoLoaded(); + postStatusMsg(R.string.player_ready_msg); + updatePlayButtonAppearance(playResource, playText); + break; + case SEEKING: + postStatusMsg(R.string.player_seeking_msg); + break; + case INITIALIZED: + checkMediaInfoLoaded(); + clearStatusMsg(); + updatePlayButtonAppearance(playResource, playText); + break; + } + } + + private void checkMediaInfoLoaded() { + mediaInfoLoaded = (mediaInfoLoaded || loadMediaInfo()); + } + + private void updatePlayButtonAppearance(int resource, CharSequence contentDescription) { + ImageButton butPlay = getPlayButton(); + butPlay.setImageResource(resource); + butPlay.setContentDescription(contentDescription); + } + + public abstract ImageButton getPlayButton(); + + public abstract void postStatusMsg(int msg); + + public abstract void clearStatusMsg(); + + public abstract boolean loadMediaInfo(); + + public abstract void onAwaitingVideoSurface(); + + /** + * Called when connection to playback service has been established or + * information has to be refreshed + */ + void queryService() { + if (BuildConfig.DEBUG) + Log.d(TAG, "Querying service info"); + if (playbackService != null) { + status = playbackService.getStatus(); + media = playbackService.getPlayable(); + /* + if (media == null) { + Log.w(TAG, + "PlaybackService has no media object. Trying to restore last played media."); + Intent serviceIntent = getPlayLastPlayedMediaIntent(); + if (serviceIntent != null) { + activity.startService(serviceIntent); + } + } + */ + onServiceQueried(); + + setupGUI(); + handleStatus(); + // make sure that new media is loaded if it's available + mediaInfoLoaded = false; + + } else { + Log.e(TAG, + "queryService() was called without an existing connection to playbackservice"); + } + } + + public abstract void onServiceQueried(); + + /** + * Should be used by classes which implement the OnSeekBarChanged interface. + */ + public float onSeekBarProgressChanged(SeekBar seekBar, int progress, + boolean fromUser, TextView txtvPosition) { + if (fromUser && playbackService != null && media != null) { + float prog = progress / ((float) seekBar.getMax()); + int duration = media.getDuration(); + txtvPosition.setText(Converter + .getDurationStringLong((int) (prog * duration))); + return prog; + } + return 0; + + } + + /** + * Should be used by classes which implement the OnSeekBarChanged interface. + */ + public void onSeekBarStartTrackingTouch(SeekBar seekBar) { + // interrupt position Observer, restart later + cancelPositionObserver(); + } + + /** + * Should be used by classes which implement the OnSeekBarChanged interface. + */ + public void onSeekBarStopTrackingTouch(SeekBar seekBar, float prog) { + if (playbackService != null) { + playbackService.seekTo((int) (prog * media.getDuration())); + setupPositionObserver(); + } + } + + /** + * Should be implemented by classes that show a video. The default implementation + * does nothing + * + * @param enable True if the screen should be kept on, false otherwise + */ + protected void setScreenOn(boolean enable) { + + } + + 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; + } + } else { + Log.w(TAG, + "Play/Pause button was pressed, but playbackservice was null!"); + } + } + + }; + } + + public OnClickListener newOnRevButtonClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (status == PlayerStatus.PLAYING) { + playbackService.seekDelta(-UserPreferences.getSeekDeltaMs()); + } + } + }; + } + + public OnClickListener newOnFFButtonClickListener() { + return new OnClickListener() { + @Override + public void onClick(View v) { + if (status == PlayerStatus.PLAYING) { + playbackService.seekDelta(UserPreferences.getSeekDeltaMs()); + } + } + }; + } + + public boolean serviceAvailable() { + return playbackService != null; + } + + public int getPosition() { + if (playbackService != null) { + return playbackService.getCurrentPosition(); + } else { + return PlaybackService.INVALID_TIME; + } + } + + public int getDuration() { + if (playbackService != null) { + return playbackService.getDuration(); + } else { + return PlaybackService.INVALID_TIME; + } + } + + public Playable getMedia() { + return media; + } + + public boolean sleepTimerActive() { + return playbackService != null && playbackService.sleepTimerActive(); + } + + public boolean sleepTimerNotActive() { + return playbackService != null && !playbackService.sleepTimerActive(); + } + + public void disableSleepTimer() { + if (playbackService != null) { + playbackService.disableSleepTimer(); + } + } + + public long getSleepTimerTimeLeft() { + if (playbackService != null) { + return playbackService.getSleepTimerTimeLeft(); + } else { + return INVALID_TIME; + } + } + + public void setSleepTimer(long time) { + if (playbackService != null) { + playbackService.setSleepTimer(time); + } + } + + public void seekToChapter(Chapter chapter) { + if (playbackService != null) { + playbackService.seekToChapter(chapter); + } + } + + public void seekTo(int time) { + if (playbackService != null) { + playbackService.seekTo(time); + } + } + + public void setVideoSurface(SurfaceHolder holder) { + if (playbackService != null) { + playbackService.setVideoSurface(holder); + } + } + + public PlayerStatus getStatus() { + return status; + } + + public boolean canSetPlaybackSpeed() { + return playbackService != null && playbackService.canSetSpeed(); + } + + public void setPlaybackSpeed(float speed) { + if (playbackService != null) { + playbackService.setSpeed(speed); + } + } + + public float getCurrentPlaybackSpeedMultiplier() { + if (canSetPlaybackSpeed()) { + return playbackService.getCurrentPlaybackSpeed(); + } else { + return -1; + } + } + + public boolean isPlayingVideo() { + if (playbackService != null) { + return PlaybackService.getCurrentMediaType() == MediaType.VIDEO; + } + return false; + } + + public Pair<Integer, Integer> getVideoSize() { + if (playbackService != null) { + return playbackService.getVideoSize(); + } else { + return null; + } + } + + + /** + * Returns true if PlaybackController can communicate with the playback + * service. + */ + public boolean isConnectedToPlaybackService() { + return playbackService != null; + } + + public void notifyVideoSurfaceAbandoned() { + if (playbackService != null) { + playbackService.notifyVideoSurfaceAbandoned(); + } + } + + /** + * Move service into INITIALIZED state if it's paused to save bandwidth + */ + public void reinitServiceIfPaused() { + if (playbackService != null + && playbackService.isStreaming() + && (playbackService.getStatus() == PlayerStatus.PAUSED || (playbackService + .getStatus() == PlayerStatus.PREPARING && playbackService + .isStartWhenPrepared() == false))) { + playbackService.reinit(); + } + } + + /** + * Refreshes the current position of the media file that is playing. + */ + public class MediaPositionObserver implements Runnable { + + public static final int WAITING_INTERVALL = 1000; + + @Override + public void run() { + if (playbackService != null && playbackService.getStatus() == PlayerStatus.PLAYING) { + activity.runOnUiThread(new Runnable() { + + @Override + public void run() { + onPositionObserverUpdate(); + } + }); + } + } + } +} 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 new file mode 100644 index 000000000..443ff0ad1 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/Timeline.java @@ -0,0 +1,161 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.content.Context; +import android.content.res.TypedArray; +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; +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; + +/** + * Connects chapter information and shownotes of a shownotesProvider, for example by making it possible to use the + * shownotes to navigate to another position in the podcast or by highlighting certain parts of the shownotesProvider's + * shownotes. + * <p/> + * A timeline object needs a shownotesProvider from which the chapter information is retrieved and shownotes are generated. + */ +public class Timeline { + private static final String TAG = "Timeline"; + + private static final String WEBVIEW_STYLE = "@font-face { font-family: 'Roboto-Light'; src: url('file:///android_asset/Roboto-Light.ttf'); } * { color: %s; font-family: roboto-Light; font-size: 11pt; } a { font-style: normal; text-decoration: none; font-weight: normal; color: #00A8DF; } a.timecode { color: #669900; } img { display: block; margin: 10 auto; max-width: %s; height: auto; } body { margin: %dpx %dpx %dpx %dpx; }"; + + + private ShownotesProvider shownotesProvider; + + + private final String colorString; + private final int pageMargin; + + public Timeline(Context context, ShownotesProvider shownotesProvider) { + if (shownotesProvider == null) throw new IllegalArgumentException("shownotesProvider = null"); + this.shownotesProvider = shownotesProvider; + + TypedArray res = context + .getTheme() + .obtainStyledAttributes( + new int[]{android.R.attr.textColorPrimary}); + int colorResource = res.getColor(0, 0); + colorString = String.format("#%06X", + 0xFFFFFF & colorResource); + res.recycle(); + + pageMargin = (int) TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, 8, context.getResources() + .getDisplayMetrics() + ); + } + + 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"); + + /** + * Applies an app-specific CSS stylesheet and adds timecode links (optional). + * <p/> + * This method does NOT change the original shownotes string of the shownotesProvider object and it should + * also not be changed by the caller. + * + * @param addTimecodes True if this method should add timecode links + * @return The processed HTML string. + */ + public String processShownotes(final boolean addTimecodes) { + final Playable playable = (shownotesProvider instanceof Playable) ? (Playable) shownotesProvider : null; + + // load shownotes + + String shownotes; + try { + shownotes = shownotesProvider.loadShownotes().call(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + if (shownotes == null) { + if (BuildConfig.DEBUG) + Log.d(TAG, "shownotesProvider contained no shownotes. Returning empty string"); + return ""; + } + + Document document = Jsoup.parse(shownotes); + + // apply style + String styleStr = String.format(WEBVIEW_STYLE, colorString, "100%", pageMargin, + pageMargin, pageMargin, pageMargin); + document.head().appendElement("style").attr("type", "text/css").text(styleStr); + + // apply timecode links + if (addTimecodes) { + Elements elementsWithTimeCodes = document.body().getElementsMatchingOwnText(TIMECODE_REGEX); + if (BuildConfig.DEBUG) + Log.d(TAG, "Recognized " + elementsWithTimeCodes.size() + " timecodes"); + for (Element element : elementsWithTimeCodes) { + Matcher matcherLong = TIMECODE_REGEX.matcher(element.text()); + StringBuffer buffer = new StringBuffer(); + while (matcherLong.find()) { + String h = matcherLong.group(1); + String group = matcherLong.group(0); + int time = (h != null) ? Converter.durationStringLongToMs(group) : + Converter.durationStringShortToMs(group); + + String rep; + if (playable == null || playable.getDuration() > time) { + rep = String.format(TIMECODE_LINK, time, group); + } else { + rep = group; + } + matcherLong.appendReplacement(buffer, rep); + } + matcherLong.appendTail(buffer); + + element.html(buffer.toString()); + } + } + + Log.i(TAG, "Out: " + document.toString()); + return document.toString(); + } + + + /** + * Returns true if the given link is a timecode link. + */ + public static boolean isTimecodeLink(String link) { + return link != null && link.matches(TIMECODE_LINK_REGEX.pattern()); + } + + /** + * Returns the time in milliseconds that is attached to this link or -1 + * if the link is no valid timecode link. + */ + public static int getTimecodeLinkTime(String link) { + if (isTimecodeLink(link)) { + Matcher m = TIMECODE_LINK_REGEX.matcher(link); + + try { + if (m.find()) { + return Integer.valueOf(m.group(1)); + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + return -1; + } + + + public void setShownotesProvider(ShownotesProvider shownotesProvider) { + Validate.notNull(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 new file mode 100644 index 000000000..dc5270d8f --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java @@ -0,0 +1,67 @@ +package de.danoeh.antennapod.core.util.playback; + +import android.media.MediaPlayer; +import android.util.Log; + +public class VideoPlayer extends MediaPlayer implements IPlayer { + private static final String TAG = "VideoPlayer"; + + @Override + public boolean canSetPitch() { + return false; + } + + @Override + public boolean canSetSpeed() { + return false; + } + + @Override + public float getCurrentPitchStepsAdjustment() { + return 1; + } + + @Override + public float getCurrentSpeedMultiplier() { + return 1; + } + + @Override + public float getMaxSpeedMultiplier() { + return 1; + } + + @Override + public float getMinSpeedMultiplier() { + return 1; + } + + @Override + public void setEnableSpeedAdjustment(boolean enableSpeedAdjustment) throws UnsupportedOperationException { + Log.e(TAG, "Setting enable speed adjustment unsupported in video player"); + throw new UnsupportedOperationException("Setting enable speed adjustment unsupported in video player"); + } + + @Override + public void setPitchStepsAdjustment(float pitchSteps) { + Log.e(TAG, "Setting pitch steps adjustment unsupported in video player"); + throw new UnsupportedOperationException("Setting pitch steps adjustment unsupported in video player"); + } + + @Override + public void setPlaybackPitch(float f) { + Log.e(TAG, "Setting playback pitch unsupported in video player"); + throw new UnsupportedOperationException("Setting playback pitch unsupported in video player"); + } + + @Override + public void setPlaybackSpeed(float f) { + Log.e(TAG, "Setting playback speed unsupported in video player"); + throw new UnsupportedOperationException("Setting playback speed 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 new file mode 100644 index 000000000..9588265b8 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/syndication/FeedDiscoverer.java @@ -0,0 +1,78 @@ +package de.danoeh.antennapod.core.util.syndication; + +import android.net.Uri; +import org.apache.commons.lang3.StringUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.File; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Finds RSS/Atom URLs in a HTML document using the auto-discovery techniques described here: + * <p/> + * http://www.rssboard.org/rss-autodiscovery + * <p/> + * http://blog.whatwg.org/feed-autodiscovery + */ +public class FeedDiscoverer { + + private static final String MIME_RSS = "application/rss+xml"; + private static final String MIME_ATOM = "application/atom+xml"; + + /** + * Discovers links to RSS and Atom feeds in the given File which must be a HTML document. + * + * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if + * a title cannot be found). + */ + public Map<String, String> findLinks(File in, String baseUrl) throws IOException { + return findLinks(Jsoup.parse(in, null), baseUrl); + } + + /** + * Discovers links to RSS and Atom feeds in the given File which must be a HTML document. + * + * @return A map which contains the feed URLs as keys and titles as values (the feed URL is also used as a title if + * a title cannot be found). + */ + public Map<String, String> findLinks(String in, String baseUrl) throws IOException { + return findLinks(Jsoup.parse(in), baseUrl); + } + + private Map<String, String> findLinks(Document document, String baseUrl) { + Map<String, String> res = new LinkedHashMap<String, String>(); + Elements links = document.head().getElementsByTag("link"); + for (Element link : links) { + String rel = link.attr("rel"); + String href = link.attr("href"); + if (!StringUtils.isEmpty(href) && + (rel.equals("alternate") || rel.equals("feed"))) { + String type = link.attr("type"); + if (type.equals(MIME_RSS) || type.equals(MIME_ATOM)) { + String title = link.attr("title"); + String processedUrl = processURL(baseUrl, href); + if (processedUrl != null) { + res.put(processedUrl, + (StringUtils.isEmpty(title)) ? href : title); + } + } + } + } + return res; + } + + private String processURL(String baseUrl, String strUrl) { + Uri uri = Uri.parse(strUrl); + if (uri.isRelative()) { + Uri res = Uri.parse(baseUrl).buildUpon().path(strUrl).build(); + return (res != null) ? res.toString() : null; + } else { + return strUrl; + } + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java new file mode 100644 index 000000000..4799d3881 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/OggInputStream.java @@ -0,0 +1,81 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +public class OggInputStream extends InputStream { + private InputStream input; + + /** True if OggInputStream is currently inside an Ogg page. */ + private boolean isInPage; + private long bytesLeft; + + public OggInputStream(InputStream input) { + super(); + isInPage = false; + this.input = input; + } + + @Override + public int read() throws IOException { + if (!isInPage) { + readOggPage(); + } + + if (isInPage && bytesLeft > 0) { + int result = input.read(); + bytesLeft -= 1; + if (bytesLeft == 0) { + isInPage = false; + } + return result; + } + return -1; + } + + private void readOggPage() throws IOException { + // find OggS + int[] buffer = new int[4]; + int c = 0; + boolean isInOggS = false; + while ((c = input.read()) != -1) { + switch (c) { + case 'O': + isInOggS = true; + buffer[0] = c; + break; + case 'g': + if (buffer[1] != c) { + buffer[1] = c; + } else { + buffer[2] = c; + } + break; + case 'S': + buffer[3] = c; + break; + default: + if (isInOggS) { + Arrays.fill(buffer, 0); + isInOggS = false; + } + } + if (buffer[0] == 'O' && buffer[1] == 'g' && buffer[2] == 'g' + && buffer[3] == 'S') { + break; + } + } + // read segments + IOUtils.skipFully(input, 22); + bytesLeft = 0; + int numSegments = input.read(); + for (int i = 0; i < numSegments; i++) { + bytesLeft += input.read(); + } + isInPage = true; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java new file mode 100644 index 000000000..c4961a3ab --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentChapterReader.java @@ -0,0 +1,101 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; + +import android.util.Log; +import de.danoeh.antennapod.core.BuildConfig; +import de.danoeh.antennapod.core.feed.Chapter; +import de.danoeh.antennapod.core.feed.VorbisCommentChapter; + +import java.util.ArrayList; +import java.util.List; + +public class VorbisCommentChapterReader extends VorbisCommentReader { + private static final String TAG = "VorbisCommentChapterReader"; + + private static final String CHAPTER_KEY = "chapter\\d\\d\\d.*"; + private static final String CHAPTER_ATTRIBUTE_TITLE = "name"; + private static final String CHAPTER_ATTRIBUTE_LINK = "url"; + + private List<Chapter> chapters; + + public VorbisCommentChapterReader() { + } + + @Override + public void onVorbisCommentFound() { + System.out.println("Vorbis comment found"); + } + + @Override + public void onVorbisCommentHeaderFound(VorbisCommentHeader header) { + chapters = new ArrayList<Chapter>(); + System.out.println(header.toString()); + } + + @Override + public boolean onContentVectorKey(String content) { + return content.matches(CHAPTER_KEY); + } + + @Override + public void onContentVectorValue(String key, String value) + throws VorbisCommentReaderException { + if (BuildConfig.DEBUG) + Log.d(TAG, "Key: " + key + ", value: " + value); + String attribute = VorbisCommentChapter.getAttributeTypeFromKey(key); + int id = VorbisCommentChapter.getIDFromKey(key); + Chapter chapter = getChapterById(id); + if (attribute == null) { + if (getChapterById(id) == null) { + // new chapter + long start = VorbisCommentChapter.getStartTimeFromValue(value); + chapter = new VorbisCommentChapter(id); + chapter.setStart(start); + chapters.add(chapter); + } else { + throw new VorbisCommentReaderException( + "Found chapter with duplicate ID (" + key + ", " + + value + ")"); + } + } else if (attribute.equals(CHAPTER_ATTRIBUTE_TITLE)) { + if (chapter != null) { + chapter.setTitle(value); + } + } else if (attribute.equals(CHAPTER_ATTRIBUTE_LINK)) { + if (chapter != null) { + chapter.setLink(value); + } + } + } + + @Override + public void onNoVorbisCommentFound() { + System.out.println("No vorbis comment found"); + } + + @Override + public void onEndOfComment() { + System.out.println("End of comment"); + for (Chapter c : chapters) { + System.out.println(c.toString()); + } + } + + @Override + public void onError(VorbisCommentReaderException exception) { + exception.printStackTrace(); + } + + private Chapter getChapterById(long id) { + for (Chapter c : chapters) { + if (((VorbisCommentChapter) c).getVorbisCommentId() == id) { + return c; + } + } + return null; + } + + public List<Chapter> getChapters() { + return chapters; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java new file mode 100644 index 000000000..5f9dd0faf --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentHeader.java @@ -0,0 +1,26 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; +public class VorbisCommentHeader { + private String vendorString; + private long userCommentLength; + + public VorbisCommentHeader(String vendorString, long userCommentLength) { + super(); + this.vendorString = vendorString; + this.userCommentLength = userCommentLength; + } + + @Override + public String toString() { + return "VorbisCommentHeader [vendorString=" + vendorString + + ", userCommentLength=" + userCommentLength + "]"; + } + + public String getVendorString() { + return vendorString; + } + + public long getUserCommentLength() { + return userCommentLength; + } + +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java new file mode 100644 index 000000000..9639b9c42 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReader.java @@ -0,0 +1,194 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; + +import org.apache.commons.io.EndianUtils; +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; + + +public abstract class VorbisCommentReader { + /** Length of first page in an ogg file in bytes. */ + private static final int FIRST_PAGE_LENGTH = 58; + private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024; + private static final int PACKET_TYPE_IDENTIFICATION = 1; + private static final int PACKET_TYPE_COMMENT = 3; + + /** Called when Reader finds identification header. */ + public abstract void onVorbisCommentFound(); + + public abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header); + + /** + * Is called every time the Reader finds a content vector. The handler + * should return true if it wants to handle the content vector. + */ + public abstract boolean onContentVectorKey(String content); + + /** + * Is called if onContentVectorKey returned true for the key. + * + * @throws VorbisCommentReaderException + */ + public abstract void onContentVectorValue(String key, String value) + throws VorbisCommentReaderException; + + public abstract void onNoVorbisCommentFound(); + + public abstract void onEndOfComment(); + + public abstract void onError(VorbisCommentReaderException exception); + + public void readInputStream(InputStream input) + throws VorbisCommentReaderException { + try { + // look for identification header + if (findIdentificationHeader(input)) { + + onVorbisCommentFound(); + input = new OggInputStream(input); + if (findCommentHeader(input)) { + VorbisCommentHeader commentHeader = readCommentHeader(input); + if (commentHeader != null) { + onVorbisCommentHeaderFound(commentHeader); + for (int i = 0; i < commentHeader + .getUserCommentLength(); i++) { + try { + long vectorLength = EndianUtils + .readSwappedUnsignedInteger(input); + String key = readContentVectorKey(input, + vectorLength).toLowerCase(); + boolean readValue = onContentVectorKey(key); + if (readValue) { + String value = readUTF8String( + input, + (int) (vectorLength - key.length() - 1)); + onContentVectorValue(key, value); + } else { + IOUtils.skipFully(input, + vectorLength - key.length() - 1); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + onEndOfComment(); + } + + } else { + onError(new VorbisCommentReaderException( + "No comment header found")); + } + } else { + onNoVorbisCommentFound(); + } + } catch (IOException e) { + onError(new VorbisCommentReaderException(e)); + } + } + + private String readUTF8String(InputStream input, long length) + throws IOException { + byte[] buffer = new byte[(int) length]; + + IOUtils.readFully(input, buffer); + Charset charset = Charset.forName("UTF-8"); + return charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString(); + } + + /** + * Looks for an identification header in the first page of the file. If an + * identification header is found, it will be skipped completely and the + * method will return true, otherwise false. + * + * @throws IOException + */ + private boolean findIdentificationHeader(InputStream input) + throws IOException { + byte[] buffer = new byte[FIRST_PAGE_LENGTH]; + IOUtils.readFully(input, buffer); + int i; + for (i = 6; i < buffer.length; i++) { + if (buffer[i - 5] == 'v' && buffer[i - 4] == 'o' + && buffer[i - 3] == 'r' && buffer[i - 2] == 'b' + && buffer[i - 1] == 'i' && buffer[i] == 's' + && buffer[i - 6] == PACKET_TYPE_IDENTIFICATION) { + return true; + } + } + return false; + } + + private boolean findCommentHeader(InputStream input) throws IOException { + char[] buffer = new char["vorbis".length() + 1]; + for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) { + char c = (char) input.read(); + int dest = -1; + switch (c) { + case PACKET_TYPE_COMMENT: + dest = 0; + break; + case 'v': + dest = 1; + break; + case 'o': + dest = 2; + break; + case 'r': + dest = 3; + break; + case 'b': + dest = 4; + break; + case 'i': + dest = 5; + break; + case 's': + dest = 6; + break; + } + if (dest >= 0) { + buffer[dest] = c; + if (buffer[1] == 'v' && buffer[2] == 'o' && buffer[3] == 'r' + && buffer[4] == 'b' && buffer[5] == 'i' + && buffer[6] == 's' && buffer[0] == PACKET_TYPE_COMMENT) { + return true; + } + } else { + Arrays.fill(buffer, (char) 0); + } + } + return false; + } + + private VorbisCommentHeader readCommentHeader(InputStream input) + throws IOException, VorbisCommentReaderException { + try { + long vendorLength = EndianUtils.readSwappedUnsignedInteger(input); + String vendorName = readUTF8String(input, vendorLength); + long userCommentLength = EndianUtils + .readSwappedUnsignedInteger(input); + return new VorbisCommentHeader(vendorName, userCommentLength); + } catch (UnsupportedEncodingException e) { + throw new VorbisCommentReaderException(e); + } + } + + private String readContentVectorKey(InputStream input, long vectorLength) + throws IOException { + StringBuffer buffer = new StringBuffer(); + for (int i = 0; i < vectorLength; i++) { + char c = (char) input.read(); + if (c == '=') { + return buffer.toString(); + } else { + buffer.append(c); + } + } + return null; // no key found + } +} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java new file mode 100644 index 000000000..89ab20db0 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/util/vorbiscommentreader/VorbisCommentReaderException.java @@ -0,0 +1,24 @@ +package de.danoeh.antennapod.core.util.vorbiscommentreader; +public class VorbisCommentReaderException extends Exception { + + public VorbisCommentReaderException() { + super(); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(String arg0, Throwable arg1) { + super(arg0, arg1); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(String arg0) { + super(arg0); + // TODO Auto-generated constructor stub + } + + public VorbisCommentReaderException(Throwable arg0) { + super(arg0); + // TODO Auto-generated constructor stub + } + +} diff --git a/core/src/main/res/drawable-hdpi-v11/ic_stat_antenna_default.png b/core/src/main/res/drawable-hdpi-v11/ic_stat_antenna_default.png Binary files differnew file mode 100644 index 000000000..37d73c734 --- /dev/null +++ b/core/src/main/res/drawable-hdpi-v11/ic_stat_antenna_default.png diff --git a/core/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png b/core/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png Binary files differnew file mode 100755 index 000000000..ad148cc6b --- /dev/null +++ b/core/src/main/res/drawable-hdpi-v11/ic_stat_authentication.png diff --git a/core/src/main/res/drawable-hdpi-v11/stat_notify_sync.png b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync.png Binary files differnew file mode 100644 index 000000000..90b39c958 --- /dev/null +++ b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync.png diff --git a/core/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png Binary files differnew file mode 100644 index 000000000..074cdee27 --- /dev/null +++ b/core/src/main/res/drawable-hdpi-v11/stat_notify_sync_error.png diff --git a/core/src/main/res/drawable-hdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-hdpi/ic_action_pause_over_video.png Binary files differnew file mode 100755 index 000000000..64b07728f --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_action_pause_over_video.png diff --git a/core/src/main/res/drawable-hdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-hdpi/ic_action_play_over_video.png Binary files differnew file mode 100755 index 000000000..a364ca7c2 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_action_play_over_video.png diff --git a/core/src/main/res/drawable-hdpi/ic_add_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_add_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..492c8f880 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_add_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_add_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_add_white_24dp.png Binary files differnew file mode 100644 index 000000000..481643ecd --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_add_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_cancel_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_cancel_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..1f4fd985f --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_cancel_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_cancel_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_cancel_white_24dp.png Binary files differnew file mode 100644 index 000000000..794b7b780 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_cancel_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_delete_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_delete_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..b72a9f3c6 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_delete_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_delete_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_delete_white_24dp.png Binary files differnew file mode 100644 index 000000000..a9eac0ca7 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_delete_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_description_grey600_36dp.png b/core/src/main/res/drawable-hdpi/ic_description_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..dd7d0734d --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_description_grey600_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_description_white_36dp.png b/core/src/main/res/drawable-hdpi/ic_description_white_36dp.png Binary files differnew file mode 100644 index 000000000..93aa3032a --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_description_white_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_done_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_done_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..6e42e1359 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_done_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_done_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_done_white_24dp.png Binary files differnew file mode 100644 index 000000000..f42a0e2d2 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_done_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_drag_handle.png b/core/src/main/res/drawable-hdpi/ic_drag_handle.png Binary files differnew file mode 100755 index 000000000..38ec201de --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_drag_handle.png diff --git a/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png Binary files differnew file mode 100755 index 000000000..e96d23252 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_drag_handle_dark.png diff --git a/core/src/main/res/drawable-hdpi/ic_expand_more_grey600_36dp.png b/core/src/main/res/drawable-hdpi/ic_expand_more_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..0f0ddf717 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_expand_more_grey600_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_expand_more_white_36dp.png b/core/src/main/res/drawable-hdpi/ic_expand_more_white_36dp.png Binary files differnew file mode 100644 index 000000000..724eb1f7a --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_expand_more_white_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_fast_forward_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_fast_forward_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..4d1c1d4ae --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_fast_forward_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_fast_forward_grey600_36dp.png b/core/src/main/res/drawable-hdpi/ic_fast_forward_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..deeff701f --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_fast_forward_grey600_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_fast_forward_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_fast_forward_white_24dp.png Binary files differnew file mode 100644 index 000000000..03a54ee75 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_fast_forward_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_fast_forward_white_36dp.png b/core/src/main/res/drawable-hdpi/ic_fast_forward_white_36dp.png Binary files differnew file mode 100644 index 000000000..60e3decf6 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_fast_forward_white_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_fast_rewind_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_fast_rewind_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..9866ee629 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_fast_rewind_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_fast_rewind_grey600_36dp.png b/core/src/main/res/drawable-hdpi/ic_fast_rewind_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..8b9bfd34b --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_fast_rewind_grey600_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_fast_rewind_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_fast_rewind_white_24dp.png Binary files differnew file mode 100644 index 000000000..b4f526e62 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_fast_rewind_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_fast_rewind_white_36dp.png b/core/src/main/res/drawable-hdpi/ic_fast_rewind_white_36dp.png Binary files differnew file mode 100644 index 000000000..5d9d35f70 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_fast_rewind_white_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_file_download_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_file_download_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..c2222fb8e --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_file_download_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_file_download_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_file_download_white_24dp.png Binary files differnew file mode 100644 index 000000000..46aeab458 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_file_download_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_hearing_grey600_18dp.png b/core/src/main/res/drawable-hdpi/ic_hearing_grey600_18dp.png Binary files differnew file mode 100644 index 000000000..294523814 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_hearing_grey600_18dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_hearing_white_18dp.png b/core/src/main/res/drawable-hdpi/ic_hearing_white_18dp.png Binary files differnew file mode 100644 index 000000000..11114b568 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_hearing_white_18dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_history_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_history_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..f87403fa5 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_history_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_history_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_history_white_24dp.png Binary files differnew file mode 100644 index 000000000..b2b1332e1 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_history_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_info_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_info_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..713a9ba9a --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_info_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_info_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_info_white_24dp.png Binary files differnew file mode 100644 index 000000000..f4b88742d --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_info_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_launcher.png b/core/src/main/res/drawable-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..994b763cc --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_launcher.png diff --git a/core/src/main/res/drawable-hdpi/ic_list_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_list_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..2aee6f902 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_list_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_list_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_list_white_24dp.png Binary files differnew file mode 100644 index 000000000..436113d27 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_list_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_more_vert_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_more_vert_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..e141502a6 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_more_vert_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_more_vert_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_more_vert_white_24dp.png Binary files differnew file mode 100644 index 000000000..fdc4a5ad2 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_more_vert_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_new.png b/core/src/main/res/drawable-hdpi/ic_new.png Binary files differnew file mode 100755 index 000000000..8ff519052 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_new.png diff --git a/core/src/main/res/drawable-hdpi/ic_new_dark.png b/core/src/main/res/drawable-hdpi/ic_new_dark.png Binary files differnew file mode 100755 index 000000000..c8581e01c --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_new_dark.png diff --git a/core/src/main/res/drawable-hdpi/ic_new_releases_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_new_releases_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..7a08c4138 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_new_releases_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_new_releases_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_new_releases_white_24dp.png Binary files differnew file mode 100644 index 000000000..0d08f329c --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_new_releases_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_pause_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_pause_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..711e0cf94 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_pause_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_pause_grey600_36dp.png b/core/src/main/res/drawable-hdpi/ic_pause_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..5e884dfe8 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_pause_grey600_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_pause_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_pause_white_24dp.png Binary files differnew file mode 100644 index 000000000..b4bdbb558 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_pause_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_pause_white_36dp.png b/core/src/main/res/drawable-hdpi/ic_pause_white_36dp.png Binary files differnew file mode 100644 index 000000000..da17d12ba --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_pause_white_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_play_arrow_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_play_arrow_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..264416933 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_play_arrow_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_play_arrow_grey600_36dp.png b/core/src/main/res/drawable-hdpi/ic_play_arrow_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..8af4c6cd6 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_play_arrow_grey600_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png Binary files differnew file mode 100644 index 000000000..164385d04 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_play_arrow_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_play_arrow_white_36dp.png b/core/src/main/res/drawable-hdpi/ic_play_arrow_white_36dp.png Binary files differnew file mode 100644 index 000000000..0cd5cb61a --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_play_arrow_white_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..51cc4dbd1 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_refresh_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png Binary files differnew file mode 100644 index 000000000..cd16fdd50 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_refresh_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_remove_red_eye_grey600_18dp.png b/core/src/main/res/drawable-hdpi/ic_remove_red_eye_grey600_18dp.png Binary files differnew file mode 100644 index 000000000..c4df13afa --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_remove_red_eye_grey600_18dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_remove_red_eye_white_18dp.png b/core/src/main/res/drawable-hdpi/ic_remove_red_eye_white_18dp.png Binary files differnew file mode 100644 index 000000000..fdf524010 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_remove_red_eye_white_18dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_search_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_search_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..ba65a135b --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_search_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_search_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_search_white_24dp.png Binary files differnew file mode 100644 index 000000000..a2fc5b2e7 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_search_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_settings_input_antenna_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_settings_input_antenna_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..9c0172ad0 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_settings_input_antenna_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_settings_input_antenna_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_settings_input_antenna_white_24dp.png Binary files differnew file mode 100644 index 000000000..9b5a9fb47 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_settings_input_antenna_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_share_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_share_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..987e9b86c --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_share_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_share_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_share_white_24dp.png Binary files differnew file mode 100644 index 000000000..93b3c219c --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_share_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_stat_antenna_default.png b/core/src/main/res/drawable-hdpi/ic_stat_antenna_default.png Binary files differnew file mode 100644 index 000000000..36d502492 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_stat_antenna_default.png diff --git a/core/src/main/res/drawable-hdpi/ic_stat_authentication.png b/core/src/main/res/drawable-hdpi/ic_stat_authentication.png Binary files differnew file mode 100755 index 000000000..c6b5efd33 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_stat_authentication.png diff --git a/core/src/main/res/drawable-hdpi/ic_timer_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_timer_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..a5fdd2c40 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_timer_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_timer_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_timer_white_24dp.png Binary files differnew file mode 100644 index 000000000..864291fa7 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_timer_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_toc_grey600_36dp.png b/core/src/main/res/drawable-hdpi/ic_toc_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..e299eb41b --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_toc_grey600_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_toc_white_36dp.png b/core/src/main/res/drawable-hdpi/ic_toc_white_36dp.png Binary files differnew file mode 100644 index 000000000..bfdc05d9e --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_toc_white_36dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_web_grey600_24dp.png b/core/src/main/res/drawable-hdpi/ic_web_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..1942f3f12 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_web_grey600_24dp.png diff --git a/core/src/main/res/drawable-hdpi/ic_web_white_24dp.png b/core/src/main/res/drawable-hdpi/ic_web_white_24dp.png Binary files differnew file mode 100644 index 000000000..952404cbc --- /dev/null +++ b/core/src/main/res/drawable-hdpi/ic_web_white_24dp.png diff --git a/core/src/main/res/drawable-hdpi/navigation_up.png b/core/src/main/res/drawable-hdpi/navigation_up.png Binary files differnew file mode 100755 index 000000000..a2cf2ba52 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/navigation_up.png diff --git a/core/src/main/res/drawable-hdpi/navigation_up_dark.png b/core/src/main/res/drawable-hdpi/navigation_up_dark.png Binary files differnew file mode 100755 index 000000000..f2374a323 --- /dev/null +++ b/core/src/main/res/drawable-hdpi/navigation_up_dark.png diff --git a/core/src/main/res/drawable-hdpi/stat_notify_sync.png b/core/src/main/res/drawable-hdpi/stat_notify_sync.png Binary files differnew file mode 100644 index 000000000..bfb8110fe --- /dev/null +++ b/core/src/main/res/drawable-hdpi/stat_notify_sync.png diff --git a/core/src/main/res/drawable-hdpi/stat_notify_sync_error.png b/core/src/main/res/drawable-hdpi/stat_notify_sync_error.png Binary files differnew file mode 100644 index 000000000..b340a313e --- /dev/null +++ b/core/src/main/res/drawable-hdpi/stat_notify_sync_error.png diff --git a/core/src/main/res/drawable-ldpi-v11/ic_stat_antenna_default.png b/core/src/main/res/drawable-ldpi-v11/ic_stat_antenna_default.png Binary files differnew file mode 100644 index 000000000..e44f42510 --- /dev/null +++ b/core/src/main/res/drawable-ldpi-v11/ic_stat_antenna_default.png diff --git a/core/src/main/res/drawable-ldpi/ic_launcher.png b/core/src/main/res/drawable-ldpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..546090dd2 --- /dev/null +++ b/core/src/main/res/drawable-ldpi/ic_launcher.png diff --git a/core/src/main/res/drawable-ldpi/ic_stat_antenna_default.png b/core/src/main/res/drawable-ldpi/ic_stat_antenna_default.png Binary files differnew file mode 100644 index 000000000..63d72970d --- /dev/null +++ b/core/src/main/res/drawable-ldpi/ic_stat_antenna_default.png diff --git a/core/src/main/res/drawable-mdpi-v11/ic_stat_antenna_default.png b/core/src/main/res/drawable-mdpi-v11/ic_stat_antenna_default.png Binary files differnew file mode 100644 index 000000000..8808dedc7 --- /dev/null +++ b/core/src/main/res/drawable-mdpi-v11/ic_stat_antenna_default.png diff --git a/core/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png b/core/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png Binary files differnew file mode 100755 index 000000000..de69b17c0 --- /dev/null +++ b/core/src/main/res/drawable-mdpi-v11/ic_stat_authentication.png diff --git a/core/src/main/res/drawable-mdpi-v11/stat_notify_sync.png b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync.png Binary files differnew file mode 100644 index 000000000..1be8677f1 --- /dev/null +++ b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync.png diff --git a/core/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png Binary files differnew file mode 100644 index 000000000..30658c583 --- /dev/null +++ b/core/src/main/res/drawable-mdpi-v11/stat_notify_sync_error.png diff --git a/core/src/main/res/drawable-mdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-mdpi/ic_action_pause_over_video.png Binary files differnew file mode 100755 index 000000000..f478ac321 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_action_pause_over_video.png diff --git a/core/src/main/res/drawable-mdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-mdpi/ic_action_play_over_video.png Binary files differnew file mode 100755 index 000000000..835ff3636 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_action_play_over_video.png diff --git a/core/src/main/res/drawable-mdpi/ic_add_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_add_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..07858bc09 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_add_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_add_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_add_white_24dp.png Binary files differnew file mode 100644 index 000000000..977dd3427 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_add_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_cancel_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_cancel_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..a7a30cc24 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_cancel_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_cancel_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_cancel_white_24dp.png Binary files differnew file mode 100644 index 000000000..df332ef3c --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_cancel_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_delete_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_delete_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..e757fdb07 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_delete_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_delete_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_delete_white_24dp.png Binary files differnew file mode 100644 index 000000000..e4ea52ef2 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_delete_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_description_grey600_36dp.png b/core/src/main/res/drawable-mdpi/ic_description_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..ac18b5750 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_description_grey600_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_description_white_36dp.png b/core/src/main/res/drawable-mdpi/ic_description_white_36dp.png Binary files differnew file mode 100644 index 000000000..f5612717f --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_description_white_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_done_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_done_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..9f860915d --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_done_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_done_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_done_white_24dp.png Binary files differnew file mode 100644 index 000000000..e91f9048b --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_done_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_drag_handle.png b/core/src/main/res/drawable-mdpi/ic_drag_handle.png Binary files differnew file mode 100755 index 000000000..4afbdc67d --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_drag_handle.png diff --git a/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png Binary files differnew file mode 100755 index 000000000..2b25c4101 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_drag_handle_dark.png diff --git a/core/src/main/res/drawable-mdpi/ic_expand_more_grey600_36dp.png b/core/src/main/res/drawable-mdpi/ic_expand_more_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..00b9f645c --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_expand_more_grey600_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_expand_more_white_36dp.png b/core/src/main/res/drawable-mdpi/ic_expand_more_white_36dp.png Binary files differnew file mode 100644 index 000000000..6c2476660 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_expand_more_white_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_fast_forward_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_fast_forward_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..c2864a726 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_fast_forward_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_fast_forward_grey600_36dp.png b/core/src/main/res/drawable-mdpi/ic_fast_forward_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..4d1c1d4ae --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_fast_forward_grey600_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_fast_forward_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_fast_forward_white_24dp.png Binary files differnew file mode 100644 index 000000000..215099f9e --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_fast_forward_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_fast_forward_white_36dp.png b/core/src/main/res/drawable-mdpi/ic_fast_forward_white_36dp.png Binary files differnew file mode 100644 index 000000000..03a54ee75 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_fast_forward_white_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_fast_rewind_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_fast_rewind_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..75adfc97e --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_fast_rewind_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_fast_rewind_grey600_36dp.png b/core/src/main/res/drawable-mdpi/ic_fast_rewind_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..9866ee629 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_fast_rewind_grey600_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_fast_rewind_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_fast_rewind_white_24dp.png Binary files differnew file mode 100644 index 000000000..a050e1bb8 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_fast_rewind_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_fast_rewind_white_36dp.png b/core/src/main/res/drawable-mdpi/ic_fast_rewind_white_36dp.png Binary files differnew file mode 100644 index 000000000..b4f526e62 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_fast_rewind_white_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_file_download_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_file_download_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..b3ef8ce73 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_file_download_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_file_download_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_file_download_white_24dp.png Binary files differnew file mode 100644 index 000000000..e089466de --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_file_download_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_hearing_grey600_18dp.png b/core/src/main/res/drawable-mdpi/ic_hearing_grey600_18dp.png Binary files differnew file mode 100644 index 000000000..df26536f9 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_hearing_grey600_18dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_hearing_white_18dp.png b/core/src/main/res/drawable-mdpi/ic_hearing_white_18dp.png Binary files differnew file mode 100644 index 000000000..0771a106d --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_hearing_white_18dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_history_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_history_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..0a62d358f --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_history_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_history_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_history_white_24dp.png Binary files differnew file mode 100644 index 000000000..b93da1006 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_history_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_info_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_info_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..2537c5f42 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_info_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_info_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_info_white_24dp.png Binary files differnew file mode 100644 index 000000000..34f980085 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_info_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_launcher.png b/core/src/main/res/drawable-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..403dfabc4 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_launcher.png diff --git a/core/src/main/res/drawable-mdpi/ic_list_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_list_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..c5c2fc195 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_list_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_list_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_list_white_24dp.png Binary files differnew file mode 100644 index 000000000..5e51ed478 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_list_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_more_vert_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_more_vert_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..4ed34354b --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_more_vert_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_more_vert_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_more_vert_white_24dp.png Binary files differnew file mode 100644 index 000000000..1d8ad18a0 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_more_vert_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_new.png b/core/src/main/res/drawable-mdpi/ic_new.png Binary files differnew file mode 100755 index 000000000..84994bd10 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_new.png diff --git a/core/src/main/res/drawable-mdpi/ic_new_dark.png b/core/src/main/res/drawable-mdpi/ic_new_dark.png Binary files differnew file mode 100755 index 000000000..b723618b4 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_new_dark.png diff --git a/core/src/main/res/drawable-mdpi/ic_new_releases_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_new_releases_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..494781a20 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_new_releases_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_new_releases_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_new_releases_white_24dp.png Binary files differnew file mode 100644 index 000000000..36e3262a4 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_new_releases_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_pause_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_pause_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..06abcb50b --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_pause_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_pause_grey600_36dp.png b/core/src/main/res/drawable-mdpi/ic_pause_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..711e0cf94 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_pause_grey600_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_pause_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_pause_white_24dp.png Binary files differnew file mode 100644 index 000000000..026f38854 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_pause_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_pause_white_36dp.png b/core/src/main/res/drawable-mdpi/ic_pause_white_36dp.png Binary files differnew file mode 100644 index 000000000..b4bdbb558 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_pause_white_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_play_arrow_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_play_arrow_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..4ff5decb0 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_play_arrow_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_play_arrow_grey600_36dp.png b/core/src/main/res/drawable-mdpi/ic_play_arrow_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..264416933 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_play_arrow_grey600_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png Binary files differnew file mode 100644 index 000000000..8d1e433a5 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_play_arrow_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_play_arrow_white_36dp.png b/core/src/main/res/drawable-mdpi/ic_play_arrow_white_36dp.png Binary files differnew file mode 100644 index 000000000..164385d04 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_play_arrow_white_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..c136c59fc --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_refresh_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_refresh_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_refresh_white_24dp.png Binary files differnew file mode 100644 index 000000000..235c84f1e --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_refresh_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_remove_red_eye_grey600_18dp.png b/core/src/main/res/drawable-mdpi/ic_remove_red_eye_grey600_18dp.png Binary files differnew file mode 100644 index 000000000..9e840f03e --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_remove_red_eye_grey600_18dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_remove_red_eye_white_18dp.png b/core/src/main/res/drawable-mdpi/ic_remove_red_eye_white_18dp.png Binary files differnew file mode 100644 index 000000000..0bcba825f --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_remove_red_eye_white_18dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_search_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_search_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..29954062c --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_search_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_search_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_search_white_24dp.png Binary files differnew file mode 100644 index 000000000..dff1e3a8a --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_search_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_settings_input_antenna_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_settings_input_antenna_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..8995d0f43 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_settings_input_antenna_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_settings_input_antenna_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_settings_input_antenna_white_24dp.png Binary files differnew file mode 100644 index 000000000..c31fa8309 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_settings_input_antenna_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_share_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_share_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..ee0248838 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_share_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_share_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_share_white_24dp.png Binary files differnew file mode 100644 index 000000000..4d0197223 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_share_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_stat_antenna_default.png b/core/src/main/res/drawable-mdpi/ic_stat_antenna_default.png Binary files differnew file mode 100644 index 000000000..8b1206b51 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_stat_antenna_default.png diff --git a/core/src/main/res/drawable-mdpi/ic_stat_authentication.png b/core/src/main/res/drawable-mdpi/ic_stat_authentication.png Binary files differnew file mode 100755 index 000000000..cadfb9643 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_stat_authentication.png diff --git a/core/src/main/res/drawable-mdpi/ic_timer_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_timer_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..186887c30 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_timer_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_timer_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_timer_white_24dp.png Binary files differnew file mode 100644 index 000000000..735ed7a00 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_timer_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_toc_grey600_36dp.png b/core/src/main/res/drawable-mdpi/ic_toc_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..23bec7945 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_toc_grey600_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_toc_white_36dp.png b/core/src/main/res/drawable-mdpi/ic_toc_white_36dp.png Binary files differnew file mode 100644 index 000000000..dc03b34b9 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_toc_white_36dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_web_grey600_24dp.png b/core/src/main/res/drawable-mdpi/ic_web_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..cdbce22a7 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_web_grey600_24dp.png diff --git a/core/src/main/res/drawable-mdpi/ic_web_white_24dp.png b/core/src/main/res/drawable-mdpi/ic_web_white_24dp.png Binary files differnew file mode 100644 index 000000000..3602c4c48 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/ic_web_white_24dp.png diff --git a/core/src/main/res/drawable-mdpi/navigation_up.png b/core/src/main/res/drawable-mdpi/navigation_up.png Binary files differnew file mode 100755 index 000000000..1ee248a79 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/navigation_up.png diff --git a/core/src/main/res/drawable-mdpi/navigation_up_dark.png b/core/src/main/res/drawable-mdpi/navigation_up_dark.png Binary files differnew file mode 100755 index 000000000..8ef44cbac --- /dev/null +++ b/core/src/main/res/drawable-mdpi/navigation_up_dark.png diff --git a/core/src/main/res/drawable-mdpi/stat_notify_sync.png b/core/src/main/res/drawable-mdpi/stat_notify_sync.png Binary files differnew file mode 100644 index 000000000..03ce57a47 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/stat_notify_sync.png diff --git a/core/src/main/res/drawable-mdpi/stat_notify_sync_error.png b/core/src/main/res/drawable-mdpi/stat_notify_sync_error.png Binary files differnew file mode 100644 index 000000000..f849b5040 --- /dev/null +++ b/core/src/main/res/drawable-mdpi/stat_notify_sync_error.png diff --git a/core/src/main/res/drawable-xhdpi-v11/ic_stat_antenna_default.png b/core/src/main/res/drawable-xhdpi-v11/ic_stat_antenna_default.png Binary files differnew file mode 100644 index 000000000..59de64c87 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi-v11/ic_stat_antenna_default.png diff --git a/core/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png b/core/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png Binary files differnew file mode 100755 index 000000000..f58fb21df --- /dev/null +++ b/core/src/main/res/drawable-xhdpi-v11/ic_stat_authentication.png diff --git a/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png Binary files differnew file mode 100644 index 000000000..b3bf21ffe --- /dev/null +++ b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync.png diff --git a/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png Binary files differnew file mode 100644 index 000000000..33582ef10 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi-v11/stat_notify_sync_error.png diff --git a/core/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png Binary files differnew file mode 100755 index 000000000..b0777a023 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_action_pause_over_video.png diff --git a/core/src/main/res/drawable-xhdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-xhdpi/ic_action_play_over_video.png Binary files differnew file mode 100755 index 000000000..24331a48c --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_action_play_over_video.png diff --git a/core/src/main/res/drawable-xhdpi/ic_add_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_add_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..0d8af34b9 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_add_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_add_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_add_white_24dp.png Binary files differnew file mode 100644 index 000000000..67042105d --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_add_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_cancel_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_cancel_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..ce0da2222 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_cancel_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_cancel_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_cancel_white_24dp.png Binary files differnew file mode 100644 index 000000000..e9e9c7a53 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_cancel_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_delete_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_delete_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..c6bb43e8b --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_delete_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png Binary files differnew file mode 100644 index 000000000..cdb230c2f --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_delete_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_description_grey600_36dp.png b/core/src/main/res/drawable-xhdpi/ic_description_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..50f854ea5 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_description_grey600_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_description_white_36dp.png b/core/src/main/res/drawable-xhdpi/ic_description_white_36dp.png Binary files differnew file mode 100644 index 000000000..60d988dd4 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_description_white_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_done_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_done_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..36ed24eec --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_done_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_done_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_done_white_24dp.png Binary files differnew file mode 100644 index 000000000..e5024472a --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_done_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_drag_handle.png b/core/src/main/res/drawable-xhdpi/ic_drag_handle.png Binary files differnew file mode 100755 index 000000000..5bdcac342 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_drag_handle.png diff --git a/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png Binary files differnew file mode 100755 index 000000000..d341c7c82 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_drag_handle_dark.png diff --git a/core/src/main/res/drawable-xhdpi/ic_expand_more_grey600_36dp.png b/core/src/main/res/drawable-xhdpi/ic_expand_more_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..921249fb2 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_expand_more_grey600_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_expand_more_white_36dp.png b/core/src/main/res/drawable-xhdpi/ic_expand_more_white_36dp.png Binary files differnew file mode 100644 index 000000000..f00aa8bd4 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_expand_more_white_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_fast_forward_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_fast_forward_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..6c26d6ea2 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_fast_forward_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_fast_forward_grey600_36dp.png b/core/src/main/res/drawable-xhdpi/ic_fast_forward_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..169a3b386 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_fast_forward_grey600_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_fast_forward_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_fast_forward_white_24dp.png Binary files differnew file mode 100644 index 000000000..b7c86059b --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_fast_forward_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_fast_forward_white_36dp.png b/core/src/main/res/drawable-xhdpi/ic_fast_forward_white_36dp.png Binary files differnew file mode 100644 index 000000000..8aec1abb0 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_fast_forward_white_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_fast_rewind_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_fast_rewind_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..d1db46a23 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_fast_rewind_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_fast_rewind_grey600_36dp.png b/core/src/main/res/drawable-xhdpi/ic_fast_rewind_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..ccac2c273 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_fast_rewind_grey600_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_fast_rewind_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_fast_rewind_white_24dp.png Binary files differnew file mode 100644 index 000000000..379435f54 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_fast_rewind_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_fast_rewind_white_36dp.png b/core/src/main/res/drawable-xhdpi/ic_fast_rewind_white_36dp.png Binary files differnew file mode 100644 index 000000000..857a1aaa2 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_fast_rewind_white_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_file_download_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_file_download_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..aa89d4977 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_file_download_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_file_download_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_file_download_white_24dp.png Binary files differnew file mode 100644 index 000000000..990dfb82b --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_file_download_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_hearing_grey600_18dp.png b/core/src/main/res/drawable-xhdpi/ic_hearing_grey600_18dp.png Binary files differnew file mode 100644 index 000000000..a0c760e6f --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_hearing_grey600_18dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_hearing_white_18dp.png b/core/src/main/res/drawable-xhdpi/ic_hearing_white_18dp.png Binary files differnew file mode 100644 index 000000000..1b8722a7f --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_hearing_white_18dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_history_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_history_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..a098d8d0f --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_history_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_history_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_history_white_24dp.png Binary files differnew file mode 100644 index 000000000..7681ac8b8 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_history_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_info_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_info_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..9dcb234fe --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_info_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_info_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_info_white_24dp.png Binary files differnew file mode 100644 index 000000000..bee33abb7 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_info_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_launcher.png b/core/src/main/res/drawable-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..857a1b12e --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_launcher.png diff --git a/core/src/main/res/drawable-xhdpi/ic_list_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_list_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..c51f8a258 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_list_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_list_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_list_white_24dp.png Binary files differnew file mode 100644 index 000000000..abefc200a --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_list_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_more_vert_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_more_vert_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..7bc63a511 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_more_vert_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png Binary files differnew file mode 100644 index 000000000..1b04eda04 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_more_vert_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_new.png b/core/src/main/res/drawable-xhdpi/ic_new.png Binary files differnew file mode 100755 index 000000000..447a9398b --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_new.png diff --git a/core/src/main/res/drawable-xhdpi/ic_new_dark.png b/core/src/main/res/drawable-xhdpi/ic_new_dark.png Binary files differnew file mode 100755 index 000000000..4a23d309c --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_new_dark.png diff --git a/core/src/main/res/drawable-xhdpi/ic_new_releases_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_new_releases_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..655658557 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_new_releases_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_new_releases_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_new_releases_white_24dp.png Binary files differnew file mode 100644 index 000000000..00a4daa62 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_new_releases_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_pause_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_pause_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..3ec598cfd --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_pause_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_pause_grey600_36dp.png b/core/src/main/res/drawable-xhdpi/ic_pause_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..7efc88511 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_pause_grey600_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png Binary files differnew file mode 100644 index 000000000..14b6d17d4 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_pause_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_pause_white_36dp.png b/core/src/main/res/drawable-xhdpi/ic_pause_white_36dp.png Binary files differnew file mode 100644 index 000000000..72dfa9fa6 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_pause_white_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_play_arrow_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_play_arrow_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..689fda259 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_play_arrow_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_play_arrow_grey600_36dp.png b/core/src/main/res/drawable-xhdpi/ic_play_arrow_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..ba62a8102 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_play_arrow_grey600_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png Binary files differnew file mode 100644 index 000000000..a55d19922 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_play_arrow_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_play_arrow_white_36dp.png b/core/src/main/res/drawable-xhdpi/ic_play_arrow_white_36dp.png Binary files differnew file mode 100644 index 000000000..043acd808 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_play_arrow_white_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..7891efffa --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_refresh_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png Binary files differnew file mode 100644 index 000000000..5f89fc257 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_refresh_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_remove_red_eye_grey600_18dp.png b/core/src/main/res/drawable-xhdpi/ic_remove_red_eye_grey600_18dp.png Binary files differnew file mode 100644 index 000000000..7047ed7ea --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_remove_red_eye_grey600_18dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_remove_red_eye_white_18dp.png b/core/src/main/res/drawable-xhdpi/ic_remove_red_eye_white_18dp.png Binary files differnew file mode 100644 index 000000000..ef2023b52 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_remove_red_eye_white_18dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_search_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_search_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..f9c0b2ec3 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_search_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_search_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_search_white_24dp.png Binary files differnew file mode 100644 index 000000000..043759acd --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_search_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_settings_input_antenna_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_settings_input_antenna_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..c30f31017 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_settings_input_antenna_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_settings_input_antenna_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_settings_input_antenna_white_24dp.png Binary files differnew file mode 100644 index 000000000..ea2313395 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_settings_input_antenna_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_share_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_share_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..88a0edd6c --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_share_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_share_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_share_white_24dp.png Binary files differnew file mode 100644 index 000000000..dd536bca2 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_share_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_stat_antenna_default.png b/core/src/main/res/drawable-xhdpi/ic_stat_antenna_default.png Binary files differnew file mode 100644 index 000000000..50d73271d --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_stat_antenna_default.png diff --git a/core/src/main/res/drawable-xhdpi/ic_stat_authentication.png b/core/src/main/res/drawable-xhdpi/ic_stat_authentication.png Binary files differnew file mode 100755 index 000000000..4adfb636c --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_stat_authentication.png diff --git a/core/src/main/res/drawable-xhdpi/ic_timer_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_timer_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..23149552f --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_timer_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_timer_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_timer_white_24dp.png Binary files differnew file mode 100644 index 000000000..896d9dcf2 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_timer_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_toc_grey600_36dp.png b/core/src/main/res/drawable-xhdpi/ic_toc_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..57a60ed9f --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_toc_grey600_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_toc_white_36dp.png b/core/src/main/res/drawable-xhdpi/ic_toc_white_36dp.png Binary files differnew file mode 100644 index 000000000..aa7ee9c02 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_toc_white_36dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_undobar_undo.png b/core/src/main/res/drawable-xhdpi/ic_undobar_undo.png Binary files differnew file mode 100644 index 000000000..91c8429ad --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_undobar_undo.png diff --git a/core/src/main/res/drawable-xhdpi/ic_web_grey600_24dp.png b/core/src/main/res/drawable-xhdpi/ic_web_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..a9f5ebc33 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_web_grey600_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/ic_web_white_24dp.png b/core/src/main/res/drawable-xhdpi/ic_web_white_24dp.png Binary files differnew file mode 100644 index 000000000..edc590a0b --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/ic_web_white_24dp.png diff --git a/core/src/main/res/drawable-xhdpi/navigation_up.png b/core/src/main/res/drawable-xhdpi/navigation_up.png Binary files differnew file mode 100755 index 000000000..f8c3e6f75 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/navigation_up.png diff --git a/core/src/main/res/drawable-xhdpi/navigation_up_dark.png b/core/src/main/res/drawable-xhdpi/navigation_up_dark.png Binary files differnew file mode 100755 index 000000000..6964e069b --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/navigation_up_dark.png diff --git a/core/src/main/res/drawable-xhdpi/undobar.9.png b/core/src/main/res/drawable-xhdpi/undobar.9.png Binary files differnew file mode 100644 index 000000000..22fa2205b --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/undobar.9.png diff --git a/core/src/main/res/drawable-xhdpi/undobar_button_focused.9.png b/core/src/main/res/drawable-xhdpi/undobar_button_focused.9.png Binary files differnew file mode 100644 index 000000000..d284ca7cb --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/undobar_button_focused.9.png diff --git a/core/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png b/core/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png Binary files differnew file mode 100644 index 000000000..e990659f0 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/undobar_button_pressed.9.png diff --git a/core/src/main/res/drawable-xhdpi/undobar_divider.9.png b/core/src/main/res/drawable-xhdpi/undobar_divider.9.png Binary files differnew file mode 100644 index 000000000..1b067d4e7 --- /dev/null +++ b/core/src/main/res/drawable-xhdpi/undobar_divider.9.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png b/core/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png Binary files differnew file mode 100755 index 000000000..fa85601cf --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_action_pause_over_video.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png b/core/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png Binary files differnew file mode 100755 index 000000000..121be211e --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_action_play_over_video.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_add_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_add_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..70e4e86e7 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_add_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png Binary files differnew file mode 100644 index 000000000..72cedcad4 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_cancel_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_cancel_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..f6f7844de --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_cancel_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_cancel_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_cancel_white_24dp.png Binary files differnew file mode 100644 index 000000000..faa409f80 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_cancel_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_delete_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_delete_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..4886ab1e9 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_delete_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png Binary files differnew file mode 100644 index 000000000..0e95e9b1d --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_delete_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_description_grey600_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_description_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..33df5d9d2 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_description_grey600_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_description_white_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_description_white_36dp.png Binary files differnew file mode 100644 index 000000000..aa5e73299 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_description_white_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_done_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_done_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..c836c5490 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_done_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_done_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_done_white_24dp.png Binary files differnew file mode 100644 index 000000000..f801e7ab3 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_done_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_drag_handle.png b/core/src/main/res/drawable-xxhdpi/ic_drag_handle.png Binary files differnew file mode 100755 index 000000000..f834699c6 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_drag_handle.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png b/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png Binary files differnew file mode 100755 index 000000000..a9408bc9d --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_drag_handle_dark.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_expand_more_grey600_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_expand_more_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..503678f1b --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_expand_more_grey600_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_expand_more_white_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_expand_more_white_36dp.png Binary files differnew file mode 100644 index 000000000..16094f9bc --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_expand_more_white_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_fast_forward_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_fast_forward_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..169a3b386 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_fast_forward_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_fast_forward_grey600_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_fast_forward_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..7b83df521 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_fast_forward_grey600_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_fast_forward_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_fast_forward_white_24dp.png Binary files differnew file mode 100644 index 000000000..8aec1abb0 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_fast_forward_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_fast_forward_white_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_fast_forward_white_36dp.png Binary files differnew file mode 100644 index 000000000..b4cf934f7 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_fast_forward_white_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..ccac2c273 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_grey600_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..940b5413d --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_grey600_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_white_24dp.png Binary files differnew file mode 100644 index 000000000..857a1aaa2 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_white_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_white_36dp.png Binary files differnew file mode 100644 index 000000000..751e5d1ba --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_fast_rewind_white_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_file_download_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_file_download_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..e61a48a4d --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_file_download_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_file_download_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_file_download_white_24dp.png Binary files differnew file mode 100644 index 000000000..95502de3f --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_file_download_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_hearing_grey600_18dp.png b/core/src/main/res/drawable-xxhdpi/ic_hearing_grey600_18dp.png Binary files differnew file mode 100644 index 000000000..111ed802c --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_hearing_grey600_18dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_hearing_white_18dp.png b/core/src/main/res/drawable-xxhdpi/ic_hearing_white_18dp.png Binary files differnew file mode 100644 index 000000000..417b39b81 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_hearing_white_18dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_history_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_history_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..632a73e42 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_history_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_history_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_history_white_24dp.png Binary files differnew file mode 100644 index 000000000..3e438ec4e --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_history_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_info_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_info_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..64445284b --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_info_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_info_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_info_white_24dp.png Binary files differnew file mode 100644 index 000000000..185d18d1a --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_info_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_launcher.png b/core/src/main/res/drawable-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 000000000..2bef52ec7 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_launcher.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_list_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_list_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..ceaf36811 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_list_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png Binary files differnew file mode 100644 index 000000000..6b7e78f83 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_list_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_more_vert_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_more_vert_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..44012b87d --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_more_vert_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png Binary files differnew file mode 100644 index 000000000..2955c02ec --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_more_vert_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_new.png b/core/src/main/res/drawable-xxhdpi/ic_new.png Binary files differnew file mode 100755 index 000000000..5e836eae4 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_new.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_new_dark.png b/core/src/main/res/drawable-xxhdpi/ic_new_dark.png Binary files differnew file mode 100755 index 000000000..bca96b751 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_new_dark.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_new_releases_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_new_releases_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..3eccc24f0 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_new_releases_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_new_releases_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_new_releases_white_24dp.png Binary files differnew file mode 100644 index 000000000..4a7083c57 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_new_releases_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_pause_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_pause_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..7efc88511 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_pause_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_pause_grey600_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_pause_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..c8ed1d00a --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_pause_grey600_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png Binary files differnew file mode 100644 index 000000000..72dfa9fa6 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_pause_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_pause_white_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_pause_white_36dp.png Binary files differnew file mode 100644 index 000000000..76833ff7d --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_pause_white_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_play_arrow_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_play_arrow_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..ba62a8102 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_play_arrow_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_play_arrow_grey600_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_play_arrow_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..1d44f8f2f --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_play_arrow_grey600_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png Binary files differnew file mode 100644 index 000000000..043acd808 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_play_arrow_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_play_arrow_white_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_play_arrow_white_36dp.png Binary files differnew file mode 100644 index 000000000..fe7ea25cc --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_play_arrow_white_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..9c1e27d74 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_refresh_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png Binary files differnew file mode 100644 index 000000000..72128fe69 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_refresh_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_remove_red_eye_grey600_18dp.png b/core/src/main/res/drawable-xxhdpi/ic_remove_red_eye_grey600_18dp.png Binary files differnew file mode 100644 index 000000000..259ddcd14 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_remove_red_eye_grey600_18dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_remove_red_eye_white_18dp.png b/core/src/main/res/drawable-xxhdpi/ic_remove_red_eye_white_18dp.png Binary files differnew file mode 100644 index 000000000..ece7aeb74 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_remove_red_eye_white_18dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_search_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_search_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..9424ae98e --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_search_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png Binary files differnew file mode 100644 index 000000000..0bbeab150 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_settings_input_antenna_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_settings_input_antenna_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..5404820db --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_settings_input_antenna_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_settings_input_antenna_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_settings_input_antenna_white_24dp.png Binary files differnew file mode 100644 index 000000000..19752dd44 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_settings_input_antenna_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_share_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_share_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..89136d7c3 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_share_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png Binary files differnew file mode 100644 index 000000000..9963c6a05 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_share_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_stat_authentication.png b/core/src/main/res/drawable-xxhdpi/ic_stat_authentication.png Binary files differnew file mode 100755 index 000000000..b274bb60f --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_stat_authentication.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_timer_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_timer_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..a9ae43327 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_timer_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_timer_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_timer_white_24dp.png Binary files differnew file mode 100644 index 000000000..276f4e1b9 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_timer_white_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_toc_grey600_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_toc_grey600_36dp.png Binary files differnew file mode 100644 index 000000000..bb35c5657 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_toc_grey600_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_toc_white_36dp.png b/core/src/main/res/drawable-xxhdpi/ic_toc_white_36dp.png Binary files differnew file mode 100644 index 000000000..fdbb3ac3a --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_toc_white_36dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_web_grey600_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_web_grey600_24dp.png Binary files differnew file mode 100644 index 000000000..9e70743e8 --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_web_grey600_24dp.png diff --git a/core/src/main/res/drawable-xxhdpi/ic_web_white_24dp.png b/core/src/main/res/drawable-xxhdpi/ic_web_white_24dp.png Binary files differnew file mode 100644 index 000000000..fcc2d1aca --- /dev/null +++ b/core/src/main/res/drawable-xxhdpi/ic_web_white_24dp.png diff --git a/core/src/main/res/drawable/borderless_button_dark.xml b/core/src/main/res/drawable/borderless_button_dark.xml new file mode 100644 index 000000000..6d263938d --- /dev/null +++ b/core/src/main/res/drawable/borderless_button_dark.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true"><shape android:shape="rectangle"> + <solid android:color="@color/selection_background_color_dark" /> + </shape></item> + <item android:state_focused="true"><shape android:shape="rectangle"> + <solid android:color="@color/selection_background_color_dark" /> + </shape></item> + <item><shape android:shape="rectangle"> + <solid android:color="@android:color/transparent" /> + </shape></item> + +</selector>
\ No newline at end of file diff --git a/core/src/main/res/drawable/overlay_button_circle_background.xml b/core/src/main/res/drawable/overlay_button_circle_background.xml new file mode 100644 index 000000000..90c51472c --- /dev/null +++ b/core/src/main/res/drawable/overlay_button_circle_background.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <gradient + android:type="radial" + android:gradientRadius="60" + android:startColor="#000000" + android:endColor="@android:color/transparent"/> +</shape>
\ No newline at end of file diff --git a/core/src/main/res/drawable/overlay_drawable.xml b/core/src/main/res/drawable/overlay_drawable.xml new file mode 100644 index 000000000..185ffefc1 --- /dev/null +++ b/core/src/main/res/drawable/overlay_drawable.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android" > + + <item> + <shape android:shape="rectangle" > + <solid android:color="#F0BABABA" /> + </shape> + </item> + <item android:top="1dp"> + <shape android:shape="rectangle" > + <solid android:color="#D2D2D2" /> + </shape> + </item> + <item android:top="2dp"> + <shape android:shape="rectangle" > + <solid android:color="@color/overlay_light" /> + </shape> + </item> + +</layer-list>
\ No newline at end of file diff --git a/core/src/main/res/drawable/overlay_drawable_dark.xml b/core/src/main/res/drawable/overlay_drawable_dark.xml new file mode 100644 index 000000000..fb78f5633 --- /dev/null +++ b/core/src/main/res/drawable/overlay_drawable_dark.xml @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android" > + + <item> + <shape android:shape="rectangle" > + <solid android:color="#45B3E1" /> + </shape> + </item> + <item android:top="1dp"> + <shape android:shape="rectangle" > + <solid android:color="@color/overlay_dark" /> + </shape> + </item> + +</layer-list>
\ No newline at end of file diff --git a/core/src/main/res/drawable/undobar_button.xml b/core/src/main/res/drawable/undobar_button.xml new file mode 100644 index 000000000..a4de91b49 --- /dev/null +++ b/core/src/main/res/drawable/undobar_button.xml @@ -0,0 +1,22 @@ +<!-- + + Copyright 2012 Roman Nurik + + 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. + +--> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:drawable="@drawable/undobar_button_pressed" android:state_pressed="true"/> + <item android:drawable="@drawable/undobar_button_focused" android:state_focused="true"/> + <item android:drawable="@android:color/transparent"/> +</selector> diff --git a/core/src/main/res/drawable/white_circle.xml b/core/src/main/res/drawable/white_circle.xml new file mode 100644 index 000000000..597b70a2d --- /dev/null +++ b/core/src/main/res/drawable/white_circle.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="oval" > + + <solid android:color="@color/white" /> + + <size + android:height="4dp" + android:width="4dp" /> + +</shape>
\ No newline at end of file diff --git a/core/src/main/res/layout/more_content_list_footer.xml b/core/src/main/res/layout/more_content_list_footer.xml new file mode 100644 index 000000000..b9947dc33 --- /dev/null +++ b/core/src/main/res/layout/more_content_list_footer.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/main" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?attr/selectableItemBackground"> + + <ImageView + android:id="@+id/imgExpand" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_margin="16dp" + android:contentDescription="@string/load_next_page_label" + android:src="?attr/navigation_expand" /> + + <ProgressBar + android:id="@+id/progBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:layout_margin="16dp" + android:indeterminateOnly="true" + android:visibility="gone" /> + +</FrameLayout>
\ No newline at end of file diff --git a/core/src/main/res/values-az/strings.xml b/core/src/main/res/values-az/strings.xml new file mode 100644 index 000000000..adb983e9e --- /dev/null +++ b/core/src/main/res/values-az/strings.xml @@ -0,0 +1,217 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Kanallar</string> + <string name="podcasts_label">PODKASTLAR</string> + <string name="episodes_label">EPIZODLAR</string> + <string name="new_label">Yeni</string> + <string name="waiting_list_label">Gözləmədə</string> + <string name="settings_label">Parametrlər</string> + <string name="downloads_label">Yükləmələr</string> + <string name="cancel_download_label">Yükləməyi ləğv et</string> + <string name="playback_history_label">Oynatma tarixiçəsi</string> + <string name="gpodnet_main_label">gpodder.net</string> + <!--New episodes fragment--> + <!--Main activity--> + <!--Webview actions--> + <string name="open_in_browser_label">Brauzerdə aç</string> + <string name="copy_url_label">URLı kopiyala</string> + <string name="share_url_label">URLı paylaş</string> + <string name="copied_url_msg">URL buferə köçürüldü</string> + <!--Playback history--> + <string name="clear_history_label">Tarixiçəni sildir</string> + <!--Other--> + <string name="confirm_label">Oldu</string> + <string name="cancel_label">Ləğv et</string> + <string name="author_label">Müəlif</string> + <string name="language_label">Dil</string> + <string name="podcast_settings_label">Parametrlər</string> + <string name="error_label">Xəta</string> + <string name="error_msg_prefix">Xəta baş verdi:</string> + <string name="refresh_label">Təzələ</string> + <string name="external_storage_error_msg">Heç bir yaddaş cihazı tapılmadı.</string> + <string name="chapters_label">Fəsillər</string> + <string name="shownotes_label">Təsvir</string> + <string name="most_recent_prefix">Ən yeni epizod:\u0020</string> + <string name="episodes_suffix">\u0020epizod</string> + <string name="length_prefix">Müddət:\u0020</string> + <string name="size_prefix">Ölçü:\u0020</string> + <string name="processing_label">Hazırlaşma</string> + <string name="loading_label">Yükləmə...</string> + <string name="close_label">Bağla</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">Kanalın URLı</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Hamısını oxunmuş kimi işarələ</string> + <string name="show_info_label">Məlumatı göstər</string> + <string name="share_link_label">Web-səhifəyi paylaş</string> + <string name="share_source_label">Kanalı paylaş</string> + <string name="feed_delete_confirmation_msg">Bütün kanallar və epizodlar silinəçək.</string> + <!--actions on feeditems--> + <string name="download_label">Yüklə</string> + <string name="play_label">Oynat</string> + <string name="pause_label">Pauza</string> + <string name="stream_label">İnternetən yayimla</string> + <string name="remove_label">Sil</string> + <string name="mark_read_label">Oxumuş kimi işarələ</string> + <string name="mark_unread_label">Oxunmamış kimi işarələ</string> + <string name="add_to_queue_label">Növbəyə əlavə et</string> + <string name="remove_from_queue_label">Növbədən sil</string> + <string name="visit_website_label">Web-səhifəsini aç</string> + <string name="support_label">Flattrla</string> + <string name="enqueue_all_new">Hamsını növbəyə əlavə et</string> + <string name="download_all">Hamısını yüklə</string> + <string name="skip_episode_label">Epizodu burax</string> + <!--Download messages and labels--> + <string name="download_pending">Yükləmə gözlənir</string> + <string name="download_running">Yükləmə gedir</string> + <string name="download_error_device_not_found">Yaddaş cihazı tapılmadı</string> + <string name="download_error_insufficient_space">Yaddaş çatmır</string> + <string name="download_error_file_error">Fayl xətası</string> + <string name="download_error_http_data_error">HTTP protokolnun xətası</string> + <string name="download_error_error_unknown">Naməlum xəta</string> + <string name="download_error_parser_exception">Parserin xətası</string> + <string name="download_error_unsupported_type">Naməlum kanal növü</string> + <string name="download_error_connection_error">Əlaqə xətasi</string> + <string name="download_error_unknown_host">Naməlum xost</string> + <string name="cancel_all_downloads_label">Yükləmələrin hamısını ləğv et</string> + <string name="download_cancelled_msg">Yükləmə ləğv olundu</string> + <string name="download_report_title">Yükləmə başa çatdı</string> + <string name="download_error_malformed_url">Yanlış URL</string> + <string name="download_error_io_error">IO xətasi</string> + <string name="download_error_request_error">Tələbin xətası</string> + <string name="downloads_left">\u0020yükləmə galdı</string> + <string name="download_notification_title">Podkast məlumatların yüklənişi</string> + <string name="download_report_content">%1$d yükləmə uğurludur, %2$d uğursuzdur</string> + <string name="download_log_title_unknown">Naməlum başliğ</string> + <string name="download_type_feed">Kanal</string> + <string name="download_type_media">Mediya fayl</string> + <string name="download_type_image">Şəkil</string> + <string name="download_request_error_dialog_message_prefix">Fayl yükləmə xətası:\u0020</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Xəta!</string> + <string name="player_stopped_msg">Heç nə oynadılmır</string> + <string name="player_preparing_msg">Hazırlanır</string> + <string name="player_ready_msg">Hazır</string> + <string name="player_seeking_msg">Axtarış</string> + <string name="playback_error_server_died">Server iştəmir</string> + <string name="playback_error_unknown">Naməlum xəta</string> + <string name="no_media_playing_label">Heç nə oynadılmır</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Buferləşmə</string> + <string name="playbackservice_notification_title">Podkast oynadılır</string> + <!--Queue operations--> + <string name="clear_queue_label">Növbəyi sil</string> + <string name="undo">Qaytar</string> + <string name="removed_from_queue">Element silindi</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattra gir</string> + <string name="flattr_auth_explanation">Girmə prosesini başlamaq üçün düyməyi basın. Flattrın giriş səhifəsinə aparılacağsınız.</string> + <string name="authenticate_label">Gir</string> + <string name="return_home_label">Baş ekrana dön</string> + <string name="flattr_auth_success">Giriş uğurludur! İndi tətbiqlədən Flattrla istifadə edə bilərsiniz.</string> + <string name="no_flattr_token_title">Heç bir Flattr tokeni tapılmadı</string> + <string name="no_flattr_token_msg">Olsun ki sizin Flattr hesabınız AntennaPod\'a qoşulmadı. Yenə Flattra girin ya da podkastın səhifəsinə keçin.</string> + <string name="authenticate_now_label">Gir</string> + <string name="action_forbidden_title">Əməliyyat qadağan olundu</string> + <string name="action_forbidden_msg">Bu əməliyyat üçün AntennaPod\'un icazəsi yoxdur. AntennaPod\'un keçid tokenin ləğv olunması bunun səbəbi ola bilər. Yenə Flattra girin ya da podkastın səhifəsinə keçin.</string> + <string name="access_revoked_title">Keçid ləğv olundu</string> + <string name="access_revoked_info">AntennaPod\'un keçid tokeni uğurlu ləğv olundu.</string> + <!--Flattr--> + <!--Variable Speed--> + <string name="download_plugin_label">Plagin yüklə</string> + <string name="no_playback_plugin_title">Plagin yüklü deyil</string> + <!--Empty list labels--> + <string name="no_items_label">Siyahıda heç nə yoxdur</string> + <string name="no_feeds_label">Hələ heç bir kanala yazilmadınız</string> + <!--Preferences--> + <string name="other_pref">Başqa</string> + <string name="about_pref">Proqram haqqinda</string> + <string name="queue_label">Növbə</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Qulaqliqı ayiranda oynatma dayanacağ</string> + <string name="pref_followQueue_sum">Oynatma başa çatanda növbədə irəlidəki epizodu oynat</string> + <string name="playback_pref">Oynatma</string> + <string name="network_pref">Şəbəkə</string> + <string name="pref_autoUpdateIntervall_title">Təzələmə intervalı</string> + <string name="pref_autoUpdateIntervall_sum">Kanalın avtomatik təzələməsinin intervalını seç ya da keçir onu</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Təkçə Wi-Fi vasitəsiilə yüklə</string> + <string name="pref_followQueue_title">Fasiləsiz oynatma</string> + <string name="pref_downloadMediaOnWifiOnly_title">Wi-Fi vasitəsiilə yükləmə</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Qulaqliqı ayır</string> + <string name="pref_mobileUpdate_title">Mobil şəbəbkə vasitəsiilə təzələmə</string> + <string name="pref_mobileUpdate_sum">Mobil şəbəbkə vasitəsiilə təzələməyə icazə vermək</string> + <string name="refreshing_label">Təzələmə</string> + <string name="flattr_settings_label">Flattr parametrləri</string> + <string name="pref_flattr_auth_title">Flattra gir</string> + <string name="pref_flattr_auth_sum">Flattr\'la istifadə etmək üçün, öz Flattr hesabınıza girin</string> + <string name="pref_flattr_this_app_title">Bu proqramı flattrla</string> + <string name="pref_flattr_this_app_sum">Flattr vasitəsiilə AntennaPodun inkişafını dəstək edin. Sağolun!</string> + <string name="pref_revokeAccess_title">Keçidi ləğv ət</string> + <string name="pref_revokeAccess_sum">Flattr hesabına keçidi ləğv et</string> + <string name="user_interface_label">İnterfeys</string> + <string name="pref_set_theme_title">Görüşü seç</string> + <string name="pref_set_theme_sum">AntennaPod\'un görüşünü dəyişdir</string> + <string name="pref_automatic_download_title">Avtomatik yükləmə</string> + <string name="pref_automatic_download_sum">Epizodların avtomatik yüklənişinin konfiqurasiyanı dəyiş</string> + <string name="pref_autodl_wifi_filter_title">Wi-Fi filtr</string> + <string name="pref_autodl_wifi_filter_sum">Seçilən Wi-Fi səbəkələr vasitəsiilə avtomatik yükləməyi icazə ver</string> + <string name="pref_episode_cache_title">Epizod keşi</string> + <string name="pref_theme_title_light">Ağ</string> + <string name="pref_theme_title_dark">Qara</string> + <string name="pref_episode_cache_unlimited">Hədsiz</string> + <string name="pref_update_interval_hours_plural">saat</string> + <string name="pref_update_interval_hours_singular">saat</string> + <string name="pref_update_interval_hours_manual">Əl ilə</string> + <!--Auto-Flattr dialog--> + <!--Search--> + <string name="search_hint">Kanalları və ya epizodları axtar</string> + <string name="found_in_shownotes_label">Təsvirlərdə tapıldı</string> + <string name="found_in_chapters_label">Fəsillərdə tapıldı</string> + <string name="search_status_no_results">Heç nə tapılmadı</string> + <string name="search_label">Axtar</string> + <string name="found_in_title_label">Başlığda tapıldı</string> + <!--OPML import and export--> + <string name="opml_import_explanation">OPML faylın idxalı üçün onu aşağıdakı qovluqa yerləşdirin və idxal prosesini başlamaq üçün düyməyi basın.</string> + <string name="start_import_label">İdxalı başla</string> + <string name="opml_import_label">OPML idxalı</string> + <string name="opml_directory_error">XƏTA!</string> + <string name="reading_opml_label">OPML faylın oxunması</string> + <string name="opml_reader_error">OPML faylını oxuyanda xəta baş verdi:</string> + <string name="opml_import_error_dir_empty">İdxal qovliqu boşdur.</string> + <string name="select_all_label">Hamısını seç</string> + <string name="deselect_all_label">Seçimi ləğv et</string> + <string name="choose_file_to_import_label">İdxal üçün fayl seç</string> + <string name="opml_export_label">OPML ixraçı</string> + <string name="exporting_label">İxrac...</string> + <string name="export_error_label">İxracın xətası</string> + <string name="opml_export_success_title">OPML ixracı uğurlu keçdi</string> + <string name="opml_export_success_sum">OPML fayl:\u0020 yazılıb</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Yuxu taymerini qoy</string> + <string name="disable_sleeptimer_label">Yuxu taymerini keçir</string> + <string name="enter_time_here_label">Vaxtı yaz</string> + <string name="sleep_timer_label">Yuxu taymeri</string> + <string name="time_left_label">Vaxt galdı:\u0020</string> + <string name="time_dialog_invalid_input">Yanlış yazi. Vaxt təkçə rəqmlərlə yazılır</string> + <!--gpodder.net--> + <string name="gpodnet_toplist_header">TOP PODKASTLAR</string> + <!--Directory chooser--> + <string name="selected_folder_label">Seçilən qovluq:</string> + <string name="create_folder_label">Qovluqu yarat</string> + <string name="choose_data_directory">Məlumat qovluqunu seç</string> + <string name="create_folder_msg">\"%1$s\" adlı qovluq yaradılsınmı?</string> + <string name="create_folder_success">Yeni qovluq yaradıldı</string> + <string name="create_folder_error_no_write_access">Bu qovluqa yazıla bilinmer</string> + <string name="create_folder_error_already_exists">Qovluq artiq var</string> + <string name="create_folder_error">Qovluq yaradılmadı</string> + <string name="folder_not_empty_dialog_title">Qovluq boş deyil</string> + <string name="folder_not_empty_dialog_msg">Seçilən qovluq boş deyil. Mediya yükləmələr və başka fayllar bu qovluqa yazılacaqlar. Necə olsa davam olsunmu?</string> + <string name="set_to_default_folder">Başlanğıc qovluqu seç</string> + <!--Online feed view--> + <string name="downloading_label">Yükləmə...</string> + <!--Content descriptions for image buttons--> + <!--Feed information screen--> + <!--AntennaPodSP--> +</resources> diff --git a/core/src/main/res/values-ca/strings.xml b/core/src/main/res/values-ca/strings.xml new file mode 100644 index 000000000..ed8f67234 --- /dev/null +++ b/core/src/main/res/values-ca/strings.xml @@ -0,0 +1,341 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Canals</string> + <string name="add_feed_label">Afegeix podcast</string> + <string name="podcasts_label">PODCASTS</string> + <string name="episodes_label">EPISODIS</string> + <string name="new_episodes_label">Episodis nous</string> + <string name="all_episodes_label">Tots els episodis</string> + <string name="new_label">Nous</string> + <string name="waiting_list_label">Llista d\'espera</string> + <string name="settings_label">Configuració</string> + <string name="add_new_feed_label">Afegeix podcast</string> + <string name="downloads_label">Baixades</string> + <string name="downloads_running_label">En execució</string> + <string name="downloads_completed_label">Completat</string> + <string name="downloads_log_label">Registre</string> + <string name="cancel_download_label">Cancel·la la baixada</string> + <string name="playback_history_label">Historial de reproducció</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">Inici de sessió a gpodder.net</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Publicats recentment</string> + <string name="episode_filter_label">Mostra només els episodis nous</string> + <!--Main activity--> + <string name="drawer_open">Obre menú</string> + <string name="drawer_close">Tanca menú</string> + <!--Webview actions--> + <string name="open_in_browser_label">Obre en un navegador</string> + <string name="copy_url_label">Copia l\'enllaç</string> + <string name="share_url_label">Comparteix l\'enllaç</string> + <string name="copied_url_msg">S\'ha copiat l\'enllaç al porta-retalls.</string> + <string name="go_to_position_label">Vés a aquesta posició</string> + <!--Playback history--> + <string name="clear_history_label">Esborra l\'historial</string> + <!--Other--> + <string name="confirm_label">D\'acord</string> + <string name="cancel_label">Cancel·la</string> + <string name="author_label">Autor</string> + <string name="language_label">Llengua</string> + <string name="podcast_settings_label">Configuració</string> + <string name="cover_label">Imatge</string> + <string name="error_label">Error</string> + <string name="error_msg_prefix">S\'ha produït un error:</string> + <string name="refresh_label">Actualitza</string> + <string name="external_storage_error_msg">L\'emmagatzemament extern no està disponible. Assegureu-vos que està muntat per què l\'aplicació funcioni correctament.</string> + <string name="chapters_label">Capítols</string> + <string name="shownotes_label">Notes del programa</string> + <string name="description_label">Descripció</string> + <string name="most_recent_prefix">Episodi més recent: \u0020</string> + <string name="episodes_suffix">\u0020episodis</string> + <string name="length_prefix">Durada:\u0020</string> + <string name="size_prefix">Mida:\u0020</string> + <string name="processing_label">S\'està processant</string> + <string name="loading_label">S\'està carregant...</string> + <string name="save_username_password_label">Desa nom d\'usuari i contrasenya</string> + <string name="close_label">Tanca</string> + <string name="retry_label">Reintenta</string> + <string name="auto_download_label">Inclou a baixades automàtiques</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">Enllaç del canal</string> + <string name="etxtFeedurlHint">URL, canal o lloc web</string> + <string name="txtvfeedurl_label">Afegeix podcast amb l\'URL</string> + <string name="podcastdirectories_label">Cerca podcast al directori</string> + <string name="podcastdirectories_descr">Podeu cercar nous podcasts al directori de gpodder.net mitjançant el seu nom, categoria o popularitat.</string> + <string name="browse_gpoddernet_label">Navega gpodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Marca-ho tot com a llegit</string> + <string name="mark_all_read_msg">S\'han marcat tots els episodis com a llegits</string> + <string name="show_info_label">Mostra informació</string> + <string name="remove_feed_label">Esborra podcast</string> + <string name="share_link_label">Comparteix l\'enllaç de la plana</string> + <string name="share_source_label">Comparteix l\'enllaç del canal</string> + <string name="feed_delete_confirmation_msg">Confirmeu que, efectivament, voleu suprimir aquest canal i tots els episodis que us n\'heu baixat.</string> + <string name="feed_remover_msg">S\'està esborrant el canal</string> + <string name="load_complete_feed">S\'ha actualitzat el canal</string> + <!--actions on feeditems--> + <string name="download_label">Baixa</string> + <string name="play_label">Reprodueix</string> + <string name="pause_label">Pausa</string> + <string name="stop_label">Atura</string> + <string name="stream_label">Reprodueix sense baixar</string> + <string name="remove_label">Suprimeix</string> + <string name="remove_episode_lable">Esborra episodi</string> + <string name="mark_read_label">Marca com a llegit</string> + <string name="mark_unread_label">Marca com a pendent</string> + <string name="add_to_queue_label">Afegeix a la cua</string> + <string name="remove_from_queue_label">Suprimeix de la cua</string> + <string name="visit_website_label">Visita el lloc web</string> + <string name="support_label">Comparteix amb Flattr</string> + <string name="enqueue_all_new">Posa-ho tot a la cua</string> + <string name="download_all">Baixa-ho tot</string> + <string name="skip_episode_label">Omet l\'episodi</string> + <!--Download messages and labels--> + <string name="download_successful">ha funcionat</string> + <string name="download_failed">ha fallat</string> + <string name="download_pending">Baixada pendent</string> + <string name="download_running">Baixada en procés</string> + <string name="download_error_device_not_found">No s\'ha trobat cap dispositiu d\'emmagatzemament</string> + <string name="download_error_insufficient_space">No hi ha prou espai</string> + <string name="download_error_file_error">Error de fitxer</string> + <string name="download_error_http_data_error">Error de dades HTTP</string> + <string name="download_error_error_unknown">Error desconegut</string> + <string name="download_error_parser_exception">Error de l\'analitzador</string> + <string name="download_error_unsupported_type">Tipus de canal no suportat</string> + <string name="download_error_connection_error">Error de connexió</string> + <string name="download_error_unknown_host">Amfitrió desconegut</string> + <string name="download_error_unauthorized">Error d\'autenticació</string> + <string name="cancel_all_downloads_label">Cancel·la totes les baixades</string> + <string name="download_cancelled_msg">S\'ha cancel·lat la baixada</string> + <string name="download_report_title">Baixades completades</string> + <string name="download_error_malformed_url">URL mal formatada</string> + <string name="download_error_io_error">Error d\'E/S</string> + <string name="download_error_request_error">Error de petició</string> + <string name="download_error_db_access">Error d\'accés a la base de dades</string> + <string name="downloads_left">\u0020Baixades pendents</string> + <string name="downloads_processing">S\'estan processant les baixades</string> + <string name="download_notification_title">S\'estan baixant les dades del podcast</string> + <string name="download_report_content">%1$d baixades finalitzades, %2$d fallides</string> + <string name="download_log_title_unknown">Títol desconegut</string> + <string name="download_type_feed">Canal</string> + <string name="download_type_media">Fitxer</string> + <string name="download_type_image">Imatge</string> + <string name="download_request_error_dialog_message_prefix">S\'ha produït un error en intentar baixar el fitxer:\u0020</string> + <string name="authentication_notification_title">Cal autenticar-se</string> + <string name="authentication_notification_msg">Es necessita un usuari i una contrasenya per accedir al recurs</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Error</string> + <string name="player_stopped_msg">No s\'està reproduint res</string> + <string name="player_preparing_msg">S\'està preparant</string> + <string name="player_ready_msg">Preparat</string> + <string name="player_seeking_msg">S\'està cercant</string> + <string name="playback_error_server_died">El servidor no està operatiu</string> + <string name="playback_error_unknown">Error desconegut</string> + <string name="no_media_playing_label">No s\'està reproduint res</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">S\'està carregant</string> + <string name="playbackservice_notification_title">Podcast en reproducció</string> + <string name="unknown_media_key">AntennaPod - Control desconegut: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">Buida la cua</string> + <string name="undo">Desfés</string> + <string name="removed_from_queue">Ítem esborrat</string> + <string name="move_to_top_label">Mou al principi</string> + <string name="move_to_bottom_label">Mou al final</string> + <!--Flattr--> + <string name="flattr_auth_label">Inici de sessió a Flattr</string> + <string name="flattr_auth_explanation">Premeu el botó per iniciar el procés d\'autenticació. Quan s\'obri la pantalla d\'inici de sessió de Flattr al vostre navegador, introduïu les vostres credencials i concediu a AntennaPod els permisos de compartir mitjançant Flattr. En finalitzar el procés, tornareu automàticament a aquesta pantalla.</string> + <string name="authenticate_label">Autenticació</string> + <string name="return_home_label">Torna a l\'inici</string> + <string name="flattr_auth_success">L\'autenticació ha acabat correctament. Ja podeu compartir amb Flattr des de l\'aplicació.</string> + <string name="no_flattr_token_title">No s\'ha trobat cap testimoni Flattr</string> + <string name="no_flattr_token_notification_msg">Sembla que el compte flattr no està vinculat amb AntennaPod. Toqueu aquí per autenticar-vos.</string> + <string name="no_flattr_token_msg">Sembla que el vostre compte de Flattr no està vinculat amb AntennaPod. Podeu connectar el vostre compte Flattr amb AntennaPod per a compartir continguts des de l\'aplicació, o bé accediu a la plana web de Flattr i compartiu els continguts des d\'allà.</string> + <string name="authenticate_now_label">Autentica</string> + <string name="action_forbidden_title">L\'acció no és permesa</string> + <string name="action_forbidden_msg">AntennaPod no té permisos per executar aquesta acció. És possible que el testimoni d\'accés de Flattr per a AntennaPod hagi estat revocat. Podeu tornar-vos a autenticar amb el servei de Flattr, o podeu visitar el web del contingut directament.</string> + <string name="access_revoked_title">L\'accés ha estat revocat</string> + <string name="access_revoked_info">El testimoni d\'accés a Flattr de l\'AntennaPod s\'ha revocat correctament. Per completar el procés, heu de suprimir aquesta aplicació de la llista d\'aplicacions aprovades que trobareu a l\'apartat de configuració del compte de la plana web de Flattr.</string> + <!--Flattr--> + <string name="flattr_click_success">S\'ha compartit una cosa per Flattr!</string> + <string name="flattr_click_success_count">S\'han compartit %d coses per Flattr!</string> + <string name="flattr_click_success_queue">Compartit per Flattr: %s.</string> + <string name="flattr_click_failure_count">No s\'han pogut compartir %d coses per Flattr!</string> + <string name="flattr_click_failure">No s\'ha compartit per Flattr: %s.</string> + <string name="flattr_click_enqueued">Es compartirà per Flattr després</string> + <string name="flattring_thing">%s s\'està compartint per Flattr</string> + <string name="flattring_label">AntennaPod està compartint per Flattr</string> + <string name="flattrd_label">AntennaPod ha compartit per Flattr</string> + <string name="flattrd_failed_label">AntennaPod no ha pogut compartir per Flattr</string> + <string name="flattr_retrieving_status">S\'estan recuperant les coses compartides per Flattr</string> + <!--Variable Speed--> + <string name="download_plugin_label">Baixa el connector</string> + <string name="no_playback_plugin_title">Connector no instal·lat</string> + <string name="set_playback_speed_label">Velocitats de reproducció</string> + <!--Empty list labels--> + <string name="no_items_label">No hi ha elements a la llista.</string> + <string name="no_feeds_label">No us heu subscrit a cap canal.</string> + <!--Preferences--> + <string name="other_pref">Altres</string> + <string name="about_pref">Quant a</string> + <string name="queue_label">Cua</string> + <string name="services_label">Serveis</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Pausa la reproducció en desconnectar els auriculars.</string> + <string name="pref_followQueue_sum">Salta al següent element de la cua en acabar la reproducció</string> + <string name="playback_pref">Reproducció</string> + <string name="network_pref">Xarxa</string> + <string name="pref_autoUpdateIntervall_title">Interval d\'actualització</string> + <string name="pref_autoUpdateIntervall_sum">Especifiqueu l\'interval en què els canals s\'actualitzen de forma automàtica, o deshabiliteu la funcionalitat.</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Només baixa fitxers a través d\'una xarxa sense fils</string> + <string name="pref_followQueue_title">Reproducció continuada</string> + <string name="pref_downloadMediaOnWifiOnly_title">Baixa a través de xarxes sense fils</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Desconnexió d\'auriculars</string> + <string name="pref_mobileUpdate_title">Actualitzacions sobre xarxes mòbils</string> + <string name="pref_mobileUpdate_sum">Permet actualitzacions a través de xarxes mòbils.</string> + <string name="refreshing_label">S\'està actualitzant</string> + <string name="flattr_settings_label">Configuració de Flattr</string> + <string name="pref_flattr_auth_title">Inici de sessió Flattr</string> + <string name="pref_flattr_auth_sum">Inicieu sessió al vostre compte Flattr per compartir continguts directament des de l\'aplicació.</string> + <string name="pref_flattr_this_app_title">Compartiu aquesta aplicació amb Flattr</string> + <string name="pref_flattr_this_app_sum">Doneu suport al desenvolupament d\'AntennaPod compartint l\'aplicació a través de Flattr. Gràcies!</string> + <string name="pref_revokeAccess_title">Revoca l\'accés</string> + <string name="pref_revokeAccess_sum">Revoqueu el permís d\'accés d\'aquesta aplicació al vostre compte Flattr.</string> + <string name="pref_auto_flattr_title">Flattr automàtic</string> + <string name="pref_auto_flattr_sum">Configura la compartició automàtica per Flattr</string> + <string name="user_interface_label">Interfície d\'usuari</string> + <string name="pref_set_theme_title">Selecció de tema</string> + <string name="pref_set_theme_sum">Canvieu l\'aparença d\'AntennaPod.</string> + <string name="pref_automatic_download_title">Baixada automàtica</string> + <string name="pref_automatic_download_sum">Configureu la baixada automàtica d\'episodis.</string> + <string name="pref_autodl_wifi_filter_title">Activa el filtre de la xarxa sense fils</string> + <string name="pref_autodl_wifi_filter_sum">Permet les baixades automàtiques només per a les xarxes sense fils seleccionades.</string> + <string name="pref_episode_cache_title">Memòria d\'episodis</string> + <string name="pref_theme_title_light">Clar</string> + <string name="pref_theme_title_dark">Fosc</string> + <string name="pref_episode_cache_unlimited">Sense límits</string> + <string name="pref_update_interval_hours_plural">hores</string> + <string name="pref_update_interval_hours_singular">hora</string> + <string name="pref_update_interval_hours_manual">Manual</string> + <string name="pref_gpodnet_authenticate_title">Inici de sessió</string> + <string name="pref_gpodnet_authenticate_sum">Inicieu sessió a gpodder.net per tal de sincronitzar les vostres subscripcions.</string> + <string name="pref_gpodnet_logout_title">Surt</string> + <string name="pref_gpodnet_logout_toast">Heu sortit de la sessió</string> + <string name="pref_gpodnet_setlogin_information_title">Dades d\'inici de sessió</string> + <string name="pref_gpodnet_setlogin_information_sum">Canvia les dades d\'inici de sessió del vostre compte de gpodder.net</string> + <string name="pref_playback_speed_title">Velocitats de reproducció</string> + <string name="pref_playback_speed_sum">Personalitzeu les velocitats disponibles per a una velocitat de reproducció d\'àudio variable</string> + <string name="pref_seek_delta_title">Salta a l\'instant</string> + <string name="pref_seek_delta_sum">Salta aquesta quantitat de segons en rebobinar o en avançar ràpidament</string> + <string name="pref_gpodnet_sethostname_title">Definex nom del servidor</string> + <string name="pref_gpodnet_sethostname_use_default_host">Utilitza el servidor per defecte</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Activa la compartició automàtica per Flattr</string> + <string name="auto_flattr_after_percent">Comparteix per Flattr l\'episodi en haver-ne reproduït el %d per cent</string> + <string name="auto_flattr_ater_beginning">Comparteix per Flattr l\'episodi en haver-ne iniciat la reproducció</string> + <string name="auto_flattr_ater_end">Comparteix per Flattr l\'episodi en acabar-se\'n la reproducció</string> + <!--Search--> + <string name="search_hint">Cerca canals o episodis</string> + <string name="found_in_shownotes_label">Trobat a notes del programa</string> + <string name="found_in_chapters_label">Trobat als capítols</string> + <string name="search_status_no_results">No s\'ha trobat cap resultat</string> + <string name="search_label">Cerca</string> + <string name="found_in_title_label">Trobat al títol</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">Els fitxers OPML us permeten moure els podcasts d\'un gestor de podcasts a un altre.</string> + <string name="opml_import_explanation">Per importar un fitxer OPML, ubiqueu-lo al següent directori i premeu el botó de sota per iniciar el procés. </string> + <string name="start_import_label">Inicia la importació</string> + <string name="opml_import_label">Importació OPML</string> + <string name="opml_directory_error">Error!</string> + <string name="reading_opml_label">S\'està llegint el fitxer OPML</string> + <string name="opml_reader_error">S\'ha produït un error en llegir el document OPML:</string> + <string name="opml_import_error_dir_empty">El directori d\'importacions és buit.</string> + <string name="select_all_label">Selecciona-ho tot</string> + <string name="deselect_all_label">Deselecciona-ho tot</string> + <string name="choose_file_to_import_label">Seleccioneu el fitxer a importar</string> + <string name="opml_export_label">Exportació OPML</string> + <string name="exporting_label">S\'està exportant...</string> + <string name="export_error_label">Error d\'exportació</string> + <string name="opml_export_success_title">S\'ha exportat l\'OPML correctament.</string> + <string name="opml_export_success_sum">El fitxer OPML s\'ha escrit a:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Defineix un temporitzador</string> + <string name="disable_sleeptimer_label">Desactiva el temporitzador</string> + <string name="enter_time_here_label">Introduïu l\'hora</string> + <string name="sleep_timer_label">Temporitzador</string> + <string name="time_left_label">Temps restant:\u0020</string> + <string name="time_dialog_invalid_input">L\'entrada no és vàlida, ja que el temps ha de ser un nombre i no ho és</string> + <string name="time_unit_seconds">segons</string> + <string name="time_unit_minutes">minuts</string> + <string name="time_unit_hours">hores</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">CATEGORIES</string> + <string name="gpodnet_toplist_header">TOP PODCASTS</string> + <string name="gpodnet_suggestions_header">SUGGERÈNCIES</string> + <string name="gpodnet_search_hint">Cerca a gpodder.net</string> + <string name="gpodnetauth_login_title">Inici de sessió</string> + <string name="gpodnetauth_login_descr">Benvingut al procés d\'inici de sessió a gpodder.net. Primerament, introduïu la informació d\'accés:</string> + <string name="gpodnetauth_login_butLabel">Entra</string> + <string name="username_label">Nom d\'usuari</string> + <string name="password_label">Contrasenya</string> + <string name="gpodnetauth_device_title">Selecció de dispositiu</string> + <string name="gpodnetauth_device_descr">Per a utilitzar gpodder.net, creeu un nou dispositiu o seleccioneu-ne un d\'existent:</string> + <string name="gpodnetauth_device_deviceID">ID de dispositiu:\u0020</string> + <string name="gpodnetauth_device_caption">Llegenda</string> + <string name="gpodnetauth_device_butCreateNewDevice">Crea nou dispositiu</string> + <string name="gpodnetauth_device_chooseExistingDevice">Seleccioneu un dispositiu existent:</string> + <string name="gpodnetauth_device_errorEmpty">L\'ID de dispositiu no pot ser buit</string> + <string name="gpodnetauth_device_errorAlreadyUsed">L\'ID de dispositiu ja existeix</string> + <string name="gpodnetauth_device_butChoose">Selecciona</string> + <string name="gpodnetauth_finish_title">Heu iniciat la sessió!</string> + <string name="gpodnetauth_finish_descr">Felicitats! El vostre compte de gpodder.net s\'ha enllaçat amb el dispositiu. D\'ara endavant, AntennaPod sincronitzarà automàticament les subscripcions del dispositiu al vostre compte.</string> + <string name="gpodnetauth_finish_butsyncnow">Sincronitza ara</string> + <string name="gpodnetauth_finish_butgomainscreen">Vés a la pantalla principal</string> + <string name="gpodnetsync_auth_error_title">Error d\'autenticació a gpodder.net</string> + <string name="gpodnetsync_auth_error_descr">Nom d\'usuari o contrasenya incorrectes</string> + <string name="gpodnetsync_error_title">Error de sincronització a gpodder.net</string> + <string name="gpodnetsync_error_descr">S\'ha produït un error durant la sincronització:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Carpeta seleccionada:</string> + <string name="create_folder_label">Crea una carpeta</string> + <string name="choose_data_directory">Selecció de la carpeta de dades</string> + <string name="create_folder_msg">Voleu crear una nova carpeta amb el nom \"%1$s\"?</string> + <string name="create_folder_success">S\'ha creat la nova carpeta</string> + <string name="create_folder_error_no_write_access">No es pot escriure dins d\'aquesta carpeta</string> + <string name="create_folder_error_already_exists">La carpeta ja existeix</string> + <string name="create_folder_error">No s\'ha pogut crear la carpeta</string> + <string name="folder_not_empty_dialog_title">La carpeta no és buida</string> + <string name="folder_not_empty_dialog_msg">La carpeta que heu seleccionat no és buida. Les baixades i altres fitxers es copiaran directament a aquesta carpeta. Voleu continuar?</string> + <string name="set_to_default_folder">Selecciona la carpeta per defecte</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Pausa la reproducció en lloc de baixar el volum quan una altra app necessiti reproduir sons</string> + <string name="pref_pausePlaybackForFocusLoss_title">Pausa en interrompre</string> + <!--Online feed view--> + <string name="subscribe_label">Subscriu</string> + <string name="subscribed_label">Subscrit</string> + <string name="downloading_label">S\'està baixant...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Mostra els capítols</string> + <string name="show_shownotes_label">Mostra les notes del programa</string> + <string name="show_cover_label">Mosta la imatge</string> + <string name="rewind_label">Rebobina</string> + <string name="fast_forward_label">Avança ràpidament</string> + <string name="media_type_audio_label">Àudio</string> + <string name="media_type_video_label">Vídeo</string> + <string name="navigate_upwards_label">Navega cap amunt</string> + <string name="butAction_label">Més accions</string> + <string name="status_playing_label">S\'està reproduïnt l\'episodi</string> + <string name="status_downloading_label">S\'està baixant l\'episodi</string> + <string name="status_downloaded_label">S\'ha baixat l\'episodi</string> + <string name="status_unread_label">L\'element és nou</string> + <string name="in_queue_label">S\'ha afegit l\'episodi a la cua</string> + <string name="new_episodes_count_label">Nombre d\'episodis nous</string> + <string name="in_progress_episodes_count_label">Nombre d\'episodis que heu començat a escoltar</string> + <string name="drag_handle_content_description">Arrossegueu l\'element per canviar-ne la posició</string> + <!--Feed information screen--> + <string name="authentication_label">Autenticació</string> + <string name="authentication_descr">Canvieu el nom d\'usuari i contrasenya per a aquest podcast i els seus episodis.</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">S\'estan important les subscripcions des de les apps de propòsit únic...</string> +</resources> diff --git a/core/src/main/res/values-cs-rCZ/strings.xml b/core/src/main/res/values-cs-rCZ/strings.xml new file mode 100644 index 000000000..a15ad6248 --- /dev/null +++ b/core/src/main/res/values-cs-rCZ/strings.xml @@ -0,0 +1,342 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Kanály</string> + <string name="add_feed_label">Přidat podcast</string> + <string name="podcasts_label">PODCASTY</string> + <string name="episodes_label">EPIZODY</string> + <string name="new_episodes_label">Nové epizody</string> + <string name="all_episodes_label">Všechny epizody</string> + <string name="new_label">Nový</string> + <string name="waiting_list_label">Seznam nepřečtených</string> + <string name="settings_label">Nastavení</string> + <string name="add_new_feed_label">Přidat podcast</string> + <string name="downloads_label">Stahování</string> + <string name="downloads_running_label">Právě běží</string> + <string name="downloads_completed_label">Dokončeno</string> + <string name="downloads_log_label">Log</string> + <string name="cancel_download_label">Zrušit stahování</string> + <string name="playback_history_label">Historie přehrávání</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">Login pro gpodder.net</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Nedávno zveřejněné</string> + <string name="episode_filter_label">Zobrazit pouze nové epizody</string> + <!--Main activity--> + <string name="drawer_open">Otevřít menu</string> + <string name="drawer_close">Zavřít menu</string> + <!--Webview actions--> + <string name="open_in_browser_label">Otevřít v prohlížeči</string> + <string name="copy_url_label">Kopírovat URL</string> + <string name="share_url_label">Sdílet URL</string> + <string name="copied_url_msg">URL zkopírováno do schránky.</string> + <string name="go_to_position_label">Jít na pozici</string> + <!--Playback history--> + <string name="clear_history_label">Smazat historii</string> + <!--Other--> + <string name="confirm_label">Potvrdit</string> + <string name="cancel_label">Zrušit</string> + <string name="author_label">Autor</string> + <string name="language_label">Jazyk</string> + <string name="podcast_settings_label">Nastavení</string> + <string name="cover_label">Obrázek</string> + <string name="error_label">Chyba</string> + <string name="error_msg_prefix">Došlo k chybě:</string> + <string name="refresh_label">Obnovit</string> + <string name="external_storage_error_msg">Není dostupné žádné externí uložiště. Pro správnou funkci aplikace se prosím ujistěte, že je připojeno externí úložiště.</string> + <string name="chapters_label">Kapitoly</string> + <string name="shownotes_label">Poznámky</string> + <string name="description_label">Popis</string> + <string name="most_recent_prefix">Poslední epizoda:\u0020</string> + <string name="episodes_suffix">\u0020epizod</string> + <string name="length_prefix">Délka:\u0020</string> + <string name="size_prefix">Velikost:\u0020</string> + <string name="processing_label">Zpracovávám</string> + <string name="loading_label">Načítám...</string> + <string name="save_username_password_label">Uložit uživatelské jméno a heslo</string> + <string name="close_label">Zavřít</string> + <string name="retry_label">Zkusit znovu</string> + <string name="auto_download_label">Zahrnout do automaticky stahovaných</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">URL kanálu</string> + <string name="etxtFeedurlHint">URL nebo webová stránka kanálu</string> + <string name="txtvfeedurl_label">Přidat podcast pomocí URL</string> + <string name="podcastdirectories_label">Najít podcast v seznamu</string> + <string name="podcastdirectories_descr">Můžete vyhledávat nové podcasty podle jména, kategorie nebo popularity v seznamu gpodder.net.</string> + <string name="browse_gpoddernet_label">Prohledávat gpodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Označit vše jako přečtené</string> + <string name="mark_all_read_msg">Označit všechny epizody jako přečtené</string> + <string name="show_info_label">Informace o zdroji</string> + <string name="remove_feed_label">Odstranit podcast</string> + <string name="share_link_label">Sdílet odkaz</string> + <string name="share_source_label">Sdílet adresu kanálu</string> + <string name="feed_delete_confirmation_msg">Prosím potvrďte, že chcete smazat tento kanál včetně všech stažených epizod.</string> + <string name="feed_remover_msg">Odstranit kanál</string> + <string name="load_complete_feed">Obnovit kompletní kanál</string> + <!--actions on feeditems--> + <string name="download_label">Stáhnout</string> + <string name="play_label">Přehrát</string> + <string name="pause_label">Pozastavit</string> + <string name="stop_label">Zastavit</string> + <string name="stream_label">Vysílat</string> + <string name="remove_label">Odstranit</string> + <string name="remove_episode_lable">Odstranit epizodu</string> + <string name="mark_read_label">Označit jako přečtené</string> + <string name="mark_unread_label">Označit jako nepřečtené</string> + <string name="add_to_queue_label">Přidat do fronty</string> + <string name="remove_from_queue_label">Odebrat z fronty</string> + <string name="visit_website_label">Navštívit stránku</string> + <string name="support_label">Flattrovat</string> + <string name="enqueue_all_new">Vše do fronty</string> + <string name="download_all">Stáhnout vše</string> + <string name="skip_episode_label">Přeskočit epizodu</string> + <!--Download messages and labels--> + <string name="download_successful">úspěšné</string> + <string name="download_failed">selhalo</string> + <string name="download_pending">Čeká na stažení</string> + <string name="download_running">Probíhá stahování</string> + <string name="download_error_device_not_found">Úložné zařízení nenalezeno</string> + <string name="download_error_insufficient_space">Nedostatek volného místa</string> + <string name="download_error_file_error">Souborová chyba</string> + <string name="download_error_http_data_error">HTTP chyba</string> + <string name="download_error_error_unknown">Neznámá chyba</string> + <string name="download_error_parser_exception">Výjimka parseru</string> + <string name="download_error_unsupported_type">Nepodporovaný typ kanálu</string> + <string name="download_error_connection_error">Chyba spojení</string> + <string name="download_error_unknown_host">Neznámý host</string> + <string name="download_error_unauthorized">Chyba přihlášení</string> + <string name="cancel_all_downloads_label">Zrušit všechna stahování</string> + <string name="download_cancelled_msg">Stahování zrušeno</string> + <string name="download_report_title">Všechna stahování dokončena</string> + <string name="download_error_malformed_url">Chybné URL</string> + <string name="download_error_io_error">IO chyba</string> + <string name="download_error_request_error">Chyba požadavku</string> + <string name="download_error_db_access">Chyba přístupu do databáze</string> + <string name="downloads_left">\u0020Stahování zbývá</string> + <string name="downloads_processing">Probíhá stahování</string> + <string name="download_notification_title">Stahuji podcast data</string> + <string name="download_report_content">%1$d úspěšných stahování, %2$d selhalo</string> + <string name="download_log_title_unknown">Neznámý název</string> + <string name="download_type_feed">Kanál</string> + <string name="download_type_media">Soubor</string> + <string name="download_type_image">Obrázek</string> + <string name="download_request_error_dialog_message_prefix">Nastala chyba při pokusu o stažení souboru:\u0020</string> + <string name="authentication_notification_title">Vyžadováno ověření</string> + <string name="authentication_notification_msg">Zdroj který jste vybrali vyžaduje zadání uživatelského jména a hesla</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Chyba!</string> + <string name="player_stopped_msg">Žádné probíhající přehrávání</string> + <string name="player_preparing_msg">Připravuji</string> + <string name="player_ready_msg">Připraven</string> + <string name="player_seeking_msg">Přetáčím</string> + <string name="playback_error_server_died">Server nereaguje</string> + <string name="playback_error_unknown">Neznámá chyba</string> + <string name="no_media_playing_label">Žádné probíhající přehrávání</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Načítání</string> + <string name="playbackservice_notification_title">Přehrávaný podcast</string> + <string name="unknown_media_key">AntennaPod - Neznámý klíč médií: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">Vyprázdnit frontu</string> + <string name="undo">Zpět</string> + <string name="removed_from_queue">Položka odebrána</string> + <string name="move_to_top_label">Posunout na začátek</string> + <string name="move_to_bottom_label">Posunout na konec</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattr přihlášení</string> + <string name="flattr_auth_explanation">Stiskněte následující tlačítko pro spuštění autentizačního procesu. Budete přesměrováni na přihlašovací obrazovku flattru a vyzváni k potvrzení udělení práv pro použití flattru aplikací AntennaPod. Po udělení práv se automaticky vrátíte na tuto obrazovku.</string> + <string name="authenticate_label">Přihlásit</string> + <string name="return_home_label">Návrat domů</string> + <string name="flattr_auth_success">Úspěšně přihlášen. Nyní můžete využít flattru přímo v aplikaci. </string> + <string name="no_flattr_token_title">Nenalezen Flattr token</string> + <string name="no_flattr_token_notification_msg">Váš flattr účet není propojen s AntennaPod. Pro ověření tapněte sem.</string> + <string name="no_flattr_token_msg">Váš flattr učet není napojen do AntenaPodu. Můžete buďto napojit váš flattr účet do AntennaPodu a využít flattru přímo v aplikaci a nebo použít flattr přímo na webových stránkách zdroje v prohlížeči. </string> + <string name="authenticate_now_label">Přihlásit</string> + <string name="action_forbidden_title">Akce zakázána</string> + <string name="action_forbidden_msg">AntennaPod nemá oprávnění pro tuto akci. Důvodem může být revokování přístupového tokenu AntennaPodu k vašemu účtu. Přístup můžete obnovit nebo využít prohlížeče k návštěvě stránky zdroje.</string> + <string name="access_revoked_title">Přístup ukončen</string> + <string name="access_revoked_info">Úspěšně revokován přístup AntennPodu k vašemu účtu. Pro dokončení tohoto procesu je ještě zapotřebí na stránkách flattru odebrat z vašeho účtu AntennaPod ze seznamu povolených aplikací.</string> + <!--Flattr--> + <string name="flattring_thing">Flattruji %s</string> + <string name="flattring_label">AntennaPod flattruje</string> + <string name="flattrd_label">AntennaPod flattroval</string> + <string name="flattrd_failed_label">AntennaPod flattr selhal</string> + <!--Variable Speed--> + <string name="download_plugin_label">Stáhnout plugin</string> + <string name="no_playback_plugin_title">Plugin není nainstalován</string> + <string name="no_playback_plugin_msg">Pro možnost měnit rychlost přehrávání musí být nainstalovaná knihovna třetí strany.\n\nKlikněte na \"Stáhnout Plugin\" ke stažení pluginu z Obchodu Play.\n\nAntennaPod nenese žádnou odpovědnost za jakékoliv problémy způsobené tímto pluginem.</string> + <string name="set_playback_speed_label">Rychlosti přehrávání</string> + <!--Empty list labels--> + <string name="no_items_label">Žádné položky v seznamu.</string> + <string name="no_feeds_label">Zatím nebyly přidány žádné kanály.</string> + <!--Preferences--> + <string name="other_pref">Ostatní</string> + <string name="about_pref">O aplikaci</string> + <string name="queue_label">Fronta</string> + <string name="services_label">Služby</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Při odpojení sluchátek automaticky pozastavit přehrávání.</string> + <string name="pref_followQueue_sum">Po přehrání položky z fronty přejít automaticky na další</string> + <string name="playback_pref">Přehrávání</string> + <string name="network_pref">Síť</string> + <string name="pref_autoUpdateIntervall_title">Interval aktualizace zdrojů</string> + <string name="pref_autoUpdateIntervall_sum">Udává interval, ve kterém se kanály automaticky aktualizují nebo tuto funkci deaktivuje</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Stahovat soubory pouze pomocí WiFi</string> + <string name="pref_followQueue_title">Kontinuální přehrávání</string> + <string name="pref_downloadMediaOnWifiOnly_title">WiFi stahování</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Odpojení sluchátek</string> + <string name="pref_mobileUpdate_title">Mobilní aktualizace</string> + <string name="pref_mobileUpdate_sum">Povolit aktualizace pomocí mobilního připojení</string> + <string name="refreshing_label">Obnovuji</string> + <string name="flattr_settings_label">Nastavení Flattr</string> + <string name="pref_flattr_auth_title">Flattr přihlášení</string> + <string name="pref_flattr_auth_sum">Přihlásit se k flattr účtu a umožnit flattrování přímo z aplikace.</string> + <string name="pref_flattr_this_app_title">Flattrovat tuto aplikaci</string> + <string name="pref_flattr_this_app_sum">Podpořit vývoj AntennaPodu na flatteru. Děkujeme!</string> + <string name="pref_revokeAccess_title">Odebrat přístup</string> + <string name="pref_revokeAccess_sum">Odebere aplikaci přístupová práva k vašemu flattr účtu.</string> + <string name="pref_auto_flattr_title">Automatický Flattr</string> + <string name="pref_auto_flattr_sum">Nastavit automatické flattrování</string> + <string name="user_interface_label">Uživatelské rozhraní</string> + <string name="pref_set_theme_title">Vybrat motiv</string> + <string name="pref_set_theme_sum">Změnit vzhled AntennaPod.</string> + <string name="pref_automatic_download_title">Automatické stahování</string> + <string name="pref_automatic_download_sum">Nastavení automatického stahování epizod.</string> + <string name="pref_autodl_wifi_filter_title">Zapnout Wi-Fi filtr</string> + <string name="pref_autodl_wifi_filter_sum">Povolit automatické stahování pouze pomocí vybraných Wi-Fi sítí.</string> + <string name="pref_episode_cache_title">Historie epizod</string> + <string name="pref_theme_title_light">Světlý</string> + <string name="pref_theme_title_dark">Tmavý</string> + <string name="pref_episode_cache_unlimited">Bez omezení</string> + <string name="pref_update_interval_hours_plural">hodin</string> + <string name="pref_update_interval_hours_singular">hodina</string> + <string name="pref_update_interval_hours_manual">Ručně</string> + <string name="pref_gpodnet_authenticate_title">Přihlásit</string> + <string name="pref_gpodnet_authenticate_sum">Přihlašte se pomocí vašeho gpodder.net účtu pro synchronizaci odebíraných podcastů. </string> + <string name="pref_gpodnet_logout_title">Odhlásit</string> + <string name="pref_gpodnet_logout_toast">Úspěšně odhlášeno</string> + <string name="pref_gpodnet_setlogin_information_title">Změna přihlašovacích údajů</string> + <string name="pref_gpodnet_setlogin_information_sum">Změní přihlašovací údaje k vašemu gpodder.net účtu.</string> + <string name="pref_playback_speed_title">Rychlosti přehrávání</string> + <string name="pref_playback_speed_sum">Přizpůsobení rychlosti je dostupné pro přehrávání zvuku různými rychlostmi</string> + <string name="pref_seek_delta_title">Čas poskočení</string> + <string name="pref_seek_delta_sum">Poskočí o vybraný počet sekund při posunu zpět nebo vpřed</string> + <string name="pref_gpodnet_sethostname_title">Nastavit hostname</string> + <string name="pref_gpodnet_sethostname_use_default_host">Použít přednastaveného hosta</string> + <string name="pref_expandNotify_title">Rozšířené upozornění</string> + <string name="pref_expandNotify_sum">Vždy zobrazovat tlačítka pro přehrávání v upozornění.</string> + <string name="pref_persistNotify_title">Pevné ovládání přehrávání</string> + <string name="pref_persistNotify_sum">Zachovat upozornění a ovládání na obrazovce uzamčení i při pozastaveném přehrávání.</string> + <string name="pref_expand_notify_unsupport_toast">Verze Androidu nižší než 4.1 nepodporují rozšířená upozornění.</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Povolit automatické flattrování</string> + <string name="auto_flattr_after_percent">Flattrovat díl jakmile bude odehráno %d procent</string> + <string name="auto_flattr_ater_beginning">Flattrovat díl po začátku přehrávání</string> + <string name="auto_flattr_ater_end">Flattrovat díl po konci přehrávání</string> + <!--Search--> + <string name="search_hint">Hledat kanály a epizody</string> + <string name="found_in_shownotes_label">Nalezeno v poznámkách k show</string> + <string name="found_in_chapters_label">Nalezeno v kapitolách</string> + <string name="search_status_no_results">Žádné výsledky</string> + <string name="search_label">Vyhledat</string> + <string name="found_in_title_label">Nalezeno v názvu</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">OPML soubory umožňují přenést vaše podcasty z jednoho přehrávače na jiný.</string> + <string name="opml_import_explanation">Pro načtení OPML souboru je třeba ho nejdříve umístit do následujícího adresáře a poté stisknout tlačítko pro zahájení procesu importu. </string> + <string name="start_import_label">Importovat</string> + <string name="opml_import_label">OPML import</string> + <string name="opml_directory_error">CHYBA!</string> + <string name="reading_opml_label">Načítání OPML souboru</string> + <string name="opml_reader_error">Nastala chyba při čtení OPML souboru:</string> + <string name="opml_import_error_dir_empty">Adresář importu je prázdný.</string> + <string name="select_all_label">Označit vše</string> + <string name="deselect_all_label">Zrušit výběr</string> + <string name="choose_file_to_import_label">Vyberte soubor k importování</string> + <string name="opml_export_label">OPML export</string> + <string name="exporting_label">Exportuji...</string> + <string name="export_error_label">Chyba exportu</string> + <string name="opml_export_success_title">OPML export byl úspěšný.</string> + <string name="opml_export_success_sum">OPML soubor byl zapsán do:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Nastavit časovač vypnutí</string> + <string name="disable_sleeptimer_label">Deaktivovat časovač vypnutí</string> + <string name="enter_time_here_label">Zadejte čas</string> + <string name="sleep_timer_label">Časovač vypnutí</string> + <string name="time_left_label">Zbývající čas:\u0020</string> + <string name="time_dialog_invalid_input">Neplatný vstup, musí být zadáno celé číslo</string> + <string name="time_unit_seconds">sekund</string> + <string name="time_unit_minutes">minut</string> + <string name="time_unit_hours">hodin</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">KATEGORIE</string> + <string name="gpodnet_toplist_header">TOP PODCASTY</string> + <string name="gpodnet_suggestions_header">DOPORUČENÉ</string> + <string name="gpodnet_search_hint">Vyhledat na gpodder.net</string> + <string name="gpodnetauth_login_title">Přihlásit</string> + <string name="gpodnetauth_login_descr">Vítejte do průvodce přihlášením ke gpodder.net účtu. Zadejte vaše přihlašovací údaje:</string> + <string name="gpodnetauth_login_butLabel">Přihlásit</string> + <string name="gpodnetauth_login_register">Pokud ještě nemáte účet, můžete ho vytvořit zde:\nhttps://gpodder.net/register/</string> + <string name="username_label">Uživatelské jméno</string> + <string name="password_label">Heslo</string> + <string name="gpodnetauth_device_title">Výběr zařízení</string> + <string name="gpodnetauth_device_descr">Vytvořte nové nebo vyberte již existující zařízení pro použití s vašim gpodder.net účtem.</string> + <string name="gpodnetauth_device_deviceID">ID zařízení:\u0020</string> + <string name="gpodnetauth_device_caption">Nadpis</string> + <string name="gpodnetauth_device_butCreateNewDevice">Vytvořit nové zařízení</string> + <string name="gpodnetauth_device_chooseExistingDevice">Vybrat existující zařízení:</string> + <string name="gpodnetauth_device_errorEmpty">ID zařízení nesmí být prázdné</string> + <string name="gpodnetauth_device_errorAlreadyUsed">ID zařízení je již obsazeno</string> + <string name="gpodnetauth_device_butChoose">Vybrat</string> + <string name="gpodnetauth_finish_title">Úspěšně přihlášeno!</string> + <string name="gpodnetauth_finish_descr">Gratulujeme! Váš gpodder.net účet je nyní úspěšně propojen s vaším zařízením. AntennaPod bude automaticky synchronizovat odebírané podcasty s nastaveným účtem na gpodder.net.</string> + <string name="gpodnetauth_finish_butsyncnow">Synchronizovat nyní</string> + <string name="gpodnetauth_finish_butgomainscreen">Přejít na hlavní obrazovku</string> + <string name="gpodnetsync_auth_error_title">gpodder.net autentizace selhala</string> + <string name="gpodnetsync_auth_error_descr">Špatné přihlašovací jméno nebo heslo</string> + <string name="gpodnetsync_error_title">gpodder.net synchronizace selhala</string> + <string name="gpodnetsync_error_descr">V průběhu synchronizace nastala chyba:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Vybraný adresář:</string> + <string name="create_folder_label">Vytvořit adresář</string> + <string name="choose_data_directory">Vybrat umístění dat</string> + <string name="create_folder_msg">Vytvořit adresář \"%1$s\"?</string> + <string name="create_folder_success">Nový adresář vytvořen</string> + <string name="create_folder_error_no_write_access">Nelze zapisovat do adresáře</string> + <string name="create_folder_error_already_exists">Adresář již existuje</string> + <string name="create_folder_error">Nelze vytvořit adresář</string> + <string name="folder_not_empty_dialog_title">Adresář není prázdný</string> + <string name="folder_not_empty_dialog_msg">Vybraný adresář není prázdný. Stažená media a ostatní soubory budou umístěny přímo do tohoto adresáře. Přesto pokračovat?</string> + <string name="set_to_default_folder">Vybrat hlavní adresář</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Místo snížení hlasitosti pozastavit přehrávání v případě, že jiná aplikace přehrává zvuk.</string> + <string name="pref_pausePlaybackForFocusLoss_title">Automatické pozastavení přehrávání</string> + <!--Online feed view--> + <string name="subscribe_label">Odebírat</string> + <string name="subscribed_label">Odebíráno</string> + <string name="downloading_label">Stahuji...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Zobrazit kapitoly</string> + <string name="show_shownotes_label">Zobrazit poznámky o pořadu</string> + <string name="show_cover_label">Zobrazit obrázek</string> + <string name="rewind_label">Posunout zpět</string> + <string name="fast_forward_label">Posunout vpřed</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Video</string> + <string name="navigate_upwards_label">Navigovat vzhůru</string> + <string name="butAction_label">Více možností</string> + <string name="status_playing_label">Epizoda se právě přehrává</string> + <string name="status_downloading_label">Epizoda je právě stahována</string> + <string name="status_downloaded_label">Epizoda je stažena</string> + <string name="status_unread_label">Nová položka</string> + <string name="in_queue_label">Epizoda je ve frontě</string> + <string name="new_episodes_count_label">Počet nových epizod</string> + <string name="in_progress_episodes_count_label">Počet epizod u kterých byl započat poslech</string> + <string name="drag_handle_content_description">Tahem změnit pozici této položky</string> + <string name="load_next_page_label">Načíst další stranu</string> + <!--Feed information screen--> + <string name="authentication_label">Ověření</string> + <string name="authentication_descr">Změnit uživatelské jméno a heslo pro tento podcast a jeho epizody.</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Importuji odběry z jednoúčelových aplikací...</string> +</resources> diff --git a/core/src/main/res/values-da/strings.xml b/core/src/main/res/values-da/strings.xml new file mode 100644 index 000000000..4f7ea5cf9 --- /dev/null +++ b/core/src/main/res/values-da/strings.xml @@ -0,0 +1,349 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Feeds</string> + <string name="add_feed_label">Tilføj podcast</string> + <string name="podcasts_label">PODCASTS</string> + <string name="episodes_label">EPISODER</string> + <string name="new_episodes_label">Nye episoder</string> + <string name="all_episodes_label">Alle episoder</string> + <string name="new_label">Nye</string> + <string name="waiting_list_label">Venteliste</string> + <string name="settings_label">Indstillinger</string> + <string name="add_new_feed_label">Tilføj podcast</string> + <string name="downloads_label">Downloads</string> + <string name="downloads_running_label">Kører</string> + <string name="downloads_completed_label">Fuldført</string> + <string name="downloads_log_label">Log</string> + <string name="cancel_download_label">Annuller Download</string> + <string name="playback_history_label">Afspilnings historik</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net login</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Nyligt udgivet</string> + <string name="episode_filter_label">Hvis kun nye episoder</string> + <!--Main activity--> + <string name="drawer_open">Åben menu</string> + <string name="drawer_close">Luk menu</string> + <!--Webview actions--> + <string name="open_in_browser_label">Åben i browser</string> + <string name="copy_url_label">Kopier URL</string> + <string name="share_url_label">Del URL</string> + <string name="copied_url_msg">URL kopieret til udklipsholderen.</string> + <string name="go_to_position_label">Gå til denne position</string> + <!--Playback history--> + <string name="clear_history_label">Fjern historik</string> + <!--Other--> + <string name="confirm_label">Bekræft</string> + <string name="cancel_label">Annuller</string> + <string name="author_label">Forfatter</string> + <string name="language_label">Sprog</string> + <string name="podcast_settings_label">Indstillinger </string> + <string name="cover_label">Billed</string> + <string name="error_label">Fejl</string> + <string name="error_msg_prefix">En fejl er opstået:</string> + <string name="refresh_label">Opdater</string> + <string name="external_storage_error_msg">Ingen ekstern harddisk er tilgængelig. Vær venlig at sørge for at den eksterne hukommelse er monteret så app\'en kan fungere korrekt.</string> + <string name="chapters_label">Kapitler</string> + <string name="shownotes_label">Afsnitsnoter</string> + <string name="description_label">Beskrivelse</string> + <string name="most_recent_prefix">Seneste episoder:\u0020</string> + <string name="episodes_suffix">\u0020episoder</string> + <string name="length_prefix">Længde:\u0020</string> + <string name="size_prefix">Størrelse:\u0020</string> + <string name="processing_label">Behandler</string> + <string name="loading_label">Indlæser...</string> + <string name="save_username_password_label">Gem brugernavn og kodeord</string> + <string name="close_label">Luk</string> + <string name="retry_label">Prøv igen</string> + <string name="auto_download_label">Inkluder i automatiske downloads</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">Feed URL</string> + <string name="etxtFeedurlHint">URL af feed eller webside</string> + <string name="txtvfeedurl_label">Tilføj Podcast med URL</string> + <string name="podcastdirectories_label">Find podcast i mappen</string> + <string name="podcastdirectories_descr">Du kan søge efter nye podcasts efter navn, kategori eller popularitet i gpodder.net biblioteket</string> + <string name="browse_gpoddernet_label">Gennemse gpodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Marker alle som læst</string> + <string name="mark_all_read_msg">Marker alle episoder som læst</string> + <string name="show_info_label">Vis information</string> + <string name="remove_feed_label">Fjern podcast</string> + <string name="share_link_label">Del webside link</string> + <string name="share_source_label">Del feed link</string> + <string name="feed_delete_confirmation_msg">Bekræft venligst at du vil fjerne dette feed og ALLE episoder du har downloadet fra dette feed.</string> + <string name="feed_remover_msg">Fjerner feed</string> + <string name="load_complete_feed">Opdater hele feed\'et</string> + <!--actions on feeditems--> + <string name="download_label">Hent</string> + <string name="play_label">Afspil</string> + <string name="pause_label">Pause</string> + <string name="stop_label">Stop</string> + <string name="stream_label">Stream</string> + <string name="remove_label">Fjern</string> + <string name="remove_episode_lable">Fjern episode</string> + <string name="mark_read_label">Marker som læst</string> + <string name="mark_unread_label">Marker som ulæst</string> + <string name="add_to_queue_label">Tilføj til kø</string> + <string name="remove_from_queue_label">Fjern fra kø</string> + <string name="visit_website_label">Besøg webside</string> + <string name="support_label">Flattr dette</string> + <string name="enqueue_all_new">Sæt alle i kø</string> + <string name="download_all">Download alle</string> + <string name="skip_episode_label">Spring episode over</string> + <!--Download messages and labels--> + <string name="download_successful">succesfuld</string> + <string name="download_failed">fejlet</string> + <string name="download_pending">Download afventer</string> + <string name="download_running">Download kører</string> + <string name="download_error_device_not_found">Kan ikke finde lager-enhed</string> + <string name="download_error_insufficient_space">Ikke nok plads</string> + <string name="download_error_file_error">Fil fejl</string> + <string name="download_error_http_data_error">HTTP data fejl</string> + <string name="download_error_error_unknown">Ukendt fejl</string> + <string name="download_error_parser_exception">Parser undtagelse</string> + <string name="download_error_unsupported_type">Feed type er ikke understøttet</string> + <string name="download_error_connection_error">Forbindelsesfejl</string> + <string name="download_error_unknown_host">Ukendt vært</string> + <string name="download_error_unauthorized">Godkendelses fejl</string> + <string name="cancel_all_downloads_label">Annuller alle downloads</string> + <string name="download_cancelled_msg">Download afbrudt</string> + <string name="download_report_title">Downloads afsluttet</string> + <string name="download_error_malformed_url">Misdannet URL</string> + <string name="download_error_io_error">IO fejl</string> + <string name="download_error_request_error">Anmode fejl</string> + <string name="download_error_db_access">Adgangsfejl i database</string> + <string name="downloads_left">\u0020Downloads tilbage</string> + <string name="downloads_processing">Bearbejder downloads</string> + <string name="download_notification_title">Downloader podcast data</string> + <string name="download_report_content">%1$d downloads lykkedes, %2$d fejlet</string> + <string name="download_log_title_unknown">Ukendt titel</string> + <string name="download_type_feed">Feed</string> + <string name="download_type_media">Medie fil</string> + <string name="download_type_image">Billede</string> + <string name="download_request_error_dialog_message_prefix">En fejl opstod under download af filen:\u0020</string> + <string name="authentication_notification_title">Godkendelses krævet</string> + <string name="authentication_notification_msg">Den ressource du efterspurgte kræver et brugernavn og et password</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Fejl!</string> + <string name="player_stopped_msg">Ingen medier afspiller</string> + <string name="player_preparing_msg">Forbereder</string> + <string name="player_ready_msg">Klar</string> + <string name="player_seeking_msg">Søger</string> + <string name="playback_error_server_died">Server døde</string> + <string name="playback_error_unknown">Ukendt fejl</string> + <string name="no_media_playing_label">Ingen medier afspiller</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Buffering</string> + <string name="playbackservice_notification_title">Afspiller podcast</string> + <string name="unknown_media_key">AntennaPod - ukendt medie nøgle: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">Fjern kø</string> + <string name="undo">Fortryd</string> + <string name="removed_from_queue">Emne slettet</string> + <string name="move_to_top_label">Flyt til toppen</string> + <string name="move_to_bottom_label">Flyt til bunden</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattr log ind</string> + <string name="flattr_auth_explanation">Tryk på knappen nedenfor for at starte godkendelsesprocessen. Du vil blive ført til flattr log ind siden i din browser og bedt om at give AntennaPod tilladelse til at flattr emner. Efter at du har givet tilladelsen vil du automatisk vende tilbage til denne side.</string> + <string name="authenticate_label">Godkender</string> + <string name="return_home_label">Retuner hjem</string> + <string name="flattr_auth_success">Godkendelse lykkedes! Du kan nu flattr emner inde i app\'en.</string> + <string name="no_flattr_token_title">Ingen flattr polet fundet</string> + <string name="no_flattr_token_notification_msg">Din flattr konto ser ikke ud til at være forbundet med AntennaPod. Tryk her for at forbinde.</string> + <string name="no_flattr_token_msg">Din flattr konto er vidst ikke forbundet til AntennaPod. Du kan forbinde din konto til AntennaPod for at flattr emner inde i app\'en, eller besøge websiden af mediet for at flattr det der.</string> + <string name="authenticate_now_label">Godkender</string> + <string name="action_forbidden_title">Forbudt handling</string> + <string name="action_forbidden_msg">AntennaPod har ikke tilladelse til denne handling. Årsagen kunne være at adgangspoletten for AntennaPod til din konto er blevet tilbagekaldt. Du kan enten godkende den igen eller besøge websiden for mediet istedet.</string> + <string name="access_revoked_title">Adgang tilbagekaldt</string> + <string name="access_revoked_info">Du har succesfuldt tilbagekaldt AntennaPods adgangs polet til din konto. For at fuldføre processen skal du fjerne denne app fra listen af godkendte applikationer i din kontos indstillinger på flattr\'s hjemmeside.</string> + <!--Flattr--> + <string name="flattr_click_success">Flattr\'et en ting!</string> + <string name="flattr_click_success_count">Flattr\'et %d ting!</string> + <string name="flattr_click_success_queue">Flattr\'et: %s.</string> + <string name="flattr_click_failure_count">Det mislykkedes at flattr %d ting!</string> + <string name="flattr_click_failure">Ikke flattr\'et: %s.</string> + <string name="flattr_click_enqueued">Ting vil blive flattr\'et senere</string> + <string name="flattring_thing">Flattr\'er %s</string> + <string name="flattring_label">AntennaPod flatttr\'er</string> + <string name="flattrd_label">AntennaPod har flattr\'et</string> + <string name="flattrd_failed_label">AntennaPod flattr mislykkedes</string> + <string name="flattr_retrieving_status">Hent flatt\'rede ting</string> + <!--Variable Speed--> + <string name="download_plugin_label">Hent Plugin</string> + <string name="no_playback_plugin_title">Plugin er ikke installeret</string> + <string name="no_playback_plugin_msg">For at variabel afspilningshastighed virker skal der installeres et 3. part bibliotek.\n\nTap \'Download Plugin\' for at downloade et gratis plugin fra Play Store\n\nHvis der findes nogen problemer med dette plugin, så er det ikke AntennaPods ansvar, og problemet bør reporteres til ejeren af pluginet. </string> + <string name="set_playback_speed_label">Afspilningshastigheder</string> + <!--Empty list labels--> + <string name="no_items_label">Der er ingen emner i denne liste.</string> + <string name="no_feeds_label">Du har endnu ikke abonneret til nogle feeds.</string> + <!--Preferences--> + <string name="other_pref">Andre</string> + <string name="about_pref">Om</string> + <string name="queue_label">Kø</string> + <string name="services_label">Tjenester</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Sæt afspilning på pause når hovedtelefoner afbrydes</string> + <string name="pref_followQueue_sum">Hop til næste medie i køen når afspilning er færdig</string> + <string name="playback_pref">Afspilning</string> + <string name="network_pref">Netværk</string> + <string name="pref_autoUpdateIntervall_title">Opdaterings interval</string> + <string name="pref_autoUpdateIntervall_sum">Specificer et interval indenfor hvilket feeds opdaterer automatisk eller deaktiver det</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Download kun medie filer over WiFi</string> + <string name="pref_followQueue_title">Kontinuerlig afspilning</string> + <string name="pref_downloadMediaOnWifiOnly_title">WiFi medie download</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Hovedtelefoner afbrudt</string> + <string name="pref_mobileUpdate_title">Mobile opdateringer</string> + <string name="pref_mobileUpdate_sum">Tillad opdateringer over mobil data forbindelse</string> + <string name="refreshing_label">Opdaterer</string> + <string name="flattr_settings_label">Flattr indstillinger</string> + <string name="pref_flattr_auth_title">Flattr log ind</string> + <string name="pref_flattr_auth_sum">Log ind til din flattr konto for at flattr emner direkte fra app\'en</string> + <string name="pref_flattr_this_app_title">Flattr denne app</string> + <string name="pref_flattr_this_app_sum">Støt udviklingen af AntennaPod ved at flattr den. Tak!</string> + <string name="pref_revokeAccess_title">Tilbagekald adgang</string> + <string name="pref_revokeAccess_sum">Tilbagekald adgangen til din flattr konto fra denne app.</string> + <string name="pref_auto_flattr_title">Flattr\'er automatisk</string> + <string name="pref_auto_flattr_sum">Konfigurer automatisk flattering</string> + <string name="user_interface_label">Brugerflade</string> + <string name="pref_set_theme_title">Vælg tema</string> + <string name="pref_set_theme_sum">Skift AntennaPods udseende.</string> + <string name="pref_automatic_download_title">Download automatisk</string> + <string name="pref_automatic_download_sum">Konfigurer automatisk download af episoder</string> + <string name="pref_autodl_wifi_filter_title">Sæt Wi-Fi filter til</string> + <string name="pref_autodl_wifi_filter_sum">Tillad kun automatisk download for de valgte Wi-Fi netværk</string> + <string name="pref_episode_cache_title">Episode cache</string> + <string name="pref_theme_title_light">Lys</string> + <string name="pref_theme_title_dark">Mørk</string> + <string name="pref_episode_cache_unlimited">Uendelig</string> + <string name="pref_update_interval_hours_plural">timer</string> + <string name="pref_update_interval_hours_singular">time</string> + <string name="pref_update_interval_hours_manual">Manuelt</string> + <string name="pref_gpodnet_authenticate_title">Log ind</string> + <string name="pref_gpodnet_authenticate_sum">Log ind med din gpodder.net konto for at synkronisere dine abonnementer.</string> + <string name="pref_gpodnet_logout_title">Log ud</string> + <string name="pref_gpodnet_logout_toast">Logget ud</string> + <string name="pref_gpodnet_setlogin_information_title">Skift login information</string> + <string name="pref_gpodnet_setlogin_information_sum">Skift din gpodder.net kontos login information.</string> + <string name="pref_playback_speed_title">Afspilningshastigheder</string> + <string name="pref_playback_speed_sum">Tilpas tilgængelige hastigheder for variabelt afspilningshastigheds plugin</string> + <string name="pref_seek_delta_title">Søg tid</string> + <string name="pref_seek_delta_sum">Søg så mange sekunder når der spoles tilbage eller frem</string> + <string name="pref_gpodnet_sethostname_title">Indstil værtsnavn</string> + <string name="pref_gpodnet_sethostname_use_default_host">Brug standard vært</string> + <string name="pref_expandNotify_title">Udvid notifikation</string> + <string name="pref_expandNotify_sum">Altid udvid notifikationen for at vise afspilningsknapper.</string> + <string name="pref_persistNotify_title">Afspilningskontrol forbliver altid</string> + <string name="pref_persistNotify_sum">Behold notifikation og låseskærmskontrol når afspilningen pauses.</string> + <string name="pref_expand_notify_unsupport_toast">Android versioner før 4.1 understøtter ikke udvidede notifikationer.</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Aktiver automatisk flattring</string> + <string name="auto_flattr_after_percent">Flattr episoder så snart %d procent er blevet afspillet</string> + <string name="auto_flattr_ater_beginning">Flattr episoder når afspilning starter</string> + <string name="auto_flattr_ater_end">Flattr episoder når afspilning slutter</string> + <!--Search--> + <string name="search_hint">Søg efter feeds eller episoder</string> + <string name="found_in_shownotes_label">Funder i showets noter</string> + <string name="found_in_chapters_label">Fundet i kapitler</string> + <string name="search_status_no_results">Fandt ingen resultater</string> + <string name="search_label">Søg</string> + <string name="found_in_title_label">Fundet i titel</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">OPML filer lader dig flytte dine podcasts fra en podcastafspiller til en anden.</string> + <string name="opml_import_explanation">For at importere en OPML fil, skal du først placere den i følgende mappe og tryk på knappen nedenfor for at starte import-processen.</string> + <string name="start_import_label">Start import</string> + <string name="opml_import_label">OPML import</string> + <string name="opml_directory_error">FEJL!</string> + <string name="reading_opml_label">Indlæser OPML fil</string> + <string name="opml_reader_error">En fejl opstod under indlæsning af opml documentet:</string> + <string name="opml_import_error_dir_empty">Import mappen er tom.</string> + <string name="select_all_label">Vælg alt</string> + <string name="deselect_all_label">Fravælg alt</string> + <string name="choose_file_to_import_label">Vælg fil at importere</string> + <string name="opml_export_label">OPML eksport</string> + <string name="exporting_label">Eksporterer...</string> + <string name="export_error_label">Eksport fejl</string> + <string name="opml_export_success_title">Opml eksport lykkedes.</string> + <string name="opml_export_success_sum">.opml filen var skrevet til:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Sæt søvn timer</string> + <string name="disable_sleeptimer_label">Fjern søvn timer</string> + <string name="enter_time_here_label">Indtast tid</string> + <string name="sleep_timer_label">Søvn timer</string> + <string name="time_left_label">Tid tilbage:\u0020</string> + <string name="time_dialog_invalid_input">Ugyldig indtastning, tid skal være et heltal</string> + <string name="time_unit_seconds">sekunder</string> + <string name="time_unit_minutes">minutter</string> + <string name="time_unit_hours">timer</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">KATEGORIER </string> + <string name="gpodnet_toplist_header">TOP PODCASTS</string> + <string name="gpodnet_suggestions_header">FORSLAG</string> + <string name="gpodnet_search_hint">Søg på gpodder.net</string> + <string name="gpodnetauth_login_title">Log ind</string> + <string name="gpodnetauth_login_descr">Velkommen til gpodder.nets login proces. Indsæt dine login informationer:</string> + <string name="gpodnetauth_login_butLabel">Log ind</string> + <string name="gpodnetauth_login_register">Hvis du ikke har en konto endnu, så kan du oprette en her:\nhttps://gpodder.net/register/</string> + <string name="username_label">Brugernavn</string> + <string name="password_label">Kodeord</string> + <string name="gpodnetauth_device_title">Enheds valg</string> + <string name="gpodnetauth_device_descr">Tilføj en ny enhed for at bruge din gpodder.net konto eller vælg en eksisterende:</string> + <string name="gpodnetauth_device_deviceID">Enhed ID:\u0020</string> + <string name="gpodnetauth_device_caption">Billedtekst</string> + <string name="gpodnetauth_device_butCreateNewDevice">Opret en ny enhed</string> + <string name="gpodnetauth_device_chooseExistingDevice">Vælg en eksisterende enhed:</string> + <string name="gpodnetauth_device_errorEmpty">Enheds ID må ikke være tomt</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Enheds ID er allerede i brug</string> + <string name="gpodnetauth_device_butChoose">Vælg</string> + <string name="gpodnetauth_finish_title">Login lykkedes!</string> + <string name="gpodnetauth_finish_descr">Tillykke! Din gpodder.net konto er nu forbundet med din enhed. AntennaPod vil fra nu af automatisk synkronisere dine abonnementer på din enhed med din gpodder.net konto.</string> + <string name="gpodnetauth_finish_butsyncnow">Start synkronisering nu</string> + <string name="gpodnetauth_finish_butgomainscreen">Gå til hovedskærmen</string> + <string name="gpodnetsync_auth_error_title">gpodder.net autentificeringfejl</string> + <string name="gpodnetsync_auth_error_descr">Forkert brugernavn eller kodeord</string> + <string name="gpodnetsync_error_title">gpodder.net synkroniseringsfejl</string> + <string name="gpodnetsync_error_descr">En fejl opstod under synkronisering:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Valgte mappe:</string> + <string name="create_folder_label">Opret mappe</string> + <string name="choose_data_directory">Vælg data mappe</string> + <string name="create_folder_msg">Opret en ny mappe med navnet \"%1$s\"?</string> + <string name="create_folder_success">Opret en ny mappe</string> + <string name="create_folder_error_no_write_access">Kan ikke skrive til denne mappe</string> + <string name="create_folder_error_already_exists">Mappen eksisterer allerede</string> + <string name="create_folder_error">Kunne ikke oprette ny mappe</string> + <string name="folder_not_empty_dialog_title">Mappen er ikke tom</string> + <string name="folder_not_empty_dialog_msg">Mappen du har valgt er ikke tom. Medie downloads og andre filer vil blive placeret i denne mappe. Forsæt alligevel?</string> + <string name="set_to_default_folder">Vælg standard mappe</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Sæt afspilning på pause i stedet for at sænke lydniveauet når en anden app vil afspille lyde</string> + <string name="pref_pausePlaybackForFocusLoss_title">Pause for afbrydelser</string> + <!--Online feed view--> + <string name="subscribe_label">Abonner</string> + <string name="subscribed_label">Abonneret</string> + <string name="downloading_label">Downloader...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Vis kapitler</string> + <string name="show_shownotes_label">Vis shownoter</string> + <string name="show_cover_label">Vis billede</string> + <string name="rewind_label">Spol tilbage</string> + <string name="fast_forward_label">Hurtigt fremad</string> + <string name="media_type_audio_label">Lyd</string> + <string name="media_type_video_label">Video</string> + <string name="navigate_upwards_label">Naviger opad</string> + <string name="butAction_label">Flere handlinger</string> + <string name="status_playing_label">Episode afspilles</string> + <string name="status_downloading_label">Episode downloades</string> + <string name="status_downloaded_label">Episode er downloadet</string> + <string name="status_unread_label">Nyt emne</string> + <string name="in_queue_label">Episode er i køen</string> + <string name="new_episodes_count_label">Antal nye episoder</string> + <string name="in_progress_episodes_count_label">Antallet af episoder du er begyndt at lytte til</string> + <string name="drag_handle_content_description">Træk for at skifte denne tings position</string> + <string name="load_next_page_label">Indlæs næste side</string> + <!--Feed information screen--> + <string name="authentication_label">Godkendelse</string> + <string name="authentication_descr">Skift dit brugernavn og kodeord for denne podcast og dets episoder.</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Importerer abonnementer fra single-purpose apps…</string> +</resources> diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml new file mode 100644 index 000000000..c80363427 --- /dev/null +++ b/core/src/main/res/values-de/strings.xml @@ -0,0 +1,349 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Feeds</string> + <string name="add_feed_label">Podcast hinzufügen</string> + <string name="podcasts_label">PODCASTS</string> + <string name="episodes_label">EPISODEN</string> + <string name="new_episodes_label">Neue Episoden</string> + <string name="all_episodes_label">Alle Episoden</string> + <string name="new_label">Neu</string> + <string name="waiting_list_label">Warteliste</string> + <string name="settings_label">Einstellungen</string> + <string name="add_new_feed_label">Podcast hinzufügen</string> + <string name="downloads_label">Downloads</string> + <string name="downloads_running_label">Aktiv</string> + <string name="downloads_completed_label">Abgeschlossen</string> + <string name="downloads_log_label">Log</string> + <string name="cancel_download_label">Download abbrechen</string> + <string name="playback_history_label">Zuletzt gespielt</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net Anmeldung</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Zuletzt veröffentlicht</string> + <string name="episode_filter_label">Nur neue Episoden anzeigen</string> + <!--Main activity--> + <string name="drawer_open">Menü öffnen</string> + <string name="drawer_close">Menü schließen</string> + <!--Webview actions--> + <string name="open_in_browser_label">Im Browser öffnen</string> + <string name="copy_url_label">URL kopieren</string> + <string name="share_url_label">URL teilen</string> + <string name="copied_url_msg">URL wurde in die Zwischenablage kopiert.</string> + <string name="go_to_position_label">Gehe zu dieser Position</string> + <!--Playback history--> + <string name="clear_history_label">Chronik löschen</string> + <!--Other--> + <string name="confirm_label">Bestätigen</string> + <string name="cancel_label">Abbrechen</string> + <string name="author_label">Autor</string> + <string name="language_label">Sprache</string> + <string name="podcast_settings_label">Einstellungen</string> + <string name="cover_label">Bild</string> + <string name="error_label">Fehler</string> + <string name="error_msg_prefix">Ein Fehler ist aufgetreten:</string> + <string name="refresh_label">Aktualisieren</string> + <string name="external_storage_error_msg">Der externe Speicher ist nicht verfügbar. Bitte stelle sicher, dass das externe Speichermedium eingelegt ist, damit die Anwendung funktioniert.</string> + <string name="chapters_label">Kapitel</string> + <string name="shownotes_label">Notizen</string> + <string name="description_label">Beschreibung</string> + <string name="most_recent_prefix">Letzte Episode:\u0020</string> + <string name="episodes_suffix">\u0020Episoden</string> + <string name="length_prefix">Länge:\u0020</string> + <string name="size_prefix">Größe:\u0020</string> + <string name="processing_label">Verarbeite</string> + <string name="loading_label">Lade ...</string> + <string name="save_username_password_label">Benutzername und Password merken</string> + <string name="close_label">Schließen</string> + <string name="retry_label">Erneut versuchen</string> + <string name="auto_download_label">Automatisch herunterladen</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">Feed URL</string> + <string name="etxtFeedurlHint">URL des Feeds oder der Webseite</string> + <string name="txtvfeedurl_label">Podcast per URL hinzufügen</string> + <string name="podcastdirectories_label">Podcast in Verzeichnis finden</string> + <string name="podcastdirectories_descr">Bei gpodder.net kannst du nach neuen Podcasts nach Name, Kategorie oder Popularität suchen.</string> + <string name="browse_gpoddernet_label">gpodder.net durchsuchen</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Markiere alle als gelesen</string> + <string name="mark_all_read_msg">Alle Episoden als gelesen markieren</string> + <string name="show_info_label">Informationen anzeigen</string> + <string name="remove_feed_label">Podcast entfernen</string> + <string name="share_link_label">Webseiten-Link teilen</string> + <string name="share_source_label">Feed-Link teilen</string> + <string name="feed_delete_confirmation_msg">Bitte bestätige, dass du diesen Feed und ALLE heruntergeladenen Episoden dieses Feeds entfernen möchtest.</string> + <string name="feed_remover_msg">Entferne Feed</string> + <string name="load_complete_feed">Kompletten Feed aktualisieren</string> + <!--actions on feeditems--> + <string name="download_label">Herunterladen</string> + <string name="play_label">Abspielen</string> + <string name="pause_label">Pausieren</string> + <string name="stop_label">Stop</string> + <string name="stream_label">Streamen</string> + <string name="remove_label">Entfernen</string> + <string name="remove_episode_lable">Episode entfernen</string> + <string name="mark_read_label">Als gelesen markieren</string> + <string name="mark_unread_label">Als ungelesen markieren</string> + <string name="add_to_queue_label">Zur Abspielliste hinzufügen</string> + <string name="remove_from_queue_label">Aus der Abspielliste entfernen</string> + <string name="visit_website_label">Webseite besuchen</string> + <string name="support_label">Flattr this</string> + <string name="enqueue_all_new">Alle zur Abspielliste hinzufügen</string> + <string name="download_all">Alle herunterladen</string> + <string name="skip_episode_label">Episode überspringen</string> + <!--Download messages and labels--> + <string name="download_successful">erfolgreich</string> + <string name="download_failed">fehlgeschlagen</string> + <string name="download_pending">Download anstehend</string> + <string name="download_running">Download läuft</string> + <string name="download_error_device_not_found">Speichermedium nicht gefunden</string> + <string name="download_error_insufficient_space">Zu wenig Speicherplatz</string> + <string name="download_error_file_error">Dateifehler</string> + <string name="download_error_http_data_error">HTTP Datenfehler</string> + <string name="download_error_error_unknown">Unbekannter Fehler</string> + <string name="download_error_parser_exception">Parserfehler</string> + <string name="download_error_unsupported_type">Nicht unterstützter Feed-Typ</string> + <string name="download_error_connection_error">Verbindungsfehler</string> + <string name="download_error_unknown_host">Unbekannter Host</string> + <string name="download_error_unauthorized">Authentifizierungsfehler</string> + <string name="cancel_all_downloads_label">Alle Downloads abbrechen</string> + <string name="download_cancelled_msg">Download abgebrochen</string> + <string name="download_report_title">Download abgeschlossen</string> + <string name="download_error_malformed_url">Fehler in URL</string> + <string name="download_error_io_error">IO Error</string> + <string name="download_error_request_error">Anfragefehler</string> + <string name="download_error_db_access">Datenbankzugriffsfehler</string> + <string name="downloads_left">\u0020Downloads übrig</string> + <string name="downloads_processing">Verarbeite Downloads</string> + <string name="download_notification_title">Lade Podcast-Daten</string> + <string name="download_report_content">%1$d Downloads erfolgreich, %2$d fehlgeschlagen</string> + <string name="download_log_title_unknown">Unbekannter Titel</string> + <string name="download_type_feed">Feed</string> + <string name="download_type_media">Mediendatei</string> + <string name="download_type_image">Bild</string> + <string name="download_request_error_dialog_message_prefix">Beim Herunterladen der Datei ist ein Fehler aufgetreten:\u0020</string> + <string name="authentication_notification_title">Authentifizierung erforderlich</string> + <string name="authentication_notification_msg">Die angeforderte Quelle erfordert einen Benutzernamen und ein Passwort</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Fehler!</string> + <string name="player_stopped_msg">Keine Medienwiedergabe</string> + <string name="player_preparing_msg">Bereite vor</string> + <string name="player_ready_msg">Fertig</string> + <string name="player_seeking_msg">Spule</string> + <string name="playback_error_server_died">Server ist offline</string> + <string name="playback_error_unknown">Unbekannter Fehler</string> + <string name="no_media_playing_label">Keine Medienwiedergabe</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Puffert</string> + <string name="playbackservice_notification_title">Spiele Podcast ab</string> + <string name="unknown_media_key">AntennaPod - Unbekannte Medientaste: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">Abspielliste leeren</string> + <string name="undo">Rückgängig</string> + <string name="removed_from_queue">Element entfernt</string> + <string name="move_to_top_label">Zum Anfang verschieben</string> + <string name="move_to_bottom_label">Zum Ende verschieben</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattr Anmeldung</string> + <string name="flattr_auth_explanation">Drücke den Button unten um den Authentifizierungsprozess zu starten. Du wirst dann zur Flattr-Anmeldeseite weitergeleitet, wo du gefragt wirst, AntennaPod die Erlaubnis zu geben, Dinge zu flattrn. Nachdem du die Erlaubnis erteilt hast, kehrst du automatisch zu diesem Bildschirm zurück.</string> + <string name="authenticate_label">Authentifizieren</string> + <string name="return_home_label">Zur Hauptseite zurückkehren</string> + <string name="flattr_auth_success">Die Authentifizierung war erfolgreich! Du kannst nun in der Anwendung Flattr verwenden.</string> + <string name="no_flattr_token_title">Kein Flattr Token gefunden</string> + <string name="no_flattr_token_notification_msg">Dein Flattr Account scheint nicht mit AntennaPod verbunden zu sein. Tippe hier zum authentifizieren.</string> + <string name="no_flattr_token_msg">Dein Flattr Account scheint nicht mit AntennaPod verbunden zu sein. Du kannst entweder deinen Account mit AntennaPod verbinden, um direkt in der Anwendung Flattr zu verwenden, oder du kannst die Flattr-Seite der Sache im Netz besuchen.</string> + <string name="authenticate_now_label">Authentifizieren</string> + <string name="action_forbidden_title">Aktion verboten</string> + <string name="action_forbidden_msg">AntennaPod besitzt keine Erlaubnis für diese Aktion. Der Grund dafür könnte sein, dass AntennaPods Zugangstoken aufgehoben worden ist. Du kannst dich entweder erneut authentifizieren oder die Flattr-Seite der Sache im Web besuchen.</string> + <string name="access_revoked_title">Zugriff widerrufen</string> + <string name="access_revoked_info">Du hast AntennaPod das Zugangstoken zu deinem Account entzogen. Um diesen Prozess abzuschließen, musst du diese Anwendung aus der Liste der zugelassenen Anwendungen in deinen Account Einstellungen auf der Flattr Webseite entfernen.</string> + <!--Flattr--> + <string name="flattr_click_success">Eine Sache wurde geflattrt!</string> + <string name="flattr_click_success_count">%d Sachen wurden geflattrt!</string> + <string name="flattr_click_success_queue">Geflattrt: %s</string> + <string name="flattr_click_failure_count">Flattrn von %d Sachen fehlgeschlagen!</string> + <string name="flattr_click_failure">Nicht geflattrt: %s</string> + <string name="flattr_click_enqueued">Sache wird später gelfattrt</string> + <string name="flattring_thing">Flattrt: %s</string> + <string name="flattring_label">AntennaPod flattrt</string> + <string name="flattrd_label">AntennaPod hat geflattrt</string> + <string name="flattrd_failed_label">AntennaPod Flattrn fehlgeschlagen</string> + <string name="flattr_retrieving_status">Rufe geflatterte Sachen ab</string> + <!--Variable Speed--> + <string name="download_plugin_label">Plugin herunterladen</string> + <string name="no_playback_plugin_title">Plugin nicht installiert</string> + <string name="no_playback_plugin_msg">Um die Wiedergabegeschwindigkeit zu verändern, muss eine Drittanbieter-Bibliothek heruntegeladen werden. Drücke auf \"Plugin herunterladen\", um ein kostenloses Plugin aus dem Play Store zu installieren. Probleme, die bei der Benutzung des Plugins auftreten, sollten dem Entwickler des Plugins gemeldet werden.</string> + <string name="set_playback_speed_label">Wiedergabegeschwindigkeiten</string> + <!--Empty list labels--> + <string name="no_items_label">Es sind keine Einträge in dieser Liste.</string> + <string name="no_feeds_label">Du hast noch keine Feeds abonniert.</string> + <!--Preferences--> + <string name="other_pref">Anderes</string> + <string name="about_pref">Über</string> + <string name="queue_label">Abspielliste</string> + <string name="services_label">Dienste</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Pausiere die Wiedergabe wenn der Kopfhörer entfernt worden ist.</string> + <string name="pref_followQueue_sum">Springe zur nächsten Episode wenn die vorherige Episode endet</string> + <string name="playback_pref">Wiedergabe</string> + <string name="network_pref">Netzwerk</string> + <string name="pref_autoUpdateIntervall_title">Aktualisierungsintervall</string> + <string name="pref_autoUpdateIntervall_sum">Lege ein Intervall fest, in dem Feeds automatisch aktualisiert werden oder deaktiviere es</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Lade Mediendateien nur über WiFi</string> + <string name="pref_followQueue_title">Durchgehendes Abspielen</string> + <string name="pref_downloadMediaOnWifiOnly_title">WiFi Medien-Download</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Kopfhörer-Trennung</string> + <string name="pref_mobileUpdate_title">Mobile Aktualisierungen</string> + <string name="pref_mobileUpdate_sum">Erlaube Aktualisierungen über die mobile Datenverbindung</string> + <string name="refreshing_label">Aktualisiere</string> + <string name="flattr_settings_label">Flattr Einstellungen</string> + <string name="pref_flattr_auth_title">Flattr Anmeldung</string> + <string name="pref_flattr_auth_sum">Melde dich mit deinem Flattr Account an, um direkt in der Anwendung zu flattrn.</string> + <string name="pref_flattr_this_app_title">Flattr diese Anwendung</string> + <string name="pref_flattr_this_app_sum">Unterstütze die Entwicklung von AntennaPod mit Flattr. Danke!</string> + <string name="pref_revokeAccess_title">Zugriff entziehen</string> + <string name="pref_revokeAccess_sum">Entziehe dieser Anwendung die Zugriffserlaubnis für deinen Flattr Account.</string> + <string name="pref_auto_flattr_title">Automatisches Flattrn</string> + <string name="pref_auto_flattr_sum">Automatisches Flattrn konfigurieren</string> + <string name="user_interface_label">Benutzeroberfläche</string> + <string name="pref_set_theme_title">Theme auswählen</string> + <string name="pref_set_theme_sum">Ändere das Aussehen von AntennaPod.</string> + <string name="pref_automatic_download_title">Automatisches Herunterladen</string> + <string name="pref_automatic_download_sum">Konfiguriere das automatische Herunterladen von Episoden.</string> + <string name="pref_autodl_wifi_filter_title">W-LAN-Filter aktivieren</string> + <string name="pref_autodl_wifi_filter_sum">Erlaube das automatische Herunterladen nur in ausgewählten W-LAN Netzwerken.</string> + <string name="pref_episode_cache_title">Episodenspeicher</string> + <string name="pref_theme_title_light">Hell</string> + <string name="pref_theme_title_dark">Dunkel</string> + <string name="pref_episode_cache_unlimited">Unbegrenzt</string> + <string name="pref_update_interval_hours_plural">Stunden</string> + <string name="pref_update_interval_hours_singular">Stunde</string> + <string name="pref_update_interval_hours_manual">Manuell</string> + <string name="pref_gpodnet_authenticate_title">Anmelden</string> + <string name="pref_gpodnet_authenticate_sum">Melde dich mit deinem gpodder.net profil an um deine Abonnements zu synchronisieren</string> + <string name="pref_gpodnet_logout_title">Abmelden</string> + <string name="pref_gpodnet_logout_toast">Abmeldung war erfolgreich</string> + <string name="pref_gpodnet_setlogin_information_title">Anmeldeinformationen ändern</string> + <string name="pref_gpodnet_setlogin_information_sum">Ändere die Anmeldeinformationen deines gpodder.net profils</string> + <string name="pref_playback_speed_title">Wiedergabegeschwindigkeiten</string> + <string name="pref_playback_speed_sum">Lege die verfügbaren Werte für die Veränderung der Wiedergabeschwindigkeit fest</string> + <string name="pref_seek_delta_title">Spul-Zeit</string> + <string name="pref_seek_delta_sum">Spule so viele Sekunden vor oder zurück</string> + <string name="pref_gpodnet_sethostname_title">Hostname ändern</string> + <string name="pref_gpodnet_sethostname_use_default_host">Standard-Host verwenden</string> + <string name="pref_expandNotify_title">Benachrichtigung erweitern</string> + <string name="pref_expandNotify_sum">Erweiterte Wiedergabebenachrichtigung mit Abspiel-, Pause- und Stop-Knöpfen anzeigen.</string> + <string name="pref_persistNotify_title">Persistente Wiedergabesteurung</string> + <string name="pref_persistNotify_sum">Zeige Wiedergabebedienelemente in der Benachrichtigung und im Lockscreen an, während die Wiedergabe pausiert ist.</string> + <string name="pref_expand_notify_unsupport_toast">Android-Versionen vor 4.1 unterstützen keine erweiterten Benachrichtigungen.</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Automatisches Flattrn aktivieren</string> + <string name="auto_flattr_after_percent">Flattr eine Episode sobald %d Prozent gespielt worden sind</string> + <string name="auto_flattr_ater_beginning">Flattr Episode, sobald die Wiedergabe beginnt</string> + <string name="auto_flattr_ater_end">Flattr Episode, sobald die Wiedergabe endet</string> + <!--Search--> + <string name="search_hint">Suche nach Feeds oder Episoden</string> + <string name="found_in_shownotes_label">In Sendungsnotizen gefunden</string> + <string name="found_in_chapters_label">In Kapiteln gefunden</string> + <string name="search_status_no_results">Keine Ergebnisse gefunden</string> + <string name="search_label">Suche</string> + <string name="found_in_title_label">In Titel gefunden</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">Mit OPML Dateien kannst du deine Podcasts von einem Podcatcher auf einen anderen übertragen</string> + <string name="opml_import_explanation">Um eine OPML Datei zu importieren, musst du diese im folgenden Ordner platzieren und den unteren Button antippen, um den Import Prozess zu starten.</string> + <string name="start_import_label">Import starten</string> + <string name="opml_import_label">OPML Import</string> + <string name="opml_directory_error">FEHLER!</string> + <string name="reading_opml_label">Lese OPML Datei</string> + <string name="opml_reader_error">Ein Fehler is beim Lesen des OPML Dokuments aufgetreten:</string> + <string name="opml_import_error_dir_empty">Der Import-Ordner ist leer.</string> + <string name="select_all_label">Alle auswählen</string> + <string name="deselect_all_label">Auswahl zurücksetzen</string> + <string name="choose_file_to_import_label">Wähle eine Datei zum Importieren aus</string> + <string name="opml_export_label">OPML Export</string> + <string name="exporting_label">Exportiere...</string> + <string name="export_error_label">Exportfehler</string> + <string name="opml_export_success_title">OPML Export erfolgreich</string> + <string name="opml_export_success_sum">Die .opml Datei wurde unter dem folgenden Pfad gespeichert:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Schlummerfunktion</string> + <string name="disable_sleeptimer_label">Schlummerfunktion deaktivieren</string> + <string name="enter_time_here_label">Zeit eingeben</string> + <string name="sleep_timer_label">Schlummerfunktion</string> + <string name="time_left_label">Zeit übrig:\u0020</string> + <string name="time_dialog_invalid_input">Ungültige Eingabe, Zeit muss eine Ganzzahl sein</string> + <string name="time_unit_seconds">Sekunden</string> + <string name="time_unit_minutes">Minuten</string> + <string name="time_unit_hours">Stunden</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">KATEGORIEN</string> + <string name="gpodnet_toplist_header">BESTE PODCASTS</string> + <string name="gpodnet_suggestions_header">VORSCHLÄGE</string> + <string name="gpodnet_search_hint">gpodder.net durchsuchen</string> + <string name="gpodnetauth_login_title">Anmeldung</string> + <string name="gpodnetauth_login_descr">Willkommen beim gpodder.net Anmeldeprozess. Gib zuerst deine Anmeldeinformationen ein:</string> + <string name="gpodnetauth_login_butLabel">Anmelden</string> + <string name="gpodnetauth_login_register">Falls du noch kein gpodder.net profil hast, kannst du hier eines erstellen: https://gpodder.net/register/</string> + <string name="username_label">Benutzername</string> + <string name="password_label">Passwort</string> + <string name="gpodnetauth_device_title">Geräte-Auswahl</string> + <string name="gpodnetauth_device_descr">Erstelle ein neues Gerät für dein gpodder.net profil oder wähle ein bereits vorhandenes:</string> + <string name="gpodnetauth_device_deviceID">Geräte-ID:\u0020</string> + <string name="gpodnetauth_device_caption">Beschreibung</string> + <string name="gpodnetauth_device_butCreateNewDevice">Neues Gerät erstellen</string> + <string name="gpodnetauth_device_chooseExistingDevice">Vorhandenes Gerät auswählen</string> + <string name="gpodnetauth_device_errorEmpty">Geräte-ID darf nicht leer sein</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Geräte-ID wird bereits verwendet</string> + <string name="gpodnetauth_device_butChoose">Auswählen</string> + <string name="gpodnetauth_finish_title">Anmeldung erfolgreich!</string> + <string name="gpodnetauth_finish_descr">Glückwunsch! Dein gpodder.net profil ist jetzt mit deinem Gerät verbunden. Von nun an wird AntennaPod automatisch deine Abonnements mit deinem gpodder.net profil synchronisieren.</string> + <string name="gpodnetauth_finish_butsyncnow">Jetzt synchronisieren</string> + <string name="gpodnetauth_finish_butgomainscreen">Zum Hauptbildschirm zurückkehren</string> + <string name="gpodnetsync_auth_error_title">gpodder.net Anmeldefehler</string> + <string name="gpodnetsync_auth_error_descr">Falscher Benutzername oder falsches Passwort</string> + <string name="gpodnetsync_error_title">gpodder.net Synchronisierungsfehler</string> + <string name="gpodnetsync_error_descr">Ein Fehler ist beim Synchronisieren aufgetreten:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Ausgewählter Ordner</string> + <string name="create_folder_label">Neuer Ordner</string> + <string name="choose_data_directory">Datenordner auswählen</string> + <string name="create_folder_msg">Neuen Ordner mit Namen \"%1$s\" erstellen?</string> + <string name="create_folder_success">Neuer Ordner angelegt</string> + <string name="create_folder_error_no_write_access">Kann in diesem Ordner nicht schreiben</string> + <string name="create_folder_error_already_exists">Ordner existiert bereits</string> + <string name="create_folder_error">Konnte Datenordner nicht erstellen</string> + <string name="folder_not_empty_dialog_title">Ordner ist nicht leer</string> + <string name="folder_not_empty_dialog_msg">Der ausgewählte Ordner ist nicht leer. Medien-Downloads und andere Daten werden direkt in diesem Ordner gespeichert. Trotzdem fortfahren?</string> + <string name="set_to_default_folder">Standardordner auswählen</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Pausiere die Wiedergabe anstatt die Lautstärke zu reduzieren, wenn eine andere Anwendung Töne abspielt</string> + <string name="pref_pausePlaybackForFocusLoss_title">Bei Unterbrechungen pausieren</string> + <!--Online feed view--> + <string name="subscribe_label">Abonnieren</string> + <string name="subscribed_label">Abonniert</string> + <string name="downloading_label">Lade herunter...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Kapitel anzeigen</string> + <string name="show_shownotes_label">Sendungsnotizen anzeigen</string> + <string name="show_cover_label">Bild anzeigen</string> + <string name="rewind_label">Zurückspulen</string> + <string name="fast_forward_label">Vorspulen</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Video</string> + <string name="navigate_upwards_label">Nach oben navigieren</string> + <string name="butAction_label">Mehr Aktionen</string> + <string name="status_playing_label">Episode wird gerade abgespielt</string> + <string name="status_downloading_label">Episode wird gerade heruntergeladen</string> + <string name="status_downloaded_label">Episode ist heruntergeladen</string> + <string name="status_unread_label">Eintrag ist neu</string> + <string name="in_queue_label">Episode befindet sich inder Abspielliste</string> + <string name="new_episodes_count_label">Anzahl neuer Episoden</string> + <string name="in_progress_episodes_count_label">Anzahl der Episoden, die du angefangen hast zu hören</string> + <string name="drag_handle_content_description">Ziehe, um die Position dieses Objekts zu verändern</string> + <string name="load_next_page_label">Nächste Seite laden</string> + <!--Feed information screen--> + <string name="authentication_label">Authentifizierung</string> + <string name="authentication_descr">Ändere den Benutzernamen und das Passwort für diesen Podcast und dessen Episoden.</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Importiere Abonnements aus Single-Purpose Apps</string> +</resources> diff --git a/core/src/main/res/values-es-rES/strings.xml b/core/src/main/res/values-es-rES/strings.xml new file mode 100644 index 000000000..cd4949530 --- /dev/null +++ b/core/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,200 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Canales</string> + <string name="podcasts_label">PODCASTS</string> + <string name="episodes_label">EPISODIOS</string> + <string name="new_label">Nuevos</string> + <string name="waiting_list_label">Lista de espera</string> + <string name="settings_label">Ajustes</string> + <string name="downloads_label">Descargas</string> + <string name="cancel_download_label">Cancelar descarga</string> + <string name="playback_history_label">Historial de reproducción</string> + <!--New episodes fragment--> + <!--Main activity--> + <!--Webview actions--> + <string name="open_in_browser_label">Abrir en el navegador</string> + <string name="copy_url_label">Copiar URL</string> + <string name="share_url_label">Compartir URL</string> + <string name="copied_url_msg">URL copiada al portapapeles.</string> + <!--Playback history--> + <string name="clear_history_label">Limpiar el historial</string> + <!--Other--> + <string name="confirm_label">Confirmar</string> + <string name="cancel_label">Cancelar</string> + <string name="author_label">Autor</string> + <string name="language_label">Idioma</string> + <string name="error_label">Error</string> + <string name="error_msg_prefix">Ha ocurrido un error:</string> + <string name="refresh_label">Actualizar</string> + <string name="external_storage_error_msg">No se encuentra un almacenamiento externo. Asegúrese de que su almacenamiento externo esté montado para que la aplicación funcione correctamente.</string> + <string name="chapters_label">Capítulos</string> + <string name="shownotes_label">Notas del programa</string> + <string name="episodes_suffix">\u0020episodios</string> + <string name="length_prefix">Duración:\u0020</string> + <string name="size_prefix">Tamaño:\u0020</string> + <string name="processing_label">Procesando</string> + <string name="loading_label">Cargando...</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">URL del canal</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Marcar todo como leído</string> + <string name="show_info_label">Información del programa</string> + <string name="share_link_label">Compartir el enlace de la web</string> + <string name="share_source_label">Compartir el enlace del canal</string> + <string name="feed_delete_confirmation_msg">Confirme que quiere eliminar este canal y TODOS los episodios descargados del mismo.</string> + <!--actions on feeditems--> + <string name="download_label">Descargar</string> + <string name="play_label">Reproducir</string> + <string name="pause_label">Pausar</string> + <string name="stream_label">Transmitir</string> + <string name="remove_label">Quitar</string> + <string name="mark_read_label">Marcar como leído</string> + <string name="mark_unread_label">Marcar como no leído</string> + <string name="add_to_queue_label">Añadir a la cola</string> + <string name="remove_from_queue_label">Quitar de la cola</string> + <string name="visit_website_label">Visitar el sitio web</string> + <string name="support_label">Añadir a Flattr</string> + <string name="enqueue_all_new">Ponerlos todos en cola</string> + <string name="download_all">Descargarlos todos</string> + <string name="skip_episode_label">Saltar episodio</string> + <!--Download messages and labels--> + <string name="download_pending">Descarga pendiente</string> + <string name="download_running">Descarga en curso</string> + <string name="download_error_device_not_found">No se ha encontrado un dispositivo de almacenamiento</string> + <string name="download_error_insufficient_space">Espacio insuficiente</string> + <string name="download_error_file_error">Error de archivo</string> + <string name="download_error_http_data_error">Error de datos HTTP</string> + <string name="download_error_error_unknown">Error desconocido</string> + <string name="download_error_parser_exception">Excepción del analizador</string> + <string name="download_error_unsupported_type">Tipo de canal no admitido</string> + <string name="download_error_connection_error">Error de conexión</string> + <string name="download_error_unknown_host">Host desconocido</string> + <string name="cancel_all_downloads_label">Cancelar todas las descargas</string> + <string name="download_cancelled_msg">Descarga cancelada</string> + <string name="download_report_title">Descargas completadas</string> + <string name="download_error_malformed_url">URL malformada</string> + <string name="download_error_io_error">Error de E/S</string> + <string name="download_error_request_error">Error de petición</string> + <string name="downloads_left">\u0020descargas restantes</string> + <string name="download_notification_title">Descargando datos del podcast</string> + <string name="download_report_content">%1$d descargas exitosas, %2$d fallidas</string> + <string name="download_log_title_unknown">Título desconocido</string> + <string name="download_type_feed">Canal</string> + <string name="download_type_media">Archivo de medios</string> + <string name="download_type_image">Imagen</string> + <string name="download_request_error_dialog_message_prefix">Ha ocurrido un error al intentar descargar el archivo:\u0020</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">¡Error!</string> + <string name="player_stopped_msg">No hay medios en reproducción</string> + <string name="player_preparing_msg">Preparando</string> + <string name="player_ready_msg">Listo</string> + <string name="player_seeking_msg">Buscando</string> + <string name="playback_error_server_died">El servidor está inactivo</string> + <string name="playback_error_unknown">Error desconocido</string> + <string name="no_media_playing_label">No hay medios en reproducción</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Almacenando</string> + <string name="playbackservice_notification_title">Reproduciendo el podcast</string> + <!--Queue operations--> + <string name="clear_queue_label">Limpiar la cola</string> + <!--Flattr--> + <string name="flattr_auth_label">Identificarse en Flattr</string> + <string name="flattr_auth_explanation">Pulse el botón inferior para comenzar la autenticación. Su navegador abrirá la pantalla de identificación de Flattr y le preguntará si quiere conceder permiso a AntennaPod para valorar cosas. Tras concederlo, volverá a esta pantalla automáticamente.</string> + <string name="authenticate_label">Autenticarse</string> + <string name="return_home_label">Volver a la pantalla principal</string> + <string name="flattr_auth_success">Autentificación exitosa. Ya puede valorar cosas en Flattr desde la aplicación.</string> + <string name="no_flattr_token_title">No se ha encontrado un token de Flattr</string> + <string name="no_flattr_token_msg">Su cuenta de Flattr no está conectada con AntennaPod. Puede conectarla o puede visitar la página web de cada cosa para valorarla desde allí.</string> + <string name="authenticate_now_label">Autenticarse</string> + <string name="action_forbidden_title">Acción prohibida</string> + <string name="action_forbidden_msg">AntennaPod no tiene permiso para realizar esta acción. La razón puede ser que se haya revocado el token de acceso de AntennaPod para su cuenta. Puede re-autenticarse o visitar la página web de la cosa.</string> + <string name="access_revoked_title">Acceso revocado</string> + <string name="access_revoked_info">Ha revocado el token de acceso de AntennaPod a su cuenta. Para completar el proceso debe eliminar esta aplicación de la lista de aplicaciones aprobadas, en los ajustes de Flattr.</string> + <!--Flattr--> + <!--Variable Speed--> + <!--Empty list labels--> + <string name="no_items_label">Esta lista no tiene elementos.</string> + <string name="no_feeds_label">No se ha suscrito a ningún canal.</string> + <!--Preferences--> + <string name="other_pref">Otros</string> + <string name="about_pref">Acerca de</string> + <string name="queue_label">Cola</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Pausar la reproducción al desconectar los auriculares</string> + <string name="pref_followQueue_sum">Saltar al siguiente elemento de la cola al acabar la reproducción</string> + <string name="playback_pref">Reproducción</string> + <string name="network_pref">Red</string> + <string name="pref_autoUpdateIntervall_title">Intervalo de actualización</string> + <string name="pref_autoUpdateIntervall_sum">Especificar el intervalo en que se actualizarán automáticamente los canales, o desactivarlo</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Solo descargar los contenidos por WiFi</string> + <string name="pref_followQueue_title">Reproducción continua</string> + <string name="pref_downloadMediaOnWifiOnly_title">Descarga de contenidos por WiFi</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Desconexión de los cascos</string> + <string name="pref_mobileUpdate_title">Actualizaciones por red móvil</string> + <string name="pref_mobileUpdate_sum">Permitir actualizaciones por red de datos móvil</string> + <string name="refreshing_label">Actualizando</string> + <string name="flattr_settings_label">Ajustes de Flattr</string> + <string name="pref_flattr_auth_title">Identificación en Flattr</string> + <string name="pref_flattr_auth_sum">Identifíquese en Flattr para valorar cosas directamente desde la aplicación</string> + <string name="pref_flattr_this_app_title">Valorar esta aplicación en Flattr</string> + <string name="pref_flattr_this_app_sum">Apoye el desarrollo de AntennaPod valorándola en Flattr. ¡Gracias!</string> + <string name="pref_revokeAccess_title">Revocar el acceso</string> + <string name="pref_revokeAccess_sum">Rescindir el permiso de acceso de esta aplicación a su cuenta de Flattr.</string> + <string name="user_interface_label">Interfaz de usuario</string> + <string name="pref_set_theme_title">Elegir un tema</string> + <string name="pref_set_theme_sum">Cambiar la apariencia de AntennaPod.</string> + <string name="pref_automatic_download_title">Descarga automática</string> + <string name="pref_automatic_download_sum">Configurar la descarga automática de episodios.</string> + <string name="pref_autodl_wifi_filter_title">Activar el filtro WiFi</string> + <string name="pref_autodl_wifi_filter_sum">Permitir la descarga automática sólo para las redes WiFi marcadas.</string> + <string name="pref_episode_cache_title">Caché de episodios</string> + <!--Auto-Flattr dialog--> + <!--Search--> + <string name="search_hint">Buscar canales o episodios</string> + <string name="found_in_shownotes_label">Encontrado en las notas del programa</string> + <string name="found_in_chapters_label">Encontrado en los capítulos</string> + <string name="search_status_no_results">No se han encontrado resultados</string> + <string name="search_label">Buscar</string> + <string name="found_in_title_label">Encontrado en el título</string> + <!--OPML import and export--> + <string name="opml_import_explanation">Para importar un archivo OPML, debe copiarlo al siguiente directorio y pulsar el botón que se muestra abajo.</string> + <string name="start_import_label">Comenzar la importación</string> + <string name="opml_import_label">Importación de OPML</string> + <string name="opml_directory_error">¡ERROR!</string> + <string name="reading_opml_label">Leyendo el archivo OPML</string> + <string name="opml_reader_error">Ha ocurrido un error al leer el archivo OPML</string> + <string name="opml_import_error_dir_empty">El directorio de importación está vacío</string> + <string name="select_all_label">Seleccionar todo</string> + <string name="deselect_all_label">Deseleccionar todo</string> + <string name="choose_file_to_import_label">Elegir qué archivo importar</string> + <string name="opml_export_label">Exportar a OPML</string> + <string name="exporting_label">Exportando...</string> + <string name="export_error_label">Error en la exportación</string> + <string name="opml_export_success_title">Exportación a OPML exitosa</string> + <string name="opml_export_success_sum">El archivo OPML se ha escrito en:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Establecer un temporizador</string> + <string name="disable_sleeptimer_label">Desactivar el temporizador</string> + <string name="enter_time_here_label">Introducir hora</string> + <string name="sleep_timer_label">Temporizador</string> + <string name="time_left_label">Tiempo restante:\u0020</string> + <string name="time_dialog_invalid_input">Entrada no válida, el tiempo debe ser un entero</string> + <!--gpodder.net--> + <!--Directory chooser--> + <string name="selected_folder_label">Carpeta seleccionada</string> + <string name="create_folder_label">Crear carpeta</string> + <string name="choose_data_directory">Elegir carpeta de datos</string> + <string name="create_folder_msg">¿Crear carpeta con nombre «%1$s»?</string> + <string name="create_folder_success">Carpeta creada</string> + <string name="create_folder_error_no_write_access">No se puede escribir a esta carpeta</string> + <string name="create_folder_error_already_exists">Ya existe la carpeta</string> + <string name="create_folder_error">No se ha podido crear la carpeta</string> + <string name="folder_not_empty_dialog_title">La carpeta no está vacía</string> + <string name="folder_not_empty_dialog_msg">La carpeta elegida no está vacía. Las descargas y otros archivos se copiarán directamente en esta carpeta. ¿Continuar igualmente?</string> + <string name="set_to_default_folder">Elegir carpeta predeterminada</string> + <!--Online feed view--> + <!--Content descriptions for image buttons--> + <!--Feed information screen--> + <!--AntennaPodSP--> +</resources> diff --git a/core/src/main/res/values-es/strings.xml b/core/src/main/res/values-es/strings.xml new file mode 100644 index 000000000..a545c59a9 --- /dev/null +++ b/core/src/main/res/values-es/strings.xml @@ -0,0 +1,349 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Canales</string> + <string name="add_feed_label">Añadir podcast</string> + <string name="podcasts_label">PODCASTS</string> + <string name="episodes_label">EPISODIOS</string> + <string name="new_episodes_label">Episodios nuevos</string> + <string name="all_episodes_label">Todos los episodios</string> + <string name="new_label">Nuevos</string> + <string name="waiting_list_label">Lista de espera</string> + <string name="settings_label">Ajustes</string> + <string name="add_new_feed_label">Añadir podcast</string> + <string name="downloads_label">Descargas</string> + <string name="downloads_running_label">En Curso</string> + <string name="downloads_completed_label">Completado</string> + <string name="downloads_log_label">Log</string> + <string name="cancel_download_label">Cancelar descarga</string> + <string name="playback_history_label">Histórico de reproducción</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">Iniciar sesión en gpodder.net</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Publicados Recientemente</string> + <string name="episode_filter_label">Mostrar solo episodios nuevos</string> + <!--Main activity--> + <string name="drawer_open">Abrir menú</string> + <string name="drawer_close">Cerrar Menú</string> + <!--Webview actions--> + <string name="open_in_browser_label">Abrir en el navegador</string> + <string name="copy_url_label">Copiar URL</string> + <string name="share_url_label">Compartir URL</string> + <string name="copied_url_msg">URL copiada al portapapeles.</string> + <string name="go_to_position_label">Ir a esta posición</string> + <!--Playback history--> + <string name="clear_history_label">Vaciar el histórico</string> + <!--Other--> + <string name="confirm_label">Confirmar</string> + <string name="cancel_label">Cancelar</string> + <string name="author_label">Autor</string> + <string name="language_label">Idioma</string> + <string name="podcast_settings_label">Ajustes</string> + <string name="cover_label">Imagen</string> + <string name="error_label">Error</string> + <string name="error_msg_prefix">Ha ocurrido un error:</string> + <string name="refresh_label">Actualizar</string> + <string name="external_storage_error_msg">No se encuentra un almacenamiento externo. Asegúrese de que su almacenamiento externo esté montado para que la aplicación funcione correctamente.</string> + <string name="chapters_label">Capítulos</string> + <string name="shownotes_label">Notas del programa</string> + <string name="description_label">Descripción</string> + <string name="most_recent_prefix">Episodio más reciente:\u0020</string> + <string name="episodes_suffix">\u0020episodios</string> + <string name="length_prefix">Duración:\u0020</string> + <string name="size_prefix">Tamaño:\u0020</string> + <string name="processing_label">Procesando</string> + <string name="loading_label">Cargando...</string> + <string name="save_username_password_label">Guardar usuario y contraseña</string> + <string name="close_label">Cerrar</string> + <string name="retry_label">Reintentar</string> + <string name="auto_download_label">Incluir en auto descargas</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">URL del canal</string> + <string name="etxtFeedurlHint">URL del feed o del sitio web</string> + <string name="txtvfeedurl_label">Añadir podcast por URL</string> + <string name="podcastdirectories_label">Buscar podcast en directorio</string> + <string name="podcastdirectories_descr">Se puede buscar nuevos podcast según nombre, categoría o popularidad en el directorio gpodder.net</string> + <string name="browse_gpoddernet_label">Explorar gpodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Marcar todo como leído</string> + <string name="mark_all_read_msg">Marcar todos los episodios como leídos</string> + <string name="show_info_label">Información del programa</string> + <string name="remove_feed_label">Eliminar podcast</string> + <string name="share_link_label">Compartir el enlace de la web</string> + <string name="share_source_label">Compartir el enlace del canal</string> + <string name="feed_delete_confirmation_msg">Confirme que quiere eliminar este canal y TODOS los episodios descargados del mismo.</string> + <string name="feed_remover_msg">Eliminando el canal</string> + <string name="load_complete_feed">Actualizar el feed completo</string> + <!--actions on feeditems--> + <string name="download_label">Descargar</string> + <string name="play_label">Reproducir</string> + <string name="pause_label">Pausar</string> + <string name="stop_label">Stop</string> + <string name="stream_label">Reproducir por streaming</string> + <string name="remove_label">Quitar</string> + <string name="remove_episode_lable">Eliminar episodio</string> + <string name="mark_read_label">Marcar como leído</string> + <string name="mark_unread_label">Marcar como no leído</string> + <string name="add_to_queue_label">Añadir a la cola</string> + <string name="remove_from_queue_label">Quitar de la cola</string> + <string name="visit_website_label">Visitar el sitio web</string> + <string name="support_label">Añadir a Flattr</string> + <string name="enqueue_all_new">Ponerlos todos en cola</string> + <string name="download_all">Descargarlos todos</string> + <string name="skip_episode_label">Omitir episodio</string> + <!--Download messages and labels--> + <string name="download_successful">exitoso</string> + <string name="download_failed">fallido</string> + <string name="download_pending">Descarga pendiente</string> + <string name="download_running">Descarga en curso</string> + <string name="download_error_device_not_found">No se ha encontrado un dispositivo de almacenamiento</string> + <string name="download_error_insufficient_space">Espacio insuficiente</string> + <string name="download_error_file_error">Error de archivo</string> + <string name="download_error_http_data_error">Error de datos HTTP</string> + <string name="download_error_error_unknown">Error desconocido</string> + <string name="download_error_parser_exception">Excepción del analizador</string> + <string name="download_error_unsupported_type">Tipo de canal no admitido</string> + <string name="download_error_connection_error">Error de conexión</string> + <string name="download_error_unknown_host">Host desconocido</string> + <string name="download_error_unauthorized">Error de autenticación</string> + <string name="cancel_all_downloads_label">Cancelar todas las descargas</string> + <string name="download_cancelled_msg">Descarga cancelada</string> + <string name="download_report_title">Descargas completadas</string> + <string name="download_error_malformed_url">URL con formato incorrecto</string> + <string name="download_error_io_error">Error de E/S</string> + <string name="download_error_request_error">Error de solicitud</string> + <string name="download_error_db_access">Error de acceso a la base de datos</string> + <string name="downloads_left">\u0020descargas restantes</string> + <string name="downloads_processing">Procesando descargas</string> + <string name="download_notification_title">Descargando datos del podcast</string> + <string name="download_report_content">%1$d descargas exitosas, %2$d fallidas</string> + <string name="download_log_title_unknown">Título desconocido</string> + <string name="download_type_feed">Canal</string> + <string name="download_type_media">Archivo de medios</string> + <string name="download_type_image">Imagen</string> + <string name="download_request_error_dialog_message_prefix">Ha ocurrido un error al intentar descargar el archivo:\u0020</string> + <string name="authentication_notification_title">Autenticación requerida</string> + <string name="authentication_notification_msg">El recurso solicitado requiere usuario y contraseña</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">¡Error!</string> + <string name="player_stopped_msg">No hay medios en reproducción</string> + <string name="player_preparing_msg">Preparando</string> + <string name="player_ready_msg">Listo</string> + <string name="player_seeking_msg">Buscando</string> + <string name="playback_error_server_died">El servidor está inactivo</string> + <string name="playback_error_unknown">Error desconocido</string> + <string name="no_media_playing_label">No hay medios en reproducción</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Almacenando</string> + <string name="playbackservice_notification_title">Reproduciendo el podcast</string> + <string name="unknown_media_key">AntennaPod - Tecla multimedia desconocida: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">Vaciar la cola</string> + <string name="undo">Deshacer</string> + <string name="removed_from_queue">Artículo eliminado</string> + <string name="move_to_top_label">Mover al principio</string> + <string name="move_to_bottom_label">Mover al final</string> + <!--Flattr--> + <string name="flattr_auth_label">Identificarse en Flattr</string> + <string name="flattr_auth_explanation">Pulse el botón inferior para comenzar la autenticación. Su navegador abrirá la pantalla de identificación de Flattr y le preguntará si quiere conceder permiso a AntennaPod para valorar cosas. Tras concederlo, volverá a esta pantalla automáticamente.</string> + <string name="authenticate_label">Autenticarse</string> + <string name="return_home_label">Volver a la pantalla principal</string> + <string name="flattr_auth_success">Autentificación exitosa. Ya puede valorar cosas en Flattr desde la aplicación.</string> + <string name="no_flattr_token_title">No se ha encontrado un token de Flattr</string> + <string name="no_flattr_token_notification_msg">Tu cuenta Flatter parece no estar conectada con AntennaPod. Pulsa aquí para autenticar.</string> + <string name="no_flattr_token_msg">Su cuenta de Flattr no está conectada con AntennaPod. Puede conectarla o puede visitar la página web de cada cosa para valorarla desde allí.</string> + <string name="authenticate_now_label">Autenticarse</string> + <string name="action_forbidden_title">Acción prohibida</string> + <string name="action_forbidden_msg">AntennaPod no tiene permiso para realizar esta acción. La razón puede ser que se haya revocado el token de acceso de AntennaPod para su cuenta. Puede re-autenticarse o visitar la página web de la cosa.</string> + <string name="access_revoked_title">Acceso revocado</string> + <string name="access_revoked_info">Ha revocado el token de acceso de AntennaPod a su cuenta. Para completar el proceso debe eliminar esta aplicación de la lista de aplicaciones aprobadas, en los ajustes de Flattr.</string> + <!--Flattr--> + <string name="flattr_click_success">¡Flattr una cosa!</string> + <string name="flattr_click_success_count">¡Flattr %d cosas!</string> + <string name="flattr_click_success_queue">Flattr: %s.</string> + <string name="flattr_click_failure_count">¡Falló Flattr de %d cosas!</string> + <string name="flattr_click_failure">No se hizo Flattr: %s.</string> + <string name="flattr_click_enqueued">Se hará Flattr de esta cosa más tarde</string> + <string name="flattring_thing">Haciendo Flattr de %s</string> + <string name="flattring_label">AntennaPod haciendo Flattr</string> + <string name="flattrd_label">AntennaPod hizo Flattr</string> + <string name="flattrd_failed_label">AntennaPod Flattr falló</string> + <string name="flattr_retrieving_status">Obteniendo lista de Flattr</string> + <!--Variable Speed--> + <string name="download_plugin_label">Descargar complemento</string> + <string name="no_playback_plugin_title">Complemento no instalado</string> + <string name="no_playback_plugin_msg">Para que funcione la reproducción a velocidad variable, es necesaria una librería de terceros.\n\nPulsa «Descargar plugin» para descargar un plugin gratuito desde el Play Store\n\nCualquier problema relacionado con este plugin no es responsabilidad de AntennaPod y deberían ser reportados al autor del plugin.</string> + <string name="set_playback_speed_label">Velocidades de reproducción</string> + <!--Empty list labels--> + <string name="no_items_label">Esta lista no tiene elementos.</string> + <string name="no_feeds_label">No se ha suscrito a ningún canal.</string> + <!--Preferences--> + <string name="other_pref">Otros</string> + <string name="about_pref">Acerca de</string> + <string name="queue_label">Cola</string> + <string name="services_label">Servicios</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Pausar la reproducción al desconectar los auriculares</string> + <string name="pref_followQueue_sum">Saltar al siguiente elemento de la cola al acabar la reproducción</string> + <string name="playback_pref">Reproducción</string> + <string name="network_pref">Red</string> + <string name="pref_autoUpdateIntervall_title">Intervalo de actualización</string> + <string name="pref_autoUpdateIntervall_sum">Especificar el intervalo en que se actualizarán automáticamente los canales, o desactivarlo</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Solo descargar los contenidos por WiFi</string> + <string name="pref_followQueue_title">Reproducción continua</string> + <string name="pref_downloadMediaOnWifiOnly_title">Descarga de contenidos por WiFi</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Desconexión de los cascos</string> + <string name="pref_mobileUpdate_title">Actualizaciones por red móvil</string> + <string name="pref_mobileUpdate_sum">Permitir actualizaciones por red de datos móvil</string> + <string name="refreshing_label">Actualizando</string> + <string name="flattr_settings_label">Ajustes de Flattr</string> + <string name="pref_flattr_auth_title">Identificación en Flattr</string> + <string name="pref_flattr_auth_sum">Identifíquese en Flattr para valorar cosas directamente desde la aplicación</string> + <string name="pref_flattr_this_app_title">Valorar esta aplicación en Flattr</string> + <string name="pref_flattr_this_app_sum">Apoye el desarrollo de AntennaPod valorándola en Flattr. ¡Gracias!</string> + <string name="pref_revokeAccess_title">Revocar el acceso</string> + <string name="pref_revokeAccess_sum">Rescindir el permiso de acceso de esta aplicación a su cuenta de Flattr.</string> + <string name="pref_auto_flattr_title">Uso de Flattr automático</string> + <string name="pref_auto_flattr_sum">Configurar flattr automático</string> + <string name="user_interface_label">Interfaz de usuario</string> + <string name="pref_set_theme_title">Elegir un tema</string> + <string name="pref_set_theme_sum">Cambiar la apariencia de AntennaPod.</string> + <string name="pref_automatic_download_title">Descarga automática</string> + <string name="pref_automatic_download_sum">Configurar la descarga automática de episodios.</string> + <string name="pref_autodl_wifi_filter_title">Activar el filtro WiFi</string> + <string name="pref_autodl_wifi_filter_sum">Permitir la descarga automática sólo para las redes WiFi marcadas.</string> + <string name="pref_episode_cache_title">Caché de episodios</string> + <string name="pref_theme_title_light">Claro</string> + <string name="pref_theme_title_dark">Oscuro</string> + <string name="pref_episode_cache_unlimited">Ilimitado</string> + <string name="pref_update_interval_hours_plural">horas</string> + <string name="pref_update_interval_hours_singular">hora</string> + <string name="pref_update_interval_hours_manual">Manual</string> + <string name="pref_gpodnet_authenticate_title">Iniciar sesión</string> + <string name="pref_gpodnet_authenticate_sum">Inicie sesión con su cuenta de gpodder.net para sincronizar sus suscripciones.</string> + <string name="pref_gpodnet_logout_title">Cerrar sesión</string> + <string name="pref_gpodnet_logout_toast">Ha cerrado la sesión correctamente.</string> + <string name="pref_gpodnet_setlogin_information_title">Cambiar información de acceso</string> + <string name="pref_gpodnet_setlogin_information_sum">Modificar datos de inicio de sesión en gpodder.net.</string> + <string name="pref_playback_speed_title">Velocidades de reproducción</string> + <string name="pref_playback_speed_sum">Personalice las velocidades disponibles para la reproducción de audio a velocidad variable</string> + <string name="pref_seek_delta_title">Intervalo de búsqueda</string> + <string name="pref_seek_delta_sum">Avanzar o retroceder esta cantidad de segundos</string> + <string name="pref_gpodnet_sethostname_title">Definir nombre de equipo</string> + <string name="pref_gpodnet_sethostname_use_default_host">Usar nombre de equipo por defecto</string> + <string name="pref_expandNotify_title">Expandir Notificación</string> + <string name="pref_expandNotify_sum">Expandir siempre la notificación para mostrar los botones de reproducción</string> + <string name="pref_persistNotify_title">Controles de reproducción persistentes</string> + <string name="pref_persistNotify_sum">Mantener la notificación y controles en pantalla de bloqueo cuando se pausa.</string> + <string name="pref_expand_notify_unsupport_toast">Las versiones de Android anteriores a la 4.1 no soportan notificaciones expandidas</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Habilitar Flattr automático</string> + <string name="auto_flattr_after_percent">Hacer Flattr del episodio en cuanto se haya reproducido el %d por ciento</string> + <string name="auto_flattr_ater_beginning">Hacer Flattr del episodio al comenzar la reproducción</string> + <string name="auto_flattr_ater_end">Hacer Flattr del episodio al terminar la reproducción</string> + <!--Search--> + <string name="search_hint">Buscar canales o episodios</string> + <string name="found_in_shownotes_label">Encontrado en las notas del programa</string> + <string name="found_in_chapters_label">Encontrado en los capítulos</string> + <string name="search_status_no_results">No se han encontrado resultados</string> + <string name="search_label">Buscar</string> + <string name="found_in_title_label">Encontrado en el título</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">Los archivos OPML le permiten migrar sus podcasts de una aplicación a otra.</string> + <string name="opml_import_explanation">Para importar un archivo OPML, debe copiarlo al siguiente directorio y pulsar el botón que se muestra abajo.</string> + <string name="start_import_label">Comenzar la importación</string> + <string name="opml_import_label">Importación de OPML</string> + <string name="opml_directory_error">¡ERROR!</string> + <string name="reading_opml_label">Leyendo el archivo OPML</string> + <string name="opml_reader_error">Ha ocurrido un error al leer el archivo OPML</string> + <string name="opml_import_error_dir_empty">El directorio de importación está vacío.</string> + <string name="select_all_label">Seleccionar todo</string> + <string name="deselect_all_label">Deseleccionar todo</string> + <string name="choose_file_to_import_label">Elegir qué archivo importar</string> + <string name="opml_export_label">Exportar a OPML</string> + <string name="exporting_label">Exportando...</string> + <string name="export_error_label">Error en la exportación</string> + <string name="opml_export_success_title">Exportación a OPML exitosa</string> + <string name="opml_export_success_sum">El archivo OPML se ha escrito en:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Establecer un temporizador</string> + <string name="disable_sleeptimer_label">Desactivar el temporizador</string> + <string name="enter_time_here_label">Introducir hora</string> + <string name="sleep_timer_label">Temporizador</string> + <string name="time_left_label">Tiempo restante:\u0020</string> + <string name="time_dialog_invalid_input">Entrada no válida, el tiempo debe ser un entero</string> + <string name="time_unit_seconds">segundos</string> + <string name="time_unit_minutes">minutos</string> + <string name="time_unit_hours">horas</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">CATEGORÍAS</string> + <string name="gpodnet_toplist_header">MEJORES PODCASTS</string> + <string name="gpodnet_suggestions_header">SUGERENCIAS</string> + <string name="gpodnet_search_hint">Buscar en gpodder.net</string> + <string name="gpodnetauth_login_title">Iniciar sesión</string> + <string name="gpodnetauth_login_descr">Bienvenido al proceso de autenticación de gpodder.net. Primero, escriba sus datos de inicio de sesión:</string> + <string name="gpodnetauth_login_butLabel">Iniciar sesión</string> + <string name="gpodnetauth_login_register">Si no tienes una cuenta aún, puedes crearla en:\nhttps://gpodder.net/register/</string> + <string name="username_label">Nombre de usuario</string> + <string name="password_label">Contraseña</string> + <string name="gpodnetauth_device_title">Selección del dispositivo</string> + <string name="gpodnetauth_device_descr">Cree un nuevo dispositivo para usar con su cuenta de gpodder.net o elija uno existente:</string> + <string name="gpodnetauth_device_deviceID">Id. de dispositivo:\u0020</string> + <string name="gpodnetauth_device_caption">Descripción</string> + <string name="gpodnetauth_device_butCreateNewDevice">Crear nuevo dispositivo</string> + <string name="gpodnetauth_device_chooseExistingDevice">Elegir dispositivo existente:</string> + <string name="gpodnetauth_device_errorEmpty">El ID de dispositivo no puede estar vacío</string> + <string name="gpodnetauth_device_errorAlreadyUsed">El ID de dispositivo ya está en uso</string> + <string name="gpodnetauth_device_butChoose">Elegir</string> + <string name="gpodnetauth_finish_title">¡Inicio de sesión correcto!</string> + <string name="gpodnetauth_finish_descr">¡Enhorabuena! Su cuenta de gpodder.net está ahora asociada con su dispositivo. A partir de ahora AntennaPod sincronizará automáticamente las suscripciones de su dispositivo con su cuenta de gpodder.net.</string> + <string name="gpodnetauth_finish_butsyncnow">Comenzar sincronización ahora</string> + <string name="gpodnetauth_finish_butgomainscreen">Ir a la pantalla principal</string> + <string name="gpodnetsync_auth_error_title">Error de autenticación de gpodder.net</string> + <string name="gpodnetsync_auth_error_descr">Usuario o contraseña incorrectos</string> + <string name="gpodnetsync_error_title">Error de sincronización de gpodder.net</string> + <string name="gpodnetsync_error_descr">Ocurrió un error de sincronización:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Carpeta seleccionada</string> + <string name="create_folder_label">Crear carpeta</string> + <string name="choose_data_directory">Elegir carpeta de datos</string> + <string name="create_folder_msg">¿Crear carpeta con nombre «%1$s»?</string> + <string name="create_folder_success">Carpeta creada</string> + <string name="create_folder_error_no_write_access">No se puede escribir a esta carpeta</string> + <string name="create_folder_error_already_exists">Ya existe la carpeta</string> + <string name="create_folder_error">No se ha podido crear la carpeta</string> + <string name="folder_not_empty_dialog_title">La carpeta no está vacía</string> + <string name="folder_not_empty_dialog_msg">La carpeta elegida no está vacía. Las descargas y otros archivos se copiarán directamente en esta carpeta. ¿Continuar igualmente?</string> + <string name="set_to_default_folder">Elegir carpeta predeterminada</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Pausar la reproducción en lugar de bajar el volumen cuando otra aplicación reproduzca sonidos</string> + <string name="pref_pausePlaybackForFocusLoss_title">Pausar durante las interrupciones</string> + <!--Online feed view--> + <string name="subscribe_label">Suscribirse</string> + <string name="subscribed_label">Suscrito</string> + <string name="downloading_label">Descargando…</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Mostrar capítulos</string> + <string name="show_shownotes_label">Mostrar notas del programa</string> + <string name="show_cover_label">Mostrar imagen</string> + <string name="rewind_label">Rebobinar</string> + <string name="fast_forward_label">Avance rápido</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Vídeo</string> + <string name="navigate_upwards_label">Navegar hacia arriba</string> + <string name="butAction_label">Más acciones</string> + <string name="status_playing_label">El episodio se está reproduciendo</string> + <string name="status_downloading_label">El episodio se está descargando</string> + <string name="status_downloaded_label">El episodio está descargado</string> + <string name="status_unread_label">El elemento es nuevo</string> + <string name="in_queue_label">El episodio está en la cola</string> + <string name="new_episodes_count_label">Cantidad de episodios nuevos</string> + <string name="in_progress_episodes_count_label">Cantidad de episodios que ha comenzado a escuchar</string> + <string name="drag_handle_content_description">Arrastrar para cambiar la posición de este ítem</string> + <string name="load_next_page_label">Cargar la página siguiente</string> + <!--Feed information screen--> + <string name="authentication_label">Autenticación</string> + <string name="authentication_descr">Cambiar nombre y contraseña de este podcast y sus episodios</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Importando subscripciones de aplicaciones de uso específico...</string> +</resources> diff --git a/core/src/main/res/values-fr/strings.xml b/core/src/main/res/values-fr/strings.xml new file mode 100644 index 000000000..1494cc42b --- /dev/null +++ b/core/src/main/res/values-fr/strings.xml @@ -0,0 +1,349 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Flux</string> + <string name="add_feed_label">Ajouter un podcast</string> + <string name="podcasts_label">PODCASTS</string> + <string name="episodes_label">ÉPISODES</string> + <string name="new_episodes_label">Nouveaux épisodes</string> + <string name="all_episodes_label">Tous les épisodes</string> + <string name="new_label">Nouveau</string> + <string name="waiting_list_label">Liste d\'attente</string> + <string name="settings_label">Préférences</string> + <string name="add_new_feed_label">Ajouter un podcast</string> + <string name="downloads_label">Téléchargements</string> + <string name="downloads_running_label">En cours</string> + <string name="downloads_completed_label">Terminé</string> + <string name="downloads_log_label">Journal d\'activités</string> + <string name="cancel_download_label">Annuler les téléchargements</string> + <string name="playback_history_label">Journal des lectures</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">identifiants gpodder.net</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Publié récemment</string> + <string name="episode_filter_label">N\'afficher que les nouveaux épisodes</string> + <!--Main activity--> + <string name="drawer_open">Ouvrir le menu</string> + <string name="drawer_close">Fermer le menu</string> + <!--Webview actions--> + <string name="open_in_browser_label">Ouvrir dans le navigateur</string> + <string name="copy_url_label">Copier l\'URL</string> + <string name="share_url_label">Partager l\'URL</string> + <string name="copied_url_msg">URL copiée dans le presse-papier</string> + <string name="go_to_position_label">Aller à cette position</string> + <!--Playback history--> + <string name="clear_history_label">Effacer le journal</string> + <!--Other--> + <string name="confirm_label">Confirmer</string> + <string name="cancel_label">Annuler</string> + <string name="author_label">Auteur</string> + <string name="language_label">Langue</string> + <string name="podcast_settings_label">Préférences</string> + <string name="cover_label">Image</string> + <string name="error_label">Erreur</string> + <string name="error_msg_prefix">Une erreur a eu lieu :</string> + <string name="refresh_label">Rafraîchir</string> + <string name="external_storage_error_msg">Aucun stockage externe n\'est disponible. Merci de connecter un volume de stockage externe pour que l\'application puisse fonctionner correctement.</string> + <string name="chapters_label">Chapitres</string> + <string name="shownotes_label">Notes d\'épisode</string> + <string name="description_label">Description</string> + <string name="most_recent_prefix">Épisode le plus récent :\u0020</string> + <string name="episodes_suffix">\u0020épisodes</string> + <string name="length_prefix">Durée :\u0020</string> + <string name="size_prefix">Taille :\u0020</string> + <string name="processing_label">Traitement en cours</string> + <string name="loading_label">En chargement...</string> + <string name="save_username_password_label">Sauvegarder votre identifiant et votre mot de passe</string> + <string name="close_label">Fermer</string> + <string name="retry_label">Réessayer</string> + <string name="auto_download_label">Télécharger automatiquement à l\'avenir</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">URL du flux</string> + <string name="etxtFeedurlHint">URL ou flux ou site web</string> + <string name="txtvfeedurl_label">Ajouter un podcast par son URL</string> + <string name="podcastdirectories_label">Trouver le podcast dans la bibliothèque</string> + <string name="podcastdirectories_descr">Vous pouvez chercher de nouveaux podcasts en filtrant par nom, catégorie ou popularité dans la bibliothèque gpodder.net</string> + <string name="browse_gpoddernet_label">Chercher sur gpodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Tous marquer comme lus</string> + <string name="mark_all_read_msg">Tous les épisodes ont été marqués comme lus</string> + <string name="show_info_label">Voir les détails</string> + <string name="remove_feed_label">Supprimer le podcast</string> + <string name="share_link_label">Partager un lien vers le site</string> + <string name="share_source_label">Partager le flux</string> + <string name="feed_delete_confirmation_msg">Veuillez confirmer que vous voulez bien supprimer ce flux et TOUS ses épisodes que vous avez téléchargés.</string> + <string name="feed_remover_msg">Flux en cours de suppression</string> + <string name="load_complete_feed">Mettre à jour tout le flux</string> + <!--actions on feeditems--> + <string name="download_label">Télécharger</string> + <string name="play_label">Lire</string> + <string name="pause_label">Pause</string> + <string name="stop_label">Stop</string> + <string name="stream_label">Lire en ligne</string> + <string name="remove_label">Supprimer</string> + <string name="remove_episode_lable">Supprimer cet épisode</string> + <string name="mark_read_label">Marquer comme lu</string> + <string name="mark_unread_label">Marquer comme non lu</string> + <string name="add_to_queue_label">Ajouter à la liste</string> + <string name="remove_from_queue_label">Supprimer de la liste</string> + <string name="visit_website_label">Visiter le site</string> + <string name="support_label">Flattr ça!</string> + <string name="enqueue_all_new">Ajouter tous à la liste</string> + <string name="download_all">Tous télécharger</string> + <string name="skip_episode_label">Passer cet épisode</string> + <!--Download messages and labels--> + <string name="download_successful">terminé</string> + <string name="download_failed">échoué</string> + <string name="download_pending">Téléchargement en attente</string> + <string name="download_running">Téléchargement en cours</string> + <string name="download_error_device_not_found">Volume de stockage non trouvé</string> + <string name="download_error_insufficient_space">Espace insuffisant</string> + <string name="download_error_file_error">Accès au fichier impossible</string> + <string name="download_error_http_data_error">Erreur de données HTTP</string> + <string name="download_error_error_unknown">Erreur inconnue</string> + <string name="download_error_parser_exception">Exception de l\'analyseur</string> + <string name="download_error_unsupported_type">Type de flux non géré</string> + <string name="download_error_connection_error">Erreur de connexion</string> + <string name="download_error_unknown_host">Hôte inconnu</string> + <string name="download_error_unauthorized">Erreur d\'authentification</string> + <string name="cancel_all_downloads_label">Annuler tous les téléchargements</string> + <string name="download_cancelled_msg">Téléchargement annulé</string> + <string name="download_report_title">Téléchargements terminés</string> + <string name="download_error_malformed_url">URL incorrecte</string> + <string name="download_error_io_error">Erreur d\'E/S</string> + <string name="download_error_request_error">Erreur de requête</string> + <string name="download_error_db_access">Problème dans l\'accès à la base de données</string> + <string name="downloads_left">\u0020téléchargements restants</string> + <string name="downloads_processing">Traitement des téléchargements</string> + <string name="download_notification_title">Téléchargement des données du podcast</string> + <string name="download_report_content">%1$d téléchargements réussis, %2$d échoués</string> + <string name="download_log_title_unknown">Titre inconnu</string> + <string name="download_type_feed">Flux</string> + <string name="download_type_media">Fichier média</string> + <string name="download_type_image">Image</string> + <string name="download_request_error_dialog_message_prefix">Une erreur s\'est produite durant le téléchargement du fichier :\u0020</string> + <string name="authentication_notification_title">Authentification requise</string> + <string name="authentication_notification_msg">La ressource que vous avez demandé nécessite un nom d\'utilisateur et un mot de passe</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Erreur !</string> + <string name="player_stopped_msg">Pas de lecture en cours</string> + <string name="player_preparing_msg">En préparation</string> + <string name="player_ready_msg">Prêt</string> + <string name="player_seeking_msg">Recherche</string> + <string name="playback_error_server_died">Le serveur ne répond pas</string> + <string name="playback_error_unknown">Erreur inconnue</string> + <string name="no_media_playing_label">Aucune lecture</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Mise en mémoire</string> + <string name="playbackservice_notification_title">Lecture de podcast en cours</string> + <string name="unknown_media_key">AntennaPod - Touche média inconnue : %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">Effacer la liste</string> + <string name="undo">Annuler</string> + <string name="removed_from_queue">Élément retiré</string> + <string name="move_to_top_label">Déplacer vers le haut de haut de la liste</string> + <string name="move_to_bottom_label">Déplacer vers le bas de la liste</string> + <!--Flattr--> + <string name="flattr_auth_label">Connecter à Flattr</string> + <string name="flattr_auth_explanation">Appuyez sur le bouton ci-dessous pour vous authentifier. Vous serez envoyés à l\'écran de connexion Flattr dans le navigateur, et il vous sera demandé de donner à AntennaPod la permission de flattr. Une fois ceci fait, vous reviendrez automatiquement à cet écran.</string> + <string name="authenticate_label">S\'authentifier</string> + <string name="return_home_label">Revenir au départ</string> + <string name="flattr_auth_success">L\'authentification a réussi. Vous pouvez maintenant flattr depuis cette application.</string> + <string name="no_flattr_token_title">Aucun jeton Flattr trouvé.</string> + <string name="no_flattr_token_notification_msg">Votre compte flattr semble ne pas être connecté à AntennaPod. Touchez ici pour vous connecter.</string> + <string name="no_flattr_token_msg">Votre compte Flattr se semble pas être connecté à AntennaPod. Vous pouvez soit connecter votre compte Flattr à AntennaPod pour pouvoir flattr depuis l\'application, ou vous pouvez aller sur le site de ce que vous voulez flattr.</string> + <string name="authenticate_now_label">S\'authentifier</string> + <string name="action_forbidden_title">Action interdite</string> + <string name="action_forbidden_msg">AntennaPod n\'a pas la permission pour cette action. Il est possible que l\'accès à votre compte depuis AntennaPod ait été révoqué. Vous pouvez vous authentifier à nouveau, ou bien visiter le site à flattr directement.</string> + <string name="access_revoked_title">Accès révoqué</string> + <string name="access_revoked_info">Vous avez révoqué le jeton d\'accès d\'AntennaPod à votre compte. Pour terminer cette opération, vous devez retirer AntennaPod de la liste des applications autorisées sur le site web de Flattr.</string> + <!--Flattr--> + <string name="flattr_click_success">Une chose de Flattré !</string> + <string name="flattr_click_success_count">%d choses de Flattré !</string> + <string name="flattr_click_success_queue">Flattré : %s.</string> + <string name="flattr_click_failure_count">Impossible de Flattrer %d choses !</string> + <string name="flattr_click_failure">Non Flattré : %s.</string> + <string name="flattr_click_enqueued">Cette chose sera Flattré plus tard</string> + <string name="flattring_thing">En train de Flattrer %s</string> + <string name="flattring_label">AntennaPod est en train de Flattrer</string> + <string name="flattrd_label">AntennaPod a Flattré</string> + <string name="flattrd_failed_label">Flattr d\'AntennaPod a échoué</string> + <string name="flattr_retrieving_status">Obtention de la liste des choses Flattrées</string> + <!--Variable Speed--> + <string name="download_plugin_label">Télécharger une extension</string> + <string name="no_playback_plugin_title">Extension non installée</string> + <string name="no_playback_plugin_msg">Pour pouvoir changer la vitesse de lecture il est nécessaire d\'installer une librairie tierce.\n\nSélectionnez \"Télécharger une extension\" pour télécharger une extension gratuite depuis le Play Store\n\nLes problèmes concernant les extensions sont de la responsabilité de leur créateur et non d\'AntennaPod. Veillez à notifier le créateur de l\'extension de tout problème.</string> + <string name="set_playback_speed_label">Vitesses de lecture</string> + <!--Empty list labels--> + <string name="no_items_label">Cette liste est vide.</string> + <string name="no_feeds_label">Vous n\'êtes encore abonné à aucun flux.</string> + <!--Preferences--> + <string name="other_pref">Autres</string> + <string name="about_pref">À propos</string> + <string name="queue_label">Liste</string> + <string name="services_label">Services</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Interrompre la lecture lorsque le casque est débranché</string> + <string name="pref_followQueue_sum">Après la fin d\'un épisode, passer au suivant</string> + <string name="playback_pref">Lecture</string> + <string name="network_pref">Réseau</string> + <string name="pref_autoUpdateIntervall_title">Intervalle de mise à jour</string> + <string name="pref_autoUpdateIntervall_sum">Indiquer un intervalle de mise à jour automatique des flux, ou le désactiver</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Ne télécharger les épisodes que par Wi-Fi</string> + <string name="pref_followQueue_title">Lecture continue</string> + <string name="pref_downloadMediaOnWifiOnly_title">Téléchargement en Wi-Fi</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Déconnexion du casque</string> + <string name="pref_mobileUpdate_title">Mise à jour mobile</string> + <string name="pref_mobileUpdate_sum">Autoriser les mises à jour à travers la connexion de données mobile</string> + <string name="refreshing_label">Mise à jour en cours</string> + <string name="flattr_settings_label">Paramètres Flattr</string> + <string name="pref_flattr_auth_title">Connexion à Flattr</string> + <string name="pref_flattr_auth_sum">Connectez-vous à votre compte Flattr pour pouvoir flattr directement depuis l\'application.</string> + <string name="pref_flattr_this_app_title">Flattr cette application</string> + <string name="pref_flattr_this_app_sum">Encouragez le développement d\'AntennaPod grâce à Flattr. Merci !</string> + <string name="pref_revokeAccess_title">Révoquer l\'accès</string> + <string name="pref_revokeAccess_sum">Révoquer la permission d\'accès à votre compte Flattr depuis cette application.</string> + <string name="pref_auto_flattr_title">Flattr automatique</string> + <string name="pref_auto_flattr_sum">Configurer les paiements flattr automatiques</string> + <string name="user_interface_label">Interface utilisateur</string> + <string name="pref_set_theme_title">Choisir un thème</string> + <string name="pref_set_theme_sum">Modifier l\'apparence d\'AntennaPod.</string> + <string name="pref_automatic_download_title">Téléchargement automatique</string> + <string name="pref_automatic_download_sum">Configurer le téléchargement automatique des épisodes.</string> + <string name="pref_autodl_wifi_filter_title">Activer le filtre Wi-Fi</string> + <string name="pref_autodl_wifi_filter_sum">Autoriser le téléchargement automatique uniquement sur les réseaux Wi-Fi sélectionnés.</string> + <string name="pref_episode_cache_title">Épisodes stockés localement</string> + <string name="pref_theme_title_light">Clair</string> + <string name="pref_theme_title_dark">Sombre</string> + <string name="pref_episode_cache_unlimited">Illimité</string> + <string name="pref_update_interval_hours_plural">heures</string> + <string name="pref_update_interval_hours_singular">heure</string> + <string name="pref_update_interval_hours_manual">Manuel</string> + <string name="pref_gpodnet_authenticate_title">Identifiant</string> + <string name="pref_gpodnet_authenticate_sum">Identifiez vous avec votre compte gpodder.net afin de synchroniser vos abonnements</string> + <string name="pref_gpodnet_logout_title">Se déconnecter</string> + <string name="pref_gpodnet_logout_toast">Vous êtes maintenant déconnecté</string> + <string name="pref_gpodnet_setlogin_information_title">Modifier les informations de connexion</string> + <string name="pref_gpodnet_setlogin_information_sum">Modifier les information de connexion pour votre compte gpodder.net</string> + <string name="pref_playback_speed_title">Vitesses de lecture</string> + <string name="pref_playback_speed_sum">Modifier la liste des vitesses disponibles pour la lecture audio</string> + <string name="pref_seek_delta_title">Chercher un moment spécifique</string> + <string name="pref_seek_delta_sum">Bouger d\'autant de secondes en rembobinant ou en faisant une avance rapide </string> + <string name="pref_gpodnet_sethostname_title">Choisir un nom de domaine</string> + <string name="pref_gpodnet_sethostname_use_default_host">Utiliser le nom de domaine par défaut</string> + <string name="pref_expandNotify_title">Etendre la notification</string> + <string name="pref_expandNotify_sum">Toujours étendre les notifications pour montrer les boutons de lecture</string> + <string name="pref_persistNotify_title">Boutons de lecture permanents</string> + <string name="pref_persistNotify_sum">Garder les notifications et les boutons de lecture sur l\'écran de verouillage quand la lecture est en pause</string> + <string name="pref_expand_notify_unsupport_toast">Les versions d\'Android antérieures à 4.1 ne sont pas compatibles avec les notifications élargies</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Activer le paiement flattr automatique</string> + <string name="auto_flattr_after_percent">Lancer un paiement flattr pour un épisode dès que %d de l\'épisode a été joué</string> + <string name="auto_flattr_ater_beginning">Lancer le paiement flattr d\'un épisode dès que la lecture commence</string> + <string name="auto_flattr_ater_end">Lancer le paiement flattr d\'un épisode à la fin de la lecture</string> + <!--Search--> + <string name="search_hint">Chercher des flux ou épisodes</string> + <string name="found_in_shownotes_label">Trouvé dans les notes</string> + <string name="found_in_chapters_label">Trouvé dans les titres de chapitre</string> + <string name="search_status_no_results">Aucun résultat trouvé</string> + <string name="search_label">Recherche</string> + <string name="found_in_title_label">Trouvé dans le titre</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">Les fichiers OPML vous permettent de bouger vos podcasts d\'un logiciel à un autre.</string> + <string name="opml_import_explanation">Pour importer un fichier OPML, copiez-le dans le répertoire suivant, et appuyez sur le bouton ci-dessous pour l\'importer.</string> + <string name="start_import_label">Démarrer l\'importation</string> + <string name="opml_import_label">Importation OPML</string> + <string name="opml_directory_error">ERREUR !</string> + <string name="reading_opml_label">Lecture du fichier OPML en cours</string> + <string name="opml_reader_error">Une erreur s\'est produite à la lecture du document OPML :</string> + <string name="opml_import_error_dir_empty">Le répertoire d\'importation est vide.</string> + <string name="select_all_label">Tout choisir</string> + <string name="deselect_all_label">Ne rien choisir</string> + <string name="choose_file_to_import_label">Choisir le fichier à importer</string> + <string name="opml_export_label">Exportation OPML</string> + <string name="exporting_label">Exportation en cours...</string> + <string name="export_error_label">Erreur d\'exportation</string> + <string name="opml_export_success_title">Exportation OPML réussie.</string> + <string name="opml_export_success_sum">Le fichier .opml a été écrit ici :\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Définir le minuteur d\'arrêt automatique</string> + <string name="disable_sleeptimer_label">Désactiver le minuteur d\'arrêt automatique</string> + <string name="enter_time_here_label">Entrer l\'heure</string> + <string name="sleep_timer_label">Arrêt automatique</string> + <string name="time_left_label">Durée restante :\u0020</string> + <string name="time_dialog_invalid_input">Entrée invalide, la durée doit être un nombre entier</string> + <string name="time_unit_seconds">secondes</string> + <string name="time_unit_minutes">minutes</string> + <string name="time_unit_hours">heures</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">CATEGORIES</string> + <string name="gpodnet_toplist_header">PODCASTS POPULAIRES</string> + <string name="gpodnet_suggestions_header">SUGGESTIONS</string> + <string name="gpodnet_search_hint">Chercher gpodder.net</string> + <string name="gpodnetauth_login_title">Se connecter</string> + <string name="gpodnetauth_login_descr">Bienvenue dans le processus de connexion à gpodder.net. Premièrement, veuillez entrer vos informations de connexion :</string> + <string name="gpodnetauth_login_butLabel">Connexion</string> + <string name="gpodnetauth_login_register">SI vous n\'avez pas encore de compte, vous pouvez en créer un ici:\https://gpodder.net/register/</string> + <string name="username_label">Identifiant</string> + <string name="password_label">Mot de passe</string> + <string name="gpodnetauth_device_title">Choix de l\'appareil</string> + <string name="gpodnetauth_device_descr">Créez un nouvel appareil à utiliser pour votre compte gpodder.net ou choisissez un appareil existant :</string> + <string name="gpodnetauth_device_deviceID">ID de l\'appareil :\u0020</string> + <string name="gpodnetauth_device_caption">Légende</string> + <string name="gpodnetauth_device_butCreateNewDevice">Créer un nouvel appareil</string> + <string name="gpodnetauth_device_chooseExistingDevice">Choisir un appareil existant :</string> + <string name="gpodnetauth_device_errorEmpty">L\'ID de l\'appareil ne peut pas être vide</string> + <string name="gpodnetauth_device_errorAlreadyUsed">L\'ID de cet appareil est déjà en cours d\'utilisation</string> + <string name="gpodnetauth_device_butChoose">Choisir</string> + <string name="gpodnetauth_finish_title">Connexion réussie !</string> + <string name="gpodnetauth_finish_descr">Félicitations ! Votre compte gpodder.net est maintenant lié à votre appareil. AntennaPod va désormais automatiquement synchroniser vos podcasts sur votre appareil avec votre compte gpodder.</string> + <string name="gpodnetauth_finish_butsyncnow">Commencer la synchronisation</string> + <string name="gpodnetauth_finish_butgomainscreen">Aller à l\'écran d\'accueil</string> + <string name="gpodnetsync_auth_error_title">Erreur d\'identification à gpodder.net</string> + <string name="gpodnetsync_auth_error_descr">Problème d\'identifiant et/ou de mot de passe</string> + <string name="gpodnetsync_error_title">Problème de synchronisation avec gpodder.net</string> + <string name="gpodnetsync_error_descr">Une erreur est apparue lors de la synchronisation :\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Répertoire choisi :</string> + <string name="create_folder_label">Créer répertoire</string> + <string name="choose_data_directory">Choisir le répertoire</string> + <string name="create_folder_msg">Créer un répertoire nommé \"%1$s\" ?</string> + <string name="create_folder_success">Répertoire créé</string> + <string name="create_folder_error_no_write_access">Impossible d\'écrire dans ce répertoire</string> + <string name="create_folder_error_already_exists">Le répertoire existe déjà</string> + <string name="create_folder_error">Impossible de créer le répertoire</string> + <string name="folder_not_empty_dialog_title">Le répertoire n\'est pas vide</string> + <string name="folder_not_empty_dialog_msg">Le répertoire que vous avez choisi n\'est pas vide. Les fichiers téléchargés seront ajoutés à ce répertoire. Continuer malgré tout ?</string> + <string name="set_to_default_folder">Choisir le répertoire par défaut</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Mettre la lecture en pause au lieu de baisser le volume quand une autre application veut jouer un son</string> + <string name="pref_pausePlaybackForFocusLoss_title">Mettre en pause lors des interruptions</string> + <!--Online feed view--> + <string name="subscribe_label">S\'abonner</string> + <string name="subscribed_label">Abonné</string> + <string name="downloading_label">Téléchargement en cours</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Afficher chapitres</string> + <string name="show_shownotes_label">Afficher notes d\'épisode</string> + <string name="show_cover_label">Afficher image</string> + <string name="rewind_label">Retour en arrière</string> + <string name="fast_forward_label">Avance rapide</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Vidéo</string> + <string name="navigate_upwards_label">Naviguer vers le haut</string> + <string name="butAction_label">Plus d\'actions</string> + <string name="status_playing_label">L\'épisode est en train d\'être joué</string> + <string name="status_downloading_label">L\'épisode est en train d\'être téléchargé</string> + <string name="status_downloaded_label">L\'épisode a été téléchargé</string> + <string name="status_unread_label">L\'élément est nouveau</string> + <string name="in_queue_label">L\'épisode est dans la liste</string> + <string name="new_episodes_count_label">Nombre de nouveaux épisodes</string> + <string name="in_progress_episodes_count_label">Nombre d\'épisodes que vous avez commencé à écouter</string> + <string name="drag_handle_content_description">Faire glisser pour changer la position de cet élément</string> + <string name="load_next_page_label">Charger la page suivante</string> + <!--Feed information screen--> + <string name="authentication_label">Authentification</string> + <string name="authentication_descr">Modifier votre identifiant et mot de passe pour ce podcast et tous ses épisodes</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Importation des abonnements à partir d\'applications à usage unique...</string> +</resources> diff --git a/core/src/main/res/values-hi-rIN/strings.xml b/core/src/main/res/values-hi-rIN/strings.xml new file mode 100644 index 000000000..b1d6c33b1 --- /dev/null +++ b/core/src/main/res/values-hi-rIN/strings.xml @@ -0,0 +1,281 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name"> ऐन्टेनापॉड</string> + <string name="feeds_label">फिड्स</string> + <string name="podcasts_label">पॉडकास्ट</string> + <string name="episodes_label">एपिसोड</string> + <string name="new_label">नया</string> + <string name="waiting_list_label">वेटिंग लिस्ट</string> + <string name="settings_label">सेटिंग्स</string> + <string name="add_new_feed_label">पॉडकास्ट जोड़ें</string> + <string name="downloads_label">डाउनलोड</string> + <string name="cancel_download_label">डाउनलोड रद्द करें</string> + <string name="playback_history_label">प्लेबैक इतिहास</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net login</string> + <!--New episodes fragment--> + <!--Main activity--> + <!--Webview actions--> + <string name="open_in_browser_label">ब्राउज़र में खोलें</string> + <string name="copy_url_label">कॉपी यूआरएल</string> + <string name="share_url_label">शेयर यूआरएल</string> + <string name="copied_url_msg">यूआरएल को क्लिपबोर्ड पर कॉपी कर लिया गया है</string> + <!--Playback history--> + <string name="clear_history_label"> हिस्ट्री हटाएँ</string> + <!--Other--> + <string name="confirm_label">पुष्टि करें</string> + <string name="cancel_label">रद्द करें</string> + <string name="author_label"> निर्माता</string> + <string name="language_label">भाषा</string> + <string name="podcast_settings_label">सेटिंग्स</string> + <string name="cover_label">तस्वीर</string> + <string name="error_label">त्रुटि</string> + <string name="error_msg_prefix">एक त्रुटि हो गई:</string> + <string name="refresh_label">ताज़ा करें</string> + <string name="external_storage_error_msg">कोई बाहरी भंडारण उपलब्ध नहीं है.सुनिश्चित करें कि आपने बाहरी भंडारण मुहिम शुरू की है ताकि अनुप्रयोग ठीक से काम कर सकते हैं</string> + <string name="chapters_label">अध्याय</string> + <string name="shownotes_label">नोट्स दिखाएँ</string> + <string name="description_label">विवरण</string> + <string name="most_recent_prefix">सबसे हाल का प्रकरण:\u0020</string> + <string name="episodes_suffix">\u0020एपिसोड</string> + <string name="length_prefix">लंबाई:\u0020</string> + <string name="size_prefix">साइज:\u0020</string> + <string name="processing_label">प्रसंस्करण</string> + <string name="loading_label">लोड हो रहा है ...</string> + <string name="save_username_password_label">यूज़रनेम और पासवर्ड सहेजें</string> + <string name="close_label">बंद करें</string> + <string name="retry_label">पुन: प्रयास</string> + <string name="auto_download_label">ऑटो डाउनलोड में शामिल करें</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">यूआरएल फ़ीड</string> + <string name="txtvfeedurl_label">यूआरएल द्वारा पॉडकास्ट जोड़ें</string> + <string name="podcastdirectories_label">पॉडकास्ट निर्देशिका</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">पढ़ने के रूप में सभी को चिह्नित करें</string> + <string name="show_info_label">जानकारी दिखाएँ</string> + <string name="remove_feed_label">पॉडकास्ट हटाएँ +</string> + <string name="share_link_label">शेयर वेबसाइट लिंक</string> + <string name="share_source_label">शेयर फ़ीड लिंक</string> + <string name="feed_delete_confirmation_msg">इसकी पुष्टि करें कि आप इस फ़ीड और इस फ़ीड के सभी प्रकरणों को हटाना चाहते हैं जिन्हें आपने डाउनलोड किया है.</string> + <string name="feed_remover_msg">फ़ीड निकाल रहा है</string> + <!--actions on feeditems--> + <string name="download_label">डाउनलोड</string> + <string name="play_label">प्ले</string> + <string name="pause_label">रोकें</string> + <string name="stream_label">स्ट्रिम</string> + <string name="remove_label"> हटाएँ</string> + <string name="mark_read_label">पढ़ा हुआ के रूप में चिह्नित करें</string> + <string name="mark_unread_label">ना पढ़ा हुआ के रूप में चिह्नित करें</string> + <string name="add_to_queue_label">क़तार में जोड़ें</string> + <string name="remove_from_queue_label">क़तार से हटाएँ</string> + <string name="visit_website_label">वेबसाइट पर जाएँ</string> + <string name="support_label">इसे Flattr करें</string> + <string name="enqueue_all_new">पंक्ति में सभी को डालें</string> + <string name="download_all">सभी डाउनलोड</string> + <string name="skip_episode_label">एपिसोड छोङें</string> + <!--Download messages and labels--> + <string name="download_successful">सफल +</string> + <string name="download_failed">डाउनलोड विफल</string> + <string name="download_pending">लंबित डाउनलोड</string> + <string name="download_running">डाउनलोड चल रहा है</string> + <string name="download_error_device_not_found">स्टोरेज डिवाइस नहीं मिला</string> + <string name="download_error_insufficient_space">अपर्याप्त स्थान</string> + <string name="download_error_file_error">फ़ाइल त्रुटि</string> + <string name="download_error_http_data_error">एचटीटीपी डेटा त्रुटि</string> + <string name="download_error_error_unknown">अज्ञात त्रुटि</string> + <string name="download_error_parser_exception">पार्सर अपवाद</string> + <string name="download_error_unsupported_type">असमर्थित फ़ीड प्रकार</string> + <string name="download_error_connection_error">कनेक्शन त्रुटि</string> + <string name="download_error_unknown_host">अज्ञात होस्ट</string> + <string name="cancel_all_downloads_label">सभी डाउनलोड रद्द करें</string> + <string name="download_cancelled_msg">डाउनलोड रद्द</string> + <string name="download_report_title">डाउनलोड पूरा हो गया है</string> + <string name="download_error_malformed_url">गलत URL</string> + <string name="download_error_io_error">आईओ त्रुटि</string> + <string name="download_error_request_error">अनुरोध त्रुटि</string> + <string name="download_error_db_access">डेटाबेस का उपयोग त्रुटि</string> + <string name="downloads_left">\u0020Downloads छोड़ा</string> + <string name="download_notification_title">पॉडकास्ट डेटा डाउनलोड करें</string> + <string name="download_report_content">%1$d डाउनलोड सफल रहा, %2$d में विफल रहा है</string> + <string name="download_log_title_unknown">अज्ञात शीर्षक</string> + <string name="download_type_feed"> फ़ीड</string> + <string name="download_type_media">मीडिया फ़ाइल</string> + <string name="download_type_image">छवि</string> + <string name="download_request_error_dialog_message_prefix">फाइल डाउनलोड करने के लिए प्रयास करते समय एक त्रुटि हुई:\u0020</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">त्रुटि!</string> + <string name="player_stopped_msg">मीडिया नहीं चल रहा</string> + <string name="player_preparing_msg">तैयार किया जा रहा है</string> + <string name="player_ready_msg">तैयार</string> + <string name="player_seeking_msg">मांग</string> + <string name="playback_error_server_died">सर्वर निरस्त</string> + <string name="playback_error_unknown">अज्ञात त्रुटि</string> + <string name="no_media_playing_label">मीडिया नहीं चल रहा</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">बफरिंग</string> + <string name="playbackservice_notification_title">प्लेईंग पॉडकास्ट</string> + <!--Queue operations--> + <string name="clear_queue_label">कतार साफ</string> + <string name="undo">पूर्ववत् करें</string> + <string name="removed_from_queue">आइटम हटाया</string> + <string name="move_to_top_label">शीर्ष पर ले जाएं</string> + <string name="move_to_bottom_label">नीचे जाएं</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattr पंजीकरण करें</string> + <string name="flattr_auth_explanation">प्रमाणीकरण प्रक्रिया शुरू करने के लिए नीचे दिए गए बटन को दबाएं. आपके ब्राउज़र में flattr लॉगिन स्क्रीन को भेजा जाएगा और flattr बातें करने के लिए अनुमति AntennaPod को देने के लिए कहा जाएगा. आपकि अनुमति देने के बाद, आप स्वतः ही इस स्क्रीन में वापस आ जाएगें.</string> + <string name="authenticate_label">प्रामाणीकरण</string> + <string name="return_home_label">होम पर लौटें</string> + <string name="flattr_auth_success">प्रमाणीकरण सफल रहा था! अब आप अनुप्रयोग के भीतर चीजों को flattr कर सकते हैं.</string> + <string name="no_flattr_token_title">कोई Flattr टोकन नहीं पाया गया</string> + <string name="no_flattr_token_msg">आपकी flattr खाते का AntennaPod से जुड़ा होना प्रतीत नहीं होता. आप या तो AntennaPod को अपने खाते से कनेक्ट कर सकते हैं अनुप्रयोग के भीतर चीजों को flattr करने के लिए या आप इसे वहाँ flattr करने के लिए वेबसाइट पर जा सकते हैं.</string> + <string name="authenticate_now_label">प्रामाणीकरण</string> + <string name="action_forbidden_title">कार्रवाई मना</string> + <string name="action_forbidden_msg">AntennaPod को इस कार्रवाई के लिए अनुमति नहीं है.इस के लिए कारण हो सकता है की आपके खाते में AntennaPod की पहुँच टोकन को निरस्त किया गया है.आप या तो फिर से प्रमाणित कर सकते हैं या बजाय किसी बात के वेबसाइट पर जा सकते हैं.</string> + <string name="access_revoked_title">प्रवेश निरस्त किया</string> + <string name="access_revoked_info">आपने सफलतापूर्वक अपने खाते में AntennaPod पहुँच टोकन निरस्त कर दिया है. इस प्रक्रिया को पूरा करने के लिए, आपको flattr वेबसाइट पर अपने खाते की सेटिंग्स में अनुमोदित आवेदनों की सूची से इस एप्लिकेशन को हटाना होगा.</string> + <!--Flattr--> + <string name="flattr_click_success">सफलतापूर्वक यह बात Flattr किया</string> + <string name="flattr_click_success_count">सफलतापूर्वक %d बातोंको Flattr किया</string> + <string name="flattr_click_success_queue">Flattr गिनती: %s</string> + <string name="flattring_label">ऐन्टेनापॉड Flattr </string> + <!--Variable Speed--> + <string name="download_plugin_label">प्लगइन डाउनलोड करें</string> + <string name="no_playback_plugin_title">प्लगइन स्थापित नहीं हुआ</string> + <string name="set_playback_speed_label">प्लेबैक गति</string> + <!--Empty list labels--> + <string name="no_items_label">इस सूची में कोई आइटम नहीं हैं.</string> + <string name="no_feeds_label">आपने अभी तक किसी भी फ़ीड की सदस्यता नहीं ली है.</string> + <!--Preferences--> + <string name="other_pref">अन्य</string> + <string name="about_pref">के बारे में</string> + <string name="queue_label">पंक्ति</string> + <string name="services_label">सेवाएं</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum"> प्लेबैक रोकें जब हेडफोन काट रहे हैं</string> + <string name="pref_followQueue_sum">प्लेबैक के पूरा होने पर अगली पंक्ति आइटम के लिए जाएँ</string> + <string name="playback_pref">प्लेबैक</string> + <string name="network_pref">संजाल</string> + <string name="pref_autoUpdateIntervall_title">अंतराल अद्यतन</string> + <string name="pref_autoUpdateIntervall_sum">फ़ीड स्वचालित रूप से ताजा कर रहे हैं जिसमें एक अंतराल निर्दिष्ट करें या उसे निष्क्रिय करें </string> + <string name="pref_downloadMediaOnWifiOnly_sum">केवल वाईफ़ाई पर मीडिया फ़ाइलें डाउनलोड करें</string> + <string name="pref_followQueue_title">सतत प्लेबैक</string> + <string name="pref_downloadMediaOnWifiOnly_title">वाईफाई मीडिया डाउनलोड करें</string> + <string name="pref_pauseOnHeadsetDisconnect_title">headphones काटना</string> + <string name="pref_mobileUpdate_title">मोबाइल अपडेट</string> + <string name="pref_mobileUpdate_sum">मोबाइल डेटा कनेक्शन पर अपडेट करने की अनुमति दें</string> + <string name="refreshing_label">रिफ्रेशिंग</string> + <string name="flattr_settings_label">Flattr सेटिंग्स</string> + <string name="pref_flattr_auth_title">Flattr पंजीकरण करें</string> + <string name="pref_flattr_auth_sum">App से सीधे अपनी बातें flattr करने के लिए अपने flattr खाते में प्रवेश करें.</string> + <string name="pref_flattr_this_app_title">इस app को Flattr करें</string> + <string name="pref_flattr_this_app_sum">यह flattring द्वारा AntennaPod के विकास का समर्थन करें. धन्यवाद!</string> + <string name="pref_revokeAccess_title">उपयोग रद्द</string> + <string name="pref_revokeAccess_sum">इस अनुप्रयोग के लिए अपने flattr खाते के लिए उपयोग की अनुमति रद्द करें.</string> + <string name="user_interface_label">यूजर इंटरफेस</string> + <string name="pref_set_theme_title">थीम का चयन करें</string> + <string name="pref_set_theme_sum">AntennaPod का प्रकटन बदलें.</string> + <string name="pref_automatic_download_title">स्वचालित डाउनलोड</string> + <string name="pref_automatic_download_sum">एपिसोड के स्वत: डाउनलोड विन्यस्त करें.</string> + <string name="pref_autodl_wifi_filter_title">वाई-फाई फिल्टर सक्षम करें</string> + <string name="pref_autodl_wifi_filter_sum">केवल चयनित वाई-फाई नेटवर्क के लिए स्वत: डाउनलोड की अनुमति दें.</string> + <string name="pref_episode_cache_title"> गुप्त एपिसोड</string> + <string name="pref_theme_title_light">हलका</string> + <string name="pref_theme_title_dark">अंधेरा</string> + <string name="pref_episode_cache_unlimited">असीमित</string> + <string name="pref_update_interval_hours_plural">घंटे</string> + <string name="pref_update_interval_hours_singular">घंटा</string> + <string name="pref_update_interval_hours_manual">मैनुअल</string> + <string name="pref_gpodnet_authenticate_title">लॉगिन</string> + <string name="pref_gpodnet_authenticate_sum">अपनी सदस्यता सिंक करने के क्रम में अपने gpodder.net खाते के साथ लॉगिन करें .</string> + <string name="pref_gpodnet_logout_title">लॉगआउट</string> + <string name="pref_gpodnet_logout_toast">लॉगआउट सफल रहा था</string> + <string name="pref_gpodnet_setlogin_information_title">प्रवेश जानकारी बदलें</string> + <string name="pref_gpodnet_setlogin_information_sum">अपने gpodder.net खाते के लिए प्रवेश जानकारी बदलें.</string> + <string name="pref_playback_speed_title">प्लेबैक गति</string> + <string name="pref_playback_speed_sum">चर गति ऑडियो प्लेबैक के लिए उपलब्ध गति बनाइए</string> + <string name="pref_gpodnet_sethostname_title">होस्टनाम सेट</string> + <string name="pref_gpodnet_sethostname_use_default_host">डिफ़ॉल्ट होस्ट का प्रयोग करें</string> + <!--Auto-Flattr dialog--> + <!--Search--> + <string name="search_hint">फ़ीड या एपिसोड के लिए खोज</string> + <string name="found_in_shownotes_label">Shownotes में मिला</string> + <string name="found_in_chapters_label">अध्यायों में मिला</string> + <string name="search_status_no_results">कोई परिणाम नहीं मिले</string> + <string name="search_label">खोज</string> + <string name="found_in_title_label">शीर्षक में मिला</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">OPML फ़ाइलें आपको एक podcatcher से दूसरे को अपने पॉडकास्ट स्थानांतरित करने के लिए अनुमति देते हैं.</string> + <string name="opml_import_explanation">एक OPML फ़ाइल आयात करने के लिए, आपको इसे निम्नलिखित निर्देशिका में डालना है और आयात की प्रक्रिया शुरू करने के लिए नीचे दिए गए बटन को प्रेस करना है.</string> + <string name="start_import_label">आयात प्रारंभ</string> + <string name="opml_import_label">OPML आयात</string> + <string name="opml_directory_error">त्रुटि!</string> + <string name="reading_opml_label">OPML फ़ाइल पढ़ना</string> + <string name="opml_reader_error">OPML दस्तावेज़ पढ़ते समय एक त्रुटि हुई है:</string> + <string name="opml_import_error_dir_empty">आयात निर्देशिका खाली है.</string> + <string name="select_all_label">सभी का चयन करें</string> + <string name="deselect_all_label">सभी का चयन रद्द करें</string> + <string name="choose_file_to_import_label">आयात करने के लिए फ़ाइल चुनें</string> + <string name="opml_export_label">OPML निर्यात</string> + <string name="exporting_label">निर्यात ...</string> + <string name="export_error_label">निर्यात त्रुटि</string> + <string name="opml_export_success_title">OPML निर्यात सफल.</string> + <string name="opml_export_success_sum">.ompl फ़ाइल लिखा गया था:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">स्लीप टाइमर सेट</string> + <string name="disable_sleeptimer_label">स्लीप टाइमर अक्षम</string> + <string name="enter_time_here_label">समय दर्ज करें</string> + <string name="sleep_timer_label">स्लीप टाइमर</string> + <string name="time_left_label">समय बचा है:\u0020</string> + <string name="time_dialog_invalid_input">अवैध इनपुट, समय को पूर्णांक में डालें</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">श्रेणियाँ</string> + <string name="gpodnet_toplist_header">शीर्ष पॉडकास्ट</string> + <string name="gpodnet_suggestions_header">सुझाव</string> + <string name="gpodnet_search_hint">gpodder.net खोज</string> + <string name="gpodnetauth_login_title">लॉगिन</string> + <string name="gpodnetauth_login_descr">Gpodder.net प्रवेश प्रक्रिया में आपका स्वागत है.पहले, अपनी प्रवेश जानकारी टाइप करें:</string> + <string name="gpodnetauth_login_butLabel">लॉगिन</string> + <string name="username_label">प्रयोक्ता नाम</string> + <string name="password_label">पासवर्ड</string> + <string name="gpodnetauth_device_title">डिवाइस चयन</string> + <string name="gpodnetauth_device_descr">अपने gpodder.net खाते के उपयोग के लिए एक नई डिवाइस बनाएँ या एक मौजूदा डिवाइस का चयन करें:</string> + <string name="gpodnetauth_device_deviceID">डिवाइस आईडी:\u0020</string> + <string name="gpodnetauth_device_caption">शीर्षक</string> + <string name="gpodnetauth_device_butCreateNewDevice">नई डिवाइस बनाएँ</string> + <string name="gpodnetauth_device_chooseExistingDevice">मौजूदा डिवाइस चुनें:</string> + <string name="gpodnetauth_device_errorEmpty">डिवाइस आईडी खाली नहीं होना चाहिए</string> + <string name="gpodnetauth_device_errorAlreadyUsed">डिवाइस आईडी पहले से ही उपयोग में</string> + <string name="gpodnetauth_device_butChoose">चुनें</string> + <string name="gpodnetauth_finish_title">लॉगिन सफल!</string> + <string name="gpodnetauth_finish_descr">बधाई हो! आपकी gpodder.net खाता अब आपके डिवाइस के साथ जुड़ा हुआ है. AntennaPod अब से स्वचालित रूप से आपके gpodder.net खाते के साथ अपने डिवाइस पर सदस्यता सिंक जाएगा.</string> + <string name="gpodnetauth_finish_butsyncnow">अब सिंक प्रारंभ करें</string> + <string name="gpodnetauth_finish_butgomainscreen">मुख्य स्क्रीन पर जाएं</string> + <string name="gpodnetsync_auth_error_title">gpodder.net प्रमाणन त्रुटि</string> + <string name="gpodnetsync_auth_error_descr">गलत उपयोगकर्ता नाम या पासवर्ड</string> + <string name="gpodnetsync_error_title">gpodder.net सिंक त्रुटि</string> + <string name="gpodnetsync_error_descr">एक त्रुटि सिंक्रनाइज़ के दौरान हुई:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">चयनित फ़ोल्डर:</string> + <string name="create_folder_label">फ़ोल्डर बनाएँ</string> + <string name="choose_data_directory">डेटा फ़ोल्डर चुनें</string> + <string name="create_folder_msg">\"%1$s\" नाम के साथ नया फ़ोल्डर बनाएँ?</string> + <string name="create_folder_success">नया फ़ोल्डर बनाया</string> + <string name="create_folder_error_no_write_access">इस फ़ोल्डर में लिख नहीं सकते</string> + <string name="create_folder_error_already_exists">फ़ोल्डर पहले से मौजूद है</string> + <string name="create_folder_error">फ़ोल्डर नहीं बना सका</string> + <string name="folder_not_empty_dialog_title">फ़ोल्डर खाली नहीं है</string> + <string name="folder_not_empty_dialog_msg">आपके द्वारा चुने गए फ़ोल्डर खाली नहीं है. मीडिया डाउनलोड और अन्य फ़ाइलें इस फ़ोल्डर में सीधे रखा जाएगा. फिर भी जारी रखें?</string> + <string name="set_to_default_folder">डिफ़ॉल्ट फ़ोल्डर चुनें</string> + <string name="pref_pausePlaybackForFocusLoss_sum">प्लेबैक रोकें बजाय ध्वनियों को कम करने के अगर कोई अन्य अनुप्रयोग इसे बजाना चाहता है </string> + <string name="pref_pausePlaybackForFocusLoss_title">रुकावट के लिए रोकें</string> + <!--Online feed view--> + <string name="subscribe_label">सदस्यता लें</string> + <string name="subscribed_label">सदस्यता ली गई</string> + <string name="downloading_label">डाउनलोड कर रहा है ...</string> + <!--Content descriptions for image buttons--> + <!--Feed information screen--> + <!--AntennaPodSP--> +</resources> diff --git a/core/src/main/res/values-it-rIT/strings.xml b/core/src/main/res/values-it-rIT/strings.xml new file mode 100644 index 000000000..b91f1a5fd --- /dev/null +++ b/core/src/main/res/values-it-rIT/strings.xml @@ -0,0 +1,287 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Feed</string> + <string name="podcasts_label">PODCAST</string> + <string name="episodes_label">EPISODI</string> + <string name="new_label">Nuovo</string> + <string name="waiting_list_label">Lista d\'attesa</string> + <string name="settings_label">Impostazioni</string> + <string name="add_new_feed_label">Aggiungi podcast</string> + <string name="downloads_label">Download</string> + <string name="cancel_download_label">Annulla download</string> + <string name="playback_history_label">Storico delle riproduzioni</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net login</string> + <!--New episodes fragment--> + <!--Main activity--> + <!--Webview actions--> + <string name="open_in_browser_label">Apri nel browser</string> + <string name="copy_url_label">Copia URL</string> + <string name="share_url_label">Condividi URL</string> + <string name="copied_url_msg">URL copiato negli appunti</string> + <!--Playback history--> + <string name="clear_history_label">Cancella lo storico</string> + <!--Other--> + <string name="confirm_label">Conferma</string> + <string name="cancel_label">Annulla</string> + <string name="author_label">Autore</string> + <string name="language_label">Lingua</string> + <string name="podcast_settings_label">Impostazioni</string> + <string name="cover_label">Immagine</string> + <string name="error_label">Errore</string> + <string name="error_msg_prefix">Un errore è stato rilevato:</string> + <string name="refresh_label">Aggiorna</string> + <string name="external_storage_error_msg">Non risulta disponibile lo spazio di archiviazione esterno. Assicurati che lo spazio di archiviazione sia montato per permettere all\'applicazione di funzionare correttamente.</string> + <string name="chapters_label">Capitoli</string> + <string name="shownotes_label">Note dell\'episodio</string> + <string name="description_label">Descrizione</string> + <string name="most_recent_prefix">Episodi Recenti:\u0020</string> + <string name="episodes_suffix">\u0020episodi</string> + <string name="length_prefix">Durata:\u0020</string> + <string name="size_prefix">Dimensione:\u0020</string> + <string name="processing_label">Elaborazione in corso</string> + <string name="loading_label">Caricamento...</string> + <string name="save_username_password_label">Salva nome utente e password</string> + <string name="close_label">Chiudi</string> + <string name="retry_label">Riprova</string> + <string name="auto_download_label">Includi nei download automatici</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">URL del feed</string> + <string name="txtvfeedurl_label">Aggiungi un Podcast tramite URL</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Segna tutti come letti</string> + <string name="show_info_label">Informazioni</string> + <string name="share_link_label">Condividi il link al sito</string> + <string name="share_source_label">Condividi il link al feed</string> + <string name="feed_delete_confirmation_msg">Per favore conferma la cancellazione di questo feed e di TUTTI gli episodi collegati che sono stati precedentemente scaricati.</string> + <string name="feed_remover_msg">Rimozione feed</string> + <!--actions on feeditems--> + <string name="download_label">Download</string> + <string name="play_label">Riproduci</string> + <string name="pause_label">Pausa</string> + <string name="stream_label">Stream</string> + <string name="remove_label">Rimuovi</string> + <string name="mark_read_label">Segna come letto</string> + <string name="mark_unread_label">Segna come non letto</string> + <string name="add_to_queue_label">Aggiungi alla coda</string> + <string name="remove_from_queue_label">Rimuovi dalla coda</string> + <string name="visit_website_label">Visita il sito</string> + <string name="support_label">Flattr this</string> + <string name="enqueue_all_new">Accoda tutti</string> + <string name="download_all">Scarica tutti</string> + <string name="skip_episode_label">Salta episodio</string> + <!--Download messages and labels--> + <string name="download_pending">Download in attesa</string> + <string name="download_running">Download in corso</string> + <string name="download_error_device_not_found">Spazio di archiviazione non trovato</string> + <string name="download_error_insufficient_space">Spazio insufficiente</string> + <string name="download_error_file_error">Errore su file</string> + <string name="download_error_http_data_error">HTTP Data Error</string> + <string name="download_error_error_unknown">Errore sconosciuto</string> + <string name="download_error_parser_exception">Parser Exception</string> + <string name="download_error_unsupported_type">Tipo di feed non supportato</string> + <string name="download_error_connection_error">Errore di connessione</string> + <string name="download_error_unknown_host">Host sconosciuto</string> + <string name="cancel_all_downloads_label">Annulla tutti i download</string> + <string name="download_cancelled_msg">Download annullato</string> + <string name="download_report_title">Download completati</string> + <string name="download_error_malformed_url">URL malformato</string> + <string name="download_error_io_error">IO Error</string> + <string name="download_error_request_error">Request error</string> + <string name="download_error_db_access">Errore di accesso al database</string> + <string name="downloads_left">\u0020Download rimasti</string> + <string name="download_notification_title">Download podcast in corso</string> + <string name="download_report_content">%1$d download con successo, %2$d ko</string> + <string name="download_log_title_unknown">Titolo sconosciuto</string> + <string name="download_type_feed">Feed</string> + <string name="download_type_media">Media file</string> + <string name="download_type_image">Immagine</string> + <string name="download_request_error_dialog_message_prefix">Rilevato errore durante il download del file:\u0020</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Errore!</string> + <string name="player_stopped_msg">Nessun media in riproduzione</string> + <string name="player_preparing_msg">Preparazione</string> + <string name="player_ready_msg">Pronto</string> + <string name="player_seeking_msg">Ricerca posizione</string> + <string name="playback_error_server_died">Server died</string> + <string name="playback_error_unknown">Errore sconosciuto</string> + <string name="no_media_playing_label">Nessun media in riproduzione</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Buffering</string> + <string name="playbackservice_notification_title">Riproduzione podcast in corso</string> + <!--Queue operations--> + <string name="clear_queue_label">Svuota la coda</string> + <string name="undo">Undo</string> + <string name="removed_from_queue">Oggetto rimosso</string> + <string name="move_to_top_label">Sposta all\'inizio</string> + <string name="move_to_bottom_label">Sposta in fondo</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattr sign-in</string> + <string name="flattr_auth_explanation">Premi il tasto seguente per iniziare il processo di autenticazione. Sarai trasferito alla pagina di login di flattr sul tuo browser e ti sarà richiesto di garantire ad AntennaPod il permesso di effettuare microdonazioni. Dopo la tua autorizzazione, sarai riportato alla seguente schermata in modo automatico.</string> + <string name="authenticate_label">Autenticazione</string> + <string name="return_home_label">Ritorna alla home</string> + <string name="flattr_auth_success">Autenticazione avvenuta con successo! Adesso puoi microdonare con flattr dall\'interno dell\'app.</string> + <string name="no_flattr_token_title">Nessun token flattr trovato</string> + <string name="no_flattr_token_msg">Il tuo account flattr non sembra essere collegato ad AntennaPod. Potresti collegare il tuo account ad AntennaPod per utilizzare flattr dall\'app oppure puoi visitare il sito per utilizzare flattr direttamente da lì.</string> + <string name="authenticate_now_label">Autenticazione</string> + <string name="action_forbidden_title">Azione inibita</string> + <string name="action_forbidden_msg">AntennaPod non ha il permesso di effettuare questa azione. La ragione potrebbe essere che il token di accesso di AntennaPod al tuo account è stato revocato. Puoi eseguire la re-autenticazione o altrimenti visitare il sito web.</string> + <string name="access_revoked_title">Accesso revocato</string> + <string name="access_revoked_info">Hai revocato l\'accesso di AntennaPod al tuo account. Al fine di completare il processo devi rimuovere l\'app dalla lista delle applicazioni autorizzare nelle impostazioni del tuo account sul sito di flattr.</string> + <!--Flattr--> + <string name="flattring_label">AntennaPod sta eseguendo Flattr</string> + <!--Variable Speed--> + <string name="download_plugin_label">Scarica Plugin</string> + <string name="no_playback_plugin_title">Plugin non installato</string> + <string name="set_playback_speed_label">Velocità di riproduzione</string> + <!--Empty list labels--> + <string name="no_items_label">Non ci sono oggetti in questa lista.</string> + <string name="no_feeds_label">Non sei ancora abbonato a nessun feed.</string> + <!--Preferences--> + <string name="other_pref">Altro</string> + <string name="about_pref">Informazioni</string> + <string name="queue_label">Coda</string> + <string name="services_label">Servizi</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Metti in pausa quanto le cuffie vengono disconnesse</string> + <string name="pref_followQueue_sum">Passa al prossimo episodio in coda quanto si completa una riproduzione</string> + <string name="playback_pref">Riproduzione</string> + <string name="network_pref">Rete</string> + <string name="pref_autoUpdateIntervall_title">Intervallo di update</string> + <string name="pref_autoUpdateIntervall_sum">Specifica un intervallo per l\'aggiornamento automatico dei feed o disabilitalo</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Abilita il download dei media solo tramite WiFi</string> + <string name="pref_followQueue_title">Playback continuo</string> + <string name="pref_downloadMediaOnWifiOnly_title">Download dei media su WiFi</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Disconnessione cuffie</string> + <string name="pref_mobileUpdate_title">Update su rete mobile</string> + <string name="pref_mobileUpdate_sum">Permetti gli aggiornamenti tramite connessione dati mobile</string> + <string name="refreshing_label">Aggiornamento</string> + <string name="flattr_settings_label">Impostazioni Flattr</string> + <string name="pref_flattr_auth_title">Flattr sign-in</string> + <string name="pref_flattr_auth_sum">Collega il tuo account flattr per utilizzare flattr direttamente dall\'app</string> + <string name="pref_flattr_this_app_title">Supporta con flattr questa app</string> + <string name="pref_flattr_this_app_sum">Supporta lo sviluppo di AntennaPod tramite flattr. Grazie!</string> + <string name="pref_revokeAccess_title">Revoca l\'accesso</string> + <string name="pref_revokeAccess_sum">Revoca il permesso, a questa applicazione, di accedere al tuo account flattr.</string> + <string name="user_interface_label">Interfaccia utente</string> + <string name="pref_set_theme_title">Seleziona il tema</string> + <string name="pref_set_theme_sum">Cambia l\'aspetto di AntennaPod</string> + <string name="pref_automatic_download_title">Download automatico</string> + <string name="pref_automatic_download_sum">Configura il download automatico degli episodi</string> + <string name="pref_autodl_wifi_filter_title">Abilita il filtro Wi-Fi</string> + <string name="pref_autodl_wifi_filter_sum">Abilita il download automatico solo per alcune reti Wi-Fi selezionate.</string> + <string name="pref_episode_cache_title">Cache degli episodi</string> + <string name="pref_theme_title_light">Light</string> + <string name="pref_theme_title_dark">Dark</string> + <string name="pref_episode_cache_unlimited">Illimitato</string> + <string name="pref_update_interval_hours_plural">ore</string> + <string name="pref_update_interval_hours_singular">ora</string> + <string name="pref_update_interval_hours_manual">Manuale</string> + <string name="pref_gpodnet_authenticate_title">Login</string> + <string name="pref_gpodnet_authenticate_sum">Effettua il login con il tuo account gpodder.net per sincronizzare le tue sottoscrizioni.</string> + <string name="pref_gpodnet_logout_title">Logout</string> + <string name="pref_gpodnet_logout_toast">Logout effettuato</string> + <string name="pref_gpodnet_setlogin_information_title">Cambia le informazioni di login</string> + <string name="pref_gpodnet_setlogin_information_sum">Cambia le informazioni di login per il tuo account gpodder.net.</string> + <string name="pref_playback_speed_title">Velocità di riproduzione</string> + <string name="pref_playback_speed_sum">Personalizza le velocità disponibili per la riproduzione audio a velocità variabile</string> + <string name="pref_gpodnet_sethostname_title">Imposta l\'hostname</string> + <string name="pref_gpodnet_sethostname_use_default_host">Usa l\'host di default</string> + <!--Auto-Flattr dialog--> + <!--Search--> + <string name="search_hint">Ricerca per Feed o Episodi</string> + <string name="found_in_shownotes_label">Trovato nelle note dell\'episodio</string> + <string name="found_in_chapters_label">Trovato nei capitoli</string> + <string name="search_status_no_results">Nessun risultato trovato</string> + <string name="search_label">Ricerca</string> + <string name="found_in_title_label">Trovato nel titolo</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">I file OPML ti permettono di spostare i tuoi podcast da un programma ad un altro.</string> + <string name="opml_import_explanation">Per importare un file OPML devi posizionarlo nella directory indicata e premere il tasto seguente in modo da iniziare il processo di importazione.</string> + <string name="start_import_label">Avvio importazione</string> + <string name="opml_import_label">Importazione OPML</string> + <string name="opml_directory_error">ERRORE!</string> + <string name="reading_opml_label">Lettura OPML file in corso</string> + <string name="opml_reader_error">Un errore è stato rilevato mentre era in corso la lettura del documento opml:</string> + <string name="opml_import_error_dir_empty">La directory di importazione è vuota.</string> + <string name="select_all_label">Seleziona tutti</string> + <string name="deselect_all_label">Deseleziona tutti</string> + <string name="choose_file_to_import_label">Scegli il file da importare</string> + <string name="opml_export_label">Esportazione su OPML</string> + <string name="exporting_label">Esportazione in corso...</string> + <string name="export_error_label">Errore di esportazione</string> + <string name="opml_export_success_title">Esportazione OPML avvenuta con successo.</string> + <string name="opml_export_success_sum">Il file .opml è stato scritto su:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Imposta timer</string> + <string name="disable_sleeptimer_label">Disabilita il timer di spegnimento</string> + <string name="enter_time_here_label">Tempo di spegnimento</string> + <string name="sleep_timer_label">Timer di spegnimento</string> + <string name="time_left_label">Tempo residuo:\u0020</string> + <string name="time_dialog_invalid_input">Input non valido, il campo deve essere un numero intero.</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">CATEGORIE</string> + <string name="gpodnet_toplist_header">TOP PODCAST</string> + <string name="gpodnet_suggestions_header">SUGGERIMENTI</string> + <string name="gpodnet_search_hint">Cerca su gpodder.net</string> + <string name="gpodnetauth_login_title">Login</string> + <string name="gpodnetauth_login_descr">Benvenuto sul processo di login di gpodder.net. Per prima cosa, inserisci le tue informazioni di login:</string> + <string name="gpodnetauth_login_butLabel">Login</string> + <string name="username_label">Username</string> + <string name="password_label">Password</string> + <string name="gpodnetauth_device_title">Scelta del dispositivo</string> + <string name="gpodnetauth_device_descr">Crea un nuovo dispositivo per utilizzare il tuo account gpodder.net o scegline uno esistente:</string> + <string name="gpodnetauth_device_deviceID">ID del dispositivo:\u0020</string> + <string name="gpodnetauth_device_caption">Caption</string> + <string name="gpodnetauth_device_butCreateNewDevice">Crea un nuovo dispositivo</string> + <string name="gpodnetauth_device_chooseExistingDevice">Scegli un dispositivo esistente:</string> + <string name="gpodnetauth_device_errorEmpty">L\'ID del dispositivo non può essere vuoto</string> + <string name="gpodnetauth_device_errorAlreadyUsed">ID di dispositivo già in uso</string> + <string name="gpodnetauth_device_butChoose">Scegli</string> + <string name="gpodnetauth_finish_title">Login effettuato!</string> + <string name="gpodnetauth_finish_descr">Congraturazioni! Il tuo account gpodder.net è stato collegato con il tuo dispositivo. Da ora AntennaPod sincronizzerà automaticamente le sottoscrizioni sul tuo dispositivo con il tuo account gpodder.net.</string> + <string name="gpodnetauth_finish_butsyncnow">Avvia la sincronizzazione</string> + <string name="gpodnetauth_finish_butgomainscreen">Schermata principale</string> + <string name="gpodnetsync_auth_error_title">gpodder.net errore di autenticazione</string> + <string name="gpodnetsync_auth_error_descr">Utente o password errata</string> + <string name="gpodnetsync_error_title">gpodder.net errore di sincronizzazione</string> + <string name="gpodnetsync_error_descr">Rilevato un errore in fase di sincronizzazione:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Seleziona la directory:</string> + <string name="create_folder_label">Crea una directory</string> + <string name="choose_data_directory">Scegli la directory per i dati</string> + <string name="create_folder_msg">Crea una nuova directory con nome \"%1$s\"?</string> + <string name="create_folder_success">Crea una nuova directory</string> + <string name="create_folder_error_no_write_access">Impossibile scrivere in questa directory</string> + <string name="create_folder_error_already_exists">La directory esiste già</string> + <string name="create_folder_error">Impossibile creare la directory</string> + <string name="folder_not_empty_dialog_title">La directory non è vuota</string> + <string name="folder_not_empty_dialog_msg">La directory che hai selezionato non è vuota. I download dei media e altri file saranno creati in questa directory. Continuare?</string> + <string name="set_to_default_folder">Scegli la directory predefinita</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Sospendi la riproduzione invece di abbassare il volume quando un\'altra app emette un suono</string> + <string name="pref_pausePlaybackForFocusLoss_title">Pausa su interruzione</string> + <!--Online feed view--> + <string name="subscribe_label">Abbonati</string> + <string name="subscribed_label">Abbonato</string> + <string name="downloading_label">Download in corso...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Mostra i capitoli</string> + <string name="show_shownotes_label">Mostra le note dell\'episodio</string> + <string name="show_cover_label">Mosta l\'immagine</string> + <string name="rewind_label">Riavvolgi</string> + <string name="fast_forward_label">Avanti veloce</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Video</string> + <string name="navigate_upwards_label">Naviga su</string> + <string name="butAction_label">Più azioni</string> + <string name="status_playing_label">L\'episodio è in corso di ripoduzione</string> + <string name="status_downloading_label">L\'episodio sta per essere scaricato</string> + <string name="status_downloaded_label">L\'episodio è stato scaricato</string> + <string name="status_unread_label">L\'oggetto è nuovo</string> + <string name="in_queue_label">L\'episodio è in coda</string> + <string name="new_episodes_count_label">Numero dei nuovi episodi</string> + <!--Feed information screen--> + <!--AntennaPodSP--> +</resources> diff --git a/core/src/main/res/values-iw-rIL/strings.xml b/core/src/main/res/values-iw-rIL/strings.xml new file mode 100644 index 000000000..78096eb5a --- /dev/null +++ b/core/src/main/res/values-iw-rIL/strings.xml @@ -0,0 +1,350 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">אנטנה-פוד</string> + <string name="feeds_label">הזנות</string> + <string name="add_feed_label">הוסף פודקאסט</string> + <string name="podcasts_label">פודקאסטים</string> + <string name="episodes_label">פרקים</string> + <string name="new_episodes_label">פרקים חדשים</string> + <string name="all_episodes_label">כל הפרקים</string> + <string name="new_label">חדש</string> + <string name="waiting_list_label">רשימת המתנה</string> + <string name="settings_label">הגדרות</string> + <string name="add_new_feed_label">הוסף פודקאסט</string> + <string name="downloads_label">הורדות</string> + <string name="downloads_running_label">פועל</string> + <string name="downloads_completed_label">סיים</string> + <string name="downloads_log_label">יומן</string> + <string name="cancel_download_label">בטל הורדה</string> + <string name="playback_history_label">היסטוריית ניגון</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">התחברות אל gpodder.net</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">פורסם לאחרונה</string> + <string name="episode_filter_label">הצג רק פרקים חדשים</string> + <!--Main activity--> + <string name="drawer_open">פתח תפריט</string> + <string name="drawer_close">סגור תפריט</string> + <!--Webview actions--> + <string name="open_in_browser_label">פתח בדפדפן</string> + <string name="copy_url_label">העתק כתובת אתר</string> + <string name="share_url_label">שתף כתובת אתר</string> + <string name="copied_url_msg">כתובת אתרהועתקה ללוח.</string> + <string name="go_to_position_label">עבור למיקום זה</string> + <!--Playback history--> + <string name="clear_history_label">נקה היסטוריה</string> + <!--Other--> + <string name="confirm_label">אישור</string> + <string name="cancel_label">בטל</string> + <string name="author_label">מחבר</string> + <string name="language_label">שפה</string> + <string name="podcast_settings_label">הגדרות</string> + <string name="cover_label">תמונה</string> + <string name="error_label">שגיאה</string> + <string name="error_msg_prefix">אירעה שגיאה:</string> + <string name="refresh_label">רענן</string> + <string name="external_storage_error_msg">אין אחסון חיצוני זמין. אנא ודא כי אחסון חיצוני הוא מותקן כך שהאפליקציה תוכל לעבוד כמו שצריך.</string> + <string name="chapters_label">פרקים</string> + <string name="shownotes_label">הערות פרק</string> + <string name="description_label">תיאור</string> + <string name="most_recent_prefix">הפרק האחרון:\u0020</string> + <string name="episodes_suffix">\u0020פרקים</string> + <string name="length_prefix">אורך:\u0020</string> + <string name="size_prefix">גודל:\u0020</string> + <string name="processing_label">מעבד</string> + <string name="loading_label">טוען...</string> + <string name="save_username_password_label">שמור שם משתמש וססמה</string> + <string name="close_label">סגור</string> + <string name="retry_label">נסה שוב</string> + <string name="auto_download_label">כלול בהורדות אוטומטיות</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">כתובת הזנה</string> + <string name="etxtFeedurlHint">כתובת של הזנה או אתר אינטרנט</string> + <string name="txtvfeedurl_label">הוסף פודקאסט לפי כתובת אתר</string> + <string name="podcastdirectories_label">חפש פודקאסט בספריה</string> + <string name="podcastdirectories_descr">אפשר לחפש פודקסטים חדשים לפי בשם, קטגוריה או פופולריות בספריית gpodder.net.</string> + <string name="browse_gpoddernet_label">עיין בgpodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">סמן הכל כנקרא</string> + <string name="mark_all_read_msg">סמן את כל הפרקים כנקרא</string> + <string name="show_info_label">הצג מידע</string> + <string name="remove_feed_label">הסר פודקאסט</string> + <string name="share_link_label">שתף קישור אתר</string> + <string name="share_source_label">שתף קישור הזנה</string> + <string name="feed_delete_confirmation_msg">אשר מחיקת הזנה זו ואת כל פרקי ההזנה שהורדת.</string> + <string name="feed_remover_msg">הסר הזנה</string> + <string name="load_complete_feed">רענן את כל ההזנה</string> + <!--actions on feeditems--> + <string name="download_label">הורד</string> + <string name="play_label">נגן</string> + <string name="pause_label">השהה</string> + <string name="stop_label">עצור</string> + <string name="stream_label">הזרם</string> + <string name="remove_label">הסר</string> + <string name="remove_episode_lable">הסר פרק</string> + <string name="mark_read_label">סמן כנקרא</string> + <string name="mark_unread_label">סמן כלא נקרא</string> + <string name="add_to_queue_label">הוסף לתור</string> + <string name="remove_from_queue_label">הסר מהתור</string> + <string name="visit_website_label">בקר באתר</string> + <string name="support_label">תרום באמצעות Flattr</string> + <string name="enqueue_all_new">הכנס לתור הכל</string> + <string name="download_all">הורד הכל</string> + <string name="skip_episode_label">דלג על הפרק</string> + <!--Download messages and labels--> + <string name="download_successful">הצלחה</string> + <string name="download_failed">כישלון</string> + <string name="download_pending">הורדה עתידית</string> + <string name="download_running">הורדה מתבצעת</string> + <string name="download_error_device_not_found">התקן איחסון לא נמצא</string> + <string name="download_error_insufficient_space">אין די שטח איחסון</string> + <string name="download_error_file_error">שגיאת קובץ</string> + <string name="download_error_http_data_error">שגיאת מידע HTTP</string> + <string name="download_error_error_unknown">שגיאה לא ידועה</string> + <string name="download_error_parser_exception">שגיאת תוכנית ניתוח</string> + <string name="download_error_unsupported_type">סוג ההזנה אינו נתמך</string> + <string name="download_error_connection_error">שגיאת חיבור</string> + <string name="download_error_unknown_host">שרת לא ידוע</string> + <string name="download_error_unauthorized">שגיאת אימות</string> + <string name="cancel_all_downloads_label">בטל את כל ההורדות</string> + <string name="download_cancelled_msg">הורדה בוטלה</string> + <string name="download_report_title">הורדות הושלמו</string> + <string name="download_error_malformed_url">כתובת אתר שגויה</string> + <string name="download_error_io_error">שגיאת קלט פלט</string> + <string name="download_error_request_error">שגיאת בקשה</string> + <string name="download_error_db_access">שגיאת גישה למסד הנתונים</string> + <string name="downloads_left">הורדות נותרוu0020\</string> + <string name="downloads_processing">מעבד הורדות</string> + <string name="download_notification_title">מוריד פודקאסט</string> + <string name="download_report_content">%1$d הורדות הצליחו, %2$d ניכשלו</string> + <string name="download_log_title_unknown">כותרת לא ידועה</string> + <string name="download_type_feed">הזנה</string> + <string name="download_type_media">קובץ מדיה</string> + <string name="download_type_image">תמונה</string> + <string name="download_request_error_dialog_message_prefix">שגיאה אירעה בעת הניסיון הורדת הקובץ:\u0020</string> + <string name="authentication_notification_title">נידרש אימות</string> + <string name="authentication_notification_msg">המשאב אותה ביקשת דורש שם משתמש וססמה</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">שגיאה!</string> + <string name="player_stopped_msg">מדיה לא מתנגנת</string> + <string name="player_preparing_msg">מתכונן</string> + <string name="player_ready_msg">מוכן</string> + <string name="player_seeking_msg">מחפש</string> + <string name="playback_error_server_died">שרת מת</string> + <string name="playback_error_unknown">שגיאה לא ידועה</string> + <string name="no_media_playing_label">מדיה לא מתנגנת</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">ממלא חוצץ</string> + <string name="playbackservice_notification_title">מנגן פודקאסט</string> + <string name="unknown_media_key">אנטנה-פוד - מפתח מדיה לא ידוע: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">נקה תור</string> + <string name="undo">בטל</string> + <string name="removed_from_queue">הסר פריט</string> + <string name="move_to_top_label">העבר למעלה</string> + <string name="move_to_bottom_label">העבר למטה</string> + <!--Flattr--> + <string name="flattr_auth_label">כניסה ל-Fattr</string> + <string name="flattr_auth_explanation">לחץ על הכפתור למטה כדי להתחיל את תהליך האימות. אתה תועבר למסך כניסת flattr בדפדפן שלך ותתבקש לתת לאנטנה-פוד רשות לתרום באמצעות flattr. לאחר שקבלת אישור, תוכל לחזור למסך זה באופן אוטומטי.</string> + <string name="authenticate_label">אימות</string> + <string name="return_home_label">חזור למסך הבית</string> + <string name="flattr_auth_success">האימות הצליח! עכשיו אתה יכול לתרום באמצעות flattr מתוך האפליקציה.</string> + <string name="no_flattr_token_title">אסימון flattr לא נמצא</string> + <string name="no_flattr_token_notification_msg">חשבון ה-flattr שלך אינו מחובר לאנטנה-פוד. הקש כאן לאימות.</string> + <string name="no_flattr_token_msg">חשבון ה-flattr שלך אינו מחובר לאנטנה-פוד. אתה יכול לקשראת לחשבונך לאנטנה-פוד לתרום באמצעות flattr מתוך האפליקציה או שאתה יכול לבקר באתר האינטרנט של הדבר לו תרצה לתרום.</string> + <string name="authenticate_now_label">אמת</string> + <string name="action_forbidden_title">הפעולה אסורה</string> + <string name="action_forbidden_msg">לאנטנה-פוד אין הרשאה לפעולה זו. הסיבה לכך יכולה להיות שאסימון הגישה של אנטנה-פוד לחשבון שלך בוטל. אתה יכול לבצע אימות מחדש או לבקר באתר האינטרנט של הדבר במקום.</string> + <string name="access_revoked_title">גישה בוטלה</string> + <string name="access_revoked_info">אסימון הגישה של אנטנה-פוד לחשבונך בוטל. על מנת להשלים את התהליך, אתה צריך להסיר יישום זה מהרשימת היישומים שאושרו בהגדרות החשבונך באתר flattr.</string> + <!--Flattr--> + <string name="flattr_click_success">תרמת ב-Flattr!</string> + <string name="flattr_click_success_count">תרמת ב-Flattr %d פעמים! </string> + <string name="flattr_click_success_queue">תרומות Flattr: %s.</string> + <string name="flattr_click_failure_count">כישלון לתרום ב-Flattr %d!</string> + <string name="flattr_click_failure">לא נתרם ב-Flattr: %s.</string> + <string name="flattr_click_enqueued">תרומות ב-Flattr מאוחר יותר</string> + <string name="flattring_thing">תורם ב-Flattr %s</string> + <string name="flattring_label">אנטנה-פוד תורם ב-Flattr</string> + <string name="flattrd_label">אנטנה-פוד תרם ב-Flattr</string> + <string name="flattrd_failed_label">כישלון תרומת אנטנה-פוד ב-Flattr</string> + <string name="flattr_retrieving_status">איחזור תרומות Flattr</string> + <!--Variable Speed--> + <string name="download_plugin_label">הורד תוסף</string> + <string name="no_playback_plugin_title">תוסף לא מותקן</string> + <string name="no_playback_plugin_msg">לאפשר השמעה במהירות משתנה, יש להתקין ספריית צד שלישית.\n\nלחץ על \'הורד תוסף\' להוריד תוסף חינמי מחנות Play.\n\n כל בעיה שתמצא בשימוש עם תוסף זה אינה באחריות של אנטנה-פוד ויש לדווחה לבעל התוסף. + </string> + <string name="set_playback_speed_label">מהירויות ניגון</string> + <!--Empty list labels--> + <string name="no_items_label">אין פריטים ברשימה זו.</string> + <string name="no_feeds_label">לא נרשמת עדיין להזנות.</string> + <!--Preferences--> + <string name="other_pref">אחר</string> + <string name="about_pref">אודות</string> + <string name="queue_label">תור</string> + <string name="services_label">שירותים</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">השהה השמעה בניתוק האוזניות </string> + <string name="pref_followQueue_sum">עבור לפריט הבא בתור כאשר הניגון מסתיים</string> + <string name="playback_pref">ניגון</string> + <string name="network_pref">רשת</string> + <string name="pref_autoUpdateIntervall_title">זמן בין עידכונים</string> + <string name="pref_autoUpdateIntervall_sum">ציין פרק זמן שבו ההזנות עוברות רענון באופן אוטומטי או לבטל ריענון</string> + <string name="pref_downloadMediaOnWifiOnly_sum">הורד קבצי מדיה רק דרך חיבור אינטרנט אלחוטי</string> + <string name="pref_followQueue_title">ניגון מתמשך</string> + <string name="pref_downloadMediaOnWifiOnly_title">הורדת מדיה דרך אינטרנט אלחוטי</string> + <string name="pref_pauseOnHeadsetDisconnect_title">ניתוק אוזניות</string> + <string name="pref_mobileUpdate_title">עידכון דרך רשת סלולרית</string> + <string name="pref_mobileUpdate_sum">אפשר עידכונים דרך רשת סלולרית</string> + <string name="refreshing_label">מרענן</string> + <string name="flattr_settings_label">הגדרות Flattr</string> + <string name="pref_flattr_auth_title">כניסה ל-Fattr</string> + <string name="pref_flattr_auth_sum">היכנס לחשבון שלך לflattr לתרום ישירות מתוך האפליקציה.</string> + <string name="pref_flattr_this_app_title">תרום באמצעות Flattr לאפליקציה זו</string> + <string name="pref_flattr_this_app_sum">תמוך בפיתוח אנטנה-פוד בתרומה עם Flattr. תודה!</string> + <string name="pref_revokeAccess_title">בטל גישה</string> + <string name="pref_revokeAccess_sum">בטל הרשאת גישה לחשבון flattr ליישום זה.</string> + <string name="pref_auto_flattr_title">תרומות Flattr אוטומטיות</string> + <string name="pref_auto_flattr_sum">הגדר תרומות flattr אוטומטיות</string> + <string name="user_interface_label">ממשק משתמש</string> + <string name="pref_set_theme_title">בחר ערכת נושא</string> + <string name="pref_set_theme_sum">שנה את מראה אנטנה-פוד</string> + <string name="pref_automatic_download_title">הורדה אוטומטית</string> + <string name="pref_automatic_download_sum">הגדר הורדה אטומטית של פרקים.</string> + <string name="pref_autodl_wifi_filter_title">אפשר סינון אינטרנט אלחוטי</string> + <string name="pref_autodl_wifi_filter_sum">אפשר הורדה אוטומטית דרך רשתות אלחוטייות נבחרות.</string> + <string name="pref_episode_cache_title">מטמון פרקים</string> + <string name="pref_theme_title_light">בהיר</string> + <string name="pref_theme_title_dark">כהה</string> + <string name="pref_episode_cache_unlimited">בלתי מוגבל</string> + <string name="pref_update_interval_hours_plural">שעות</string> + <string name="pref_update_interval_hours_singular">שעה</string> + <string name="pref_update_interval_hours_manual">ידני</string> + <string name="pref_gpodnet_authenticate_title">כניסה</string> + <string name="pref_gpodnet_authenticate_sum">כנס עם חשבון gpodder.net שלך על מנת לסנכרן את ההרשמות שלך.</string> + <string name="pref_gpodnet_logout_title">התנתקות</string> + <string name="pref_gpodnet_logout_toast">ההתנתקות הייתה מוצלחת</string> + <string name="pref_gpodnet_setlogin_information_title">שינוי פרטי התחברות</string> + <string name="pref_gpodnet_setlogin_information_sum">שנה פרטי התחברות של חשבון gpodder.net.</string> + <string name="pref_playback_speed_title">מהירויות ניגון</string> + <string name="pref_playback_speed_sum">התאמת המהיריות הזמינות לניגון במהירות משתנה</string> + <string name="pref_seek_delta_title">זמן דילוג</string> + <string name="pref_seek_delta_sum">דלג מספר שניות זה בדילוג לאחור או קדימה</string> + <string name="pref_gpodnet_sethostname_title">הגדר שם שרת</string> + <string name="pref_gpodnet_sethostname_use_default_host">השתמש בשרת ברירת מידל</string> + <string name="pref_expandNotify_title">הרחב הודעה</string> + <string name="pref_expandNotify_sum">תמיד הרחב את ההודעה כדי להראות את לחצני הפעלה.</string> + <string name="pref_persistNotify_title">פקדי הפעלה קבועים</string> + <string name="pref_persistNotify_sum">שמר בקרי הודעה ומסך נעילה בעת השהיית השמעה.</string> + <string name="pref_expand_notify_unsupport_toast">גרסאות אנדרויד לפני 4.1 לא תומכות בהודעות מורחבות.</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">הפעל תרומות flattr אוטומטיות</string> + <string name="auto_flattr_after_percent">תרום באמצעות flattr כשנוגן %d אחוזים מהפרק</string> + <string name="auto_flattr_ater_beginning">תרום באמצעות flattr כשניגון פרק מתחיל</string> + <string name="auto_flattr_ater_end">תרום באמצעות flattr כשניגון פרק מסתיים</string> + <!--Search--> + <string name="search_hint">חפש הזנות או פרקים</string> + <string name="found_in_shownotes_label">נמצא בהערות פרק</string> + <string name="found_in_chapters_label">נמצא בפרקים</string> + <string name="search_status_no_results">אין תוצאות</string> + <string name="search_label">חיפוש</string> + <string name="found_in_title_label">נמצא בכותרת</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">קבצי OPML יאפשרו לכך לנייד פודקאסטים מלוכד פודקאסטים אחד למשנו.</string> + <string name="opml_import_explanation">לייבא קובץ OPML, אתה צריך למקם אותו בספרייה הבאה וללחוץ על הכפתור למטה כדי להתחיל את תהליך היבוא.</string> + <string name="start_import_label">התחל יבוא</string> + <string name="opml_import_label">יבוא OPML</string> + <string name="opml_directory_error">שגיאה!</string> + <string name="reading_opml_label">קורא קובץ OPML</string> + <string name="opml_reader_error">אירעה שגיאה בזמן קריאת קובץ OPML:</string> + <string name="opml_import_error_dir_empty">ספריית היבוא ריקה.</string> + <string name="select_all_label">בחר הכל</string> + <string name="deselect_all_label">בטל בחירות</string> + <string name="choose_file_to_import_label">בחר קובץ ליבוא</string> + <string name="opml_export_label">יצוא OPML</string> + <string name="exporting_label">מייצא...</string> + <string name="export_error_label">שגיאת יצוא</string> + <string name="opml_export_success_title">יצוא OPML הצליח.</string> + <string name="opml_export_success_sum">קובץ OPML נכתב ל:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">קבע טיימר שינה</string> + <string name="disable_sleeptimer_label">בטל טיימר שינה</string> + <string name="enter_time_here_label">קבע זמן</string> + <string name="sleep_timer_label">טיימר שינה</string> + <string name="time_left_label">זמן נותר:\u0020</string> + <string name="time_dialog_invalid_input">קלט לא חוקי, זמן חייב להיות מספר שלם</string> + <string name="time_unit_seconds">שניות</string> + <string name="time_unit_minutes">דקות</string> + <string name="time_unit_hours">שעות</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">קטגוריות</string> + <string name="gpodnet_toplist_header">פודקאסטים בכירים</string> + <string name="gpodnet_suggestions_header">המלצות</string> + <string name="gpodnet_search_hint">חפש ב-gpodder.net</string> + <string name="gpodnetauth_login_title">התחברות</string> + <string name="gpodnetauth_login_descr">ברוך הבא להתחברות ל-gpodder.net. ראשית, הקלד את פרטי הכניסה שלך:</string> + <string name="gpodnetauth_login_butLabel">התחברות</string> + <string name="gpodnetauth_login_register">אם אין לך עדיין חשבון, תוכל לפתוח אחד דרך: \nhttps://gpodder.net/register/</string> + <string name="username_label">שם משתמש:</string> + <string name="password_label">ססמה:</string> + <string name="gpodnetauth_device_title">בחירת מכשיר</string> + <string name="gpodnetauth_device_descr">צור מכשיר חדש לשימוש עבור חשבון gpodder.net או לבחר אחד קיים:</string> + <string name="gpodnetauth_device_deviceID">מזהה מכשיר:\u0020</string> + <string name="gpodnetauth_device_caption">כותרת</string> + <string name="gpodnetauth_device_butCreateNewDevice">צור מכשיר חדש</string> + <string name="gpodnetauth_device_chooseExistingDevice">בחר מכשיר קיים:</string> + <string name="gpodnetauth_device_errorEmpty">מזהה המכשיר אינו יכול להיות ריק</string> + <string name="gpodnetauth_device_errorAlreadyUsed">מזהה המכשיר בשימוש</string> + <string name="gpodnetauth_device_butChoose">בחר</string> + <string name="gpodnetauth_finish_title">התחברות מוצלחת!</string> + <string name="gpodnetauth_finish_descr">מזל טוב! חשבון gpodder.net שלך מקושר כעת עם המכשיר שלך. אנטנה-פוד מעתה יסנכרן באופן אוטומטי הרשמות במכשיר שלך עם חשבון gpodder.net שלך.</string> + <string name="gpodnetauth_finish_butsyncnow">התחל סנכרון כעת</string> + <string name="gpodnetauth_finish_butgomainscreen">עבור למסך הראשי</string> + <string name="gpodnetsync_auth_error_title">שגיאת אימות של gpodder.net</string> + <string name="gpodnetsync_auth_error_descr">שם משתמש או ססמה שגויים</string> + <string name="gpodnetsync_error_title">שגיאת סנכרון של gpodder.net</string> + <string name="gpodnetsync_error_descr">שגיאה במהל סינכרון:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">תיקיה נבחרת:</string> + <string name="create_folder_label">צור תיקיה</string> + <string name="choose_data_directory">בחר תיקיית מידע</string> + <string name="create_folder_msg">צור תיקיה חדשה בשם \"%1$s\"?</string> + <string name="create_folder_success">תיקיה חדשה נוצרה</string> + <string name="create_folder_error_no_write_access">לא ניתן לכתוב לתיקה זו</string> + <string name="create_folder_error_already_exists">תיקה כבר קיימת</string> + <string name="create_folder_error">לא ניתן ליצור תיקיה</string> + <string name="folder_not_empty_dialog_title">התיקיה אינה ריקה</string> + <string name="folder_not_empty_dialog_msg">התיקייה שבחרת אינה ריקה. הורדות מדיה וקבצים אחרים יהיו ממוקמות ישירות בתיקייה זו. להמשיך בכל זאת?</string> + <string name="set_to_default_folder">בחר תיקיית ברירת מחדל</string> + <string name="pref_pausePlaybackForFocusLoss_sum">השהה ניגון במקום החלשת עוצמת שמע כשאפליקציה אחרת מנגנת</string> + <string name="pref_pausePlaybackForFocusLoss_title">השהה בזמן הפרעה</string> + <!--Online feed view--> + <string name="subscribe_label">הרשם</string> + <string name="subscribed_label">נרשם</string> + <string name="downloading_label">מוריד...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">הצג פרקים</string> + <string name="show_shownotes_label">הצג הערות פרק</string> + <string name="show_cover_label">הצג תמונה</string> + <string name="rewind_label">דלג לאחור</string> + <string name="fast_forward_label">הרץ קדימה</string> + <string name="media_type_audio_label">שמע</string> + <string name="media_type_video_label">וידאו</string> + <string name="navigate_upwards_label">נווט למעלה</string> + <string name="butAction_label">עוד פעולות</string> + <string name="status_playing_label">הפרק מתנגן</string> + <string name="status_downloading_label">הפרק יורד</string> + <string name="status_downloaded_label">הפרק ירד</string> + <string name="status_unread_label">פריט חדש</string> + <string name="in_queue_label">הפרק בתור</string> + <string name="new_episodes_count_label">מספר הפרקים החדשים</string> + <string name="in_progress_episodes_count_label">מספר הפרקים שהתחלת להאזין להם</string> + <string name="drag_handle_content_description">גרור לשינוי מיקום פריט זה</string> + <string name="load_next_page_label">טען את הדף הבא</string> + <!--Feed information screen--> + <string name="authentication_label">אימות</string> + <string name="authentication_descr">שנה את שם המשתמש והסיסמה שלך לפודקאסט ופרקים שלו.</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">מייבא רישום מאפליקציות יעודיות...</string> +</resources> diff --git a/core/src/main/res/values-ja/strings.xml b/core/src/main/res/values-ja/strings.xml new file mode 100644 index 000000000..fd5039e33 --- /dev/null +++ b/core/src/main/res/values-ja/strings.xml @@ -0,0 +1,349 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">フィード</string> + <string name="add_feed_label">フィードを追加</string> + <string name="podcasts_label">ポッドキャスト</string> + <string name="episodes_label">エピソード</string> + <string name="new_episodes_label">新エピソード</string> + <string name="all_episodes_label">全てのエピソード</string> + <string name="new_label">新</string> + <string name="waiting_list_label">待ちリスト</string> + <string name="settings_label">設定</string> + <string name="add_new_feed_label">フィードを追加</string> + <string name="downloads_label">ダウンロード</string> + <string name="downloads_running_label">実行中</string> + <string name="downloads_completed_label">完了</string> + <string name="downloads_log_label">ログ</string> + <string name="cancel_download_label">ダウンロードをキャンセル</string> + <string name="playback_history_label">再生履歴</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net ログイン</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">最近公開された</string> + <string name="episode_filter_label">新しいエピソードのみ表示</string> + <!--Main activity--> + <string name="drawer_open">メニューを開く</string> + <string name="drawer_close">メニューを閉じる</string> + <!--Webview actions--> + <string name="open_in_browser_label">ブラウザーで開く</string> + <string name="copy_url_label">URLをコピー</string> + <string name="share_url_label">URLを共有</string> + <string name="copied_url_msg">URLをクリップボードにコピーしました。</string> + <string name="go_to_position_label">この場所に移動</string> + <!--Playback history--> + <string name="clear_history_label">履歴をクリア</string> + <!--Other--> + <string name="confirm_label">確認</string> + <string name="cancel_label">キャンセル</string> + <string name="author_label">作者</string> + <string name="language_label">言語</string> + <string name="podcast_settings_label">設定</string> + <string name="cover_label">映像</string> + <string name="error_label">エラー</string> + <string name="error_msg_prefix">エラーが発生しました。</string> + <string name="refresh_label">更新</string> + <string name="external_storage_error_msg">外部ストレージが利用できません。アプリが正常に動作できるように、外部ストレージがマウントされていることを確認してください。</string> + <string name="chapters_label">チャプター</string> + <string name="shownotes_label">ショーノート</string> + <string name="description_label">説明</string> + <string name="most_recent_prefix">最も最近のエピソード:\u0020</string> + <string name="episodes_suffix">\u0020エピソード</string> + <string name="length_prefix">長さ:\u0020</string> + <string name="size_prefix">サイズ:\u0020</string> + <string name="processing_label">処理中</string> + <string name="loading_label">読み込み中...</string> + <string name="save_username_password_label">ユーザ名とパスワードを保存する</string> + <string name="close_label">閉じる</string> + <string name="retry_label">再試行</string> + <string name="auto_download_label">自動ダウンロードに含む</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">フィードURL</string> + <string name="etxtFeedurlHint">フィードまたはWebサイトのURL</string> + <string name="txtvfeedurl_label">URLでポッドキャストを追加</string> + <string name="podcastdirectories_label">ディレクトリでポッドキャストを検索</string> + <string name="podcastdirectories_descr">名前、カテゴリや人気で、gpodder.netディレクトリ内の新しいポッドキャストを検索することができます。</string> + <string name="browse_gpoddernet_label">gpodder.netを参照</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">既読としてマーク</string> + <string name="mark_all_read_msg">すべてのエピソードを既読にしました</string> + <string name="show_info_label">情報を表示</string> + <string name="remove_feed_label">ポッドキャストを削除</string> + <string name="share_link_label">Webサイトのリンクを共有</string> + <string name="share_source_label">フィードのリンクを共有</string> + <string name="feed_delete_confirmation_msg">このフィードと、このフィードのダウンロードしたすべてのエピソードを削除することを確認してください。</string> + <string name="feed_remover_msg">フィードの削除中</string> + <string name="load_complete_feed">フィードをすべて更新</string> + <!--actions on feeditems--> + <string name="download_label">ダウンロード</string> + <string name="play_label">再生</string> + <string name="pause_label">一時停止</string> + <string name="stop_label">停止</string> + <string name="stream_label">ストリーム</string> + <string name="remove_label">削除</string> + <string name="remove_episode_lable">エピソードを削除</string> + <string name="mark_read_label">既読にする</string> + <string name="mark_unread_label">未読にする</string> + <string name="add_to_queue_label">キューに追加</string> + <string name="remove_from_queue_label">キューから削除</string> + <string name="visit_website_label">Webサイトを訪問</string> + <string name="support_label">これをFlattr</string> + <string name="enqueue_all_new">すべてキューに入れる</string> + <string name="download_all">すべてダウンロード</string> + <string name="skip_episode_label">エピソードをスキップ</string> + <!--Download messages and labels--> + <string name="download_successful">完成</string> + <string name="download_failed">失敗</string> + <string name="download_pending">ダウンロードは保留中</string> + <string name="download_running">ダウンロード実行中</string> + <string name="download_error_device_not_found">ストレージ デバイスが見つかりません</string> + <string name="download_error_insufficient_space">スペースが不足しています</string> + <string name="download_error_file_error">ファイルエラー</string> + <string name="download_error_http_data_error">HTTPデータエラー</string> + <string name="download_error_error_unknown">不明なエラー</string> + <string name="download_error_parser_exception">解析エラー</string> + <string name="download_error_unsupported_type">サポートしないフィードタイプ</string> + <string name="download_error_connection_error">接続エラー</string> + <string name="download_error_unknown_host">ホスト不明</string> + <string name="download_error_unauthorized">認証エラー</string> + <string name="cancel_all_downloads_label">すべてのダウンロードをキャンセル</string> + <string name="download_cancelled_msg">ダウンロードをキャンセルしました</string> + <string name="download_report_title">ダウンロードが完了しました</string> + <string name="download_error_malformed_url">不正な形式のURL</string> + <string name="download_error_io_error">IOエラー</string> + <string name="download_error_request_error">リクエストエラー</string> + <string name="download_error_db_access">データベース アクセスエラー</string> + <string name="downloads_left">\u0020ダウンロード残</string> + <string name="downloads_processing">ダウンロード処理中</string> + <string name="download_notification_title">ポッドキャストデータをダウンロード中</string> + <string name="download_report_content">%1$d ダウンロード成功, %2$d 失敗</string> + <string name="download_log_title_unknown">タイトル不明</string> + <string name="download_type_feed">フィード</string> + <string name="download_type_media">メディアファイル</string> + <string name="download_type_image">画像</string> + <string name="download_request_error_dialog_message_prefix">ファイルのダウンロード中にエラーが発生しました:\u0020</string> + <string name="authentication_notification_title">認証が必要です</string> + <string name="authentication_notification_msg">リクエストしたリソースは、ユーザー名とパスワードが必要です</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">エラー!</string> + <string name="player_stopped_msg">再生するメディアがありません</string> + <string name="player_preparing_msg">準備中</string> + <string name="player_ready_msg">準備完了</string> + <string name="player_seeking_msg">シーク中</string> + <string name="playback_error_server_died">サーバーがダウンしています</string> + <string name="playback_error_unknown">不明なエラー</string> + <string name="no_media_playing_label">再生するメディアがありません</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">バッファー中</string> + <string name="playbackservice_notification_title">ポッドキャストを再生中</string> + <string name="unknown_media_key">AntennaPod - 不明なメディアキー: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">キューをクリア</string> + <string name="undo">元に戻す</string> + <string name="removed_from_queue">アイテムを削除しました</string> + <string name="move_to_top_label">先頭に移動</string> + <string name="move_to_bottom_label">最後に移動</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattrにサインイン</string> + <string name="flattr_auth_explanation">認証処理を開始するには、下のボタンを押します。お使いのブラウザでflattrのログイン画面に転送され、AntennaPodにflattrする許可を与えるように求められます。あなたが許可を与えた後、自動的にこの画面に戻ります。</string> + <string name="authenticate_label">認証</string> + <string name="return_home_label">ホームに戻る</string> + <string name="flattr_auth_success">認証が成功しました! アプリからflattrすることができます。</string> + <string name="no_flattr_token_title">Flattrのトークンが見つかりません</string> + <string name="no_flattr_token_notification_msg">あなたのflattrアカウントがAntennaPodに接続していないようです。認証するにはここをタップしてください。</string> + <string name="no_flattr_token_msg">あなたのflattrアカウントがAntennaPodに接続していないようです。アプリからflattrするためにAntennaPodのアカウントに接続するか、またはflattrするためにWebサイトを訪問してください。</string> + <string name="authenticate_now_label">認証</string> + <string name="action_forbidden_title">操作は禁止されています</string> + <string name="action_forbidden_msg">AntennaPodにこの操作をする権限がありません。あなたのアカウントへのAntennaPodのアクセストークンが取り消されたことが考えられます。あなたは再認証するか、またはその代わりにWebサイトを訪問してください。</string> + <string name="access_revoked_title">アクセスが取り消されました</string> + <string name="access_revoked_info">あなたのアカウントへのAntennaPodのアクセストークンを取り消しました。処理を完了するには、flattrのWebサイトで、あなたのアカウント設定で承認されたアプリケーションのリストから、このアプリを削除する必要があります。</string> + <!--Flattr--> + <string name="flattr_click_success">1 件Flattrしました!</string> + <string name="flattr_click_success_count">%d 件Flattrしました!</string> + <string name="flattr_click_success_queue">Flattrしました: %s.</string> + <string name="flattr_click_failure_count">Flattrに失敗 %d 件!</string> + <string name="flattr_click_failure">Flattrしませんでした: %s.</string> + <string name="flattr_click_enqueued">後でFlattrされます</string> + <string name="flattring_thing">Flattr中 %s</string> + <string name="flattring_label">AntennaPod がFlattr中</string> + <string name="flattrd_label">AntennaPod がFlattrしました</string> + <string name="flattrd_failed_label">AntennaPod はFlattrに失敗しました</string> + <string name="flattr_retrieving_status">Flattrの取得中</string> + <!--Variable Speed--> + <string name="download_plugin_label">プラグインをダウンロード</string> + <string name="no_playback_plugin_title">プラグイン はインストールされていません</string> + <string name="no_playback_plugin_msg">再生速度の変更が機能するには、サードパーティのライブラリをインストールしておく必要があります。\n\n\'プラグインをダウンロード\' をタップして、Google Playから無料のプラグインをダウンロードしてください\n\nこのプラグインを使用して見つかったすべての問題は AntennaPodの責任ではなく、プラグインのオーナーに報告すべきです。</string> + <string name="set_playback_speed_label">再生速度</string> + <!--Empty list labels--> + <string name="no_items_label">このリストにはアイテムがありません。</string> + <string name="no_feeds_label">まだフィードを何も購読していません。</string> + <!--Preferences--> + <string name="other_pref">その他</string> + <string name="about_pref">について</string> + <string name="queue_label">キュー</string> + <string name="services_label">サービス</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">ヘッドフォンの接続が切断された時、再生を一時停止</string> + <string name="pref_followQueue_sum">再生が完了した時に次のキューのアイテムに移動</string> + <string name="playback_pref">再生</string> + <string name="network_pref">ネットワーク</string> + <string name="pref_autoUpdateIntervall_title">更新間隔</string> + <string name="pref_autoUpdateIntervall_sum">フィードが自動的に更新される間隔を指定するか、または無効にしてください</string> + <string name="pref_downloadMediaOnWifiOnly_sum">WiFi接続時のみメディアファイルをダウンロード</string> + <string name="pref_followQueue_title">連続再生</string> + <string name="pref_downloadMediaOnWifiOnly_title">WiFiメディアダウンロード</string> + <string name="pref_pauseOnHeadsetDisconnect_title">ヘッドフォン切断</string> + <string name="pref_mobileUpdate_title">モバイル更新</string> + <string name="pref_mobileUpdate_sum">モバイルデータ接続時に更新を許可する</string> + <string name="refreshing_label">更新中</string> + <string name="flattr_settings_label">Flattr設定</string> + <string name="pref_flattr_auth_title">Flattrにサインイン</string> + <string name="pref_flattr_auth_sum">アプリから直接Flattrするため、あなたのFlattrアカウントにサインインしてください。</string> + <string name="pref_flattr_this_app_title">このアプリをFlattr</string> + <string name="pref_flattr_this_app_sum">FlattrしてAntennaPodの開発を支援してください。ありがとうございます!</string> + <string name="pref_revokeAccess_title">アクセスを取り消し</string> + <string name="pref_revokeAccess_sum">あなたのflattrアカウントへのこのアプリのアクセス許可を取り消します。</string> + <string name="pref_auto_flattr_title">自動Flattr</string> + <string name="pref_auto_flattr_sum">自動Flattrを構成</string> + <string name="user_interface_label">インターフェース</string> + <string name="pref_set_theme_title">テーマを選択</string> + <string name="pref_set_theme_sum">AntennaPodの外観を変更します。</string> + <string name="pref_automatic_download_title">自動ダウンロード</string> + <string name="pref_automatic_download_sum">エピソードの自動ダウンロードを構成します。</string> + <string name="pref_autodl_wifi_filter_title">Wi-Fiフィルターを有効にする</string> + <string name="pref_autodl_wifi_filter_sum">選択したWi-Fiネットワークに対してのみ自動ダウンロードを許可します。</string> + <string name="pref_episode_cache_title">エピソードキャッシュ</string> + <string name="pref_theme_title_light">ライト</string> + <string name="pref_theme_title_dark">ダーク</string> + <string name="pref_episode_cache_unlimited">無制限</string> + <string name="pref_update_interval_hours_plural">時間</string> + <string name="pref_update_interval_hours_singular">時間</string> + <string name="pref_update_interval_hours_manual">手動</string> + <string name="pref_gpodnet_authenticate_title">ログイン</string> + <string name="pref_gpodnet_authenticate_sum">購読を同期するために、あなたのgpodder.netアカウントにログインします。</string> + <string name="pref_gpodnet_logout_title">ログアウト</string> + <string name="pref_gpodnet_logout_toast">ログアウトしました</string> + <string name="pref_gpodnet_setlogin_information_title">ログイン情報を変更</string> + <string name="pref_gpodnet_setlogin_information_sum">gpodder.netアカウントのログイン情報を変更します。</string> + <string name="pref_playback_speed_title">再生速度</string> + <string name="pref_playback_speed_sum">可変速度音声再生に使用可能な速度をカスタマイズします</string> + <string name="pref_seek_delta_title">シーク時間</string> + <string name="pref_seek_delta_sum">巻き戻しまたは早送り時にこの秒数でシークします</string> + <string name="pref_gpodnet_sethostname_title">ホスト名をセット</string> + <string name="pref_gpodnet_sethostname_use_default_host">デフォルトホストを使用</string> + <string name="pref_expandNotify_title">拡張通知</string> + <string name="pref_expandNotify_sum">常に再生ボタンを表示するように通知を展開します。</string> + <string name="pref_persistNotify_title">永続再生コントロール</string> + <string name="pref_persistNotify_sum">再生が一時停止された時に、通知およびロック画面のコントロールを保持します。</string> + <string name="pref_expand_notify_unsupport_toast">Androidバージョン4.1以前では、拡張通知をサポートしていません。</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">自動Flattrを有効にする</string> + <string name="auto_flattr_after_percent">%d %再生したらエピソードをFlattr </string> + <string name="auto_flattr_ater_beginning">再生を開始した時にエピソードをFlattr</string> + <string name="auto_flattr_ater_end">再生が終了した時にエピソードをFlattr</string> + <!--Search--> + <string name="search_hint">フィードまたはエピソードを検索</string> + <string name="found_in_shownotes_label">ショーノートで見つかりました</string> + <string name="found_in_chapters_label">チャプターで見つかりました</string> + <string name="search_status_no_results">見つかりませんでした</string> + <string name="search_label">検索</string> + <string name="found_in_title_label">タイトルで見つかりました</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">OPMLファイルで、あるポッドキャッチャーから別のものにポッドキャストを移動することができます。</string> + <string name="opml_import_explanation">OPMLファイルをインポートするには、次のディレクトリにファイルを置き、以下のボタンを押してインポート処理を開始します。</string> + <string name="start_import_label">インポート開始</string> + <string name="opml_import_label">OPMLインポート</string> + <string name="opml_directory_error">エラー!</string> + <string name="reading_opml_label">OPMLファイルを読み込み中</string> + <string name="opml_reader_error">OPMLドキュメントの読み込み中にエラーが発生しました。</string> + <string name="opml_import_error_dir_empty">インポートディレクトリが空です。</string> + <string name="select_all_label">すべてを選択</string> + <string name="deselect_all_label">選択解除</string> + <string name="choose_file_to_import_label">インポートするファイルを選択してください</string> + <string name="opml_export_label">OPMLエクスポート</string> + <string name="exporting_label">エクスポート中...</string> + <string name="export_error_label">エクスポートエラー</string> + <string name="opml_export_success_title">OPMLをエクスポートしました。</string> + <string name="opml_export_success_sum">.opml ファイルを書き込みました:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">スリープタイマーをセット</string> + <string name="disable_sleeptimer_label">スリープタイマーを無効にする</string> + <string name="enter_time_here_label">時間を入力</string> + <string name="sleep_timer_label">スリープタイマー</string> + <string name="time_left_label">残り時間:\u0020</string> + <string name="time_dialog_invalid_input">入力が正しくありません、時間は数字で入力してください</string> + <string name="time_unit_seconds">秒</string> + <string name="time_unit_minutes">分</string> + <string name="time_unit_hours">時</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">カテゴリー</string> + <string name="gpodnet_toplist_header">トップ ボッドキャスト</string> + <string name="gpodnet_suggestions_header">おススメ</string> + <string name="gpodnet_search_hint">gpodder.netを検索</string> + <string name="gpodnetauth_login_title">ログイン</string> + <string name="gpodnetauth_login_descr">gpodder.netログインへようこそ。まずログイン情報を入力してください。</string> + <string name="gpodnetauth_login_butLabel">ログイン</string> + <string name="gpodnetauth_login_register">まだアカウントをお持ちでなければ、ここで作成することができます。\nhttps://gpodder.net/register/</string> + <string name="username_label">ユーザー名</string> + <string name="password_label">パスワード</string> + <string name="gpodnetauth_device_title">端末選択</string> + <string name="gpodnetauth_device_descr">gpodder.net アカウントで使用する新しい端末を作成するか、既存のものを選択してください。</string> + <string name="gpodnetauth_device_deviceID">端末ID:\u0020</string> + <string name="gpodnetauth_device_caption">キャプション</string> + <string name="gpodnetauth_device_butCreateNewDevice">新しい端末を作成</string> + <string name="gpodnetauth_device_chooseExistingDevice">既存の端末を選択:</string> + <string name="gpodnetauth_device_errorEmpty">端末IDは空にできません</string> + <string name="gpodnetauth_device_errorAlreadyUsed">端末IDは既に使用しています</string> + <string name="gpodnetauth_device_butChoose">選択</string> + <string name="gpodnetauth_finish_title">ログインされました!</string> + <string name="gpodnetauth_finish_descr">おめでとうございます! あなたのgpodder.netアカウントが今お使いの端末とリンクされました。 AntennaPodは今から自動的にgpodder.netアカウントを使用して端末の購読を同期します。</string> + <string name="gpodnetauth_finish_butsyncnow">今すぐ同期を開始</string> + <string name="gpodnetauth_finish_butgomainscreen">メイン画面に移動</string> + <string name="gpodnetsync_auth_error_title">gpodder.net 認証エラー</string> + <string name="gpodnetsync_auth_error_descr">ユーザー名またはパスワードが間違っています</string> + <string name="gpodnetsync_error_title">gpodder.net 同期エラー</string> + <string name="gpodnetsync_error_descr">同期中にエラーが発生しました:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">選択したフォルダー:</string> + <string name="create_folder_label">フォルダーを作成</string> + <string name="choose_data_directory">データ フォルダーを選択</string> + <string name="create_folder_msg">名前 \"%1$s\" で新しいフォルダーを作成しますか?</string> + <string name="create_folder_success">新しいフォルダーを作成しました</string> + <string name="create_folder_error_no_write_access">このフォルダーに書き込みできません</string> + <string name="create_folder_error_already_exists">フォルダーは既に存在します</string> + <string name="create_folder_error">フォルダーを作成できません</string> + <string name="folder_not_empty_dialog_title">フォルダーが空ではありません</string> + <string name="folder_not_empty_dialog_msg">選択したフォルダは空ではありません。メディアのダウンロードやその他のファイルはこのフォルダに直接配置されます。続けますか?</string> + <string name="set_to_default_folder">デフォルトのフォルダーを選択</string> + <string name="pref_pausePlaybackForFocusLoss_sum">他のアプリがサウンドを再生したい時に、音量を下げる代わりに再生の一時停止します</string> + <string name="pref_pausePlaybackForFocusLoss_title">割り込み時に一時停止</string> + <!--Online feed view--> + <string name="subscribe_label">購読</string> + <string name="subscribed_label">購読しました</string> + <string name="downloading_label">ダウンロード中...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">チャプターを表示</string> + <string name="show_shownotes_label">ショーノートを表示</string> + <string name="show_cover_label">映像を表示</string> + <string name="rewind_label">巻き戻し</string> + <string name="fast_forward_label">早送り</string> + <string name="media_type_audio_label">音声</string> + <string name="media_type_video_label">動画</string> + <string name="navigate_upwards_label">上に移動</string> + <string name="butAction_label">その他の操作</string> + <string name="status_playing_label">エピソードを再生中</string> + <string name="status_downloading_label">エピソードをダウンロード中</string> + <string name="status_downloaded_label">エピソードをダウンロードしました</string> + <string name="status_unread_label">新しいアイテムです</string> + <string name="in_queue_label">エピソードはキューの中にあります</string> + <string name="new_episodes_count_label">新しいエピソードの数</string> + <string name="in_progress_episodes_count_label">聞き始めたエピソードの数</string> + <string name="drag_handle_content_description">このアイテムの場所を変更するにはドラッグしてください</string> + <string name="load_next_page_label">次のページをロード</string> + <!--Feed information screen--> + <string name="authentication_label">認証</string> + <string name="authentication_descr">このポッドキャストとそのエピソード用のあなたのユーザー名とパスワードを変更します。</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">単一目的のアプリから購読をインポート中…</string> +</resources> diff --git a/core/src/main/res/values-ko/strings.xml b/core/src/main/res/values-ko/strings.xml new file mode 100644 index 000000000..a29de1582 --- /dev/null +++ b/core/src/main/res/values-ko/strings.xml @@ -0,0 +1,347 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">안테나팟</string> + <string name="feeds_label">피드</string> + <string name="add_feed_label">팟캐스트 추가</string> + <string name="podcasts_label">팟캐스트</string> + <string name="episodes_label">에피소드</string> + <string name="new_episodes_label">새 에피소드</string> + <string name="all_episodes_label">모든 에피소드</string> + <string name="new_label">신규</string> + <string name="waiting_list_label">추가 대기 목록</string> + <string name="settings_label">설정</string> + <string name="add_new_feed_label">팟캐스트 추가</string> + <string name="downloads_label">다운로드</string> + <string name="downloads_running_label">실행 중</string> + <string name="downloads_completed_label">마침</string> + <string name="downloads_log_label">기록</string> + <string name="cancel_download_label">다운로드 취소</string> + <string name="playback_history_label">재생 기록</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net 로그인</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">최근에 발표</string> + <string name="episode_filter_label">새 에피소드만 표시</string> + <!--Main activity--> + <string name="drawer_open">메뉴 열기</string> + <string name="drawer_close">메뉴 닫기</string> + <!--Webview actions--> + <string name="open_in_browser_label">브라우저에서 열기</string> + <string name="copy_url_label">URL 복사</string> + <string name="share_url_label">URL 공유</string> + <string name="copied_url_msg">URL을 클립보드에 복사했습니다.</string> + <string name="go_to_position_label">이 위치로 이동</string> + <!--Playback history--> + <string name="clear_history_label">기록 지우기</string> + <!--Other--> + <string name="confirm_label">확인</string> + <string name="cancel_label">취소</string> + <string name="author_label">저자</string> + <string name="language_label">언어</string> + <string name="podcast_settings_label">설정</string> + <string name="cover_label">그림</string> + <string name="error_label">오류</string> + <string name="error_msg_prefix">오류가 발생했습니다:</string> + <string name="refresh_label">새로 고침</string> + <string name="external_storage_error_msg">외부 저장 장치가 없습니다. 앱이 제대로 동작하려면 외부 저장장치를 마운트하십시오.</string> + <string name="chapters_label">챕터</string> + <string name="shownotes_label">프로그램 메모</string> + <string name="description_label">설명</string> + <string name="most_recent_prefix">가장 최근 에피소드:\u0020</string> + <string name="episodes_suffix">\u0020에피소드</string> + <string name="length_prefix">길이:\u0020</string> + <string name="size_prefix">크기:\u0020</string> + <string name="processing_label">처리 중</string> + <string name="loading_label">읽어들이는 중...</string> + <string name="save_username_password_label">사용자 이름 및 암호 저장</string> + <string name="close_label">닫기</string> + <string name="retry_label">다시 시도</string> + <string name="auto_download_label">자동 다운로드에 포함</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">피드 URL</string> + <string name="etxtFeedurlHint">피드의 URL 또는 홈페이지</string> + <string name="txtvfeedurl_label">URL로 팟캐스트를 추가</string> + <string name="podcastdirectories_label">디렉터리에서 팟캐스트 찾기</string> + <string name="podcastdirectories_descr">gpodder.net 디렉터리에서 이름, 분류, 인기에 따라 새 팟캐스트를 검색할 수 있습니다</string> + <string name="browse_gpoddernet_label">gpodder.net 둘러보기</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">모두 읽은 것으로 표시</string> + <string name="mark_all_read_msg">모든 에피소드 읽은 것으로 표시</string> + <string name="show_info_label">정보 표시</string> + <string name="remove_feed_label">팟캐스트 제거</string> + <string name="share_link_label">홈페이지 링크 공유</string> + <string name="share_source_label">피드 링크 공유</string> + <string name="feed_delete_confirmation_msg">이 피드와 이 피드에서 다운로드한 모든 에피소드를 삭제하시려면 확인을 누르십시오.</string> + <string name="feed_remover_msg">피드 삭제하는 중</string> + <!--actions on feeditems--> + <string name="download_label">다운로드</string> + <string name="play_label">재생</string> + <string name="pause_label">일시 중지</string> + <string name="stop_label">중지</string> + <string name="stream_label">스트리밍</string> + <string name="remove_label">제거</string> + <string name="remove_episode_lable">에피소드 제거</string> + <string name="mark_read_label">읽은 것으로 표시</string> + <string name="mark_unread_label">읽지 않은 것으로 표시</string> + <string name="add_to_queue_label">대기열에 추가</string> + <string name="remove_from_queue_label">대기열에서 제거</string> + <string name="visit_website_label">홈페이지 보기</string> + <string name="support_label">Flattr하기</string> + <string name="enqueue_all_new">모두 대기열에 추가</string> + <string name="download_all">모두 다운로드</string> + <string name="skip_episode_label">에피소드 건너뛰기</string> + <!--Download messages and labels--> + <string name="download_successful">성공</string> + <string name="download_failed">실패</string> + <string name="download_pending">다운로드 지연 중</string> + <string name="download_running">다운로드 실행 중</string> + <string name="download_error_device_not_found">저장 장치가 없습니다</string> + <string name="download_error_insufficient_space">저장 공간이 부족합니다</string> + <string name="download_error_file_error">파일 오류</string> + <string name="download_error_http_data_error">HTTP 데이터 오류</string> + <string name="download_error_error_unknown">알 수 없는 오류</string> + <string name="download_error_parser_exception">파서 프로그램 예외</string> + <string name="download_error_unsupported_type">지원하지 않는 피드 종류</string> + <string name="download_error_connection_error">연결 오류</string> + <string name="download_error_unknown_host">알 수 없는 호스트</string> + <string name="download_error_unauthorized">인증 오류</string> + <string name="cancel_all_downloads_label">모든 다운로드 취소</string> + <string name="download_cancelled_msg">다운로드 취소됨</string> + <string name="download_report_title">다운로드 마침</string> + <string name="download_error_malformed_url">URL 형식 틀림</string> + <string name="download_error_io_error">입출력 오류</string> + <string name="download_error_request_error">요청 오류</string> + <string name="download_error_db_access">데이터베이스 접근 오류</string> + <string name="downloads_left">개\u0020다운로드 남음</string> + <string name="downloads_processing">다운로드 처리 중</string> + <string name="download_notification_title">팟캐스트 데이터 다운로드 중</string> + <string name="download_report_content">다운로드 %1$d개 성공, %2$d개 실패</string> + <string name="download_log_title_unknown">알 수 없는 제목</string> + <string name="download_type_feed">피드</string> + <string name="download_type_media">미디어 파일</string> + <string name="download_type_image">그림</string> + <string name="download_request_error_dialog_message_prefix">파일을 다운로드하는 중 오류가 발생했습니다:\u0020</string> + <string name="authentication_notification_title">인증이 필요합니다</string> + <string name="authentication_notification_msg">요청한 자원은 사용자 이름과 암호가 필요합니다</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">오류!</string> + <string name="player_stopped_msg">재생 중인 미디어 없음</string> + <string name="player_preparing_msg">준비하는 중</string> + <string name="player_ready_msg">준비 완료</string> + <string name="player_seeking_msg">이동 중</string> + <string name="playback_error_server_died">서버가 죽었습니다</string> + <string name="playback_error_unknown">알 수 없는 오류</string> + <string name="no_media_playing_label">재생 중인 미디어 없음</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">버퍼링 중</string> + <string name="playbackservice_notification_title">팟캐스트 재생 중</string> + <string name="unknown_media_key">안테나팟 - 알 수 없는 미디어 키: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">대기열 지우기</string> + <string name="undo">실행 취소</string> + <string name="removed_from_queue">항목을 제거했습니다</string> + <string name="move_to_top_label">맨 위로 이동</string> + <string name="move_to_bottom_label">맨 아래로 이동</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattr 로그인</string> + <string name="flattr_auth_explanation">인증 절차를 시작하려면 아래 버튼을 누르십시오. 브라우저의 Flattr 로그인 화면으로 이동하고, 안테나팟에 Flattr를 사용을 허락 여부를 물어봅니다. 허락을 하면 자동으로 이 화면으로 돌아옵니다.</string> + <string name="authenticate_label">인증</string> + <string name="return_home_label">홈으로 돌아가기</string> + <string name="flattr_auth_success">인증이 성공했습니다! 이제 앱에서 Flattr 기능을 사용할 수 있습니다.</string> + <string name="no_flattr_token_title">Flattr 토큰이 없습니다</string> + <string name="no_flattr_token_notification_msg">flattr 계정을 안테나팟에 연결하지 않았습니다. 인증하려면 누르십시오.</string> + <string name="no_flattr_token_msg">Flattr 계정이 안테나팟에 연결되지 않은 것 같습니다. 앱 안에서 안테나팟을 Flattr 계정에 연결할 수도 있고, Flattr 홈페이지에서 Flattr할 거리를 선택할 수 있습니다.</string> + <string name="authenticate_now_label">인증</string> + <string name="action_forbidden_title">금지된 동작입니다</string> + <string name="action_forbidden_msg">안테나팟에 이 동작을 할 권한이 없습니다. 가능한 원인은 안테나팟이 계정에 접근할 때 사용하는 토큰이 철회된 경우입니다. 다시 인증할 수도 있고, 직접 웹페이지를 이용할 수도 있습니다.</string> + <string name="access_revoked_title">접근이 철회되었습니다</string> + <string name="access_revoked_info">안테나팟에서 계정에 대한 접근 토큰을 성공적으로 철회했습니다. 절차를 마치려면 Flattr 홈페이지, 계정 설정의 허용하는 응용 프로그램 목록에서 이 앱을 제거해야 합니다.</string> + <!--Flattr--> + <string name="flattr_click_success">1개 Flattr했습니다!</string> + <string name="flattr_click_success_count">%d개 Flattr했습니다!</string> + <string name="flattr_click_success_queue">Flattr함: %s</string> + <string name="flattr_click_failure_count">%d개 Flattr하는데 실패했습니다!</string> + <string name="flattr_click_failure">Flattr하지 않음: %s.</string> + <string name="flattr_click_enqueued">나중에 Flattr합니다</string> + <string name="flattring_thing">%s Flattr하는 중</string> + <string name="flattring_label">안테나팟에서 Flattr하는 중</string> + <string name="flattrd_label">안테나팟에서 Flattr했음</string> + <string name="flattrd_failed_label">안테나팟에서 Flattr 실패</string> + <string name="flattr_retrieving_status">Flattr한 내용 가져오는 중</string> + <!--Variable Speed--> + <string name="download_plugin_label">플러그인 다운로드</string> + <string name="no_playback_plugin_title">플러그인을 설치하지 않았습니다</string> + <string name="no_playback_plugin_msg">재생 속도 조절이 동작하려면 외부 라이브러리를 설치해야 합니다.\n\nPlay Store에서 무료 플러그인을 다운로드하려면 \'플러그인 다운로드\'를 누르십시오.\n\n이 플러그인을 사용하면서 생기는 문제는 안테나팟의 책임이 아니므로 해당 플러그인의 관리자에게 문의하십시오.</string> + <string name="set_playback_speed_label">재생 속도</string> + <!--Empty list labels--> + <string name="no_items_label">이 목록에 항목이 없습니다.</string> + <string name="no_feeds_label">아직 어떤 피드도 구독하지 않았습니다.</string> + <!--Preferences--> + <string name="other_pref">기타</string> + <string name="about_pref">정보</string> + <string name="queue_label">대기열</string> + <string name="services_label">서비스</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">헤드폰의 연결이 끊어졌을 때 재생을 일시 중지</string> + <string name="pref_followQueue_sum">재생을 마쳤을 때 다음 대기열로 이동</string> + <string name="playback_pref">재생</string> + <string name="network_pref">네트워크</string> + <string name="pref_autoUpdateIntervall_title">업데이트 주기</string> + <string name="pref_autoUpdateIntervall_sum">피드를 새로 고칠 주기를 지정하거나 새로 고침을 하지 않음</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Wi-Fi를 통해서만 미디어 파일 다운로드</string> + <string name="pref_followQueue_title">연속 재생</string> + <string name="pref_downloadMediaOnWifiOnly_title">Wi-Fi 미디어 다운로드</string> + <string name="pref_pauseOnHeadsetDisconnect_title">헤드폰 연결 끊김</string> + <string name="pref_mobileUpdate_title">휴대전화망 업데이트</string> + <string name="pref_mobileUpdate_sum">휴대전화 데이터 연결을 통해 업데이트 허용</string> + <string name="refreshing_label">새로 고치는 중</string> + <string name="flattr_settings_label">Flattr 설정</string> + <string name="pref_flattr_auth_title">Flattr 로그인</string> + <string name="pref_flattr_auth_sum">Flattr 계정에 로그인하면 앱에서 직접 Flattr할 수 있습니다.</string> + <string name="pref_flattr_this_app_title">이 앱 Flattr하기</string> + <string name="pref_flattr_this_app_sum">Flattr해서 안테나팟 개발을 지원할 수 있습니다. 고맙습니다!</string> + <string name="pref_revokeAccess_title">접근 철회</string> + <string name="pref_revokeAccess_sum">이 앱이 Flattr 계정에 접근할 권한을 철회합니다.</string> + <string name="pref_auto_flattr_title">자동 Flattr</string> + <string name="pref_auto_flattr_sum">자동 flattr 설정</string> + <string name="user_interface_label">사용자 인터페이스</string> + <string name="pref_set_theme_title">테마 선택</string> + <string name="pref_set_theme_sum">안테나팟의 겉모양을 바꿉니다.</string> + <string name="pref_automatic_download_title">자동 다운로드</string> + <string name="pref_automatic_download_sum">에피소드 자동 다운로드를 설정합니다.</string> + <string name="pref_autodl_wifi_filter_title">Wi-Fi 필터 사용</string> + <string name="pref_autodl_wifi_filter_sum">선택한 Wi-Fi 네트워크에 대해서만 자동 다운로드를 허용합니다.</string> + <string name="pref_episode_cache_title">에피소드 임시 저장</string> + <string name="pref_theme_title_light">밝게</string> + <string name="pref_theme_title_dark">어둡게</string> + <string name="pref_episode_cache_unlimited">무제한</string> + <string name="pref_update_interval_hours_plural">시간</string> + <string name="pref_update_interval_hours_singular">시간</string> + <string name="pref_update_interval_hours_manual">수동 지정</string> + <string name="pref_gpodnet_authenticate_title">로그인</string> + <string name="pref_gpodnet_authenticate_sum">gpodder.net 계정으로 로그인해서 구독 정보를 동기화</string> + <string name="pref_gpodnet_logout_title">로그아웃</string> + <string name="pref_gpodnet_logout_toast">로그아웃 성공</string> + <string name="pref_gpodnet_setlogin_information_title">로그인 정보 바꾸기</string> + <string name="pref_gpodnet_setlogin_information_sum">gpodder.net 계정의 로그인 정보를 바꿉니다.</string> + <string name="pref_playback_speed_title">재생 속도</string> + <string name="pref_playback_speed_sum">여러가지 오디오 재생 속도 직접 설정</string> + <string name="pref_seek_delta_title">넘기기 간격</string> + <string name="pref_seek_delta_sum">뒤나 앞으로 이동할 때 몇 초를 넘어갈지 지정합니다</string> + <string name="pref_gpodnet_sethostname_title">호스트 이름 설정</string> + <string name="pref_gpodnet_sethostname_use_default_host">기본 호스트 사용</string> + <string name="pref_expandNotify_title">알림 확장</string> + <string name="pref_expandNotify_sum">항상 알림에서 재생 버튼이 표시되도록 확장</string> + <string name="pref_persistNotify_title">재생 조작 고정</string> + <string name="pref_persistNotify_sum">재생이 일시 중지했을 때에도 알림과 잠금 화면의 조작 기능 유지</string> + <string name="pref_expand_notify_unsupport_toast">안드로이드 4.1 전 버전에서는 알림 확장을 지원하지 않습니다.</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">자동 flattr 사용</string> + <string name="auto_flattr_after_percent">%d 퍼센트를 재생하면 에피소드에 flattr합니다</string> + <string name="auto_flattr_ater_beginning">재생이 시작하면 에피소드 flattr</string> + <string name="auto_flattr_ater_end">재생이 끝나면 에피소드 flattr</string> + <!--Search--> + <string name="search_hint">피드나 에피소드 검색</string> + <string name="found_in_shownotes_label">프로그램 메모에서 발견</string> + <string name="found_in_chapters_label">챕터에서 발견</string> + <string name="search_status_no_results">검색 결과가 없습니다</string> + <string name="search_label">검색</string> + <string name="found_in_title_label">제목에서 발견</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">OPML 파일을 이용하면 팟캐스트 목록을 한 팟캐스트 프로그램에서 다른 팟캐스트 프로그램으로 옮길 수 있습니다.</string> + <string name="opml_import_explanation">OPML 파일을 가져오려면, 다음 디렉터리에 파일을 저장하고 아래 버튼을 누르면 가져오기 처리를 시작합니다.</string> + <string name="start_import_label">가져오기 시작</string> + <string name="opml_import_label">OPML 가져오기</string> + <string name="opml_directory_error">오류!</string> + <string name="reading_opml_label">OPML 파일을 읽는 중</string> + <string name="opml_reader_error">OPML 문서를 읽는 중 오류가 발생했습니다:</string> + <string name="opml_import_error_dir_empty">가져오기 디렉터리가 비어 있습니다.</string> + <string name="select_all_label">모두 선택</string> + <string name="deselect_all_label">모두 선택 해제</string> + <string name="choose_file_to_import_label">가져올 파일을 고르십시오</string> + <string name="opml_export_label">OPML 내보내기</string> + <string name="exporting_label">내보내는 중...</string> + <string name="export_error_label">내보내기 오류</string> + <string name="opml_export_success_title">OPML 내보내기가 성공했습니다.</string> + <string name="opml_export_success_sum">OPML 파일을 다음에 저장했습니다:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">취침 타이머 설정</string> + <string name="disable_sleeptimer_label">취침 타이머 사용 않음</string> + <string name="enter_time_here_label">시간 입력</string> + <string name="sleep_timer_label">취침 타이머</string> + <string name="time_left_label">남은 시간:\u0020</string> + <string name="time_dialog_invalid_input">입력이 잘못되었습니다. 시간으로 숫자를 입력해야 합니다.</string> + <string name="time_unit_seconds">초</string> + <string name="time_unit_minutes">분</string> + <string name="time_unit_hours">시간</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">분류</string> + <string name="gpodnet_toplist_header">상위 팟캐스트</string> + <string name="gpodnet_suggestions_header">추천</string> + <string name="gpodnet_search_hint">gpodder.net 검색</string> + <string name="gpodnetauth_login_title">로그인</string> + <string name="gpodnetauth_login_descr">gpodder.net 로그인입니다. 먼저 로그인 정보를 입력하십시오:</string> + <string name="gpodnetauth_login_butLabel">로그인</string> + <string name="gpodnetauth_login_register">아직 계정이 없으면 다음에서 만들 수 있습니다:\nhttps://gpodder.net/register/</string> + <string name="username_label">사용자 이름</string> + <string name="password_label">암호</string> + <string name="gpodnetauth_device_title">장치 선택</string> + <string name="gpodnetauth_device_descr">gpodder.net 계정에서 사용할 장치를 새로 만들거나 기존 장치를 선택하십시오:</string> + <string name="gpodnetauth_device_deviceID">장치 아이디:\u0020</string> + <string name="gpodnetauth_device_caption">설명</string> + <string name="gpodnetauth_device_butCreateNewDevice">새 장치 만들기</string> + <string name="gpodnetauth_device_chooseExistingDevice">기존 장치 선택:</string> + <string name="gpodnetauth_device_errorEmpty">장치 ID는 비어 있으면 안 됩니다</string> + <string name="gpodnetauth_device_errorAlreadyUsed">장치 ID를 이미 사용 중입니다</string> + <string name="gpodnetauth_device_butChoose">선택</string> + <string name="gpodnetauth_finish_title">로그인이 성공했습니다!</string> + <string name="gpodnetauth_finish_descr">축하합니다! gpodder.net 계정이 장치와 연결되었습니다. 이제 안테나팟에서 gpodder.net 계정의 구독 정보와 자동으로 동기화합니다.</string> + <string name="gpodnetauth_finish_butsyncnow">지금 동기화 시작</string> + <string name="gpodnetauth_finish_butgomainscreen">메인 화면으로 이동</string> + <string name="gpodnetsync_auth_error_title">gpodder.net 인증 오류</string> + <string name="gpodnetsync_auth_error_descr">잘못된 사용자 이름 또는 암호</string> + <string name="gpodnetsync_error_title">gpodder.net 동기화 오류</string> + <string name="gpodnetsync_error_descr">동기화 중에 오류가 발생했습니다:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">선택한 폴더:</string> + <string name="create_folder_label">폴더 만들기</string> + <string name="choose_data_directory">데이터 폴더 선택</string> + <string name="create_folder_msg">이름이 \"%1$s\"인 폴더를 만드시겠습니까?</string> + <string name="create_folder_success">새 폴더를 만들었습니다</string> + <string name="create_folder_error_no_write_access">이 폴더에 쓸 수 없습니다</string> + <string name="create_folder_error_already_exists">폴더가 이미 있습니다</string> + <string name="create_folder_error">폴더를 만들 수 없습니다</string> + <string name="folder_not_empty_dialog_title">폴더가 비어 있지 않습니다</string> + <string name="folder_not_empty_dialog_msg">선택한 폴더가 비어 있지 않습니다. 다운로드한 미디어 파일 및 기타 파일이 이 폴더에 저장됩니다. 그래도 계속 하시겠습니까?</string> + <string name="set_to_default_folder">기본 폴더 선택</string> + <string name="pref_pausePlaybackForFocusLoss_sum">다른 앱이 소리를 낼 때 볼륨을 줄이지 않고 재생을 일시 중지</string> + <string name="pref_pausePlaybackForFocusLoss_title">끼어들면 일시 중지</string> + <!--Online feed view--> + <string name="subscribe_label">구독</string> + <string name="subscribed_label">구독함</string> + <string name="downloading_label">다운로드하는 중...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">챕터 보이기</string> + <string name="show_shownotes_label">프로그램 메모 표시</string> + <string name="show_cover_label">그림 보이기</string> + <string name="rewind_label">뒤로 감기</string> + <string name="fast_forward_label">앞으로 감기</string> + <string name="media_type_audio_label">오디오</string> + <string name="media_type_video_label">비디오</string> + <string name="navigate_upwards_label">위 단계로 이동</string> + <string name="butAction_label">기타 동작</string> + <string name="status_playing_label">에피소드를 재생하는 중입니다</string> + <string name="status_downloading_label">에피소드를 다운로드하는 중입니다</string> + <string name="status_downloaded_label">에피소드를 다운로드했습니다</string> + <string name="status_unread_label">새로운 항목입니다</string> + <string name="in_queue_label">에피소드가 대기열에 들어 있습니다</string> + <string name="new_episodes_count_label">새 에피소드 개수</string> + <string name="in_progress_episodes_count_label">듣기를 시작한 에피소드 개수</string> + <string name="drag_handle_content_description">이 항목의 위치를 바꾸려면 드래그하십시오</string> + <!--Feed information screen--> + <string name="authentication_label">인증</string> + <string name="authentication_descr">이 팟캐스트와 에피소드에 대한 사용자 이름과 비밀번호를 바꿉니다.</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">단일 용도 앱에서 구독 정보를 가져옵니다...</string> +</resources> diff --git a/core/src/main/res/values-land/styles.xml b/core/src/main/res/values-land/styles.xml new file mode 100644 index 000000000..d964ef3d4 --- /dev/null +++ b/core/src/main/res/values-land/styles.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="Theme.MediaPlayer" parent="@style/Theme.AppCompat.Light"> + <item name="android:windowActionBarOverlay">true</item> + </style> +</resources>
\ No newline at end of file diff --git a/core/src/main/res/values-large/dimens.xml b/core/src/main/res/values-large/dimens.xml new file mode 100644 index 000000000..2da283c5b --- /dev/null +++ b/core/src/main/res/values-large/dimens.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <dimen name="thumbnail_length">170dp</dimen> + <dimen name="thumbnail_length_queue_item">64dp</dimen> + <dimen name="thumbnail_length_downloaded_item">64dp</dimen> + <dimen name="queue_title_text_size">@dimen/text_size_medium</dimen> +</resources>
\ No newline at end of file diff --git a/core/src/main/res/values-nl/strings.xml b/core/src/main/res/values-nl/strings.xml new file mode 100644 index 000000000..9df199324 --- /dev/null +++ b/core/src/main/res/values-nl/strings.xml @@ -0,0 +1,303 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Feeds</string> + <string name="podcasts_label">PODCASTS</string> + <string name="episodes_label">AFLEVERINGEN</string> + <string name="new_label">Nieuw</string> + <string name="waiting_list_label">Wachtlijst</string> + <string name="settings_label">Instellingen</string> + <string name="add_new_feed_label">Podcast toevoegen</string> + <string name="downloads_label">Downloads</string> + <string name="cancel_download_label">Annuleer download</string> + <string name="playback_history_label">Afspeelgeschiedenis</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net login</string> + <!--New episodes fragment--> + <!--Main activity--> + <!--Webview actions--> + <string name="open_in_browser_label">In de browser openen</string> + <string name="copy_url_label">URL kopieren</string> + <string name="share_url_label">URL delen</string> + <string name="copied_url_msg">URL naar klembord gekopieerd.</string> + <!--Playback history--> + <string name="clear_history_label">Geschiedenis wissen</string> + <!--Other--> + <string name="confirm_label">Bevestig</string> + <string name="cancel_label">Annuleer</string> + <string name="author_label">Auteur</string> + <string name="language_label">Taal</string> + <string name="podcast_settings_label">Instellingen</string> + <string name="cover_label">Beeld</string> + <string name="error_label">Fout</string> + <string name="error_msg_prefix">Er is een fout opgetreden:</string> + <string name="refresh_label">Verversen</string> + <string name="external_storage_error_msg">Geen externe opslag beschikbaar. Zorg ervoor dat de externe opslag gemonteerd is, zodat de app goed kan werken.</string> + <string name="chapters_label">Hoofdstukken</string> + <string name="shownotes_label">Shownotes</string> + <string name="description_label">Beschrijving</string> + <string name="most_recent_prefix">Meest recente aflevering:\u0020</string> + <string name="episodes_suffix">\u0020afleveringen</string> + <string name="length_prefix">Lengte:\u0020</string> + <string name="size_prefix">Grootte:\u0020</string> + <string name="processing_label">Aan het verwerken</string> + <string name="loading_label">Laden...</string> + <string name="save_username_password_label">Gebruikersnaam en wachtwoord opslaan</string> + <string name="close_label">Sluiten</string> + <string name="retry_label">Opnieuw proberen</string> + <string name="auto_download_label">Voor het automatisch downloaden beschouwen</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">Feed URL</string> + <string name="txtvfeedurl_label">Podcast toevoegen bij URL</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Alles als gelezen markeren</string> + <string name="show_info_label">Toon informatie</string> + <string name="share_link_label">Website link delen</string> + <string name="share_source_label">Feed link delen</string> + <string name="feed_delete_confirmation_msg">Bevestig dat u deze feed en ALLE afleveringen van deze feed die u hebt gedownload wilt verwijderen.</string> + <string name="feed_remover_msg">Feed verwijderen</string> + <!--actions on feeditems--> + <string name="download_label">Download</string> + <string name="play_label">Spelen</string> + <string name="pause_label">Pauze</string> + <string name="stream_label">Stream</string> + <string name="remove_label">Verwijderen</string> + <string name="mark_read_label">Als gelezen markeren</string> + <string name="mark_unread_label">Als ongelezen markeren</string> + <string name="add_to_queue_label">Voeg toe aan wachtrij</string> + <string name="remove_from_queue_label">Verwijder van wachtrij</string> + <string name="visit_website_label">Website bezoeken</string> + <string name="support_label">Flattr dit</string> + <string name="enqueue_all_new">Alle in wachtrij plaatsen</string> + <string name="download_all">Alles downloaden</string> + <string name="skip_episode_label">Aflevering overslaan</string> + <!--Download messages and labels--> + <string name="download_pending">Download in afwachting</string> + <string name="download_running">Aan het downloaden</string> + <string name="download_error_device_not_found">Opslagmedium niet gevonden</string> + <string name="download_error_insufficient_space">Onvoldoende ruimte</string> + <string name="download_error_file_error">Bestandsfout</string> + <string name="download_error_http_data_error">HTTP data fout</string> + <string name="download_error_error_unknown">Onbekende fout</string> + <string name="download_error_parser_exception">Parser Exception</string> + <string name="download_error_unsupported_type">Niet ondersteunde feed soort</string> + <string name="download_error_connection_error">Verbindingsfout</string> + <string name="download_error_unknown_host">Onbekende host</string> + <string name="download_error_unauthorized">Authenticatie fout</string> + <string name="cancel_all_downloads_label">Alle downloads annuleren</string> + <string name="download_cancelled_msg">Download geannuleerd</string> + <string name="download_report_title">Downloads afgerond</string> + <string name="download_error_malformed_url">Misvormde URL</string> + <string name="download_error_io_error">IO fout</string> + <string name="download_error_request_error">Fout in de aanvraag</string> + <string name="download_error_db_access">Databasetoegangsfout</string> + <string name="downloads_left">Nog \u0020 downloads</string> + <string name="download_notification_title">Podcast gegevens aan het downloaden</string> + <string name="download_report_content">%1$d downloads geslaagd, %2$d mislukt</string> + <string name="download_log_title_unknown">Onbekende titel</string> + <string name="download_type_feed">Feed</string> + <string name="download_type_media">Mediabestand</string> + <string name="download_type_image">Beeld</string> + <string name="download_request_error_dialog_message_prefix">Er is een fout opgetreden bij het downloaden van bestand:\u0020</string> + <string name="authentication_notification_title">Authenticatie vereist</string> + <string name="authentication_notification_msg">De opgevraagde bron vereist een gebruikersnaam en een wachtwoord</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Fout!</string> + <string name="player_stopped_msg">Geen media aan het afspelen</string> + <string name="player_preparing_msg">Voorbereiding</string> + <string name="player_ready_msg">Klaar</string> + <string name="player_seeking_msg">Aan het opzoeken</string> + <string name="playback_error_server_died">Server antwoord niet</string> + <string name="playback_error_unknown">Onbekende fout</string> + <string name="no_media_playing_label">Geen media aan het afspelen</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Buffering</string> + <string name="playbackservice_notification_title">Podcast aan het afspelen</string> + <!--Queue operations--> + <string name="clear_queue_label">Wachtrij leeg maken</string> + <string name="undo">Ongedaan maken</string> + <string name="removed_from_queue">Item verwijderd</string> + <string name="move_to_top_label">Naar boven verplaatsen</string> + <string name="move_to_bottom_label">Naar beneden verplaatsen</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattr inloggen</string> + <string name="flattr_auth_explanation">Druk op onderstaande knop om het verificatieproces te starten. U wordt doorgestuurd naar de Flattr inlogscherm in uw browser en wordt gevraagd om toestemming aan AntennaPod te geven om dingen te Flattr\'en. Nadat u toestemming hebt gegeven, keert u automatisch terug naar dit scherm.</string> + <string name="authenticate_label">Authenticeren</string> + <string name="return_home_label">Terug naar de startscherm</string> + <string name="flattr_auth_success">Authenticatie is geslaagd! U kunt nu dingen vanuit de app Flattr\'en.</string> + <string name="no_flattr_token_title">Geen Flattr token gevonden</string> + <string name="no_flattr_token_msg">Uw Flattr account lijkt niet aangesloten te zijn op AntennaPod. U kunt uw account aan AntennaPod sluiten om dingen vanuit de app te Flattr\'en, of u kunt op de website van het ding terecht om het daar te Flattr\'en.</string> + <string name="authenticate_now_label">Authenticeren</string> + <string name="action_forbidden_title">Actie verboden</string> + <string name="action_forbidden_msg">AntennaPod heeft geen toestemming voor deze actie. De reden hiervoor zou kunnen zijn dat de toegang token van AntennaPod voor uw account ingetrokken is. U kunt opnieuw authenticeren, of de website van het ding bezoeken.</string> + <string name="access_revoked_title">Toegang ingetrokken</string> + <string name="access_revoked_info">U heeft met succes het toegangstoken van AntennaPod tot uw account ingetrokken. Om het proces te voltooien, moet u deze app uit de lijst van goedgekeurde applicaties in uw accountinstellingen op de Flattr website verwijderen.</string> + <!--Flattr--> + <string name="flattr_click_success">Een ding geflattr\'d</string> + <string name="flattr_click_success_count">%d dingen geflattr\'d!</string> + <string name="flattr_click_success_queue">Geflattr\'d: %s.</string> + <string name="flattr_click_failure_count">Kon %d dingen niet flattr\'n!</string> + <string name="flattr_click_failure">Niet geflattr\'d: %s.</string> + <string name="flattr_click_enqueued">Ding wordt later geflattr\'d</string> + <string name="flattring_thing">%s aan het flattren</string> + <string name="flattring_label">AntennaPod is aan het flattren</string> + <string name="flattrd_label">AntennaPod heeft geflattr\'d</string> + <string name="flattrd_failed_label">AntennaPod flattr niet gelukt</string> + <string name="flattr_retrieving_status">Geflattr\'de dingen aan het ontvangen</string> + <!--Variable Speed--> + <string name="download_plugin_label">Plugin downloaden</string> + <string name="no_playback_plugin_title">Plugin niet geinstalleerd</string> + <string name="set_playback_speed_label">Afspeelsnelheden</string> + <!--Empty list labels--> + <string name="no_items_label">Er zijn geen items in deze lijst.</string> + <string name="no_feeds_label">U bent nog tot geen enkele feed geabonneerd.</string> + <!--Preferences--> + <string name="other_pref">Overig</string> + <string name="about_pref">Over AntennaPod</string> + <string name="queue_label">Wachtrij</string> + <string name="services_label">Services</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Afspelen pauzeren wanneer de hoofdtelefoon wordt losgekoppeld</string> + <string name="pref_followQueue_sum">Volgende wachtrij item afspelen als de episode voltooid is</string> + <string name="playback_pref">Afspelen</string> + <string name="network_pref">Netwerk</string> + <string name="pref_autoUpdateIntervall_title">Update interval</string> + <string name="pref_autoUpdateIntervall_sum">Voer een tijdsinterval in waarin de feeds automatisch worden vernieuwd, of schakel het uit</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Download mediabestanden alleen via WiFi</string> + <string name="pref_followQueue_title">Continu afspelen</string> + <string name="pref_downloadMediaOnWifiOnly_title">WiFi download van media</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Loskoppeling van de hoofdtelefoon</string> + <string name="pref_mobileUpdate_title">Mobiele updates</string> + <string name="pref_mobileUpdate_sum">Updates toestaan via de mobiele dataverbinding</string> + <string name="refreshing_label">Aan het verversen</string> + <string name="flattr_settings_label">Flattr settings</string> + <string name="pref_flattr_auth_title">Flattr inlog</string> + <string name="pref_flattr_auth_sum">Log in je Flattr account om dingen rechtstreeks vanuit de app te flattr\'en.</string> + <string name="pref_flattr_this_app_title">Flattr deze app</string> + <string name="pref_flattr_this_app_sum">Ondersteun de ontwikkeling van AntennaPod door het te flattr\'en. Bedankt!</string> + <string name="pref_revokeAccess_title">Toegang intrekken</string> + <string name="pref_revokeAccess_sum">Trek de toegang van deze app in tot je Flattr account.</string> + <string name="pref_auto_flattr_title">Automatische Flattr</string> + <string name="user_interface_label">User Interface</string> + <string name="pref_set_theme_title">Kies theme</string> + <string name="pref_set_theme_sum">Verander het uiterlijk van AntennaPod.</string> + <string name="pref_automatic_download_title">Automatisch downloaden</string> + <string name="pref_automatic_download_sum">Configureer het automatisch downloaden van afleveringen.</string> + <string name="pref_autodl_wifi_filter_title">Wi-Fi filter inschakelen</string> + <string name="pref_autodl_wifi_filter_sum">Automatisch downloaden alleen toestaan voor geselecteerde Wi-Fi-netwerken.</string> + <string name="pref_episode_cache_title">Afleveringen cache</string> + <string name="pref_theme_title_light">Licht</string> + <string name="pref_theme_title_dark">Donker</string> + <string name="pref_episode_cache_unlimited">Onbeperkt</string> + <string name="pref_update_interval_hours_plural">uren</string> + <string name="pref_update_interval_hours_singular">uur</string> + <string name="pref_update_interval_hours_manual">Handmatig</string> + <string name="pref_gpodnet_authenticate_title">Log in</string> + <string name="pref_gpodnet_authenticate_sum">Log met je gpodder.net account in om je abonnementen te synchroniseren.</string> + <string name="pref_gpodnet_logout_title">Log uit</string> + <string name="pref_gpodnet_logout_toast">Uitlog was succesvol</string> + <string name="pref_gpodnet_setlogin_information_title">Aanmeldingsgegevens wijzigen</string> + <string name="pref_gpodnet_setlogin_information_sum">Wijzig de aanmeldingsgegevens van je gpodder.net account.</string> + <string name="pref_playback_speed_title">Afspeelsnelheden</string> + <string name="pref_playback_speed_sum">Pas de beschikbare snelheden aan voor de variabele audio afspeelsnelheid</string> + <string name="pref_gpodnet_sethostname_title">Definieer hostname</string> + <string name="pref_gpodnet_sethostname_use_default_host">Gebruik standaard host</string> + <!--Auto-Flattr dialog--> + <!--Search--> + <string name="search_hint">Feeds of afleveringen zoeken</string> + <string name="found_in_shownotes_label">Gevonden in de shownotes</string> + <string name="found_in_chapters_label">Gevonden in hoofdstukken</string> + <string name="search_status_no_results">Er zijn geen resultaten gevonden</string> + <string name="search_label">Zoeken</string> + <string name="found_in_title_label">Gevonden in de titel</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">Met OPML-bestanden kan je podcasts van de ene naar de andere podcatcher verplaatsen.</string> + <string name="opml_import_explanation">Om een OPML-bestand te importeren moet je het in de volgende map zetten en op onderstaande knop drukken.</string> + <string name="start_import_label">Start importeren</string> + <string name="opml_import_label">OPML import</string> + <string name="opml_directory_error">FOUT!</string> + <string name="reading_opml_label">OPML-bestand aan het lezen</string> + <string name="opml_reader_error">Er is een fout opgetreden bij het lezen van het OPML-bestand:</string> + <string name="opml_import_error_dir_empty">De import map is leeg.</string> + <string name="select_all_label">Selecteer alles</string> + <string name="deselect_all_label">Deselecteer alles</string> + <string name="choose_file_to_import_label">Kies het te importeren bestand</string> + <string name="opml_export_label">OPML export</string> + <string name="exporting_label">Aan het exporteren...</string> + <string name="export_error_label">Export fout</string> + <string name="opml_export_success_title">OPML export succesvol.</string> + <string name="opml_export_success_sum"> Het OPML-bestand is in \u0020 geplaatst</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Slaap timer instellen</string> + <string name="disable_sleeptimer_label">Slaap timer uitschakelen</string> + <string name="enter_time_here_label">Voer tijd in</string> + <string name="sleep_timer_label">Slaap timer</string> + <string name="time_left_label">Resterende tijd:\u0020</string> + <string name="time_dialog_invalid_input">Ongeldige invoer, de tijd moet een geheel getal zijn</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">CATEGORIEËN</string> + <string name="gpodnet_toplist_header">TOP PODCASTS</string> + <string name="gpodnet_suggestions_header">SUGGESTIES</string> + <string name="gpodnet_search_hint">Zoek gpodder.net</string> + <string name="gpodnetauth_login_title">Log in</string> + <string name="gpodnetauth_login_descr">Welkom op de gpodder.net login proces. Eerst, typ je login gegevens:</string> + <string name="gpodnetauth_login_butLabel">Log in</string> + <string name="username_label">Gebruikersnaam</string> + <string name="password_label">Wachtwoord</string> + <string name="gpodnetauth_device_title">Apparaatselectie</string> + <string name="gpodnetauth_device_descr">Maak een nieuw apparaat aan om voor je gpodder.net account te gebruiken of kies een bestaande:</string> + <string name="gpodnetauth_device_deviceID">Device ID:\u0020</string> + <string name="gpodnetauth_device_caption">Titel</string> + <string name="gpodnetauth_device_butCreateNewDevice">Maak een nieuw apparaat aan</string> + <string name="gpodnetauth_device_chooseExistingDevice">Kies een bestaand apparaat:</string> + <string name="gpodnetauth_device_errorEmpty">Apparaat ID mag niet leeg zijn</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Apparaat ID al in gebruik</string> + <string name="gpodnetauth_device_butChoose">Kies</string> + <string name="gpodnetauth_finish_title">Login succesvol</string> + <string name="gpodnetauth_finish_descr">Gefeliciteerd! Jou gpodder.net account is nu verbonden met je apparaat. AntennaPod zal voortaan abonnementen op je apparaat automatisch met je gpodder.net account synchroniseren.</string> + <string name="gpodnetauth_finish_butsyncnow">Synchronisatie nu starten</string> + <string name="gpodnetauth_finish_butgomainscreen">Terug naar hoofdscherm</string> + <string name="gpodnetsync_auth_error_title">gpodder.net authenticatie fout</string> + <string name="gpodnetsync_auth_error_descr">Ongeldig gebruikersnaam of wachtwoord</string> + <string name="gpodnetsync_error_title">gpodder.net synchronisatie fout</string> + <string name="gpodnetsync_error_descr">Er is een fout opgetreden tijdens het synchroniseren:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Geselecteerde map:</string> + <string name="create_folder_label">Map aanmaken</string> + <string name="choose_data_directory">Kies data map</string> + <string name="create_folder_msg">Maak een nieuwe map aan met de naam \"%1$s\"?</string> + <string name="create_folder_success">Nieuwe map aangemaakt</string> + <string name="create_folder_error_no_write_access">Kan in deze map niet schrijven</string> + <string name="create_folder_error_already_exists">Map bestaat al</string> + <string name="create_folder_error">Kon map niet aanmaken</string> + <string name="folder_not_empty_dialog_title">Map is niet leeg</string> + <string name="folder_not_empty_dialog_msg">De map die je hebt gekozen is niet leeg. Media downloads en andere bestanden zullen rechtstreeks in deze map geplaatst worden. Toch doorgaan?</string> + <string name="set_to_default_folder">Kies default map</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Het afspelen onderbreken in plaats van het volume te verlagen wanneer er een andere app geluiden af wilt spelen</string> + <string name="pref_pausePlaybackForFocusLoss_title">Pauze voor onderbrekingen</string> + <!--Online feed view--> + <string name="subscribe_label">Abonneren</string> + <string name="subscribed_label">Geabonneerd</string> + <string name="downloading_label">Aan het downloaden</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Hoofdstukken tonen</string> + <string name="show_shownotes_label">Shownotes tonen</string> + <string name="show_cover_label">Beeld tonen</string> + <string name="rewind_label">Terugspoelen</string> + <string name="fast_forward_label">Vooruitspoelen</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Video</string> + <string name="navigate_upwards_label">Navigeer naar boven</string> + <string name="butAction_label">Meer acties</string> + <string name="status_playing_label">Aflevering wordt gespeeld</string> + <string name="status_downloading_label">Aflevering wordt gedownload</string> + <string name="status_downloaded_label">Aflevering is gedownload</string> + <string name="status_unread_label">Item is nieuw</string> + <string name="in_queue_label">Aflevering is in de queue</string> + <string name="new_episodes_count_label">Aantal nieuwe afleveringen</string> + <string name="in_progress_episodes_count_label">Aantal afleveringen dat begonnen te luisteren zijn</string> + <!--Feed information screen--> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Abonnementen aan het importeren vanuit single-purpose apps...</string> +</resources> diff --git a/core/src/main/res/values-pl-rPL/strings.xml b/core/src/main/res/values-pl-rPL/strings.xml new file mode 100644 index 000000000..34ef6e04c --- /dev/null +++ b/core/src/main/res/values-pl-rPL/strings.xml @@ -0,0 +1,350 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Kanały</string> + <string name="add_feed_label">Dodaj podcast</string> + <string name="podcasts_label">PODCASTY</string> + <string name="episodes_label">ODCINKI</string> + <string name="new_episodes_label">Nowe odcinki</string> + <string name="all_episodes_label">Wszystkie odcinki</string> + <string name="new_label">Nowy</string> + <string name="waiting_list_label">Lista oczekujących</string> + <string name="settings_label">Ustawienia</string> + <string name="add_new_feed_label">Dodaj podcast</string> + <string name="downloads_label">Pobrane</string> + <string name="downloads_running_label">W toku</string> + <string name="downloads_completed_label">Ukończone</string> + <string name="downloads_log_label">Dziennik</string> + <string name="cancel_download_label">Anuluj pobieranie</string> + <string name="playback_history_label">Historia odtwarzania</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net login</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Ostatnio opublikowane</string> + <string name="episode_filter_label">Pokaż tylko nowe odcinki</string> + <!--Main activity--> + <string name="drawer_open">Otwórz menu</string> + <string name="drawer_close">Zamknij menu</string> + <!--Webview actions--> + <string name="open_in_browser_label">Otwórz w przeglądarce</string> + <string name="copy_url_label">Kopiuj adres</string> + <string name="share_url_label">Udostępnij adres</string> + <string name="copied_url_msg">Skopiowano adres do schowka.</string> + <string name="go_to_position_label">Przejdź do tego miejsca</string> + <!--Playback history--> + <string name="clear_history_label">Wyczyść historię</string> + <!--Other--> + <string name="confirm_label">Potwierdź </string> + <string name="cancel_label">Anuluj</string> + <string name="author_label">Autor</string> + <string name="language_label">Język</string> + <string name="podcast_settings_label">Ustawienia</string> + <string name="cover_label">Obraz</string> + <string name="error_label">Błąd</string> + <string name="error_msg_prefix">Wystąpił błąd:</string> + <string name="refresh_label">Odśwież</string> + <string name="external_storage_error_msg">Brak zewnętrznej pamięci. Sprawdź czy jest ona podłączona żeby aplikacja mogła pracować poprawnie.</string> + <string name="chapters_label">Rozdziały</string> + <string name="shownotes_label">Opis odcinka</string> + <string name="description_label">Opis</string> + <string name="most_recent_prefix">Najnowszy odcinek:\u0020</string> + <string name="episodes_suffix">:\u0020odcinków</string> + <string name="length_prefix">Długość:\u0020</string> + <string name="size_prefix">Rozmiar:\u0020</string> + <string name="processing_label">Przetwarzanie</string> + <string name="loading_label">Ładowanie...</string> + <string name="save_username_password_label">Zapisz nazwę użytkownika i hasło</string> + <string name="close_label">Zamknij</string> + <string name="retry_label">Spróbuj ponownie</string> + <string name="auto_download_label">Dołącz do automatycznego pobierania</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">Adres kanału</string> + <string name="etxtFeedurlHint">Adres URL kanału lub strony internetowej</string> + <string name="txtvfeedurl_label">Dodaj podcast przez adres</string> + <string name="podcastdirectories_label">Znajdź podcast w folderze</string> + <string name="podcastdirectories_descr">Możesz wyszukiwać nowe podcasty ze względu na nazwę, kategorię lub popularność na gpodder.net</string> + <string name="browse_gpoddernet_label">Przeglądaj gpodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Oznacz wszystkie jako przeczytane</string> + <string name="mark_all_read_msg">Wszystkie odcinki zaznaczone jako przeczytane</string> + <string name="show_info_label">Pokaż informacje</string> + <string name="remove_feed_label">Usuń podcast</string> + <string name="share_link_label">Udostępnij stronę</string> + <string name="share_source_label">Udostępnij kanał</string> + <string name="feed_delete_confirmation_msg">Potwierdź chęć usunięcia tego kanału wraz ze WSZYSTKIMI odcinkami, które zostały pobrane.</string> + <string name="feed_remover_msg">Usuwanie kanału</string> + <string name="load_complete_feed">Odśwież cały kanał</string> + <!--actions on feeditems--> + <string name="download_label">Pobierz</string> + <string name="play_label">Odtwórz</string> + <string name="pause_label">Pauza</string> + <string name="stop_label">Stop</string> + <string name="stream_label">Strumień</string> + <string name="remove_label">Usuń</string> + <string name="remove_episode_lable">Usuń odcinek</string> + <string name="mark_read_label">Oznacz jako przeczytane</string> + <string name="mark_unread_label">Oznacz jako nieprzeczytane</string> + <string name="add_to_queue_label">Dodaj do kolejki</string> + <string name="remove_from_queue_label">Usuń z kolejki</string> + <string name="visit_website_label">Odwiedź stronę</string> + <string name="support_label">Wspomóż na Flattr</string> + <string name="enqueue_all_new">Dodaj wszystko do kolejki</string> + <string name="download_all">Pobierz wszystkie</string> + <string name="skip_episode_label">Pomiń odcinek</string> + <!--Download messages and labels--> + <string name="download_successful">Operacja zakończona sukcesem</string> + <string name="download_failed">Operacja nie powiodła się</string> + <string name="download_pending">Pobieranie w toku</string> + <string name="download_running">Pobieram</string> + <string name="download_error_device_not_found">Nie znaleziono urządzenia docelowego</string> + <string name="download_error_insufficient_space">Niewystarczająca ilość pamięci</string> + <string name="download_error_file_error">Błąd pliku</string> + <string name="download_error_http_data_error">Błąd danych HTTP</string> + <string name="download_error_error_unknown">Nieznany błąd</string> + <string name="download_error_parser_exception">Wyjątek parsera</string> + <string name="download_error_unsupported_type">Nieobsługiwany typ kanału</string> + <string name="download_error_connection_error">Błąd połączenia</string> + <string name="download_error_unknown_host">Nieznany host</string> + <string name="download_error_unauthorized">Błąd autoryzacji</string> + <string name="cancel_all_downloads_label">Anuluj wszystkie pobierania</string> + <string name="download_cancelled_msg">Pobieranie anulowane</string> + <string name="download_report_title">Pobieranie ukończone</string> + <string name="download_error_malformed_url">Niepoprawny adres</string> + <string name="download_error_io_error">Błąd wejścia/wyjścia</string> + <string name="download_error_request_error">Błąd żądania</string> + <string name="download_error_db_access">Błąd dostępu do bazy danych</string> + <string name="downloads_left">:\u0020pobrań pozostało</string> + <string name="downloads_processing">Przetwarzanie pobranych</string> + <string name="download_notification_title">Pobieranie danych podcastu</string> + <string name="download_report_content">%1$d pobierania poprawne, %2$d nieudane</string> + <string name="download_log_title_unknown">Nieznany tytuł</string> + <string name="download_type_feed">Kanał</string> + <string name="download_type_media">Plik multimedialny</string> + <string name="download_type_image">Obraz</string> + <string name="download_request_error_dialog_message_prefix">Wystąpił błąd przy próbie pobierania:\u0020</string> + <string name="authentication_notification_title">Wymagana autoryzacja</string> + <string name="authentication_notification_msg">Żądany zasób wymaga podania nazwy użytkownika oraz hasła</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Błąd!</string> + <string name="player_stopped_msg">Żadne media nie odtwarzane </string> + <string name="player_preparing_msg">Przygotowuję</string> + <string name="player_ready_msg">Gotowe</string> + <string name="player_seeking_msg">Szukam</string> + <string name="playback_error_server_died">Serwer zdechł</string> + <string name="playback_error_unknown">Nieznany błąd</string> + <string name="no_media_playing_label">Żadne media nie odtwarzane </string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Buferowanie</string> + <string name="playbackservice_notification_title">Odtwarzenie podcastu </string> + <string name="unknown_media_key">AntennaPod - Nieznany klawisz: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">Wyczyść kolejkę</string> + <string name="undo">Cofnij</string> + <string name="removed_from_queue">Element usunięty</string> + <string name="move_to_top_label">Przesuń na górę</string> + <string name="move_to_bottom_label">Przesuń na dół</string> + <!--Flattr--> + <string name="flattr_auth_label">Logowanie do Flattr</string> + <string name="flattr_auth_explanation">Naciśnij przycisk poniżej by zacząć proces autoryzacji. Zostaniesz przekierowany na stronę logowania do flattr w przeglądarce i zostaniesz poproszony o przyznanie zezwolenia AntennaPod-owi na flattr-owanie. Po daniu zezwolenia powrócisz do tej strony automatycznie.</string> + <string name="authenticate_label">Autoryzacja</string> + <string name="return_home_label">Wróć do ekranu głównego</string> + <string name="flattr_auth_success">Autoryzacja się powiodła. Możesz teraz używać flattr w aplikacji.</string> + <string name="no_flattr_token_title">Nie znaleziono tokenu Flattr</string> + <string name="no_flattr_token_notification_msg">Twoje konto flattr nie jest połączone z AntennaPod. Naciśnij tutaj aby autoryzować.</string> + <string name="no_flattr_token_msg">Twoje konto Flattr wydaje się nie być podłączone do AntennaPod. Możesz połączyć konto do AntennaPod by przez program flattr-ować lub możesz odwiedzić stronę wątku by zrobić to tam.</string> + <string name="authenticate_now_label">Autoryzuj</string> + <string name="action_forbidden_title">Akcja zabroniona</string> + <string name="action_forbidden_msg">AntennaPod nie ma zezwolenia na tą akcję. Powodem może być fakt iż dostęp dla AntennaPod do Twojego konta został cofnięty. Możesz ponownie autoryzować aplikację lub odwiedzić stronę. </string> + <string name="access_revoked_title">Anulowano dostęp</string> + <string name="access_revoked_info">Odwołałeś dostęp AntennaPod do swojego konta. W celu zakończenia procesu musisz usunąć aplikację z listy aplikacji dozwolonych na koncie Flattr.</string> + <!--Flattr--> + <string name="flattr_click_success">Poprawnie z-flattr-owano</string> + <string name="flattr_click_success_count">Z-flattr-owano %d elementów</string> + <string name="flattr_click_success_queue">Z-flattr-owano: %s</string> + <string name="flattr_click_failure_count">Flattr-owanie %d elementów nie powiodło się</string> + <string name="flattr_click_failure">Flattr-owanie zakończone niepowodzeniem: %s</string> + <string name="flattr_click_enqueued">Elementy zostaną z-flattr-owane później</string> + <string name="flattring_thing">Flattr-owanie %s</string> + <string name="flattring_label">Flattr-uję</string> + <string name="flattrd_label">AntennaPod z-flattr-owała</string> + <string name="flattrd_failed_label">Flattr-owanie AntennaPod nie powiodło się</string> + <string name="flattr_retrieving_status">Wyszukiwanie z-flattr-owanych elementów</string> + <!--Variable Speed--> + <string name="download_plugin_label">Pobierz wtyczkę</string> + <string name="no_playback_plugin_title">Wtyczka nie zainstalowana</string> + <string name="no_playback_plugin_msg">Aby odtwarzać ze zmienną prędkością niezbędna jest instalacja biblioteki innej firmy.\n\nDotknij \'Pobierz wtyczkę\' aby pobrać darmową wtyczkę z Play Store\n\nAntennaPod nie odpowiada za problemy wynikłe z używania tej wtyczki, które powinny zostać zgłoszone bezpośrednio autorowi wtyczki.</string> + <string name="set_playback_speed_label">Prędkość odtwarzania</string> + <!--Empty list labels--> + <string name="no_items_label">Brak elementów na tej liście.</string> + <string name="no_feeds_label">Nie subskrybowałeś jeszcze żadnego kanału.</string> + <!--Preferences--> + <string name="other_pref">Inne</string> + <string name="about_pref">O...</string> + <string name="queue_label">Kolejka</string> + <string name="services_label">Usługi</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Wstrzymaj odtwarzanie kiedy słuchawki zostaną odłączone</string> + <string name="pref_followQueue_sum">Przeskocz do następnego elementu kolejki po zakończeniu odtwarzania</string> + <string name="playback_pref">Odtwarzanie</string> + <string name="network_pref">Sieć</string> + <string name="pref_autoUpdateIntervall_title">Częstość aktualizacji</string> + <string name="pref_autoUpdateIntervall_sum">Określ częstotliwość automatycznego odświeżania lub je wyłącz</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Pobieraj pliki tylko przez WiFi</string> + <string name="pref_followQueue_title">Odtwarzanie ciągłe</string> + <string name="pref_downloadMediaOnWifiOnly_title">WiFi media pobrane</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Słuchawki odłączone</string> + <string name="pref_mobileUpdate_title">Aktualizacje mobilne</string> + <string name="pref_mobileUpdate_sum">Zezwól na aktualizacje poprzez sieć komórkową</string> + <string name="refreshing_label">Odświeżanie</string> + <string name="flattr_settings_label">Ustawienia Flattr</string> + <string name="pref_flattr_auth_title">Logowanie do Flattr</string> + <string name="pref_flattr_auth_sum">Zaloguj się do konta Flattr aby wspierać twórców bezpośrednio z aplikacji.</string> + <string name="pref_flattr_this_app_title">Wesprzyj aplikację na Flattr</string> + <string name="pref_flattr_this_app_sum">Wesprzyj twórcę AntennaPod przez Flattr. Dzięki!</string> + <string name="pref_revokeAccess_title">Anuluj dostęp</string> + <string name="pref_revokeAccess_sum">Anuluj dostęp tej aplikacji do konta Flattr </string> + <string name="pref_auto_flattr_title">Automatyczne wsparcie na Flattr</string> + <string name="pref_auto_flattr_sum">Skonfiguruj automatyczne flattr-owanie</string> + <string name="user_interface_label">Interfejs użytkownika</string> + <string name="pref_set_theme_title">Wybierz motyw</string> + <string name="pref_set_theme_sum">Zmień wygląd AntennaPod.</string> + <string name="pref_automatic_download_title">Automatyczne pobieranie</string> + <string name="pref_automatic_download_sum">Skonfiguruj automatyczne pobieranie odcinków.</string> + <string name="pref_autodl_wifi_filter_title">Włącz filtr Wi-Fi</string> + <string name="pref_autodl_wifi_filter_sum">Zezwól na automatyczne pobieranie tylko dla określonych sieci Wi-Fi.</string> + <string name="pref_episode_cache_title">Pamięć podręczna odcinków</string> + <string name="pref_theme_title_light">Jasny</string> + <string name="pref_theme_title_dark">Ciemny</string> + <string name="pref_episode_cache_unlimited">Nielimitowane</string> + <string name="pref_update_interval_hours_plural">godziny</string> + <string name="pref_update_interval_hours_singular">godzina</string> + <string name="pref_update_interval_hours_manual">Instrukcja</string> + <string name="pref_gpodnet_authenticate_title">Zaloguj</string> + <string name="pref_gpodnet_authenticate_sum">Zaloguj się swoim kontem na gpodder.net w celu synchronizacji Twoich subskrypcji.</string> + <string name="pref_gpodnet_logout_title">Wyloguj</string> + <string name="pref_gpodnet_logout_toast">Wylogowanie się powiodło</string> + <string name="pref_gpodnet_setlogin_information_title">Zmień informacje logowania</string> + <string name="pref_gpodnet_setlogin_information_sum">Zmień dane logowania konta gpodder.net.</string> + <string name="pref_playback_speed_title">Prędkość odtwarzania</string> + <string name="pref_playback_speed_sum">Dostosuj prędkości dostępne dla odtwarzania audio o zmiennej prędkości</string> + <string name="pref_seek_delta_title">Seek time</string> + <string name="pref_seek_delta_sum">Przeskocz o tyle sekund przewijając</string> + <string name="pref_gpodnet_sethostname_title">Ustaw nazwę hosta</string> + <string name="pref_gpodnet_sethostname_use_default_host">Użyj domyślnego hosta</string> + <string name="pref_expandNotify_title">Rozwiń Powiadomienia</string> + <string name="pref_expandNotify_sum">Zawsze rozwijaj powiadomienie żeby pokazać przyciski odtwarzacza.</string> + <string name="pref_persistNotify_title">Stałe przyciski odtwarzacza</string> + <string name="pref_persistNotify_sum">Utrzymuj powiadomienie i przyciski odtwarzania na ekranie blokady gdy odtwarzanie jest wstrzymane.</string> + <string name="pref_expand_notify_unsupport_toast">Android starszy niż 4.1 nie wspiera rozszerzonych powiadomień.</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Włącz automatyczne wspieranie na flattr.</string> + <string name="auto_flattr_after_percent">Z-flattr-uj odcinki odegrane %d procentach.</string> + <string name="auto_flattr_ater_beginning">Z-flattr-uj odcinek gdy odtawarzanie zostanie rozpoczęte</string> + <string name="auto_flattr_ater_end">Z-flattr-uj odcinek gdy odtwarzanie zostanie zakończone</string> + <!--Search--> + <string name="search_hint">Szukaj kanałów lub odcinków</string> + <string name="found_in_shownotes_label">Znaleziono w notatkach</string> + <string name="found_in_chapters_label">Znaleziono w rozdziałach</string> + <string name="search_status_no_results">Brak wyników</string> + <string name="search_label">Szukaj</string> + <string name="found_in_title_label">Znaleziono w tytułach</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">Pliki OPML pozwalają na przenoszenie podcastów między aplikacjami.</string> + <string name="opml_import_explanation">W celu importu pliku OPML musisz umieścić go w poniższym folderze i nacisnąć przycisk poniżej w celu rozpoczęcia importu.</string> + <string name="start_import_label">Rozpocznij import</string> + <string name="opml_import_label">Import OPML</string> + <string name="opml_directory_error">BŁĄD!</string> + <string name="reading_opml_label">Odczytuję plik OPML</string> + <string name="opml_reader_error">Wystąpił błąd w czasie odczytu dokumentu OPML:</string> + <string name="opml_import_error_dir_empty">Katalog importowania jest pusty.</string> + <string name="select_all_label">Zaznacz wszystko</string> + <string name="deselect_all_label">Odznacz wszystko</string> + <string name="choose_file_to_import_label">Wybierz plik do importu</string> + <string name="opml_export_label">Eksport OPML</string> + <string name="exporting_label">Eksportowanie...</string> + <string name="export_error_label">Błąd eksportu</string> + <string name="opml_export_success_title">Eksport OPML udany.</string> + <string name="opml_export_success_sum">Plik .opml został zapisany do:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Ustaw czas do wyłączenia</string> + <string name="disable_sleeptimer_label">Wyłącz wyłącznik czasowy</string> + <string name="enter_time_here_label">Podaj czas</string> + <string name="sleep_timer_label">Wyłącznik czasowy</string> + <string name="time_left_label">Pozostały czas:\u0020</string> + <string name="time_dialog_invalid_input">Błąd wpisu, czas musi być liczbą całkowitą</string> + <string name="time_unit_seconds">sekundy</string> + <string name="time_unit_minutes">minuty</string> + <string name="time_unit_hours">godziny</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">KATEGORIE</string> + <string name="gpodnet_toplist_header">TOP PODCASTY</string> + <string name="gpodnet_suggestions_header">SUGESTIE</string> + <string name="gpodnet_search_hint">Szukaj na gpodder.net</string> + <string name="gpodnetauth_login_title">Login</string> + <string name="gpodnetauth_login_descr">Witamy w procesie logowania do gpodder.net. Najpierw podaj swoje dane logowania:</string> + <string name="gpodnetauth_login_butLabel">Login</string> + <string name="gpodnetauth_login_register">Jeśli nie masz jeszcze konta, możesz utworzyć je tutaj: +https://gpodder.net/register/</string> + <string name="username_label">Nazwa użytkownika</string> + <string name="password_label">Hasło</string> + <string name="gpodnetauth_device_title">Wybór urządzenia</string> + <string name="gpodnetauth_device_descr">Utwórz nowe urządzenie dla swojego konta na gpodder.net lub wybierz istniejące:</string> + <string name="gpodnetauth_device_deviceID">Identyfikator urządzenia:\u0020</string> + <string name="gpodnetauth_device_caption">Tytuł</string> + <string name="gpodnetauth_device_butCreateNewDevice">Utwórz nowe urządzenie</string> + <string name="gpodnetauth_device_chooseExistingDevice">Wybierz istniejące urządzenie:</string> + <string name="gpodnetauth_device_errorEmpty">Identyfikator urządzenia nie może być pusty</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Identyfikator urządzenia w użyciu</string> + <string name="gpodnetauth_device_butChoose">Wybierz</string> + <string name="gpodnetauth_finish_title">Logowanie zakończone sukcesem!</string> + <string name="gpodnetauth_finish_descr">Gratulacje! Twoje konto na gpodder.net jest połączone z urządzeniem. AntennaPod będzie automatycznie synchronizować subskrypcje na urządzeniu z kontem na gpodder.net. </string> + <string name="gpodnetauth_finish_butsyncnow">Rozpocznij synchronizację</string> + <string name="gpodnetauth_finish_butgomainscreen">Idź do strony głównej</string> + <string name="gpodnetsync_auth_error_title">Błąd autoryzacji na gpodder.net</string> + <string name="gpodnetsync_auth_error_descr">Niepoprawna nazwa użytkownika lub hasło</string> + <string name="gpodnetsync_error_title">Błąd synchronizacji z gpodder.net</string> + <string name="gpodnetsync_error_descr">Wystąpił błąd podczas synchronizacji:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Wybrany folder:</string> + <string name="create_folder_label">Utwórz folder</string> + <string name="choose_data_directory">Wybierz folder danych</string> + <string name="create_folder_msg">Utworzyć nowy folder o nazwie \"%1$s\"?</string> + <string name="create_folder_success">Utworzono nowy folder</string> + <string name="create_folder_error_no_write_access">Nie można zapisać do tego folderu</string> + <string name="create_folder_error_already_exists">Folder już istnieje</string> + <string name="create_folder_error">Nie można utworzyć folderu</string> + <string name="folder_not_empty_dialog_title">Folder nie jest pusty</string> + <string name="folder_not_empty_dialog_msg">Wybrany folder nie jest pusty. Pobierane media i inne pliki będą umieszczane bezpośrednio w folderze, czy kontynuować?</string> + <string name="set_to_default_folder">Wybierz domyślny folder</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Wstrzymaj odtwarzanie zamiast wyciszenia jeśli inna aplikacja chce odtworzyć dźwięk.</string> + <string name="pref_pausePlaybackForFocusLoss_title">Wstrzymaj przy przerwaniu</string> + <!--Online feed view--> + <string name="subscribe_label">Subskrybuj</string> + <string name="subscribed_label">Subskrybowane</string> + <string name="downloading_label">Pobieranie...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Pokaż rozdziały</string> + <string name="show_shownotes_label">Pokaż opis odcinka</string> + <string name="show_cover_label">Pokaż obraz</string> + <string name="rewind_label">Cofnij</string> + <string name="fast_forward_label">Przewiń</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Wideo</string> + <string name="navigate_upwards_label">Przesuń w górę</string> + <string name="butAction_label">Więcej akcji</string> + <string name="status_playing_label">Odcinek jest odtwarzany</string> + <string name="status_downloading_label">Odcinek jest pobierany</string> + <string name="status_downloaded_label">Odcinek pobrany</string> + <string name="status_unread_label">Nowa pozycja</string> + <string name="in_queue_label">Odcinek jest w kolejce</string> + <string name="new_episodes_count_label">Liczba nowych odcinków</string> + <string name="in_progress_episodes_count_label">Liczba odcinków, których zacząłeś słuchać</string> + <string name="drag_handle_content_description">Przeciągnij aby zmienić pozycję elementu</string> + <string name="load_next_page_label">Załaduj następną stronę</string> + <!--Feed information screen--> + <string name="authentication_label">Autoryzacja</string> + <string name="authentication_descr">Zmień swoją nazwę użytkownika oraz hasło dla tego podcastu i jego odcinków</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Importowanie subskrybcji z jednozadaniowych aplikacji</string> +</resources> diff --git a/core/src/main/res/values-pt-rBR/strings.xml b/core/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 000000000..ef63e718e --- /dev/null +++ b/core/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,278 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Feeds</string> + <string name="podcasts_label">PODCASTS</string> + <string name="episodes_label">EPISÓDIOS</string> + <string name="new_label">Novo</string> + <string name="waiting_list_label">Lista de espera</string> + <string name="settings_label">Configurações</string> + <string name="add_new_feed_label">Adicionar podcast</string> + <string name="downloads_label">Downloads</string> + <string name="cancel_download_label">Cancelar Download</string> + <string name="playback_history_label">Histórico de reprodução</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net login</string> + <!--New episodes fragment--> + <!--Main activity--> + <!--Webview actions--> + <string name="open_in_browser_label">Abrir no navegador</string> + <string name="copy_url_label">Copiar URL</string> + <string name="share_url_label">Compartilhar URL</string> + <string name="copied_url_msg">URL copiada para área de transferência.</string> + <!--Playback history--> + <string name="clear_history_label">Apagar histórico</string> + <!--Other--> + <string name="confirm_label">Confirmar</string> + <string name="cancel_label">Cancelar</string> + <string name="author_label">Autor</string> + <string name="language_label">Idioma</string> + <string name="podcast_settings_label">Configurações</string> + <string name="cover_label">Capa</string> + <string name="error_label">Erro</string> + <string name="error_msg_prefix">Um erro ocorreu:</string> + <string name="refresh_label">Atualizar</string> + <string name="external_storage_error_msg">Não há dispositivos de armazenamento externo disponíveis. Por favor, certifique-se de que um dispositivo de armazenamento externo está montado para que o aplicativo possa funcionar adequadamente.</string> + <string name="chapters_label">Capítulos</string> + <string name="shownotes_label">Notas do podcast</string> + <string name="description_label">Descrição</string> + <string name="most_recent_prefix">Episódio mais recente:\u0020</string> + <string name="episodes_suffix">\u0020episódios</string> + <string name="length_prefix">Duração:\u0020</string> + <string name="size_prefix">Tamanho:\u0020</string> + <string name="processing_label">Processando</string> + <string name="loading_label">Carregando...</string> + <string name="save_username_password_label">Salvar nome do usuário e senha</string> + <string name="close_label">Fechar</string> + <string name="retry_label">Tentar novamente</string> + <string name="auto_download_label">Incluir em downloads automáticos</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">URL do Feed</string> + <string name="txtvfeedurl_label">Adicionar podcast por URL</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Marcar todos como lido</string> + <string name="show_info_label">Mostrar informação</string> + <string name="share_link_label">Compartilhar link do site</string> + <string name="share_source_label">Compartilhar link do feed</string> + <string name="feed_delete_confirmation_msg">Por favor confirme que você deseja apagar este feed e TODOS os episódios que você fez download deste feed.</string> + <string name="feed_remover_msg">Removendo feed</string> + <!--actions on feeditems--> + <string name="download_label">Download</string> + <string name="play_label">Reproduzir</string> + <string name="pause_label">Pausar</string> + <string name="stream_label">Stream</string> + <string name="remove_label">Remover</string> + <string name="mark_read_label">Marcar como lido</string> + <string name="mark_unread_label">Marcar como não lido</string> + <string name="add_to_queue_label">Adicionar à fila</string> + <string name="remove_from_queue_label">Remover da fila</string> + <string name="visit_website_label">Visitar Website</string> + <string name="support_label">Adicionar ao Flattr</string> + <string name="enqueue_all_new">Enfileirar todos</string> + <string name="download_all">Baixar todos</string> + <string name="skip_episode_label">Pular episódio</string> + <!--Download messages and labels--> + <string name="download_pending">Download pendente</string> + <string name="download_running">Download em execução</string> + <string name="download_error_device_not_found">Dispositivo de armazenamento não encontrado</string> + <string name="download_error_insufficient_space">Espaço insuficiente</string> + <string name="download_error_file_error">Erro de arquivo</string> + <string name="download_error_http_data_error">Erro de HTTP Data</string> + <string name="download_error_error_unknown">Erro desconhecido</string> + <string name="download_error_parser_exception">Parser Exception</string> + <string name="download_error_unsupported_type">Tipo de feed não suportado</string> + <string name="download_error_connection_error">Erro de conexão</string> + <string name="download_error_unknown_host">Host desconhecido</string> + <string name="cancel_all_downloads_label">Cancelar todos os downloads</string> + <string name="download_cancelled_msg">Download cancelado</string> + <string name="download_report_title">Downloads finalizados</string> + <string name="download_error_malformed_url">URL inválida</string> + <string name="download_error_io_error">Erro de IO</string> + <string name="download_error_request_error">Erro de requisição</string> + <string name="download_error_db_access">Erro no acesso ao Banco de dados</string> + <string name="downloads_left">\u0020Downloads restantes</string> + <string name="download_notification_title">Baixando dados do podcast</string> + <string name="download_report_content">%1$d downloads com sucesso, %2$d falharam</string> + <string name="download_log_title_unknown">Título desconhecido</string> + <string name="download_type_feed">Feed</string> + <string name="download_type_media">Arquivo de mídia</string> + <string name="download_type_image">Imagem</string> + <string name="download_request_error_dialog_message_prefix">Ocorreu um erro durante download do arquivo:\u0020</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Erro!</string> + <string name="player_stopped_msg">Nenhuma mídia tocando</string> + <string name="player_preparing_msg">Preparando</string> + <string name="player_ready_msg">Pronto</string> + <string name="player_seeking_msg">Buscando</string> + <string name="playback_error_server_died">Servidor morreu</string> + <string name="playback_error_unknown">Erro desconhecido</string> + <string name="no_media_playing_label">Nenhuma mídia tocando</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Armazenando</string> + <string name="playbackservice_notification_title">Reproduzindo podcast</string> + <!--Queue operations--> + <string name="clear_queue_label">Limpar fila</string> + <string name="undo">Desfazer</string> + <string name="removed_from_queue">Item removido</string> + <string name="move_to_top_label">Mover para o topo</string> + <string name="move_to_bottom_label">Mover para o fim</string> + <!--Flattr--> + <string name="flattr_auth_label">Logar no Flattr</string> + <string name="flattr_auth_explanation">Pressione o botão abaixo para iniciar o processo de autenticação. Você será direcionado para a tela de login do Flattr, que pedirá autorização para que o AntennaPod utilize o Flattr. Após conceder a permissão, você retornará a esta tela automaticamente.</string> + <string name="authenticate_label">Autenticar</string> + <string name="return_home_label">Retornar ao início</string> + <string name="flattr_auth_success">Autenticado com sucesso! Agora você poderá utilizar o Flattr de dentro do AntennaPod.</string> + <string name="no_flattr_token_title">Nenhum token do Flattr encontrado</string> + <string name="no_flattr_token_msg">Sua conta Flattr não está conectada ao AntennaPod. Você pode conectar sua conta ao AntennaPod para usar o Flattr de dentro da aplicação ou pode visitar o website do feed para usar o Flattr por lá.</string> + <string name="authenticate_now_label">Autenticar</string> + <string name="action_forbidden_title">Ação proibida</string> + <string name="action_forbidden_msg">AntennaPod não tem permissão para esta ação. A permissão de acesso do AntennaPod pode ter sido revogada. Você pode re-autenticar ou visitar o website do feed.</string> + <string name="access_revoked_title">Acesso revogado</string> + <string name="access_revoked_info">Você revogou o token de acesso do AntennaPod com sucesso. Para finalizar o processo, você deve remover esta app da lista de aplicativos aprovados nas configurações de sua conta no website do Flattr.</string> + <!--Flattr--> + <!--Variable Speed--> + <string name="download_plugin_label">Download Plugin</string> + <string name="no_playback_plugin_title">Plugin Não Instalado</string> + <string name="set_playback_speed_label">Velocidades de Reprodução</string> + <!--Empty list labels--> + <string name="no_items_label">Não existem itens nesta lista.</string> + <string name="no_feeds_label">Você ainda não assinou nenhum feed.</string> + <!--Preferences--> + <string name="other_pref">Outros</string> + <string name="about_pref">Sobre</string> + <string name="queue_label">Fila</string> + <string name="services_label">Serviços</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Interromper a reprodução quando o fone de ouvido for desconectado</string> + <string name="pref_followQueue_sum">Pular para próximo item da fila quando a reprodução terminar</string> + <string name="playback_pref">Reprodução</string> + <string name="network_pref">Rede</string> + <string name="pref_autoUpdateIntervall_title">Intervalo de atualização</string> + <string name="pref_autoUpdateIntervall_sum">Especifica o intervalo com que os feeds serão atualizados automaticamente ou desabilita esta funcionalidade</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Fazer download dos arquivos apenas via rede WiFi</string> + <string name="pref_followQueue_title">Reprodução contínua</string> + <string name="pref_downloadMediaOnWifiOnly_title">Download de mídia via WiFi</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Fones de ouvido desconectados</string> + <string name="pref_mobileUpdate_title">Atualizações via Rede de Dados Celular</string> + <string name="pref_mobileUpdate_sum">Permite atualizações quando conectado na rede de dados celular</string> + <string name="refreshing_label">Atualizando</string> + <string name="flattr_settings_label">Configurações do Flattr</string> + <string name="pref_flattr_auth_title">Logar no Flattr</string> + <string name="pref_flattr_auth_sum">Loga na sua conta Flattr para utilizá-lo diretamente da aplicação</string> + <string name="pref_flattr_this_app_title">Registra este aplicativo no Flattr</string> + <string name="pref_flattr_this_app_sum">Suportar o desenvolvimento do AntennaPod usando o Flattr. Obrigado!</string> + <string name="pref_revokeAccess_title">Revogar acesso</string> + <string name="pref_revokeAccess_sum">Cancelar permissão de acesso à sua conta Flattr</string> + <string name="user_interface_label">Interface com usuário</string> + <string name="pref_set_theme_title">Selecionar tema</string> + <string name="pref_set_theme_sum">Altera a aparência do AntennaPod</string> + <string name="pref_automatic_download_title">Download automático</string> + <string name="pref_automatic_download_sum">Configurar download automático de episódios.</string> + <string name="pref_autodl_wifi_filter_title">Habilitar filtro Wi-Fi</string> + <string name="pref_autodl_wifi_filter_sum">Permitir download automático somente pelas redes Wi-Fi selecionadas.</string> + <string name="pref_episode_cache_title">Cache de episódios</string> + <string name="pref_theme_title_light">Claro</string> + <string name="pref_theme_title_dark">Escuro</string> + <string name="pref_episode_cache_unlimited">Ilimitado</string> + <string name="pref_update_interval_hours_plural">horas</string> + <string name="pref_update_interval_hours_singular">hora</string> + <string name="pref_update_interval_hours_manual">Manual</string> + <string name="pref_gpodnet_authenticate_title">Login</string> + <string name="pref_gpodnet_authenticate_sum">Faça o login na sua conta gpodder.net para sincronizar suas assinaturas.</string> + <string name="pref_gpodnet_logout_title">Sair</string> + <string name="pref_gpodnet_logout_toast">Saiu com sucesso</string> + <string name="pref_gpodnet_setlogin_information_title">Alterar informações de login</string> + <string name="pref_gpodnet_setlogin_information_sum">Alterar informações de login da sua conta gpodder.net</string> + <string name="pref_playback_speed_title">Velocidades de Reprodução</string> + <string name="pref_playback_speed_sum">Personalize as velocidades variáveis de reprodução de áudio.</string> + <string name="pref_gpodnet_sethostname_title">Configurar hostname</string> + <string name="pref_gpodnet_sethostname_use_default_host">Usar host padrão</string> + <!--Auto-Flattr dialog--> + <!--Search--> + <string name="search_hint">Procurar por Feeds ou Episódios</string> + <string name="found_in_shownotes_label">Encontrado nas notas do podcast</string> + <string name="found_in_chapters_label">Encontrado nos capítulos</string> + <string name="search_status_no_results">Nenhum resultado encontrado</string> + <string name="search_label">Pesquisar</string> + <string name="found_in_title_label">Encontrado no título</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">Arquivos OPML permitem que você mova seus podcasts de um programa de podcasts para outro.</string> + <string name="opml_import_explanation">Para importar um arquivo OPML, você precisa armazená-lo neste diretório e pressionar o botão abaixo para iniciar o processo de importação.</string> + <string name="start_import_label">Iniciar importação</string> + <string name="opml_import_label">Importação de OPML</string> + <string name="opml_directory_error">ERRO!</string> + <string name="reading_opml_label">Lendo arquivo OPML</string> + <string name="opml_reader_error">Ocorreu um erro durante a leitura do documento OPML:</string> + <string name="opml_import_error_dir_empty">O diretório de importação está vazio.</string> + <string name="select_all_label">Selecionar todos</string> + <string name="deselect_all_label">Remover seleção</string> + <string name="choose_file_to_import_label">Escolher arquivo para importar</string> + <string name="opml_export_label">Exportar OPML</string> + <string name="exporting_label">Exportando...</string> + <string name="export_error_label">Erro na exportação</string> + <string name="opml_export_success_title">OMPL exportado com sucesso</string> + <string name="opml_export_success_sum">O arquivo .opml foi gravado em:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Configura desligamento automático</string> + <string name="disable_sleeptimer_label">Desabilita desligamento automático</string> + <string name="enter_time_here_label">Informe a duração</string> + <string name="sleep_timer_label">Desligamento automático</string> + <string name="time_left_label">Tempo restante:\u0020</string> + <string name="time_dialog_invalid_input">Entrada inválida, a duração precisa ser um número inteiro</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">CATEGORIAS</string> + <string name="gpodnet_toplist_header">TOP PODCASTS</string> + <string name="gpodnet_suggestions_header">SUGESTÕES</string> + <string name="gpodnet_search_hint">Buscar no gpodder.net</string> + <string name="gpodnetauth_login_title">Login</string> + <string name="gpodnetauth_login_descr">Bem-vindo ao processo de login gpodder.net. Primeiramente, digite suas informações:</string> + <string name="gpodnetauth_login_butLabel">Login</string> + <string name="username_label">Nome do usuário</string> + <string name="password_label">Senha</string> + <string name="gpodnetauth_device_title">Seleção de dispositivo</string> + <string name="gpodnetauth_device_descr">Crie um novo dispositivo para usar em sua conta gpodder.net ou escolha um já existente:</string> + <string name="gpodnetauth_device_deviceID">ID do dispositivo:\u0020</string> + <string name="gpodnetauth_device_caption">Descrição do dispositivo</string> + <string name="gpodnetauth_device_butCreateNewDevice">Criar novo dispositivo</string> + <string name="gpodnetauth_device_chooseExistingDevice">Escolher dispositivo existente:</string> + <string name="gpodnetauth_device_errorEmpty">ID do dispostivo não pode estar em branco</string> + <string name="gpodnetauth_device_errorAlreadyUsed">ID do dispositivo já está em uso</string> + <string name="gpodnetauth_device_butChoose">Escolher</string> + <string name="gpodnetauth_finish_title">Login realizado com sucesso!</string> + <string name="gpodnetauth_finish_descr">Parabéns! Sua conta gpodder.net agora está conectada ao seu dispositivo. O AntennaPod irá, daqui em diante, sincronizar automaticamente assinaturas do seu dispositivo com sua conta gpodder.net.</string> + <string name="gpodnetauth_finish_butsyncnow">Iniciar sincronização agora</string> + <string name="gpodnetauth_finish_butgomainscreen">Ir para tela principal</string> + <string name="gpodnetsync_auth_error_title">gpodder.net: erro de autenticação</string> + <string name="gpodnetsync_auth_error_descr">Nome do usuário ou senha incorreta</string> + <string name="gpodnetsync_error_title">gpodder.net: erro de sincronização</string> + <string name="gpodnetsync_error_descr">Ocorreu um erro durante a sincronização:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Selecionar pasta:</string> + <string name="create_folder_label">Criar pasta</string> + <string name="choose_data_directory">Escolher pasta de dados</string> + <string name="create_folder_msg">Criar nova pasta com o nome \"%1$s\"?</string> + <string name="create_folder_success">Nova pasta criada</string> + <string name="create_folder_error_no_write_access">Não é possível escrever nesta pasta</string> + <string name="create_folder_error_already_exists">Pasta já existente</string> + <string name="create_folder_error">Não foi possível criar pasta</string> + <string name="folder_not_empty_dialog_title">A pasta não está vazia</string> + <string name="folder_not_empty_dialog_msg">A pasta que você selecionou não está vazia. Os downloads de mídia e outros arquivos serão colocados diretamente nesta pasta. Deseja mesmo continuar?</string> + <string name="set_to_default_folder">Escolher pasta padrão</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Pause a reprodução em vez de abaixar o volume quando outro aplicativo reproduzir sons</string> + <string name="pref_pausePlaybackForFocusLoss_title">Pausar em interrupções</string> + <!--Online feed view--> + <string name="subscribe_label">Assinar</string> + <string name="subscribed_label">Assinado</string> + <string name="downloading_label">Baixando...</string> + <!--Content descriptions for image buttons--> + <string name="show_cover_label">Mostrar imagem</string> + <string name="butAction_label">Mais ações</string> + <string name="status_playing_label">Episódio está sendo reproduzido</string> + <string name="status_downloaded_label">Episódio foi baixado</string> + <string name="status_unread_label">Item é novo</string> + <string name="in_queue_label">Episódio está na fila</string> + <string name="new_episodes_count_label">Numero de novos episódios</string> + <!--Feed information screen--> + <!--AntennaPodSP--> +</resources> diff --git a/core/src/main/res/values-pt/strings.xml b/core/src/main/res/values-pt/strings.xml new file mode 100644 index 000000000..3a0f300e6 --- /dev/null +++ b/core/src/main/res/values-pt/strings.xml @@ -0,0 +1,349 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Fontes</string> + <string name="add_feed_label">Adicionar podcast</string> + <string name="podcasts_label">Podcasts</string> + <string name="episodes_label">Episódios</string> + <string name="new_episodes_label">Novos episódios</string> + <string name="all_episodes_label">Todos os episódios</string> + <string name="new_label">Novo</string> + <string name="waiting_list_label">Lista de espera</string> + <string name="settings_label">Definições</string> + <string name="add_new_feed_label">Adicionar podcast</string> + <string name="downloads_label">Transferências</string> + <string name="downloads_running_label">Em curso</string> + <string name="downloads_completed_label">Terminadas</string> + <string name="downloads_log_label">Registo</string> + <string name="cancel_download_label">Cancelar transferência</string> + <string name="playback_history_label">Histórico de reprodução</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">Acesso gpodder.net</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Publicados recentemente</string> + <string name="episode_filter_label">Mostrar apenas novos episódios</string> + <!--Main activity--> + <string name="drawer_open">Abrir menu</string> + <string name="drawer_close">Fechar menu</string> + <!--Webview actions--> + <string name="open_in_browser_label">Abrir no navegador</string> + <string name="copy_url_label">Copiar URL</string> + <string name="share_url_label">Partilhar URL</string> + <string name="copied_url_msg">URL copiado para a área de transferência.</string> + <string name="go_to_position_label">Ir para esta posição</string> + <!--Playback history--> + <string name="clear_history_label">Limpar histórico</string> + <!--Other--> + <string name="confirm_label">Confirmar</string> + <string name="cancel_label">Cancelar</string> + <string name="author_label">Autor</string> + <string name="language_label">Idioma</string> + <string name="podcast_settings_label">Definições</string> + <string name="cover_label">Imagem</string> + <string name="error_label">Erro</string> + <string name="error_msg_prefix">Ocorreu um erro:</string> + <string name="refresh_label">Atualizar</string> + <string name="external_storage_error_msg">Não existe um cartão SD. Certifique-se que inseriu o cartão corretamente.</string> + <string name="chapters_label">Capítulos</string> + <string name="shownotes_label">Notas</string> + <string name="description_label">Descrição</string> + <string name="most_recent_prefix">Episódio mais recente:\u0020</string> + <string name="episodes_suffix">\u0020episódios</string> + <string name="length_prefix">Duração:\u0020</string> + <string name="size_prefix">Tamanho:\u0020</string> + <string name="processing_label">A processar...</string> + <string name="loading_label">A carregar...</string> + <string name="save_username_password_label">Gravar utilizador e senha</string> + <string name="close_label">Fechar</string> + <string name="retry_label">Tentar novamente</string> + <string name="auto_download_label">Incluir nas transferências automáticas</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">URL da fonte</string> + <string name="etxtFeedurlHint">URL da fonte ou sítio web</string> + <string name="txtvfeedurl_label">Adicionar podcast via URL</string> + <string name="podcastdirectories_label">Localizar podcasts no diretório</string> + <string name="podcastdirectories_descr">Pode procurar os novos podcasts no gPodder.net por nome, categoria ou popularidade.</string> + <string name="browse_gpoddernet_label">Procurar no gPodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Marcar tudo como lido</string> + <string name="mark_all_read_msg">Marcar todos os episódios como lidos</string> + <string name="show_info_label">Mostrar informações</string> + <string name="remove_feed_label">Remover podcast</string> + <string name="share_link_label">Partilhar ligação do sítio web</string> + <string name="share_source_label">Partilhar ligação da fonte</string> + <string name="feed_delete_confirmation_msg">Confirme a eliminação desta fonte e de todos os episódios a ela petencentes.</string> + <string name="feed_remover_msg">Remover fonte</string> + <string name="load_complete_feed">Atualizar todas as páginas da fonte</string> + <!--actions on feeditems--> + <string name="download_label">Transferir</string> + <string name="play_label">Reproduzir</string> + <string name="pause_label">Pausa</string> + <string name="stop_label">Parar</string> + <string name="stream_label">Emitir</string> + <string name="remove_label">Remover</string> + <string name="remove_episode_lable">Remover episódio</string> + <string name="mark_read_label">Marcar como lido</string> + <string name="mark_unread_label">Marcar como novo</string> + <string name="add_to_queue_label">Adicionar à fila</string> + <string name="remove_from_queue_label">Remover da fila</string> + <string name="visit_website_label">Aceder ao sítio web</string> + <string name="support_label">Flattr</string> + <string name="enqueue_all_new">Colocar tudo na fila</string> + <string name="download_all">Transferir tudo</string> + <string name="skip_episode_label">Ignorar episódio</string> + <!--Download messages and labels--> + <string name="download_successful">sucesso</string> + <string name="download_failed">falha</string> + <string name="download_pending">Transferência pendente</string> + <string name="download_running">Transferência atual</string> + <string name="download_error_device_not_found">Cartão SD não encontrado</string> + <string name="download_error_insufficient_space">Espaço insuficiente</string> + <string name="download_error_file_error">Erro no ficheiro</string> + <string name="download_error_http_data_error">Erro HTTP</string> + <string name="download_error_error_unknown">Erro desconhecido</string> + <string name="download_error_parser_exception">Exceção do processador</string> + <string name="download_error_unsupported_type">Fonte não suportada</string> + <string name="download_error_connection_error">Erro de ligação</string> + <string name="download_error_unknown_host">Servidor desconhecido</string> + <string name="download_error_unauthorized">Erro de autenticação</string> + <string name="cancel_all_downloads_label">Cancelar transferências</string> + <string name="download_cancelled_msg">Transferência cancelada</string> + <string name="download_report_title">Transferências terminadas</string> + <string name="download_error_malformed_url">URL inválido</string> + <string name="download_error_io_error">Erro I/O</string> + <string name="download_error_request_error">Erro de pedido</string> + <string name="download_error_db_access">Erro de acesso à base de dados</string> + <string name="downloads_left">\u0020Transferências em falta</string> + <string name="downloads_processing">Processamento de transferências</string> + <string name="download_notification_title">A transferir dados...</string> + <string name="download_report_content">%1$d transferências efetuadas, %2$d falhadas</string> + <string name="download_log_title_unknown">Título desconhecido</string> + <string name="download_type_feed">Fonte</string> + <string name="download_type_media">Ficheiro multimédia</string> + <string name="download_type_image">Imagem</string> + <string name="download_request_error_dialog_message_prefix">Ocorreu um erro ao transferir o ficheiro:\u0020</string> + <string name="authentication_notification_title">Requer autenticação</string> + <string name="authentication_notification_msg">O recurso solicitado requer um utilizador e uma senha</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Erro!</string> + <string name="player_stopped_msg">Nada em reprodução</string> + <string name="player_preparing_msg">A preparar</string> + <string name="player_ready_msg">Pronto</string> + <string name="player_seeking_msg">A procurar</string> + <string name="playback_error_server_died">Erro de servidor</string> + <string name="playback_error_unknown">Erro desconhecido</string> + <string name="no_media_playing_label">Nada em reprodução</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">A processar...</string> + <string name="playbackservice_notification_title">Reproduzir podcast</string> + <string name="unknown_media_key">Tecla multimédia desconhecida: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">Limpar fila</string> + <string name="undo">Anular</string> + <string name="removed_from_queue">Item removido</string> + <string name="move_to_top_label">Mover para o topo</string> + <string name="move_to_bottom_label">Mover para o fundo</string> + <!--Flattr--> + <string name="flattr_auth_label">Sessão Flattr</string> + <string name="flattr_auth_explanation">Prima o botão abaixo para iniciar a autenticação. O seu navegador web abrirá o ecrã da sessão flattr e ser-lhe-á solicitada a permissão para o AntennaPod efetuar as alterações. Após ser dada a permissão, voltará novamente a este ecrã.</string> + <string name="authenticate_label">Autenticar</string> + <string name="return_home_label">Voltar ao ecrã</string> + <string name="flattr_auth_success">Autenticação efetuada! Já pode fazer o flattr com a aplicação.</string> + <string name="no_flattr_token_title">Token flattr não encontrado</string> + <string name="no_flattr_token_notification_msg">Parece que a sua conta flattr não está integrada ao AntennaPod. Clique aqui para autenticar.</string> + <string name="no_flattr_token_msg">Parece que a sua conta flattr não está vinculada ao AntennaPod. Pode vincular a sua conta ao AntennaPod ou aceder ao sítio web para fazer o flattr.</string> + <string name="authenticate_now_label">Autenticar</string> + <string name="action_forbidden_title">Ação negada</string> + <string name="action_forbidden_msg">O AntennaPod não possui as permissões para esta ação. É possível que o token de acesso ao flattr via AntennaPod tenha sido revogado. Pode efetuar nova autenticação ou aceder ao sítio web do item.</string> + <string name="access_revoked_title">Acesso revogado</string> + <string name="access_revoked_info">Você revogou o token de acesso do AntennaPod à sua conta. Para concluir o processo, tem que remover esta aplicação da lista de aplicações presentes nas definições de conta no sítio web do flattr.</string> + <!--Flattr--> + <string name="flattr_click_success">Flattr de um item!</string> + <string name="flattr_click_success_count">Flattr de %d itens!</string> + <string name="flattr_click_success_queue">Flattr: %s</string> + <string name="flattr_click_failure_count">Falha ao efetuar flattr de %d itens!</string> + <string name="flattr_click_failure">Não flattr: %s.</string> + <string name="flattr_click_enqueued">O flattr deste item será feito mais tarde</string> + <string name="flattring_thing">Flattring %s</string> + <string name="flattring_label">O AntennaPod está a flattring</string> + <string name="flattrd_label">O AntennaPod fez o flattr</string> + <string name="flattrd_failed_label">O AntennaPod não fez o flattr</string> + <string name="flattr_retrieving_status">A obter itens com flattr</string> + <!--Variable Speed--> + <string name="download_plugin_label">Transferir extra</string> + <string name="no_playback_plugin_title">Extra não instalado</string> + <string name="no_playback_plugin_msg">Para que a velocidade de reprodução variável funcione, tem que instalar um biblioteca de terceiros.\n\nClique em Transferir extra para a transferir no Google Play.\n\nQuaisquer problemas que ocorram na utilização do extra devem ser reportados diretamente ao seu programador.</string> + <string name="set_playback_speed_label">Velocidades de reprodução</string> + <!--Empty list labels--> + <string name="no_items_label">Não existem itens na lista.</string> + <string name="no_feeds_label">Ainda não possui quaisquer fontes.</string> + <!--Preferences--> + <string name="other_pref">Outras</string> + <string name="about_pref">Sobre</string> + <string name="queue_label">Fila</string> + <string name="services_label">Serviços</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Parar reprodução ao remover os auscultadores</string> + <string name="pref_followQueue_sum">Ir para a faixa seguinte ao terminar a reprodução</string> + <string name="playback_pref">Reprodução</string> + <string name="network_pref">Rede</string> + <string name="pref_autoUpdateIntervall_title">Intervalo entre atualizações</string> + <string name="pref_autoUpdateIntervall_sum">Indique o intervalo de tempo entre as atualizações de fontes ou desative a opção</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Apenas transferir pelas redes sem fios</string> + <string name="pref_followQueue_title">Reprodução contínua</string> + <string name="pref_downloadMediaOnWifiOnly_title">Transferência Wi-Fi</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Auscultadores removidos</string> + <string name="pref_mobileUpdate_title">Atualizações móveis</string> + <string name="pref_mobileUpdate_sum">Permitir atualizações através da rede de dados</string> + <string name="refreshing_label">A atualizar</string> + <string name="flattr_settings_label">Definições flattr</string> + <string name="pref_flattr_auth_title">Sessão flattr</string> + <string name="pref_flattr_auth_sum">Inicie sessão na sua conta flattr para fazer o flattr no AntennaPod.</string> + <string name="pref_flattr_this_app_title">Flattr desta aplicação</string> + <string name="pref_flattr_this_app_sum">Ajude no desenvolvimento do AntennaPod através do Flattr. Obrigado!</string> + <string name="pref_revokeAccess_title">Revogar acesso</string> + <string name="pref_revokeAccess_sum">Revogar permissões de acesso da aplicação à sua conta flattr.</string> + <string name="pref_auto_flattr_title">Flattr automático</string> + <string name="pref_auto_flattr_sum">Configurar flattr automático</string> + <string name="user_interface_label">Interface</string> + <string name="pref_set_theme_title">Tema</string> + <string name="pref_set_theme_sum">Mudar o aspeto do AntennaPod.</string> + <string name="pref_automatic_download_title">Transferência automática</string> + <string name="pref_automatic_download_sum">Configure a transferência automática dos episódios.</string> + <string name="pref_autodl_wifi_filter_title">Ativar filtro Wi-Fi</string> + <string name="pref_autodl_wifi_filter_sum">Apenas permitir transferências automáticas através de redes sem fios.</string> + <string name="pref_episode_cache_title">Cache de episódios</string> + <string name="pref_theme_title_light">Claro</string> + <string name="pref_theme_title_dark">Escuro</string> + <string name="pref_episode_cache_unlimited">Sem limite</string> + <string name="pref_update_interval_hours_plural">horas</string> + <string name="pref_update_interval_hours_singular">hora</string> + <string name="pref_update_interval_hours_manual">Manual</string> + <string name="pref_gpodnet_authenticate_title">Acesso</string> + <string name="pref_gpodnet_authenticate_sum">Aceda à sua conta gpodder.net para poder sincronizar as subscrições.</string> + <string name="pref_gpodnet_logout_title">Sair</string> + <string name="pref_gpodnet_logout_toast">Sessão terminada</string> + <string name="pref_gpodnet_setlogin_information_title">Mudar informação de acesso</string> + <string name="pref_gpodnet_setlogin_information_sum">Mudar informação de acesso à sua conta gpodder.net.</string> + <string name="pref_playback_speed_title">Velocidades de reprodução</string> + <string name="pref_playback_speed_sum">Personalize as velocidades de reprodução disponíveis.</string> + <string name="pref_seek_delta_title">Intervalo de procura</string> + <string name="pref_seek_delta_sum">Ao recuar ou avançar, procurar este valor de segundos</string> + <string name="pref_gpodnet_sethostname_title">Definir nome de servidor</string> + <string name="pref_gpodnet_sethostname_use_default_host">Utilizar pré-definição</string> + <string name="pref_expandNotify_title">Expansão de notificação</string> + <string name="pref_expandNotify_sum">Expandir sempre a notificação para mostrar os botões de reprodução.</string> + <string name="pref_persistNotify_title">Controlos de reprodução persistentes</string> + <string name="pref_persistNotify_sum">Manter controlos de notificação e ecrã de bloqueio ao colocar a reprodução em pausa.</string> + <string name="pref_expand_notify_unsupport_toast">As versões Android anteriores à 4.1 não possuem suporte à expansão de notificações.</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Ativar flattr automático</string> + <string name="auto_flattr_after_percent">Flattr de episódios ao atingir %d porcento de reprodução</string> + <string name="auto_flattr_ater_beginning">Flattr de episodios ao iniciar a reprodução</string> + <string name="auto_flattr_ater_end">Flattr de episódios ao terminar a reprodução</string> + <!--Search--> + <string name="search_hint">Procurar fontes ou episódios</string> + <string name="found_in_shownotes_label">Encontrado nas notas</string> + <string name="found_in_chapters_label">Encontrado nos capítulos</string> + <string name="search_status_no_results">Nenhum resultado</string> + <string name="search_label">Procura</string> + <string name="found_in_title_label">Encontrado no título</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">Os ficheiros OPML permitem-lhe mover os podcasts entre aplicações.</string> + <string name="opml_import_explanation">Para importar um ficheiro OPML, tem que o colocar neste diretório e premir o botão abaixo para iniciar o processo.</string> + <string name="start_import_label">Iniciar importação</string> + <string name="opml_import_label">Importação OPML</string> + <string name="opml_directory_error">Erro!</string> + <string name="reading_opml_label">A ler ficheiro OPML</string> + <string name="opml_reader_error">Ocorreu um erro ao ler o ficheiro OPML:</string> + <string name="opml_import_error_dir_empty">O diretório de importação está vazio.</string> + <string name="select_all_label">Marcar tudo</string> + <string name="deselect_all_label">Desmarcar tudo</string> + <string name="choose_file_to_import_label">Escolha o ficheiro a importar</string> + <string name="opml_export_label">Exportação OPML</string> + <string name="exporting_label">Exportação...</string> + <string name="export_error_label">Erro de exportação</string> + <string name="opml_export_success_title">Exportação efetuada.</string> + <string name="opml_export_success_sum">O ficheiro .opml foi gravado em:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Definir temporizador</string> + <string name="disable_sleeptimer_label">Desativar temporizador</string> + <string name="enter_time_here_label">Introduza o tempo</string> + <string name="sleep_timer_label">Temporizador</string> + <string name="time_left_label">Tempo restante:\u0020</string> + <string name="time_dialog_invalid_input">Valor inválido. Tem que ser um inteiro.</string> + <string name="time_unit_seconds">segundos</string> + <string name="time_unit_minutes">minutos</string> + <string name="time_unit_hours">horas</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">Categorias</string> + <string name="gpodnet_toplist_header">Melhores</string> + <string name="gpodnet_suggestions_header">Sugestões</string> + <string name="gpodnet_search_hint">Procurar no gpodder.net</string> + <string name="gpodnetauth_login_title">Acesso</string> + <string name="gpodnetauth_login_descr">Bem-vindo ao processo de acesso ao gpodder.net. Introduza os dados de acesso:</string> + <string name="gpodnetauth_login_butLabel">Acesso</string> + <string name="gpodnetauth_login_register">Se ainda não possui uma conta, pode criar uma em:\nhttps://gpodder.net/register/</string> + <string name="username_label">Utilizador</string> + <string name="password_label">Senha</string> + <string name="gpodnetauth_device_title">Seleção de dispositivo</string> + <string name="gpodnetauth_device_descr">Criar um novo dispositivo ou escolher um existente para aceder à sua conta gpodder.net</string> + <string name="gpodnetauth_device_deviceID">ID do dispositivo:\u0020</string> + <string name="gpodnetauth_device_caption">Legenda</string> + <string name="gpodnetauth_device_butCreateNewDevice">Criar novo dispositivo</string> + <string name="gpodnetauth_device_chooseExistingDevice">Escolher dispositivo:</string> + <string name="gpodnetauth_device_errorEmpty">ID do dispositivo não pode estar vazia</string> + <string name="gpodnetauth_device_errorAlreadyUsed">ID de dispositivo já utilizada</string> + <string name="gpodnetauth_device_butChoose">Escolher</string> + <string name="gpodnetauth_finish_title">Sessão iniciada!</string> + <string name="gpodnetauth_finish_descr">Parabéns! A sua conta gpodder.net está vinculada ao seu dispositivo. Agora, já pode sincronizar as subscrições no dispositivo com a sua conta gpodder.net.</string> + <string name="gpodnetauth_finish_butsyncnow">Sincronizar agora</string> + <string name="gpodnetauth_finish_butgomainscreen">Ir para o ecrã principal</string> + <string name="gpodnetsync_auth_error_title">Erro de autenticação gpodder.net</string> + <string name="gpodnetsync_auth_error_descr">Utilizador ou senha inválido</string> + <string name="gpodnetsync_error_title">Erro de sincronização gpodder.net</string> + <string name="gpodnetsync_error_descr">Ocorreu um erro ao sincronizar:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Diretório escolhido:</string> + <string name="create_folder_label">Criar diretório</string> + <string name="choose_data_directory">Escolha o diretório</string> + <string name="create_folder_msg">Criar um diretório com o nome \"%1$s\"?</string> + <string name="create_folder_success">Novo diretório criado</string> + <string name="create_folder_error_no_write_access">Não é possível gravar neste diretório</string> + <string name="create_folder_error_already_exists">O diretório já existe</string> + <string name="create_folder_error">Não é possível criar o diretório</string> + <string name="folder_not_empty_dialog_title">Diretório não vazio</string> + <string name="folder_not_empty_dialog_msg">O diretório escolhido não está vazio. As transferências serão colocadas neste diretório. Continuar?</string> + <string name="set_to_default_folder">Escolha a pasta pré-definida</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Pausa na reprodução em vez de baixar o volume se outra aplicação quiser reproduzir sons</string> + <string name="pref_pausePlaybackForFocusLoss_title">Pausa nas interrupções</string> + <!--Online feed view--> + <string name="subscribe_label">Subscrever</string> + <string name="subscribed_label">Subscrito</string> + <string name="downloading_label">Transferência...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Mostrar capítulos</string> + <string name="show_shownotes_label">Mostrar notas</string> + <string name="show_cover_label">Mostrar imagem</string> + <string name="rewind_label">Recuar</string> + <string name="fast_forward_label">Avanço rápido</string> + <string name="media_type_audio_label">Áudio</string> + <string name="media_type_video_label">Vídeo</string> + <string name="navigate_upwards_label">Navegar para cima</string> + <string name="butAction_label">Mais ações</string> + <string name="status_playing_label">Episódio em reprodução</string> + <string name="status_downloading_label">Episódio a ser transferido</string> + <string name="status_downloaded_label">Episódio transferido</string> + <string name="status_unread_label">Novo item</string> + <string name="in_queue_label">Episódio está na fila</string> + <string name="new_episodes_count_label">Número de novos episódios</string> + <string name="in_progress_episodes_count_label">Número de episódios que já foi iniciada a reprodução</string> + <string name="drag_handle_content_description">Arraste para mudar a posição deste item</string> + <string name="load_next_page_label">Carregar próxima página</string> + <!--Feed information screen--> + <string name="authentication_label">Autenticação</string> + <string name="authentication_descr">Altere o seu nome de utilizador e senha para este podcast e seus episódios.</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Importar subscrições de aplicações single-purpose...</string> +</resources> diff --git a/core/src/main/res/values-ro-rRO/strings.xml b/core/src/main/res/values-ro-rRO/strings.xml new file mode 100644 index 000000000..6cc93e4a9 --- /dev/null +++ b/core/src/main/res/values-ro-rRO/strings.xml @@ -0,0 +1,244 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Feeduri</string> + <string name="podcasts_label">PODCASTURI</string> + <string name="episodes_label">EPISOADE</string> + <string name="new_label">Nou</string> + <string name="waiting_list_label">Listă de așteptare</string> + <string name="settings_label">Setări</string> + <string name="downloads_label">Descărcări</string> + <string name="cancel_download_label">Anulează descărcare</string> + <string name="playback_history_label">Istorie ascultare</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">autentificare gpodder.net</string> + <!--New episodes fragment--> + <!--Main activity--> + <!--Webview actions--> + <string name="open_in_browser_label">Deschide în browser</string> + <string name="copy_url_label">Copiază URL</string> + <string name="share_url_label">Împarte URL</string> + <string name="copied_url_msg">URL copiat în clipboard</string> + <!--Playback history--> + <string name="clear_history_label">Golește istoric</string> + <!--Other--> + <string name="confirm_label">Confirmă</string> + <string name="cancel_label">Anulează</string> + <string name="author_label">Autor</string> + <string name="language_label">Limbă</string> + <string name="podcast_settings_label">Setări</string> + <string name="error_label">Eroare</string> + <string name="error_msg_prefix">A avut loc o eroare:</string> + <string name="refresh_label">Reîncarcă</string> + <string name="external_storage_error_msg">Nu exista stocare externă. Asigurați-vă că stocarea externă este conectată pentru ca aplicația să funcționeze corespunzător.</string> + <string name="chapters_label">Capitole</string> + <string name="shownotes_label">Notițe</string> + <string name="description_label">Descriere</string> + <string name="most_recent_prefix">Cel mai recent episod:\u0020</string> + <string name="episodes_suffix">\u0020episoade</string> + <string name="length_prefix">Durată:\u0020</string> + <string name="size_prefix">Dimensiune:\u0020</string> + <string name="processing_label">Procesează</string> + <string name="loading_label">Încărcare...</string> + <string name="save_username_password_label">Salvează numele de utilizator și parola</string> + <string name="close_label">închide</string> + <string name="retry_label">Reîncearcă</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">Adresă feed</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Marchează toate ca citite</string> + <string name="show_info_label">Arată informații</string> + <string name="share_link_label">Împarte adresă website</string> + <string name="share_source_label">Împarte adresă feed</string> + <string name="feed_delete_confirmation_msg">Confirmați ștergerea feedului și a TUTUROR episoadelor pe care le-ați descărcat.</string> + <!--actions on feeditems--> + <string name="download_label">Descarcă</string> + <string name="play_label">Play</string> + <string name="pause_label">Pauză</string> + <string name="stream_label">Stream</string> + <string name="remove_label">Elimină</string> + <string name="mark_read_label">Marchează ca citit</string> + <string name="mark_unread_label">Marchează ca necitit</string> + <string name="add_to_queue_label">Adaugă la Coadă</string> + <string name="remove_from_queue_label">Șterge din Coadă</string> + <string name="visit_website_label">Vizitează Website</string> + <string name="support_label">Flattr aceasta</string> + <string name="enqueue_all_new">Adaugă toate în coadă</string> + <string name="download_all">Descarcă toate</string> + <string name="skip_episode_label">Sari peste episod</string> + <!--Download messages and labels--> + <string name="download_pending">Descărcare în așteptare</string> + <string name="download_running">Se descarcă</string> + <string name="download_error_device_not_found">Mediu de stocare lipsă</string> + <string name="download_error_insufficient_space">Spațiu insuficient</string> + <string name="download_error_file_error">Eroare fișier</string> + <string name="download_error_http_data_error">Eroare Date HTTP</string> + <string name="download_error_error_unknown">Eroare necunoscută</string> + <string name="download_error_parser_exception">Excepție parser</string> + <string name="download_error_unsupported_type">Tip de feed nesuportat</string> + <string name="download_error_connection_error">Eroare de conexiune</string> + <string name="download_error_unknown_host">Host necunoscut</string> + <string name="cancel_all_downloads_label">Anulează toate descărcările</string> + <string name="download_cancelled_msg">Descărcare anulată</string> + <string name="download_report_title">Descărcări terminate</string> + <string name="download_error_malformed_url">URL malformat</string> + <string name="download_error_io_error">Eroare IO</string> + <string name="download_error_request_error">Eroare cerere</string> + <string name="downloads_left">\u0020descărcări rămase</string> + <string name="download_notification_title">Descarcă date podcast</string> + <string name="download_report_content">%1$d descărcari cu succes, %2$d eșuate</string> + <string name="download_log_title_unknown">Titlu necunoscut</string> + <string name="download_type_feed">Feed</string> + <string name="download_type_media">Fișier media</string> + <string name="download_type_image">Imagine</string> + <string name="download_request_error_dialog_message_prefix">O eroare a avut loc când se descărca fișierul:\u0020</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Eroare!</string> + <string name="player_stopped_msg">Nu se ascultă nimic</string> + <string name="player_preparing_msg">Pregătește</string> + <string name="player_ready_msg">Pregătit</string> + <string name="player_seeking_msg">Căutare</string> + <string name="playback_error_server_died">Server mort</string> + <string name="playback_error_unknown">Eroare necuonscută</string> + <string name="no_media_playing_label">Nu se ascultă nimic</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Buffering</string> + <string name="playbackservice_notification_title">Cântă podcast</string> + <!--Queue operations--> + <string name="clear_queue_label">Golește coada</string> + <string name="undo">Refă</string> + <string name="removed_from_queue">Element înlăturat</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattr sign-in</string> + <string name="flattr_auth_explanation">Apăsați butonul de mai jos pentru a începe procesul de autentificare. Veți fi îndreptat spre pagina de logare flattr în browser și veți fi rugat să acordați permisiuni AntennaPod sa flattr. După ce veți acorda permisiunile veți fi readuși la acest ecran automat.</string> + <string name="authenticate_label">Autentificare</string> + <string name="return_home_label">Întoarcere acasă</string> + <string name="flattr_auth_success">Autentificare cu succes! Acum puteți flattr din aplicație.</string> + <string name="no_flattr_token_title">Nu s-a găsit token Flattr</string> + <string name="no_flattr_token_msg">Contul flattr nu pare șa fie conectat la AntennaPod. Puteți fie conecta contul cu AntennaPod pentru a flattr lucruri din aplicație sau puteți vizita site-ul pentru a flattr acolo.</string> + <string name="authenticate_now_label">Autentificați-vă</string> + <string name="action_forbidden_title">Acțiune interzisă</string> + <string name="action_forbidden_msg">AntennaPod nu are permisiuni pentru această acțiune. Motivul poate fi că tokenul de acces al AntennaPod pentru contul vostru a fost revocat. Vă puteți fie re-autentifica fie vizita direct site-ul.</string> + <string name="access_revoked_title">Acces revocat</string> + <string name="access_revoked_info">Ați revocat cu succes accesul AntennaPod la contul vostru. Pentru a completa acest proces trebuie să ștergeți aplicația din lista de aplicații aprobate din setările contului de pe site-ul flattr.</string> + <!--Flattr--> + <!--Variable Speed--> + <string name="download_plugin_label">Descarcă plugin</string> + <string name="no_playback_plugin_title">Plugin neinstalat</string> + <string name="set_playback_speed_label">Viteze de ascultare</string> + <!--Empty list labels--> + <string name="no_items_label">Nu sunt elemente în listă.</string> + <string name="no_feeds_label">Nu v-ați abonat la nici un feed momentan.</string> + <!--Preferences--> + <string name="other_pref">Altele</string> + <string name="about_pref">Despre</string> + <string name="queue_label">Coadă</string> + <string name="services_label">Servicii</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Pune pauză când căștile sunt deconectate</string> + <string name="pref_followQueue_sum">Sari la următorul element din coadă cand se termină ascultarea</string> + <string name="playback_pref">Ascultare</string> + <string name="network_pref">Rețea</string> + <string name="pref_autoUpdateIntervall_title">Interval actualizare</string> + <string name="pref_autoUpdateIntervall_sum">Specifică un interval în care feedurile sunt actualizate automat sau oprește funcția</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Descarcă fișiere media doar pe WiFi</string> + <string name="pref_followQueue_title">Ascultare continuă</string> + <string name="pref_downloadMediaOnWifiOnly_title">Descărcare media pe WiFi</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Căști deconectate</string> + <string name="pref_mobileUpdate_title">Actualizări mobile</string> + <string name="pref_mobileUpdate_sum">Permite actualizări pe conexiunea de date mobilă</string> + <string name="refreshing_label">Reîncarcare</string> + <string name="flattr_settings_label">Setări Flattr</string> + <string name="pref_flattr_auth_title">Sign-in Flattr</string> + <string name="pref_flattr_auth_sum">Logați la contul flattr pentru a flattr lucruri direct din aplicație.</string> + <string name="pref_flattr_this_app_title">Flattr această aplicație</string> + <string name="pref_flattr_this_app_sum">Ajutați dezvoltarea AntennaPod prin flattr. Mulțumesc!</string> + <string name="pref_revokeAccess_title">Revocare acces</string> + <string name="pref_revokeAccess_sum">Revocă accesul permisiunilor pentru contul de flattr.</string> + <string name="user_interface_label">Interfața grafică</string> + <string name="pref_set_theme_title">Alege temă</string> + <string name="pref_set_theme_sum">Schimbă aspectul AntennaPod.</string> + <string name="pref_automatic_download_title">Descărcare automată</string> + <string name="pref_automatic_download_sum">Configurează descărcarea automată a episoadelor.</string> + <string name="pref_autodl_wifi_filter_title">Pornește filtru Wi-Fi</string> + <string name="pref_autodl_wifi_filter_sum">Pornește descărcarea automată doar pentru rețele Wi-Fi selectate.</string> + <string name="pref_episode_cache_title">Cache de episoade</string> + <string name="pref_theme_title_light">Deschis</string> + <string name="pref_theme_title_dark">Întunecat</string> + <string name="pref_episode_cache_unlimited">Nelimitat</string> + <string name="pref_update_interval_hours_plural">ore</string> + <string name="pref_update_interval_hours_singular">oră</string> + <string name="pref_update_interval_hours_manual">Manual</string> + <string name="pref_gpodnet_authenticate_title">Autentificare</string> + <string name="pref_playback_speed_title">Viteze de ascutare</string> + <string name="pref_playback_speed_sum">Modifică vitezele disponibile pentru viteza de ascultare.</string> + <!--Auto-Flattr dialog--> + <!--Search--> + <string name="search_hint">Caută feeduri sau episoade</string> + <string name="found_in_shownotes_label">Găsit în notițe</string> + <string name="found_in_chapters_label">Găsit în capitole</string> + <string name="search_status_no_results">Nu s-a găsit nici un rezultat</string> + <string name="search_label">Caută</string> + <string name="found_in_title_label">Găsit în titlu</string> + <!--OPML import and export--> + <string name="opml_import_explanation">Pentru a importa un fișier OPML trebuie să-l salvați în următorul director și apăsați butonul de mai jos pentru a începe procesul.</string> + <string name="start_import_label">Începe importarea</string> + <string name="opml_import_label">OPML import</string> + <string name="opml_directory_error">EROARE!</string> + <string name="reading_opml_label">Citește fișierul OPML</string> + <string name="opml_reader_error">A avut loc o eroare la citirea documentului opml:</string> + <string name="opml_import_error_dir_empty">Directorul de import este gol.</string> + <string name="select_all_label">Selectează toate</string> + <string name="deselect_all_label">Deselectează toate</string> + <string name="choose_file_to_import_label">Alege fișier pentru import</string> + <string name="opml_export_label">Exportă OPML</string> + <string name="exporting_label">Exportă...</string> + <string name="export_error_label">Eroare exportare</string> + <string name="opml_export_success_title">Exportare opml cu succes.</string> + <string name="opml_export_success_sum">Fișierul .opml a fost scris în:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Setează cronometru somn</string> + <string name="disable_sleeptimer_label">Oprește cronometru somn</string> + <string name="enter_time_here_label">Introdu timp</string> + <string name="sleep_timer_label">Cronometru somn</string> + <string name="time_left_label">Timp rămas:\u0020</string> + <string name="time_dialog_invalid_input">Input invalid, timpul trebuie să fie un întreg</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">CATEGORII</string> + <string name="gpodnet_suggestions_header">SUGESTII</string> + <string name="gpodnetauth_login_title">Autentificare</string> + <string name="gpodnetauth_login_butLabel">Conectare</string> + <string name="username_label">Utilizator</string> + <string name="password_label">Parolă</string> + <string name="gpodnetauth_device_title">Alegere Dispozitiv</string> + <string name="gpodnetauth_device_deviceID">ID dispozitiv:\u0020</string> + <string name="gpodnetauth_device_butCreateNewDevice">Creează dispozitiv nou</string> + <string name="gpodnetauth_device_errorEmpty">ID-ul dispozitivului nu trebuie să fie gol</string> + <string name="gpodnetauth_device_errorAlreadyUsed">ID-ul de dispozitiv este deja în uz</string> + <string name="gpodnetauth_device_butChoose">Alege</string> + <string name="gpodnetauth_finish_butsyncnow">Începe sincronizarea acum</string> + <string name="gpodnetauth_finish_butgomainscreen">Mergi la ecranul principal</string> + <string name="gpodnetsync_auth_error_title">eroare de autentificare la gpodder.net</string> + <string name="gpodnetsync_auth_error_descr">Nume utilizator sau parolă greșite</string> + <string name="gpodnetsync_error_title">eroare de sincronizare gpodder.net</string> + <!--Directory chooser--> + <string name="selected_folder_label">Fișier selectat:</string> + <string name="create_folder_label">Crează fișier</string> + <string name="choose_data_directory">Alege fișier date</string> + <string name="create_folder_msg">Crează fișier nou cu numele \"%1$s\"?</string> + <string name="create_folder_success">A fost creat fișierul</string> + <string name="create_folder_error_no_write_access">Nu poate fi scris în fișier</string> + <string name="create_folder_error_already_exists">Fișier deja existent</string> + <string name="create_folder_error">Nu poate creea fișier</string> + <string name="folder_not_empty_dialog_title">Fișierul nu este gol</string> + <string name="folder_not_empty_dialog_msg">Fișierul selectat nu este gol. Descărcările media și alte fișiere vor fi plasate direct în acest director. Continuați oricum?</string> + <string name="set_to_default_folder">Alege fișier implicit</string> + <!--Online feed view--> + <string name="subscribe_label">Abonează-te</string> + <string name="subscribed_label">Abonat</string> + <string name="downloading_label">Se descarcă...</string> + <!--Content descriptions for image buttons--> + <!--Feed information screen--> + <!--AntennaPodSP--> +</resources> diff --git a/core/src/main/res/values-ru/strings.xml b/core/src/main/res/values-ru/strings.xml new file mode 100644 index 000000000..140e7ea3a --- /dev/null +++ b/core/src/main/res/values-ru/strings.xml @@ -0,0 +1,336 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Каналы</string> + <string name="add_feed_label">Добавить подкаст</string> + <string name="podcasts_label">Подкасты</string> + <string name="episodes_label">Выпуски</string> + <string name="new_episodes_label">Новые выпуски</string> + <string name="all_episodes_label">Все выпуски</string> + <string name="new_label">Новые</string> + <string name="waiting_list_label">В ожидании</string> + <string name="settings_label">Настройки</string> + <string name="add_new_feed_label">Добавить подкаст</string> + <string name="downloads_label">Загрузки</string> + <string name="downloads_running_label">Выполняется</string> + <string name="downloads_completed_label">Завершено</string> + <string name="downloads_log_label">Журнал</string> + <string name="cancel_download_label">Отменить загрузку</string> + <string name="playback_history_label">История воспроизведения</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">Войти на gpodder.net</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Свежие</string> + <string name="episode_filter_label">Только новые</string> + <!--Main activity--> + <string name="drawer_open">Открыть меню</string> + <string name="drawer_close">Закрыть меню</string> + <!--Webview actions--> + <string name="open_in_browser_label">Открыть в браузере</string> + <string name="copy_url_label">Скопировать ссылку</string> + <string name="share_url_label">Поделиться ссылкой</string> + <string name="copied_url_msg">Ссылка скопирована в буфер</string> + <string name="go_to_position_label">Перейти к этой позиции</string> + <!--Playback history--> + <string name="clear_history_label">Очистить историю</string> + <!--Other--> + <string name="confirm_label">Подтвердить</string> + <string name="cancel_label">Отмена</string> + <string name="author_label">Автор</string> + <string name="language_label">Язык</string> + <string name="podcast_settings_label">Настройки</string> + <string name="cover_label">Обложка</string> + <string name="error_label">Ошибка</string> + <string name="error_msg_prefix">Произошла ошибка:</string> + <string name="refresh_label">Обновить</string> + <string name="external_storage_error_msg">Внешний носитель недоступен. Убедитесь что внешний носитель установлен, иначе приложение не сможет нормально работать.</string> + <string name="chapters_label">Главы</string> + <string name="shownotes_label">Примечания к выпуску</string> + <string name="description_label">Описание</string> + <string name="most_recent_prefix">Последний выпуск:\u0020</string> + <string name="episodes_suffix">\u0020выпуск(ов)</string> + <string name="length_prefix">Продолжительность:\u0020</string> + <string name="size_prefix">Размер:\u0020</string> + <string name="processing_label">Обработка</string> + <string name="loading_label">Загрузка...</string> + <string name="save_username_password_label">Сохранить имя пользователя и пароль</string> + <string name="close_label">Закрыть</string> + <string name="retry_label">Повторить</string> + <string name="auto_download_label">Добавить в автозагрузки</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">URL канала</string> + <string name="etxtFeedurlHint">Адрес канала или сайта</string> + <string name="txtvfeedurl_label">Добавить подкаст по URL</string> + <string name="podcastdirectories_label">Найти подкаст в каталоге</string> + <string name="podcastdirectories_descr">Вы можете искать новые подкасты в каталоге gpodder.net по имени, категории или популярности/</string> + <string name="browse_gpoddernet_label">Просмотр gpodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Отметить как прослушанное</string> + <string name="mark_all_read_msg">Отметить все выпуски как прослушанные</string> + <string name="show_info_label">Показать информацию</string> + <string name="remove_feed_label">Удалить подкаст</string> + <string name="share_link_label">Поделиться ссылкой на сайт</string> + <string name="share_source_label">Ссылка на канал</string> + <string name="feed_delete_confirmation_msg">Подтвердите удаление канала и всех выпусков, загруженных с этого канала.</string> + <string name="feed_remover_msg">Удаление канала</string> + <!--actions on feeditems--> + <string name="download_label">Загрузить</string> + <string name="play_label">Воспроизвести</string> + <string name="pause_label">Пауза</string> + <string name="stop_label">Остановить</string> + <string name="stream_label">Воспроизвести из сети</string> + <string name="remove_label">Удалить</string> + <string name="remove_episode_lable">Удалить</string> + <string name="mark_read_label">Отметить как прочитанное</string> + <string name="mark_unread_label">Отметить как непрочитанное</string> + <string name="add_to_queue_label">Добавить в очередь</string> + <string name="remove_from_queue_label">Удалить из очереди</string> + <string name="visit_website_label">Посетить сайт</string> + <string name="support_label">Поддержать через Flattr</string> + <string name="enqueue_all_new">Добавить всё в очередь</string> + <string name="download_all">Загрузить всё</string> + <string name="skip_episode_label">Пропустить выпуск</string> + <!--Download messages and labels--> + <string name="download_successful">успешно</string> + <string name="download_failed">не удалось</string> + <string name="download_pending">Загрузка в ожидании</string> + <string name="download_running">Загрузка в процессе</string> + <string name="download_error_device_not_found">Устройство хранения не найдено</string> + <string name="download_error_insufficient_space">Недостаточно места</string> + <string name="download_error_file_error">Ошибка файла</string> + <string name="download_error_http_data_error">Ошибка данных HTTP</string> + <string name="download_error_error_unknown">Неизвестная ошибка</string> + <string name="download_error_parser_exception">Ошибка обработки</string> + <string name="download_error_unsupported_type">Неподдерживаемый тип канала</string> + <string name="download_error_connection_error">Ошибка соединения</string> + <string name="download_error_unknown_host">Неизвестный узел</string> + <string name="download_error_unauthorized">Ошибка авторизации</string> + <string name="cancel_all_downloads_label">Отменить все загрузки</string> + <string name="download_cancelled_msg">Загрузка отменена</string> + <string name="download_report_title">Загрузки завершены</string> + <string name="download_error_malformed_url">Неправильный адрес</string> + <string name="download_error_io_error">Ошибка ввода-вывода</string> + <string name="download_error_request_error">Ошибка запроса</string> + <string name="download_error_db_access">Ошибка доступа к базе данных</string> + <string name="downloads_left">Осталось\u0020загрузок</string> + <string name="downloads_processing">Производится загрузка</string> + <string name="download_notification_title">Получение данных подкаста</string> + <string name="download_report_content">%1$d загрузок завершено, %2$d не удалось</string> + <string name="download_log_title_unknown">Неизвестное название</string> + <string name="download_type_feed">Канал</string> + <string name="download_type_media">Медиафайл</string> + <string name="download_type_image">Изображение</string> + <string name="download_request_error_dialog_message_prefix">Ошибка при загрузки файла:\u0020</string> + <string name="authentication_notification_title">Необходима авторизация</string> + <string name="authentication_notification_msg">Для доступа к ресурсу необходимо ввести имя пользователя и пароль</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Ошибка</string> + <string name="player_stopped_msg">Ничего не воспроизводится</string> + <string name="player_preparing_msg">Подготовка</string> + <string name="player_ready_msg">Готово</string> + <string name="player_seeking_msg">Перемотка</string> + <string name="playback_error_server_died">Сервер недоступен</string> + <string name="playback_error_unknown">Неизвестная ошибка</string> + <string name="no_media_playing_label">Ничего не воспроизводится</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Буферизация</string> + <string name="playbackservice_notification_title">Воспроизведение подкаста</string> + <!--Queue operations--> + <string name="clear_queue_label">Очистить очередь</string> + <string name="undo">Отмена</string> + <string name="removed_from_queue">Удалено</string> + <string name="move_to_top_label">Переместить вверх</string> + <string name="move_to_bottom_label">Переместить вниз</string> + <!--Flattr--> + <string name="flattr_auth_label">Авторизоваться в Flattr</string> + <string name="flattr_auth_explanation">Нажмите кнопку, чтобы начать процесс авторизации. Вы будете перенаправлены на сайт Flattr, где нужно будет разрешить AntennaPod использовать ваш аккаунт. После этого вы автоматически будете перенаправлены обратно.</string> + <string name="authenticate_label">Авторизовать</string> + <string name="return_home_label">Вернуться к началу</string> + <string name="flattr_auth_success">Успешная авторизация. Теперь можно использовать Flattr прямо из приложения.</string> + <string name="no_flattr_token_title">Токен Flattr не найден</string> + <string name="no_flattr_token_msg">Кажется, ваш аккаунт Flattr не подключен к AntennaPod. Можно подключить аккаунт к AntennaPod или посетить сайт канала, чтобы пожертвовать через Flattr прямо на сайте.</string> + <string name="authenticate_now_label">Авторизоваться</string> + <string name="action_forbidden_title">Действие запрещено</string> + <string name="action_forbidden_msg">AntennaPod не имеет прав для выполнения этого действия. Возможно, доступ к вашему аккаунту был отозван. Можно авторизоваться заново или посетить сайт, которому вы пожертвовали через Flattr.</string> + <string name="access_revoked_title">Доступ отозван</string> + <string name="access_revoked_info">Вы успешно отключили AntennaPod от аккаунта в Flattr. Чтобы завершить этот процесс нужно удалить AntennaPod из списка приложений подключенных к аккаунту на сайте Flattr.</string> + <!--Flattr--> + <string name="flattr_click_success">Один поддержан через Flattr!</string> + <string name="flattr_click_success_count">Поддержано через Flattr: %d.</string> + <string name="flattr_click_success_queue">Поддержано через Flattr: %s.</string> + <string name="flattr_click_failure_count">Не удалось поддержать через Flattr: %d!</string> + <string name="flattr_click_failure">Не поддержано через Flattr: %s.</string> + <string name="flattr_click_enqueued">Будет поддержано через Flattr потом</string> + <string name="flattring_thing">%s поддерживается через Flattr</string> + <string name="flattring_label">AntennaPod поддерживается через Flattr</string> + <string name="flattrd_label">Вы поддержали AntennaPod через Flattr</string> + <string name="flattrd_failed_label">Ошибка</string> + <string name="flattr_retrieving_status">Получение списка поддержаного через Flattr</string> + <!--Variable Speed--> + <string name="download_plugin_label">Загрузить плагин</string> + <string name="no_playback_plugin_title">Плагин не установлен</string> + <string name="set_playback_speed_label">Скорость воспроизведения</string> + <!--Empty list labels--> + <string name="no_items_label">Список пуст</string> + <string name="no_feeds_label">Вы еще не подписаны ни на один канал</string> + <!--Preferences--> + <string name="other_pref">Прочее</string> + <string name="about_pref">О программе</string> + <string name="queue_label">Очередь</string> + <string name="services_label">Сервисы</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Приостановить воспроизведение, когда наушники отсоединены</string> + <string name="pref_followQueue_sum">После завершения воспроизведения перейти к следующему в очереди</string> + <string name="playback_pref">Воспроизведение</string> + <string name="network_pref">Сеть</string> + <string name="pref_autoUpdateIntervall_title">Интервал обновлений</string> + <string name="pref_autoUpdateIntervall_sum">Укажите интервал через который каналы обновляются автоматически, или отключите его</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Загружать файлы только через Wi-Fi</string> + <string name="pref_followQueue_title">Непрерывное воспроизведение</string> + <string name="pref_downloadMediaOnWifiOnly_title">Загрузка по Wi-Fi</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Наушники отсоединены</string> + <string name="pref_mobileUpdate_title">Мобильные обновления</string> + <string name="pref_mobileUpdate_sum">Позволить обновления через мобильное интернет-подключение</string> + <string name="refreshing_label">Обновление</string> + <string name="flattr_settings_label">Настройки Flattr</string> + <string name="pref_flattr_auth_title">Авторизация Flattr</string> + <string name="pref_flattr_auth_sum">Авторизуйтесь во Flattr чтобы поддерживать каналы прямо из приложения</string> + <string name="pref_flattr_this_app_title">Поддержать это приложение в Flattr</string> + <string name="pref_flattr_this_app_sum">Поддержите разработку AntennaPod через Flattr. Спасибо!</string> + <string name="pref_revokeAccess_title">Отозвать доступ</string> + <string name="pref_revokeAccess_sum">Отменить доступ этого приложения к вашему аккаунту Flattr.</string> + <string name="pref_auto_flattr_title">Автоматически поддерживать через Flattr</string> + <string name="user_interface_label">Интерфейс</string> + <string name="pref_set_theme_title">Выбор темы</string> + <string name="pref_set_theme_sum">Изменить тему оформления AntennaPod</string> + <string name="pref_automatic_download_title">Автоматическая загрузка</string> + <string name="pref_automatic_download_sum">Настроить автоматическую загрузку выпусков.</string> + <string name="pref_autodl_wifi_filter_title">Включить фильтр Wi-Fi</string> + <string name="pref_autodl_wifi_filter_sum">Разрешать автоматическую загрузку только для выбранных сетей Wi-Fi.</string> + <string name="pref_episode_cache_title">Кэш выпусков</string> + <string name="pref_theme_title_light">Светлая</string> + <string name="pref_theme_title_dark">Тёмная</string> + <string name="pref_episode_cache_unlimited">Неограничен</string> + <string name="pref_update_interval_hours_plural">ч.</string> + <string name="pref_update_interval_hours_singular">ч.</string> + <string name="pref_update_interval_hours_manual">Вручную</string> + <string name="pref_gpodnet_authenticate_title">Войти</string> + <string name="pref_gpodnet_authenticate_sum">Вход в ваш аккаунт gpodder.net для синхронизации ваших подписок.</string> + <string name="pref_gpodnet_logout_title">Выход из gpodder.net</string> + <string name="pref_gpodnet_logout_toast">Выход произведён успешно</string> + <string name="pref_gpodnet_setlogin_information_title">Изменить информацию авторизации</string> + <string name="pref_gpodnet_setlogin_information_sum">Изменить информацию авторизации для аккаунта gpodder.net</string> + <string name="pref_playback_speed_title">Скорость воспроизведения</string> + <string name="pref_playback_speed_sum">Настроить скорости воспроизведения</string> + <string name="pref_gpodnet_sethostname_title">Задать имя узла</string> + <string name="pref_gpodnet_sethostname_use_default_host">Использовать узел по умолчанию</string> + <string name="pref_expandNotify_title">Расширенное уведомление</string> + <string name="pref_expandNotify_sum">Всегда показывать в уведомлении кнопки управления вопспроизведением</string> + <string name="pref_persistNotify_title">Постоянный контрооль воспроизведения</string> + <string name="pref_persistNotify_sum">Сохранять уведомление и кнопки воспроизведения на экране блокировки во время паузы.</string> + <string name="pref_expand_notify_unsupport_toast">Версии Android ниже 4.1 не поддерживают расширенные уведомления.</string> + <!--Auto-Flattr dialog--> + <!--Search--> + <string name="search_hint">Поиск каналов или выпусков</string> + <string name="found_in_shownotes_label">Найдено в описании выпуска</string> + <string name="found_in_chapters_label">Найдено в главах</string> + <string name="search_status_no_results">Ничего не найдено</string> + <string name="search_label">Поиск</string> + <string name="found_in_title_label">Найдено в заголовке</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">OPML файлы позволяют перемещать ваши подкасты из одного менеджера подкастов в другой.</string> + <string name="opml_import_explanation">Для импорта файла OPML его нужно поместить в указанный каталог и нажать кнопку внизу для запуска импорта.</string> + <string name="start_import_label">Начать импорт</string> + <string name="opml_import_label">Импорт OPML</string> + <string name="opml_directory_error">Ошибка</string> + <string name="reading_opml_label">Чтение файла OPML</string> + <string name="opml_reader_error">Ошибка чтения файла OPML</string> + <string name="opml_import_error_dir_empty">Каталог для импорта пуст.</string> + <string name="select_all_label">Отметить все</string> + <string name="deselect_all_label">Снять все отметки</string> + <string name="choose_file_to_import_label">Выбрать файл для импорта</string> + <string name="opml_export_label">Экспорт в OPML</string> + <string name="exporting_label">Экспортируется...</string> + <string name="export_error_label">Ошибка экспорта</string> + <string name="opml_export_success_title">Экспорт OPML завершён.</string> + <string name="opml_export_success_sum">Файл OPML был записан в:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Установить таймер сна</string> + <string name="disable_sleeptimer_label">Отключить таймер сна</string> + <string name="enter_time_here_label">Введите время</string> + <string name="sleep_timer_label">Таймер сна</string> + <string name="time_left_label">Осталось времени:\u0020</string> + <string name="time_dialog_invalid_input">Неправильный ввод, время должно быть в виде числа</string> + <string name="time_unit_seconds">с</string> + <string name="time_unit_minutes">м</string> + <string name="time_unit_hours">ч</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">Категории</string> + <string name="gpodnet_toplist_header">Лучшее</string> + <string name="gpodnet_suggestions_header">Рекомендации</string> + <string name="gpodnet_search_hint">Искать на gpodder.net</string> + <string name="gpodnetauth_login_title">Войти</string> + <string name="gpodnetauth_login_descr">Добро пожаловать в процесс авторизации на gpodder.net. Сначала введите вашу информацию для авторизации:</string> + <string name="gpodnetauth_login_butLabel">Войти</string> + <string name="username_label">Имя пользователя</string> + <string name="password_label">Пароль</string> + <string name="gpodnetauth_device_title">Выбор устройства</string> + <string name="gpodnetauth_device_descr">Создайте новое устройство, чтобы использовать ваш аккаунт на gpodder.net или выберите существующее:</string> + <string name="gpodnetauth_device_deviceID">Идентификатор устройства:\u0020</string> + <string name="gpodnetauth_device_caption">Название устройства</string> + <string name="gpodnetauth_device_butCreateNewDevice">Создать новое устройство</string> + <string name="gpodnetauth_device_chooseExistingDevice">Выберите существующее устройство:</string> + <string name="gpodnetauth_device_errorEmpty">Поле с Device ID не должно быть пустым</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Device ID уже используется</string> + <string name="gpodnetauth_device_butChoose">Выберите</string> + <string name="gpodnetauth_finish_title">Авторизация успешна!</string> + <string name="gpodnetauth_finish_descr">Поздравляем! Ваш аккаунт на gpodder.net теперь связан с вашим устройством. AntennaPod теперь сможет автоматически синхронизировать ваши подписки с аккаунтом gpodder.net</string> + <string name="gpodnetauth_finish_butsyncnow">Начать синхронизацию</string> + <string name="gpodnetauth_finish_butgomainscreen">Перейти на главный экран</string> + <string name="gpodnetsync_auth_error_title">Ошибка авторизации на gpodder.net</string> + <string name="gpodnetsync_auth_error_descr">Неправильное имя пользователя или пароль</string> + <string name="gpodnetsync_error_title">Ошибка синхронизации с gpodder.net</string> + <string name="gpodnetsync_error_descr">Произошла ошибка во время синхронизации:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Выбранная папка:</string> + <string name="create_folder_label">Создать папку</string> + <string name="choose_data_directory">Выбрать папку для хранения данных</string> + <string name="create_folder_msg">Создать папку \"%1$s\"?</string> + <string name="create_folder_success">Новая папка создана</string> + <string name="create_folder_error_no_write_access">Запись в эту папку невозможна</string> + <string name="create_folder_error_already_exists">Папка уже существует</string> + <string name="create_folder_error">Невозможно создать папку</string> + <string name="folder_not_empty_dialog_title">Папка не пуста</string> + <string name="folder_not_empty_dialog_msg">Выбранная папка не пуста. Загрузки и прочие файлы будут сохранены в эту папку. Продолжить?</string> + <string name="set_to_default_folder">Выберите папку по умолчанию</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Пауза вместо уменьшения громкости, когда другое приложение проигрывает звуки</string> + <string name="pref_pausePlaybackForFocusLoss_title">Пауза при прерывании</string> + <!--Online feed view--> + <string name="subscribe_label">Подписаться</string> + <string name="subscribed_label">Подписка оформлена</string> + <string name="downloading_label">Загрузка...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Показать главы</string> + <string name="show_shownotes_label">Показать примечания к выпуску</string> + <string name="show_cover_label">Показать изображение</string> + <string name="rewind_label">Назад</string> + <string name="fast_forward_label">Вперед</string> + <string name="media_type_audio_label">Аудио</string> + <string name="media_type_video_label">Видео</string> + <string name="navigate_upwards_label">Перейти выше</string> + <string name="butAction_label">Другие действия</string> + <string name="status_playing_label">Выпуск воспроизводится</string> + <string name="status_downloading_label">Выпуск загружается</string> + <string name="status_downloaded_label">Выпуск загружен</string> + <string name="status_unread_label">Новый элемент</string> + <string name="in_queue_label">Выпуск в очереди</string> + <string name="new_episodes_count_label">Количество новых выпусков</string> + <string name="in_progress_episodes_count_label">Количество начатых выпусков</string> + <string name="drag_handle_content_description">Перетяните чтобы изменить позицию этого элемента</string> + <string name="load_next_page_label">Загрузить следующую страницу</string> + <!--Feed information screen--> + <string name="authentication_label">Авторизация</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Импорт подписок из одноцелевых приложений…</string> +</resources> diff --git a/core/src/main/res/values-sv-rSE/strings.xml b/core/src/main/res/values-sv-rSE/strings.xml new file mode 100644 index 000000000..187008426 --- /dev/null +++ b/core/src/main/res/values-sv-rSE/strings.xml @@ -0,0 +1,349 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Flöden</string> + <string name="add_feed_label">Lägg till podcast</string> + <string name="podcasts_label">PODCASTS</string> + <string name="episodes_label">AVSNITT</string> + <string name="new_episodes_label">Nya episoder</string> + <string name="all_episodes_label">Alla episoder</string> + <string name="new_label">Ny</string> + <string name="waiting_list_label">Väntelista</string> + <string name="settings_label">Inställningar</string> + <string name="add_new_feed_label">Lägg till podcast</string> + <string name="downloads_label">Nedladdningar</string> + <string name="downloads_running_label">Körs</string> + <string name="downloads_completed_label">Färdiga</string> + <string name="downloads_log_label">Logg</string> + <string name="cancel_download_label">Avbryt nedladdning</string> + <string name="playback_history_label">Uppspelningshistorik</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net login</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Nyligen publicerade</string> + <string name="episode_filter_label">Visa bara nya episoder</string> + <!--Main activity--> + <string name="drawer_open">Öppna meny</string> + <string name="drawer_close">Stäng meny</string> + <!--Webview actions--> + <string name="open_in_browser_label">Öppna i webbläsare</string> + <string name="copy_url_label">Kopiera URL</string> + <string name="share_url_label">Dela URL</string> + <string name="copied_url_msg">Kopierade URL till clipboard.</string> + <string name="go_to_position_label">Gå hit</string> + <!--Playback history--> + <string name="clear_history_label">Rensa historik</string> + <!--Other--> + <string name="confirm_label">Bekräfta</string> + <string name="cancel_label">Avbryt</string> + <string name="author_label">Skapare</string> + <string name="language_label">Språk</string> + <string name="podcast_settings_label">Inställningar</string> + <string name="cover_label">Bild</string> + <string name="error_label">Fel</string> + <string name="error_msg_prefix">Ett fel inträffade:</string> + <string name="refresh_label">Uppdatera</string> + <string name="external_storage_error_msg">Ingen extern lagring är tillgänglig. Se till att montera en extern lagringsenhet så att appen kan fungera korrekt.</string> + <string name="chapters_label">Kapitel</string> + <string name="shownotes_label">Shownotes</string> + <string name="description_label">Beskrivning</string> + <string name="most_recent_prefix">Senaste avsnittet:\u0020</string> + <string name="episodes_suffix">\u0020episoder</string> + <string name="length_prefix">Längd:\u0020</string> + <string name="size_prefix">Storlek:\u0020</string> + <string name="processing_label">Bearbetar</string> + <string name="loading_label">Laddar...</string> + <string name="save_username_password_label">Spara användarnamn och lösenord</string> + <string name="close_label">Stäng</string> + <string name="retry_label">Försök igen</string> + <string name="auto_download_label">Inkludera i automatiska nedladdningar</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">Flödets URL</string> + <string name="etxtFeedurlHint">URL till flöde eller webbsida</string> + <string name="txtvfeedurl_label">Lägg till podcast via URL</string> + <string name="podcastdirectories_label">Hitta podcast i mapp</string> + <string name="podcastdirectories_descr">Du kan söka efter podcasts baserat på namn, kategori eller populäritet med tjänsten gpodder.net</string> + <string name="browse_gpoddernet_label">Bläddra på gpodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Markera alla som lästa</string> + <string name="mark_all_read_msg">Markera alla episoder som lästa</string> + <string name="show_info_label">Visa information</string> + <string name="remove_feed_label">Ta bort podcast</string> + <string name="share_link_label">Dela hemsidans länk</string> + <string name="share_source_label">Dela flödeslänk</string> + <string name="feed_delete_confirmation_msg">Bekräfta att du vill ta bort denna feed och ALLA avsnitt av denna feed som du har hämtat.</string> + <string name="feed_remover_msg">Tar bort flöde</string> + <string name="load_complete_feed">Uppdatera hela flödet</string> + <!--actions on feeditems--> + <string name="download_label">Ladda ned</string> + <string name="play_label">Spela</string> + <string name="pause_label">Pausa</string> + <string name="stop_label">Stopp</string> + <string name="stream_label">Strömma</string> + <string name="remove_label">Ta bort</string> + <string name="remove_episode_lable">Ta bort episod</string> + <string name="mark_read_label">Markera som läst</string> + <string name="mark_unread_label">Markera som oläst</string> + <string name="add_to_queue_label">Lägg till i kön</string> + <string name="remove_from_queue_label">Ta bort från Kön</string> + <string name="visit_website_label">Besök websidan</string> + <string name="support_label">Flattra det här</string> + <string name="enqueue_all_new">Lägg till alla i kön</string> + <string name="download_all">Ladda ner alla</string> + <string name="skip_episode_label">Hoppa över episod</string> + <!--Download messages and labels--> + <string name="download_successful">lyckades</string> + <string name="download_failed">misslyckades</string> + <string name="download_pending">Avvaktar nedladdning</string> + <string name="download_running">Nedladdning pågår</string> + <string name="download_error_device_not_found">Lagringsenhet hittades inte</string> + <string name="download_error_insufficient_space">Otillräckligt utrymme</string> + <string name="download_error_file_error">Filfel</string> + <string name="download_error_http_data_error">HTTP data fel</string> + <string name="download_error_error_unknown">Okänt fel</string> + <string name="download_error_parser_exception">Tolkningsfel</string> + <string name="download_error_unsupported_type">Flödestyp utan stöd</string> + <string name="download_error_connection_error">Anslutningsfel</string> + <string name="download_error_unknown_host">Okänd värd</string> + <string name="download_error_unauthorized">Autentiseringsproblem</string> + <string name="cancel_all_downloads_label">Avbryt alla nedladdningar</string> + <string name="download_cancelled_msg">Nedladdning avbruten</string> + <string name="download_report_title">Nedladdningar färdiga</string> + <string name="download_error_malformed_url">Felaktig webbadress</string> + <string name="download_error_io_error">IO fel</string> + <string name="download_error_request_error">Request fel</string> + <string name="download_error_db_access">Ingen tillgång till databasen</string> + <string name="downloads_left">\u0020Nedladdningar kvar</string> + <string name="downloads_processing">Bearbetar nedladdningar</string> + <string name="download_notification_title">Laddar ner podcastdata</string> + <string name="download_report_content">%1$d nedladdningar lyckades, %2$d misslyckades</string> + <string name="download_log_title_unknown">Okänd titel</string> + <string name="download_type_feed">Flöde</string> + <string name="download_type_media">Mediafil</string> + <string name="download_type_image">Bild</string> + <string name="download_request_error_dialog_message_prefix">Ett fel uppstod vid försöket att ladda ner filen:\u0020</string> + <string name="authentication_notification_title">Autentisering krävs</string> + <string name="authentication_notification_msg">Resursen du begärde kräver ett användarnamn och ett lösenord</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Fel! </string> + <string name="player_stopped_msg">Inget media spelar</string> + <string name="player_preparing_msg">Förbereder</string> + <string name="player_ready_msg">Beredd</string> + <string name="player_seeking_msg">Söker</string> + <string name="playback_error_server_died">Servern dog</string> + <string name="playback_error_unknown">Okänt fel</string> + <string name="no_media_playing_label">Inget media spelar</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Buffrar</string> + <string name="playbackservice_notification_title">Spelar podcast</string> + <string name="unknown_media_key">AntannaPod - Okänd mediaknapp: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">Rensa kön</string> + <string name="undo">Ångra</string> + <string name="removed_from_queue">Föremålet avlägsnades</string> + <string name="move_to_top_label">Flytta längst upp</string> + <string name="move_to_bottom_label">Flytta längst ned</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattr inloggning</string> + <string name="flattr_auth_explanation">Tryck på knappen nedan för att starta autentiseringen. Du kommer att vidarebefordras till Flattrs inloggningsskärm i din webbläsare och uppmanas att ge AntennaPod tillstånd att Flattra saker. Efter att du har gett tillstånd, kommer du automatiskt tillbaka till den här skärmen.</string> + <string name="authenticate_label">Autentisera</string> + <string name="return_home_label">Återgå till Startsidan</string> + <string name="flattr_auth_success">Autentiseringen lyckades! Du kan nu Flattra saker i appen.</string> + <string name="no_flattr_token_title">Ingen Flattr token hittades</string> + <string name="no_flattr_token_notification_msg">Ditt Flattr-konto verkar inte vara anslutet till AntennaPod. Tryck här för att autentisera.</string> + <string name="no_flattr_token_msg">Ditt Flattr konto verkar inte vara ansluten till AntennaPod. Du kan antingen ansluta ditt konto till AntennaPod att Flattr saker i app eller så kan du besöka webbplatsen för att Flattr det där.</string> + <string name="authenticate_now_label">Autentisera</string> + <string name="action_forbidden_title">Åtgärd förbjuden</string> + <string name="action_forbidden_msg">AntennaPod saknar behörighet för den här åtgärden. Anledningen till detta kan vara att AntennaPods tillgång till ditt konto har återkallats. Du kan antingen åter autentisera AntennaPod eller besöka hemsidan istället.</string> + <string name="access_revoked_title">Tillgång återkallad</string> + <string name="access_revoked_info">Du har nu återkallat AntennaPods tillgång till ditt konto. För att slutföra processen, måste du ta bort den här appen från listan godkända appar i dina kontoinställningar på Flattrs hemsida.</string> + <!--Flattr--> + <string name="flattr_click_success">Flattrade en sak!</string> + <string name="flattr_click_success_count">Flattrade %d saker!</string> + <string name="flattr_click_success_queue">Flattrade: %s.</string> + <string name="flattr_click_failure_count">Misslyckades att flattra %d saker!</string> + <string name="flattr_click_failure">Ej flattrade: %s.</string> + <string name="flattr_click_enqueued">Saker som kommer att flattras senare</string> + <string name="flattring_thing">Flattrar %s</string> + <string name="flattring_label">AnntennaPod flattrar</string> + <string name="flattrd_label">AntennaPod har flattrat</string> + <string name="flattrd_failed_label">AntennaPod misslyckades att flattra</string> + <string name="flattr_retrieving_status">Hämtar flattrade saker</string> + <!--Variable Speed--> + <string name="download_plugin_label">Ladda ner tillägg</string> + <string name="no_playback_plugin_title">Tillägg ej installerat</string> + <string name="no_playback_plugin_msg">För att variabel uppspelningshastighet skall fungera måste ett tredjepartstillägg installeras.\n\nTryck på \'Ladda ner tillägg\' för att ladda ner ett gratis tillägg från Play Store.\n\nAntennaPod ansvarar inte för problem med detta tillägg och de bör rapporteras till tilläggets skapare.</string> + <string name="set_playback_speed_label">Uppspelningshastigheter</string> + <!--Empty list labels--> + <string name="no_items_label">Det finns inget i denna lista.</string> + <string name="no_feeds_label">Du har inte prenumererat på något flöde ännu.</string> + <!--Preferences--> + <string name="other_pref">Annat</string> + <string name="about_pref">Om</string> + <string name="queue_label">Kö</string> + <string name="services_label">Tjänster</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Pausa uppspelningen när hörlurarna bortkopplas</string> + <string name="pref_followQueue_sum">Hoppa till nästa i kön när uppspelningen är klar</string> + <string name="playback_pref">Uppspelning</string> + <string name="network_pref">Nätverk </string> + <string name="pref_autoUpdateIntervall_title">Uppdateringsintervall</string> + <string name="pref_autoUpdateIntervall_sum">Ange ett intervall för att automatiskt uppdatera flödet eller avaktivera det</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Hämta mediefiler endast över WiFi</string> + <string name="pref_followQueue_title">Kontinuerlig uppspelning</string> + <string name="pref_downloadMediaOnWifiOnly_title">WiFi nedladdning</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Hörlurar bortkopplade</string> + <string name="pref_mobileUpdate_title">Mobila uppdateringar</string> + <string name="pref_mobileUpdate_sum">Tillåt uppdateringar via mobil dataanslutning</string> + <string name="refreshing_label">Uppdaterar</string> + <string name="flattr_settings_label">Flattr inställningar</string> + <string name="pref_flattr_auth_title">Flattr inloggning</string> + <string name="pref_flattr_auth_sum">För att Flattra saker direkt från appen, logga in på ditt Flattr-konto.</string> + <string name="pref_flattr_this_app_title">Flattra den här appen</string> + <string name="pref_flattr_this_app_sum">Stöd utvecklingen av AntennaPod genom att flattra den. Tack!</string> + <string name="pref_revokeAccess_title">Återkalla åtkomst</string> + <string name="pref_revokeAccess_sum">Återkalla behörigheten till ditt Flattr-konto för denna app.</string> + <string name="pref_auto_flattr_title">Automatisk Flattring</string> + <string name="pref_auto_flattr_sum">Konfigurerar automatisk Flattring</string> + <string name="user_interface_label">Användargränssnitt</string> + <string name="pref_set_theme_title">Välj tema</string> + <string name="pref_set_theme_sum">Ändra utseendet på AntennaPod.</string> + <string name="pref_automatic_download_title">Automatisk nedladdning</string> + <string name="pref_automatic_download_sum">Konfigurera automatisk nedladdning av episoder.</string> + <string name="pref_autodl_wifi_filter_title">Aktivera WiFi filtrering</string> + <string name="pref_autodl_wifi_filter_sum">Tillåt automatisk nedladdning endast för utvalda WiFi-nätverk.</string> + <string name="pref_episode_cache_title">Avsnittscache</string> + <string name="pref_theme_title_light">Ljust</string> + <string name="pref_theme_title_dark">Mörkt</string> + <string name="pref_episode_cache_unlimited">Obegränsat</string> + <string name="pref_update_interval_hours_plural">timmar</string> + <string name="pref_update_interval_hours_singular">timme</string> + <string name="pref_update_interval_hours_manual">Manuell</string> + <string name="pref_gpodnet_authenticate_title">Logga in</string> + <string name="pref_gpodnet_authenticate_sum">Logga in med ditt gpodder.net konto för att synkronisera dina prenumerationer.</string> + <string name="pref_gpodnet_logout_title">Logga ut</string> + <string name="pref_gpodnet_logout_toast">Utloggning lyckades</string> + <string name="pref_gpodnet_setlogin_information_title">Ändra inloggningsinformation</string> + <string name="pref_gpodnet_setlogin_information_sum">Ändra inloggningsinformationen för ditt gpodder.net konto.</string> + <string name="pref_playback_speed_title">Uppspelningshastigheter</string> + <string name="pref_playback_speed_sum">Anpassa de tillgängliga hastigheterna för variabel uppspelningshastighet.</string> + <string name="pref_seek_delta_title">Söktid</string> + <string name="pref_seek_delta_sum">Sök så här många sekunder vid snabbspolning bakåt eller framåt</string> + <string name="pref_gpodnet_sethostname_title">Sätt värdnamn</string> + <string name="pref_gpodnet_sethostname_use_default_host">Använd standardvärden</string> + <string name="pref_expandNotify_title">Expandera notifieringar</string> + <string name="pref_expandNotify_sum">Expandera alltid notifieringen för att visa uppspelningskontrollerna.</string> + <string name="pref_persistNotify_title">Bestående uppspelningskontroller</string> + <string name="pref_persistNotify_sum">Behåll notifiering och kontroller på låsskärmen när uppspelningen pausas.</string> + <string name="pref_expand_notify_unsupport_toast">Androidversioner före 4.1 har inte stöd för expanderade notifieringar.</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Aktivera automatisk Flattring</string> + <string name="auto_flattr_after_percent">Flattra episoden så snart %d procent har spelats</string> + <string name="auto_flattr_ater_beginning">Flattra episoden när den startas</string> + <string name="auto_flattr_ater_end">Flattra episoden när den spelats klart</string> + <!--Search--> + <string name="search_hint">Sök efter flöden eller avsnitt</string> + <string name="found_in_shownotes_label">Hittad i shownotes</string> + <string name="found_in_chapters_label">Hittad i kapitel</string> + <string name="search_status_no_results">Inga resultat hittades</string> + <string name="search_label">Sök</string> + <string name="found_in_title_label">Hittad i titeln</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">OPML-filer låter dig flytta dina podcasts från en podcatcher till en annan.</string> + <string name="opml_import_explanation">Om du vill importera en OPML-fil, måste du placera den i följande katalog och tryck på knappen nedan för att starta importen.</string> + <string name="start_import_label">Påbörja importering</string> + <string name="opml_import_label">Importera OPML-fil</string> + <string name="opml_directory_error">FEL! </string> + <string name="reading_opml_label">Läser OPML-fil</string> + <string name="opml_reader_error">Ett fel har skett vid iläsning av opml dokumentet:</string> + <string name="opml_import_error_dir_empty">Katalogen är tom.</string> + <string name="select_all_label">Välj alla</string> + <string name="deselect_all_label">Avmarkera alla</string> + <string name="choose_file_to_import_label">Välj fil att importera</string> + <string name="opml_export_label">OPML export</string> + <string name="exporting_label">Exporterar...</string> + <string name="export_error_label">Exporteringsfel</string> + <string name="opml_export_success_title">OPML export lyckades</string> + <string name="opml_export_success_sum">.opml filen skrevs till:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Ställ in sömntimer</string> + <string name="disable_sleeptimer_label">Stäng av sömntimer</string> + <string name="enter_time_here_label">Ange tid</string> + <string name="sleep_timer_label">Sömntimer</string> + <string name="time_left_label">Återstående tid:\u0020</string> + <string name="time_dialog_invalid_input">Ogiltigt tal, tiden måste vara ett heltal</string> + <string name="time_unit_seconds">sekunder</string> + <string name="time_unit_minutes">minuter</string> + <string name="time_unit_hours">timmar</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">KATEGORIER</string> + <string name="gpodnet_toplist_header">BÄSTA PODCASTS</string> + <string name="gpodnet_suggestions_header">FÖRSLAG</string> + <string name="gpodnet_search_hint">Sök på gpodder.net</string> + <string name="gpodnetauth_login_title">Inloggning</string> + <string name="gpodnetauth_login_descr">Välkommen till inloggningsprocessen för gpodder.net. Först, skriv in din inloggningsinformation:</string> + <string name="gpodnetauth_login_butLabel">Logga in</string> + <string name="gpodnetauth_login_register">Om du inte har ett konto än, så kan du skapa ett här:\nhttps://gpodder.net/register/</string> + <string name="username_label">Användarnamn</string> + <string name="password_label">Lösenord</string> + <string name="gpodnetauth_device_title">Enhetsval</string> + <string name="gpodnetauth_device_descr">Skapa en ny enhet för ditt gpodder.net konto eller välj en befintlig:</string> + <string name="gpodnetauth_device_deviceID">Enhets ID:\u0020</string> + <string name="gpodnetauth_device_caption">Rubrik</string> + <string name="gpodnetauth_device_butCreateNewDevice">Skapa ny enhet</string> + <string name="gpodnetauth_device_chooseExistingDevice">Välj befintlig enhet:</string> + <string name="gpodnetauth_device_errorEmpty">Enhets ID måste fyllas i</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Enhets ID används redan</string> + <string name="gpodnetauth_device_butChoose">Välj</string> + <string name="gpodnetauth_finish_title">Inloggning lyckades!</string> + <string name="gpodnetauth_finish_descr">Grattis! Ditt gpodder.net konto är nu länkat med din enhet. AntennaPod kommer från och med nu automatiskt synkronisera dina prenumerationer på din enhet med ditt gpodder.net konto.</string> + <string name="gpodnetauth_finish_butsyncnow">Starta synkronisering nu</string> + <string name="gpodnetauth_finish_butgomainscreen">Gå till huvudskärmen</string> + <string name="gpodnetsync_auth_error_title">gpodder.net autentiseringsfel</string> + <string name="gpodnetsync_auth_error_descr">Fel användarnamn eller lösenord</string> + <string name="gpodnetsync_error_title">gpodder.net synkroniseringsfel</string> + <string name="gpodnetsync_error_descr">Ett fel uppstod under synkronisering:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Vald mapp:</string> + <string name="create_folder_label">Skapa mapp</string> + <string name="choose_data_directory">Välj mapp</string> + <string name="create_folder_msg">Skapa ny mapp med namnet \"%1$s\"?</string> + <string name="create_folder_success">Skapade ny mapp</string> + <string name="create_folder_error_no_write_access">Kan inte skriva till den här mappen</string> + <string name="create_folder_error_already_exists">Mappen finns redan</string> + <string name="create_folder_error">Kunde inte skapa mapp</string> + <string name="folder_not_empty_dialog_title">Mappen är inte tom</string> + <string name="folder_not_empty_dialog_msg">Den mapp du har valt är inte tom. Filer kommer att placeras direkt i denna mapp. Fortsätt ändå?</string> + <string name="set_to_default_folder">Välj standardmapp</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Pausa uppspelning istället för att sänka volymen när en annan app vill spela ljud</string> + <string name="pref_pausePlaybackForFocusLoss_title">Pausa för avbrott</string> + <!--Online feed view--> + <string name="subscribe_label">Prenumerera</string> + <string name="subscribed_label">Prenumererar</string> + <string name="downloading_label">Laddar ner...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Visa kapitel</string> + <string name="show_shownotes_label">Visa shownotes</string> + <string name="show_cover_label">Visa bild</string> + <string name="rewind_label">Backa</string> + <string name="fast_forward_label">Snabbspola</string> + <string name="media_type_audio_label">Ljud</string> + <string name="media_type_video_label">Video</string> + <string name="navigate_upwards_label">Navigera upp</string> + <string name="butAction_label">Fler åtgärder</string> + <string name="status_playing_label">Episoden spelas</string> + <string name="status_downloading_label">Episoden laddas ner</string> + <string name="status_downloaded_label">Episoden är nedladdad</string> + <string name="status_unread_label">Föremålet är nytt</string> + <string name="in_queue_label">Episoden är i kön</string> + <string name="new_episodes_count_label">Antal nya episoder</string> + <string name="in_progress_episodes_count_label">Antal episoder du har börjat lyssna på</string> + <string name="drag_handle_content_description">Dra för att ändra dess position</string> + <string name="load_next_page_label">Ladda nästa sida</string> + <!--Feed information screen--> + <string name="authentication_label">Autentisering</string> + <string name="authentication_descr">Byt ditt användarnamn och lösenord för den här podcasten och dess episoder.</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Importerar prenumerationer från appar gjorda för ett enda syfte...</string> +</resources> diff --git a/core/src/main/res/values-tr/strings.xml b/core/src/main/res/values-tr/strings.xml new file mode 100644 index 000000000..4fe2c69cd --- /dev/null +++ b/core/src/main/res/values-tr/strings.xml @@ -0,0 +1,349 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Beslemeler</string> + <string name="add_feed_label">Cep yayını ekle</string> + <string name="podcasts_label">CEP YAYINLARI</string> + <string name="episodes_label">BÖLÜMLER</string> + <string name="new_episodes_label">Yeni bölümler</string> + <string name="all_episodes_label">Tüm bölümler</string> + <string name="new_label">Yeni</string> + <string name="waiting_list_label">Bekleme listesi</string> + <string name="settings_label">Ayarlar</string> + <string name="add_new_feed_label">Cep Yayını ekle</string> + <string name="downloads_label">İndirilenler</string> + <string name="downloads_running_label">Çalışıyor</string> + <string name="downloads_completed_label">Tamamlandı</string> + <string name="downloads_log_label">Günlük</string> + <string name="cancel_download_label">İndirmeyi İptal Et</string> + <string name="playback_history_label">Çalma geçmişi</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net giriş</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Son yayınlananlar</string> + <string name="episode_filter_label">Sadece yeni bölümleri göster</string> + <!--Main activity--> + <string name="drawer_open">Münüyü aç</string> + <string name="drawer_close">Menüyü kapat</string> + <!--Webview actions--> + <string name="open_in_browser_label">Tarayıcıda aç</string> + <string name="copy_url_label">URL\'yi kopyala</string> + <string name="share_url_label">URL\'yi paylaş</string> + <string name="copied_url_msg">URL panoya kopyalandı.</string> + <string name="go_to_position_label">Bu pozisyona git</string> + <!--Playback history--> + <string name="clear_history_label">Geçmişi temizle</string> + <!--Other--> + <string name="confirm_label">Onayla</string> + <string name="cancel_label">İptal</string> + <string name="author_label">Yayıncı</string> + <string name="language_label">Dil</string> + <string name="podcast_settings_label">Ayarlar</string> + <string name="cover_label">Resim</string> + <string name="error_label">Hata</string> + <string name="error_msg_prefix">Bir hata oluştu:</string> + <string name="refresh_label">Yenile</string> + <string name="external_storage_error_msg">Harici depolama yok. Lütfen uygulamanın düzgün çalışabilmesi için harici depolama aygıtının bağlandığından emin olun.</string> + <string name="chapters_label">Kısımlar</string> + <string name="shownotes_label">Notlar</string> + <string name="description_label">Tanım</string> + <string name="most_recent_prefix">Son Bölüm:\u0020 </string> + <string name="episodes_suffix">\u0020bölüm</string> + <string name="length_prefix">Uzunluk:\u0020</string> + <string name="size_prefix">Boyut:\u0020</string> + <string name="processing_label">İşleniyor</string> + <string name="loading_label">Yükleniyor...</string> + <string name="save_username_password_label">Kullanıcı adı ve şifreyi kaydet</string> + <string name="close_label">Kapat</string> + <string name="retry_label">Yeniden dene</string> + <string name="auto_download_label">Otomatik indirmelere dahil et</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">Besleme Adresi</string> + <string name="etxtFeedurlHint">Besleme veya web sayfası URLsi</string> + <string name="txtvfeedurl_label">URL ile cep yayını ekle</string> + <string name="podcastdirectories_label">Dizinde cep yayını bul</string> + <string name="podcastdirectories_descr">gdpodder.net dizininde yeni cep yayınlarını isme, kategoriye veya popülerliğe göre arayabilirsiniz.</string> + <string name="browse_gpoddernet_label">gpodder.net\'e gözat</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Hepsini okundu olarak işaretle</string> + <string name="mark_all_read_msg">Tüm bölümler okundu olarak işaretlendi</string> + <string name="show_info_label">Bilgiyi göster</string> + <string name="remove_feed_label">Cep yayını kaldır</string> + <string name="share_link_label">Web sayfası bağlantısı paylaş</string> + <string name="share_source_label">Besleme bağlantısını paylaş</string> + <string name="feed_delete_confirmation_msg">Lütfen bu beslemeyi ve bu beslemeye ait indirilmiş BÜTÜN bölümleri silme isteğinizi onaylayın.</string> + <string name="feed_remover_msg">Besleme kaldırılıyor</string> + <string name="load_complete_feed">Tüm beslemeyi yenile</string> + <!--actions on feeditems--> + <string name="download_label">İndir</string> + <string name="play_label">Oynat</string> + <string name="pause_label">Duraklat</string> + <string name="stop_label">Durdur</string> + <string name="stream_label">Akış</string> + <string name="remove_label">Kaldır</string> + <string name="remove_episode_lable">Bölümü kaldır</string> + <string name="mark_read_label">Okundu olarak işaretle</string> + <string name="mark_unread_label">Okunmadı olarak işaretle</string> + <string name="add_to_queue_label">Kuyruğa Ekle</string> + <string name="remove_from_queue_label">Kuyruktan Kaldır</string> + <string name="visit_website_label">Siteyi Ziyaret Et</string> + <string name="support_label">Flattr ile destekle</string> + <string name="enqueue_all_new">Hepsini kuyruğa ekle</string> + <string name="download_all">Hepsini indir</string> + <string name="skip_episode_label">Bölümü atla</string> + <!--Download messages and labels--> + <string name="download_successful">başarılı</string> + <string name="download_failed">başarısız</string> + <string name="download_pending">İndirme bekleniyor</string> + <string name="download_running">İndiriliyor</string> + <string name="download_error_device_not_found">Depolama aygıtı bulunamadı</string> + <string name="download_error_insufficient_space">Yetersiz alan</string> + <string name="download_error_file_error">Dosya hatası</string> + <string name="download_error_http_data_error">HTTP Veri Hatası</string> + <string name="download_error_error_unknown">Bilinmeyen Hata</string> + <string name="download_error_parser_exception">Ayrıştırıcı İstisnası</string> + <string name="download_error_unsupported_type">Desteklenmeyen Besleme türü</string> + <string name="download_error_connection_error">Baplantı hatası</string> + <string name="download_error_unknown_host">Bilinmeyen sunucu</string> + <string name="download_error_unauthorized">Yetkilendirme hatası</string> + <string name="cancel_all_downloads_label">Bütün indirmeleri iptal et</string> + <string name="download_cancelled_msg">İndirme iptal edildi</string> + <string name="download_report_title">İndirme tamamlandı</string> + <string name="download_error_malformed_url">Bozuk URL</string> + <string name="download_error_io_error">G/Ç Hatası</string> + <string name="download_error_request_error">İstek hatası</string> + <string name="download_error_db_access">Veritabanı erişim hatası</string> + <string name="downloads_left">\u0020Kalan indirme</string> + <string name="downloads_processing">İndirmeler işleniyor</string> + <string name="download_notification_title">Cep yayını verileri indiriliyor</string> + <string name="download_report_content">%1$d indirme başarılı, %2$d başarısız</string> + <string name="download_log_title_unknown">bilinmeyen başlık</string> + <string name="download_type_feed">Besleme</string> + <string name="download_type_media">Medya dosyası</string> + <string name="download_type_image">Resim</string> + <string name="download_request_error_dialog_message_prefix">Dosyayı indirmeye çalışırken bir hata oluştu:\u0020</string> + <string name="authentication_notification_title">Yetkilendirme gerekiyor</string> + <string name="authentication_notification_msg">İstediğiniz kaynak kullanıcı adı ve şifre istiyor</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Hata!</string> + <string name="player_stopped_msg">Çalınan medya yok</string> + <string name="player_preparing_msg">Hazırlanıyor</string> + <string name="player_ready_msg">Hazır</string> + <string name="player_seeking_msg">Aranıyor</string> + <string name="playback_error_server_died">Sunucu geberdi</string> + <string name="playback_error_unknown">Bilinmeyen Hata</string> + <string name="no_media_playing_label">Çalınan medya yok</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Ara belleğe alınıyor</string> + <string name="playbackservice_notification_title">Cep yayını çalınıyor</string> + <string name="unknown_media_key">AntennaPod - Bilinmeyen medya anahtarı: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">Kuyruğu temizle</string> + <string name="undo">Geri al</string> + <string name="removed_from_queue">Öge kaldırıldı</string> + <string name="move_to_top_label">En üste taşı</string> + <string name="move_to_bottom_label">En alta taşı</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattr giriş</string> + <string name="flattr_auth_explanation">Yetkilendirme işlemini başlatmak için aşağıdaki butona basın. Tarayıcınızda flattr giriş ekranına yönlendirileceksiniz ve AntennaPod\'un flattr ile etkileşime girebilmesi için izniniz istenecek. İzin verdikten sonra otomatik olarak bu ekrana döneceksiniz.</string> + <string name="authenticate_label">Yetkilendir</string> + <string name="return_home_label">Anasayfaya dön</string> + <string name="flattr_auth_success">Yetkilendirme başarılı! Şimdi istediğiniz şeyleri uygulama içinden flattr ile destekleyebilirsiniz.</string> + <string name="no_flattr_token_title">Flattr jetonu bulunamadı</string> + <string name="no_flattr_token_notification_msg">Flattr hesabınız AntennaPod ile bağlı görünmüyor. Yetkilendirme için buraya tıklayın.</string> + <string name="no_flattr_token_msg">Flattr hesabınız AntennaPod\'a bağlanmamış. Bazı şeyleri uyuglama içinden flattr\'lamak için hesabınızı AntennaPod\'a bağlayabilirsiniz veya o şeyin sitesine gidip oradan yapabilirsiniz.</string> + <string name="authenticate_now_label">Yetkilendir</string> + <string name="action_forbidden_title">Yasaklanmış eylem</string> + <string name="action_forbidden_msg">AntennaPod bu eylem için izni yok. Bunun sebebi AntennaPod\'un hesabınıza erişimi iptal edilmiş olabilir. Yeniden erişim izni verebilirsiniz veya bu şeyin web sayfasını ziyaret edebilirsiniz.</string> + <string name="access_revoked_title">Erişim iptal edildi</string> + <string name="access_revoked_info">AntennaPod\'un hesabınıza erişebilmesini başarıyla iptal ettiniz. İşlemi tamamlamak için flattr web sayfasındaki hesap ayarları kısmından bu uygulamayı doğrulanan uygulamalar içinden çıkarmalısınız.</string> + <!--Flattr--> + <string name="flattr_click_success">Bir öğe Flattr\'landı!</string> + <string name="flattr_click_success_count">%d öğe Flattr\'landı!</string> + <string name="flattr_click_success_queue">Flattr\'landı: %s.</string> + <string name="flattr_click_failure_count">%d öğe Flattr\'lanamadı!</string> + <string name="flattr_click_failure">Flattr\'lanmadı: %s.</string> + <string name="flattr_click_enqueued">Daha sonra Flattr\'lanacak</string> + <string name="flattring_thing">%s Flattr\'lanıyor</string> + <string name="flattring_label">AntennaPod flattr\'lıyor</string> + <string name="flattrd_label">AntennaPod flattr\'ladı</string> + <string name="flattrd_failed_label">AntennaPod flattr\'layamadı</string> + <string name="flattr_retrieving_status">Flattr\'lanan öğeler alınıyor</string> + <!--Variable Speed--> + <string name="download_plugin_label">Eklentiyi İndir</string> + <string name="no_playback_plugin_title">Eklenti Yüklenmedi</string> + <string name="no_playback_plugin_msg">Değişken hızlı yürütmenin çalışabilmesi için üçüncü parti bir kütüphane yüklemelisiniz.\n\nEklentiyi Play Store\'dan indirmek için \'Eklentiyi İndir\' butonuna tıklayın\n\nBu eklentiden doğacak hatalar AntennaPod\'un sorumluluğunda değildir ve bu durum eklenti sahibine bildirilmelidir.</string> + <string name="set_playback_speed_label">Çalma hızları</string> + <!--Empty list labels--> + <string name="no_items_label">Bu listede hiç öge yok.</string> + <string name="no_feeds_label">Henüz hiçbir beslemeye abone değilsiniz.</string> + <!--Preferences--> + <string name="other_pref">Diğer</string> + <string name="about_pref">Hakkında</string> + <string name="queue_label">Kuyruk</string> + <string name="services_label">Servisler</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Kulaklıklar çıkarıldığında çalmayı duraklat</string> + <string name="pref_followQueue_sum">Çalma tamamlandığında kuyruktaki diğer öğeye geç</string> + <string name="playback_pref">Çalma</string> + <string name="network_pref">Ağ</string> + <string name="pref_autoUpdateIntervall_title">Güncelleme aralığı</string> + <string name="pref_autoUpdateIntervall_sum">Beslemeleri yenilemek için bir aralık belirtin veya devre dışı bırakın.</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Medya dosyalarını sadece kablosuz bağlantı üzerinden indir</string> + <string name="pref_followQueue_title">Devamlı çalma</string> + <string name="pref_downloadMediaOnWifiOnly_title">Kablosuz medya indirmesi</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Kulaklık bağlı değil</string> + <string name="pref_mobileUpdate_title">Mobil güncellemeler</string> + <string name="pref_mobileUpdate_sum">Mobil veri üzerinden güncellemelere izin ver</string> + <string name="refreshing_label">Yenileniyor</string> + <string name="flattr_settings_label">Flattr ayarları</string> + <string name="pref_flattr_auth_title">Flattr giriş</string> + <string name="pref_flattr_auth_sum">Diğer şeyleri uygulama içinden Flattr\'layabilmek için Flattr hesabınıza oturum açın.</string> + <string name="pref_flattr_this_app_title">Bu uygulamayı Flattr\'la</string> + <string name="pref_flattr_this_app_sum">AntennaPod\'u flattr\'layarak geliştirilmesine destek olun. Teşekkürler!</string> + <string name="pref_revokeAccess_title">Erişimi İptal et</string> + <string name="pref_revokeAccess_sum">Bu uygulamanın flattr hesabınıza erişim iznini iptal edin.</string> + <string name="pref_auto_flattr_title">Otomatik Flattr</string> + <string name="pref_auto_flattr_sum">Otomatik Flattr\'lamayı Yapılandır</string> + <string name="user_interface_label">Kullanıcı Arayüzü</string> + <string name="pref_set_theme_title">Temayı seç</string> + <string name="pref_set_theme_sum">AntennaPod\'un görüntüsünü değiştir.</string> + <string name="pref_automatic_download_title">Otomatk indirme</string> + <string name="pref_automatic_download_sum">Bölümlerin otomatik indirilmesini yapılandır.</string> + <string name="pref_autodl_wifi_filter_title">Wi-Fi filtresini etkinleştir</string> + <string name="pref_autodl_wifi_filter_sum">Seçilen kablosuz ağlar için otomatik indirmeye izin ver.</string> + <string name="pref_episode_cache_title">Bölüm ön belleği</string> + <string name="pref_theme_title_light">Aydınlık</string> + <string name="pref_theme_title_dark">Karanlık</string> + <string name="pref_episode_cache_unlimited">Sınırsız</string> + <string name="pref_update_interval_hours_plural">saat</string> + <string name="pref_update_interval_hours_singular">saat</string> + <string name="pref_update_interval_hours_manual">El ile</string> + <string name="pref_gpodnet_authenticate_title">Giriş</string> + <string name="pref_gpodnet_authenticate_sum">Üyeliklerinizi senkronize etmek için gpodder.net hesabınızla oturum açın.</string> + <string name="pref_gpodnet_logout_title">Çıkış</string> + <string name="pref_gpodnet_logout_toast">Çıkış başarılı</string> + <string name="pref_gpodnet_setlogin_information_title">Giriş bilgisini değiştir</string> + <string name="pref_gpodnet_setlogin_information_sum">gpodder.net hesabınız için giriş bilgisini değiştirin.</string> + <string name="pref_playback_speed_title">Çalma hızları</string> + <string name="pref_playback_speed_sum">Değişken hızlı ses yürütmesi için kullanılabilir hızları özelleştirin</string> + <string name="pref_seek_delta_title">Arama zamanı</string> + <string name="pref_seek_delta_sum">Geri veya ileri sararken bu kadar saniye atla</string> + <string name="pref_gpodnet_sethostname_title">Sunucu ismini ayarla</string> + <string name="pref_gpodnet_sethostname_use_default_host">Varsayılan sunucuyu kullan</string> + <string name="pref_expandNotify_title">Bildirimi Genişlet</string> + <string name="pref_expandNotify_sum">Çalma tuşlarını göstermek için bildirimi her zaman genişlet.</string> + <string name="pref_persistNotify_title">Kalıcı oynatma kontrolleri</string> + <string name="pref_persistNotify_sum">Çalma duraklatıldığında bildirim ve ekran kilidi ayarlarını sakla.</string> + <string name="pref_expand_notify_unsupport_toast">Android 4.1 öncesi sürümler genişletilmiş bildirimleri desteklememektedir.</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Otomatik Flattr\'lamayı etkinleştir</string> + <string name="auto_flattr_after_percent">Bölümün yüzde %d kısmı oynatıldığında Flattr\'la</string> + <string name="auto_flattr_ater_beginning">Oynatma başladığında bölümü Flattr\'la</string> + <string name="auto_flattr_ater_end">Oynatma bittiğinde bölümü Flattr\'la</string> + <!--Search--> + <string name="search_hint">Besleme veya Bölüm Ara</string> + <string name="found_in_shownotes_label">Nolarda bulundu</string> + <string name="found_in_chapters_label">Kısımlarda bulundu</string> + <string name="search_status_no_results">Sonuç bulunamadı</string> + <string name="search_label">Ara</string> + <string name="found_in_title_label">Başlıkta bulundu</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">OPML dosyaları cep yayınlarını bir cihazdan diğerine aktarmanıza yarar.</string> + <string name="opml_import_explanation">Bir OPML dosyasını içe aktarmak için onu aşağıdaki klasöre koyun ve aşağıdaki butona tıklayın.</string> + <string name="start_import_label">İçe aktarmayı başlat</string> + <string name="opml_import_label">OPML içe aktar</string> + <string name="opml_directory_error">HATA!</string> + <string name="reading_opml_label">OPML dosyası okunuyor</string> + <string name="opml_reader_error">OPML dosyası okunurken bir hata oluştu:</string> + <string name="opml_import_error_dir_empty">İça aktarma dizini boş</string> + <string name="select_all_label">Hepsini seç</string> + <string name="deselect_all_label">Tüm seçimleri geri al</string> + <string name="choose_file_to_import_label">İçe aktarılacak dosyayı seç</string> + <string name="opml_export_label">OPML dışa aktar</string> + <string name="exporting_label">Dışa aktarılıyor...</string> + <string name="export_error_label">Dışa aktarma hatası</string> + <string name="opml_export_success_title">Opml dışa aktarma başarılı</string> + <string name="opml_export_success_sum">.opml dosyasy yazıldı: \u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Zamanlayıcıyı ayarla</string> + <string name="disable_sleeptimer_label">Zamanlayıcıyı devre dışı bırak</string> + <string name="enter_time_here_label">Zamanı gir</string> + <string name="sleep_timer_label">Zamanlayıcı</string> + <string name="time_left_label">Kalan süre:\u0020</string> + <string name="time_dialog_invalid_input">Geçersiz giriş, zaman bir tam sayı olmalıdır</string> + <string name="time_unit_seconds">saniye</string> + <string name="time_unit_minutes">dakika</string> + <string name="time_unit_hours">saat</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">KATEGORİLER</string> + <string name="gpodnet_toplist_header">POPÜLER CEP YAYINLARI</string> + <string name="gpodnet_suggestions_header">ÖNERİLER</string> + <string name="gpodnet_search_hint">gpodder.net\'te ara</string> + <string name="gpodnetauth_login_title">Giriş</string> + <string name="gpodnetauth_login_descr">gpodder.net giriş işlemine hoşgeldiniz. Önce giriş bilgilerinizi yazın:</string> + <string name="gpodnetauth_login_butLabel">Giriş</string> + <string name="gpodnetauth_login_register">Eğer bir hesabınız yoksa, buradan bir tane oluşturabilirsiniz:\nhttps://gpodder.net/register/</string> + <string name="username_label">Kullanıcı adı</string> + <string name="password_label">Parola</string> + <string name="gpodnetauth_device_title">Cihaz Seçimi</string> + <string name="gpodnetauth_device_descr">gpodder.net hesabınızla kullanmak için yeni bir cihaz oluşturun veya var olan bir tanesini seçin:</string> + <string name="gpodnetauth_device_deviceID">Cihaz Kimliği:\u0020</string> + <string name="gpodnetauth_device_caption">Başlık</string> + <string name="gpodnetauth_device_butCreateNewDevice">Yeni cihaz oluştur</string> + <string name="gpodnetauth_device_chooseExistingDevice">Bir cihaz seç:</string> + <string name="gpodnetauth_device_errorEmpty">Cihaz ID\'si boş olamaz</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Cihaz ID\'si zaten var</string> + <string name="gpodnetauth_device_butChoose">Seç</string> + <string name="gpodnetauth_finish_title">Giriş başarılı</string> + <string name="gpodnetauth_finish_descr">Tebrikler! gpodder.net hesabınız cihazınızla ilişkilendirildi. AntennaPod bundan sonra gpodder.net hesabınızla üyeliklerinizi otomatik olarak senkronize edecek.</string> + <string name="gpodnetauth_finish_butsyncnow">Senkronizasyonu başlat</string> + <string name="gpodnetauth_finish_butgomainscreen">Ana ekrana git</string> + <string name="gpodnetsync_auth_error_title">gpodder.net yetkilendirme hatası</string> + <string name="gpodnetsync_auth_error_descr">Yanlış kullanıcı adı veya şifre</string> + <string name="gpodnetsync_error_title">gpodder.net senkronizasyon hatası</string> + <string name="gpodnetsync_error_descr">Senkronize edilirken bir hata oluştu:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Seçilen dizin:</string> + <string name="create_folder_label">Dizin yarat</string> + <string name="choose_data_directory">Veri dizinini seç</string> + <string name="create_folder_msg">\"%1$s\" isminde yeni dizin oluştur?</string> + <string name="create_folder_success">Yeni dizin yaratıldı</string> + <string name="create_folder_error_no_write_access">Bu dizine yazılamadı</string> + <string name="create_folder_error_already_exists">Dizin zaten var</string> + <string name="create_folder_error">Dizin yaratılamadı</string> + <string name="folder_not_empty_dialog_title">Dizin boş değil</string> + <string name="folder_not_empty_dialog_msg">Seçtiğiniz dizin dolu. Medya indirmeleri ve diğer dosyalar doğrudan bu dizine yerleştirilecek. Devam edilsin mi?</string> + <string name="set_to_default_folder">Vaysayılan dizini seç</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Başka bir uygulama ses çalmak istediğinde sesi kısmak yerine yürütmeyi duraklat</string> + <string name="pref_pausePlaybackForFocusLoss_title">Kesintiler için duraklat</string> + <!--Online feed view--> + <string name="subscribe_label">Üye ol</string> + <string name="subscribed_label">Üye olundu</string> + <string name="downloading_label">İndiriliyor...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Kısımları göster</string> + <string name="show_shownotes_label">Notları göster</string> + <string name="show_cover_label">Resmi göster</string> + <string name="rewind_label">Geri sar</string> + <string name="fast_forward_label">İleri sar</string> + <string name="media_type_audio_label">Ses</string> + <string name="media_type_video_label">Vidyo</string> + <string name="navigate_upwards_label">Yukarı çık</string> + <string name="butAction_label">Daha fazla eylem</string> + <string name="status_playing_label">Bölüm çalınıyor</string> + <string name="status_downloading_label">Bölüm indiriliyor</string> + <string name="status_downloaded_label">Bölüm indirildi</string> + <string name="status_unread_label">Bu öğe yeni</string> + <string name="in_queue_label">Bölüm kuyrukta</string> + <string name="new_episodes_count_label">Yeni bölümlerin sayısı</string> + <string name="in_progress_episodes_count_label">Dinlemeye başladığınız yeni bölümlerin sayısı</string> + <string name="drag_handle_content_description">Bu öğenin pozisyonunu değiştirmek için sürükleyin</string> + <string name="load_next_page_label">Sonraki sayfayı yükle</string> + <!--Feed information screen--> + <string name="authentication_label">Yetkilendirme</string> + <string name="authentication_descr">Bu cep yayını ve içerdiği bölümler için kullanıcı adı şifreyi değiştir.</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Üyelikler tek-amaçlı uygulamalardan içe aktarılıyor...</string> +</resources> diff --git a/core/src/main/res/values-uk-rUA/strings.xml b/core/src/main/res/values-uk-rUA/strings.xml new file mode 100644 index 000000000..966dac3f9 --- /dev/null +++ b/core/src/main/res/values-uk-rUA/strings.xml @@ -0,0 +1,349 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Канали</string> + <string name="add_feed_label">Додати подкаст</string> + <string name="podcasts_label">Подкасти</string> + <string name="episodes_label">Епізоди</string> + <string name="new_episodes_label">Нові епізоди</string> + <string name="all_episodes_label">Всі епізоди</string> + <string name="new_label">Нові</string> + <string name="waiting_list_label">Черга</string> + <string name="settings_label">Налаштування</string> + <string name="add_new_feed_label">Додати подкаст</string> + <string name="downloads_label">Завантаження</string> + <string name="downloads_running_label">В процесі</string> + <string name="downloads_completed_label">Завершено</string> + <string name="downloads_log_label">Журнал</string> + <string name="cancel_download_label">Скасувати завантаження</string> + <string name="playback_history_label">Що грало</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net логін</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">Щойно опубліковано</string> + <string name="episode_filter_label">Показати тількі нові епізоди</string> + <!--Main activity--> + <string name="drawer_open">Показати меню</string> + <string name="drawer_close">Сховати меню</string> + <!--Webview actions--> + <string name="open_in_browser_label">Відкрити в браузері</string> + <string name="copy_url_label">Копія URL</string> + <string name="share_url_label">Поділитися URL</string> + <string name="copied_url_msg">Копіювати URL в clipboard</string> + <string name="go_to_position_label">До цієї позиції</string> + <!--Playback history--> + <string name="clear_history_label">Забути</string> + <!--Other--> + <string name="confirm_label"> Підтвердити</string> + <string name="cancel_label">Скасувати</string> + <string name="author_label">Автор</string> + <string name="language_label">Мова</string> + <string name="podcast_settings_label">Налаштування</string> + <string name="cover_label">Зображення</string> + <string name="error_label">Помилка</string> + <string name="error_msg_prefix">Трапилась помілка:</string> + <string name="refresh_label">Оновити</string> + <string name="external_storage_error_msg">Немає доступної флешки. Зовнішній носій потрібен для коректної роботи додатку</string> + <string name="chapters_label">Глави</string> + <string name="shownotes_label">Нотатки до епізода</string> + <string name="description_label">Опис</string> + <string name="most_recent_prefix">Найновіший епізод:\u0020</string> + <string name="episodes_suffix">\u0020епізодів</string> + <string name="length_prefix">Довжина:\u0020</string> + <string name="size_prefix">Розмір:\u0020</string> + <string name="processing_label">Обробка</string> + <string name="loading_label">Завантаження категорій ...</string> + <string name="save_username_password_label">Зберегти ім\'я користувача та пароль</string> + <string name="close_label">Закрити</string> + <string name="retry_label">Повторити знову</string> + <string name="auto_download_label">Включити до автозавантаження</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">Посилання на канал</string> + <string name="etxtFeedurlHint">URL канала або сайта</string> + <string name="txtvfeedurl_label">Додати подкаст за URL</string> + <string name="podcastdirectories_label">Знайти подкаст в каталозі</string> + <string name="podcastdirectories_descr">В каталозі gpodder.net можливий пошук за назвою, категорією або популярністю.</string> + <string name="browse_gpoddernet_label">Переглянути gpodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">Все прочитано</string> + <string name="mark_all_read_msg">Позначити всі епізоди як переглянуті</string> + <string name="show_info_label">Інформація</string> + <string name="remove_feed_label">Видалити подкаст</string> + <string name="share_link_label">Поділитися URL сайту</string> + <string name="share_source_label">Поділитися URL каналу</string> + <string name="feed_delete_confirmation_msg">Ви впенені що хочете видаліти канал та всі завантажені епізоди</string> + <string name="feed_remover_msg">Удаляю канал</string> + <string name="load_complete_feed">Оновити канал цілком</string> + <!--actions on feeditems--> + <string name="download_label">Завантажити</string> + <string name="play_label">Грати</string> + <string name="pause_label">Пауза</string> + <string name="stop_label">Стоп</string> + <string name="stream_label">Прослухати без завантаження</string> + <string name="remove_label">Видалити</string> + <string name="remove_episode_lable">Видалити епізод</string> + <string name="mark_read_label">Прочитано</string> + <string name="mark_unread_label">Непрочитано</string> + <string name="add_to_queue_label">Додати до черги</string> + <string name="remove_from_queue_label">Видалити з черги</string> + <string name="visit_website_label">Відкрити сайт</string> + <string name="support_label">Підтримати за допомогою Flattr</string> + <string name="enqueue_all_new">Додати до черги</string> + <string name="download_all">Завантажити все</string> + <string name="skip_episode_label">Пропустити епізод</string> + <!--Download messages and labels--> + <string name="download_successful">успішно</string> + <string name="download_failed">з помилками</string> + <string name="download_pending">Потрібно завантажити</string> + <string name="download_running">Завантаження</string> + <string name="download_error_device_not_found">Немає куди зберігати</string> + <string name="download_error_insufficient_space">Мало місця</string> + <string name="download_error_file_error">Помилка файлу</string> + <string name="download_error_http_data_error">Помилка HTTP</string> + <string name="download_error_error_unknown">Щось трапилось</string> + <string name="download_error_parser_exception">Помилка парсера</string> + <string name="download_error_unsupported_type">Непідтримую такий канал</string> + <string name="download_error_connection_error">Помилка з\'єднання</string> + <string name="download_error_unknown_host">Невідомий host</string> + <string name="download_error_unauthorized">Помилка автентифікації</string> + <string name="cancel_all_downloads_label">Скасувати всі завантаження</string> + <string name="download_cancelled_msg">Відмінено завантаження</string> + <string name="download_report_title">Завантажили</string> + <string name="download_error_malformed_url">Невірний URL</string> + <string name="download_error_io_error">Помилка IO</string> + <string name="download_error_request_error">Помилка запиту</string> + <string name="download_error_db_access">Помилка бази даних</string> + <string name="downloads_left">\0020 залишилось завантажити</string> + <string name="downloads_processing">Обробка завантаженого</string> + <string name="download_notification_title">Завантаження даних подкасту</string> + <string name="download_report_content">Завантажилось %1$d успішно, %2$d з помилками</string> + <string name="download_log_title_unknown">Невідома назва</string> + <string name="download_type_feed">Канал</string> + <string name="download_type_media">Файл з медіа</string> + <string name="download_type_image">Зображення</string> + <string name="download_request_error_dialog_message_prefix">Помилка при завантажені файлу:\u0020</string> + <string name="authentication_notification_title">Потрібна автентифікація</string> + <string name="authentication_notification_msg">Для доступа до цього ресурса потрібні ім\'я та пароль </string> + <!--Mediaplayer messages--> + <string name="player_error_msg">Помилка!</string> + <string name="player_stopped_msg"> Нічого грати</string> + <string name="player_preparing_msg">Підготовка</string> + <string name="player_ready_msg">Готов</string> + <string name="player_seeking_msg">Шукаю</string> + <string name="playback_error_server_died">Сервер помер</string> + <string name="playback_error_unknown">Невідома помилка</string> + <string name="no_media_playing_label">Німає що грати</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Буферізую</string> + <string name="playbackservice_notification_title">Грає подкаст</string> + <string name="unknown_media_key">AntennaPod - Невідомий медіа ключ: %1$d</string> + <!--Queue operations--> + <string name="clear_queue_label">Очистити чергу</string> + <string name="undo">Скасувати</string> + <string name="removed_from_queue">Видалено</string> + <string name="move_to_top_label">Догори</string> + <string name="move_to_bottom_label">Донизу</string> + <!--Flattr--> + <string name="flattr_auth_label">Увійти до Flattr</string> + <string name="flattr_auth_explanation">Нажміть цю кнопку для початку авторізації. Буде відкрито flattr в браузері, буде запит на дозвіл доступу Antennapod до flattr. Після надання доступу ви повернетесь до цього екрану автоматично</string> + <string name="authenticate_label">Ввісти ім\'я та пароль</string> + <string name="return_home_label">Повернення до початку</string> + <string name="flattr_auth_success">Вийшло авторізуватись. Тепер ви можете flattr things за допомогою додатку</string> + <string name="no_flattr_token_title">Немає flattr token</string> + <string name="no_flattr_token_notification_msg">Ваш обліковий запис flattr, здається, не підключений до AntennaPod. Натисніть тут для підключення.</string> + <string name="no_flattr_token_msg">Здається ваш обліковий запис flattr не під\'єднано до AntennaPod. Ви можете або під\'єднати її або відкривати web сторінку в браузері</string> + <string name="authenticate_now_label">Пароль та логін</string> + <string name="action_forbidden_title">Заборонено</string> + <string name="action_forbidden_msg">AntennaPod не маэ дозвілу це зробити. Можливо відкликаний доступ до AntennaPod. Або ввідіть логін пароль в налаштуваннях або зробить це на сайті</string> + <string name="access_revoked_title">Доступ відкликано</string> + <string name="access_revoked_info">Ви відкликали доступ AntennaPod до облікового запису. Для закінчення процессу вам потрібно видалити додаток з затвержденного списку в вашому облікову запису на сайті flattr</string> + <!--Flattr--> + <string name="flattr_click_success">Flattr\'ed one thing!</string> + <string name="flattr_click_success_count">Flattr\'ed %d things!</string> + <string name="flattr_click_success_queue">Flattr\'ed: %s.</string> + <string name="flattr_click_failure_count">Failed to flattr %d things!</string> + <string name="flattr_click_failure">Not flattr\'ed: %s.</string> + <string name="flattr_click_enqueued">Thing will be flattr\'ed later</string> + <string name="flattring_thing">Flattring %s</string> + <string name="flattring_label">AntennaPod is flattring</string> + <string name="flattrd_label">AntennaPod has flattr\'ed</string> + <string name="flattrd_failed_label">AntennaPod flattr failed</string> + <string name="flattr_retrieving_status">Retrieving flattr\'ed things</string> + <!--Variable Speed--> + <string name="download_plugin_label">Завантажити Plugin</string> + <string name="no_playback_plugin_title">Plugin не встановлено</string> + <string name="no_playback_plugin_msg">Для керування швидкістю програвання потрібно встановити додаток.\n\nНатисніть \"Завантажити додаток\" для завантаження безкоштовного додатку з Play Store\n\nЯкщо при використанні plugin виникнуть будь які проблеми - це відповідальність автора додатка, а не автора AntennaPod.</string> + <string name="set_playback_speed_label">Швидкість програвання</string> + <!--Empty list labels--> + <string name="no_items_label">Нічного в цьому списку</string> + <string name="no_feeds_label">Немає підписаних каналів </string> + <!--Preferences--> + <string name="other_pref">Інше</string> + <string name="about_pref">О</string> + <string name="queue_label">Черга</string> + <string name="services_label">Сервіси</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Зупинятись коли навушники витягнуті</string> + <string name="pref_followQueue_sum">До наступної черги коли дограє до кінця</string> + <string name="playback_pref">Грає</string> + <string name="network_pref">Мережа</string> + <string name="pref_autoUpdateIntervall_title">Коли оновлювати</string> + <string name="pref_autoUpdateIntervall_sum">Визначати як час для автооновлювання або відключити автооновлення</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Завантажувати тільки через Wifi</string> + <string name="pref_followQueue_title">Грати безперервно</string> + <string name="pref_downloadMediaOnWifiOnly_title">Завантаження через Wifi</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Навушники витягнуті</string> + <string name="pref_mobileUpdate_title">Мобільне оновлення</string> + <string name="pref_mobileUpdate_sum">Дозволити оновлення через оператора зв\'язку</string> + <string name="refreshing_label">Оновлення</string> + <string name="flattr_settings_label">Налаштування Flattr</string> + <string name="pref_flattr_auth_title">Увійти до Flattr</string> + <string name="pref_flattr_auth_sum">Увійти в облікову flattr в flattr things напряму з додатку</string> + <string name="pref_flattr_this_app_title">Flattr цій додаток</string> + <string name="pref_flattr_this_app_sum">Підтримайте розробку AntennaPod за допомогою flattr. Дякую!</string> + <string name="pref_revokeAccess_title">Відкликати доступ</string> + <string name="pref_revokeAccess_sum">Відкликати дозвіл на доступ до вашого flattr з цього додатку</string> + <string name="pref_auto_flattr_title">Automatic Flattr</string> + <string name="pref_auto_flattr_sum">Налаштування автоматичного заохочення авторів через сервіс flattr</string> + <string name="user_interface_label">Зовнішній вид</string> + <string name="pref_set_theme_title">Обрати тему</string> + <string name="pref_set_theme_sum">Змінити появу AntennaPod</string> + <string name="pref_automatic_download_title">Автоматичне завантаження</string> + <string name="pref_automatic_download_sum">Налаштування автоматичного завантаження епізодів</string> + <string name="pref_autodl_wifi_filter_title">Увімкнути фільтр Wi-Fi</string> + <string name="pref_autodl_wifi_filter_sum">Дозволити автоматичне завантаження тільки в цих Wi-Fi мережах</string> + <string name="pref_episode_cache_title">Кеш епізодів</string> + <string name="pref_theme_title_light">Світла</string> + <string name="pref_theme_title_dark">Темна</string> + <string name="pref_episode_cache_unlimited">Без обмежень</string> + <string name="pref_update_interval_hours_plural">годин</string> + <string name="pref_update_interval_hours_singular">година</string> + <string name="pref_update_interval_hours_manual">Інструкція</string> + <string name="pref_gpodnet_authenticate_title">Логін</string> + <string name="pref_gpodnet_authenticate_sum">Увійти до свого облікового запису gpodder.net для сінхронізації ваших каналів</string> + <string name="pref_gpodnet_logout_title">Виход</string> + <string name="pref_gpodnet_logout_toast">Успішно закрили доступ</string> + <string name="pref_gpodnet_setlogin_information_title">Змінити інформацію для входу</string> + <string name="pref_gpodnet_setlogin_information_sum">Змінити вашу інформацію для вашего gpodder.net облікового запису</string> + <string name="pref_playback_speed_title">Швидкість програвання</string> + <string name="pref_playback_speed_sum">Налаштування швідкості доступно для змінної швидкості програвання</string> + <string name="pref_seek_delta_title">Час перемотки</string> + <string name="pref_seek_delta_sum">Перейти на таку кількість секунд при перемотуванні назад або вперед</string> + <string name="pref_gpodnet_sethostname_title">Встановити ім\'я хоста</string> + <string name="pref_gpodnet_sethostname_use_default_host">Використати хост по замовчанню</string> + <string name="pref_expandNotify_title">Розгорнути повідомлення</string> + <string name="pref_expandNotify_sum">Завжди розгортати повідомлення, щоб показати кнопки керування.</string> + <string name="pref_persistNotify_title">Завжди показувати елементи керування відтворенням</string> + <string name="pref_persistNotify_sum">Показувати повідомлення та елементи керування на lockscreen в режимі паузи.</string> + <string name="pref_expand_notify_unsupport_toast">Android до версії 4.1 не підтримує розширені повідомлення.</string> + <!--Auto-Flattr dialog--> + <string name="auto_flattr_enable">Включити автоматичне заохочення авторів через сервіс flattr</string> + <string name="auto_flattr_after_percent">Заохотити автора через Flattr щойно %d відсотків епізода було відтворено</string> + <string name="auto_flattr_ater_beginning">Заохотити автора через Flattr коли починається відтворення</string> + <string name="auto_flattr_ater_end">Заохотити автора через Flattr коли закінчується відтворення</string> + <!--Search--> + <string name="search_hint">Пошук каналів та епізодів</string> + <string name="found_in_shownotes_label">Знайдено у примітках</string> + <string name="found_in_chapters_label">Знайдено в главах</string> + <string name="search_status_no_results">Жодних результатів немає</string> + <string name="search_label">Пошук</string> + <string name="found_in_title_label">Знайдено у назві</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">OPML файли дозволяют вам перенести подкасти з однієї программи до іншої</string> + <string name="opml_import_explanation">Для імпорту OPML файлу, скопіюйте його в цю папку та натіснить кнопку внизу для початку імпорту</string> + <string name="start_import_label">Почати імпорт</string> + <string name="opml_import_label">OPML імпорт</string> + <string name="opml_directory_error">Помилка!</string> + <string name="reading_opml_label">Читаємо OPML файл</string> + <string name="opml_reader_error">Трапилась помілка коли читали OPML документ:</string> + <string name="opml_import_error_dir_empty">Директорія імпорту пуста</string> + <string name="select_all_label">Обрати все</string> + <string name="deselect_all_label">Убрати виділення</string> + <string name="choose_file_to_import_label">Обрати файл для імпорту</string> + <string name="opml_export_label">OPML экспорт</string> + <string name="exporting_label">Експорт ...</string> + <string name="export_error_label">Помилка експорту</string> + <string name="opml_export_success_title">OPML експорт успішний</string> + <string name="opml_export_success_sum">OPML файл записаний в:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">Таймер сну</string> + <string name="disable_sleeptimer_label">Вимкнути засинання</string> + <string name="enter_time_here_label">Встановити час</string> + <string name="sleep_timer_label">Таймер сну</string> + <string name="time_left_label">Залишилось:\u0020</string> + <string name="time_dialog_invalid_input">Помилка вводу, час повинен бути цілим</string> + <string name="time_unit_seconds">секунд</string> + <string name="time_unit_minutes">хвилин</string> + <string name="time_unit_hours">годин</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">КАТЕГОРІЇ</string> + <string name="gpodnet_toplist_header">ТОП ПОДКАСТІВ</string> + <string name="gpodnet_suggestions_header">РЕКОМЕНДАЦІЇ</string> + <string name="gpodnet_search_hint">Пошук на gpodder.net</string> + <string name="gpodnetauth_login_title">Логін</string> + <string name="gpodnetauth_login_descr">Ласкаво просимо до gpodder.net. Зпочатку заповнить вашу інформацію для входу</string> + <string name="gpodnetauth_login_butLabel">Логін</string> + <string name="gpodnetauth_login_register">Якщо у вас немає облікового запису, ви можете створити його тут:\nhttps://gpodder.net/register/</string> + <string name="username_label">Ім\'я користувача</string> + <string name="password_label">Пароль</string> + <string name="gpodnetauth_device_title">Обрати пристрій</string> + <string name="gpodnetauth_device_descr">Під\'єднати новий пристрій к gpodder.net обліковому запису о обрати інсуючий</string> + <string name="gpodnetauth_device_deviceID">ID Пристрою:\u0020</string> + <string name="gpodnetauth_device_caption">Заголовок</string> + <string name="gpodnetauth_device_butCreateNewDevice">Створити новий пристрій</string> + <string name="gpodnetauth_device_chooseExistingDevice">Вибрати існуючий пристрій</string> + <string name="gpodnetauth_device_errorEmpty">ID пристрою не можете бути пустим</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Таке ID пристрою вже є</string> + <string name="gpodnetauth_device_butChoose">Обрати</string> + <string name="gpodnetauth_finish_title">Успішно зайшли</string> + <string name="gpodnetauth_finish_descr">Поздоровляємо! Ваш обліковий запис на gpodder.net зараз пов\'язаний за вашим пристроєм</string> + <string name="gpodnetauth_finish_butsyncnow">Почати синхронізацію</string> + <string name="gpodnetauth_finish_butgomainscreen">Перейти до основного екрана</string> + <string name="gpodnetsync_auth_error_title">Помилка авторізації на gpodder.net</string> + <string name="gpodnetsync_auth_error_descr">Помилка в імені користувача або паролі</string> + <string name="gpodnetsync_error_title">gpodder.net помилка синхронізації</string> + <string name="gpodnetsync_error_descr">Трапилась помилка при сінхронизації:\u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">Обрати папку:</string> + <string name="create_folder_label">Нова папка</string> + <string name="choose_data_directory">Обрати папку</string> + <string name="create_folder_msg">Створити папку з ім\'ям \"%1$s\"?</string> + <string name="create_folder_success">Створена нова папка</string> + <string name="create_folder_error_no_write_access">Не можу записати в цю папку</string> + <string name="create_folder_error_already_exists">Папка вже є</string> + <string name="create_folder_error">Не можу создати папку</string> + <string name="folder_not_empty_dialog_title">В папці щось є</string> + <string name="folder_not_empty_dialog_msg">В папці щось є. Всі завантаження зберігаються в цю папку. Все рівно продовжувати?</string> + <string name="set_to_default_folder">Обрати папку по замовчанню</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Призупиняти програвання замість зниження гучності коли інша програма хоче програти звук</string> + <string name="pref_pausePlaybackForFocusLoss_title">Пауза для перевивання</string> + <!--Online feed view--> + <string name="subscribe_label">Підписатися</string> + <string name="subscribed_label">Підписано</string> + <string name="downloading_label">Завантаження...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">Показати глави</string> + <string name="show_shownotes_label">Показати нотатки</string> + <string name="show_cover_label">Показати зображення</string> + <string name="rewind_label">Перемотка назад</string> + <string name="fast_forward_label">Перемотка вперед</string> + <string name="media_type_audio_label">Звук</string> + <string name="media_type_video_label">Відео</string> + <string name="navigate_upwards_label">Догори</string> + <string name="butAction_label">Додаткові дії</string> + <string name="status_playing_label">Епізод програється</string> + <string name="status_downloading_label">Епізод завантажується</string> + <string name="status_downloaded_label">Епізод завантажено</string> + <string name="status_unread_label">Нове</string> + <string name="in_queue_label">Епізод чекає в черзі</string> + <string name="new_episodes_count_label">Кількість нових епізодів</string> + <string name="in_progress_episodes_count_label">Кількість епізодів що ви почали слухати</string> + <string name="drag_handle_content_description">Перетягніть щоб змінити позицію</string> + <string name="load_next_page_label">Завантажити наступну сторінку</string> + <!--Feed information screen--> + <string name="authentication_label">Автентикація</string> + <string name="authentication_descr">Змінити ваші логін та пароль для подкаста та епізодів</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">Імпорт подкастів з інших програм...</string> +</resources> diff --git a/core/src/main/res/values-v11/colors.xml b/core/src/main/res/values-v11/colors.xml new file mode 100644 index 000000000..520efaa06 --- /dev/null +++ b/core/src/main/res/values-v11/colors.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="selection_background_color_dark">#286E8A</color> + <color name="selection_background_color_light">#81CFEA</color> +</resources>
\ No newline at end of file diff --git a/core/src/main/res/values-v14/dimens.xml b/core/src/main/res/values-v14/dimens.xml new file mode 100644 index 000000000..090a476a8 --- /dev/null +++ b/core/src/main/res/values-v14/dimens.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <dimen name="widget_margin">0dp</dimen> + +</resources>
\ No newline at end of file diff --git a/core/src/main/res/values-v14/styles.xml b/core/src/main/res/values-v14/styles.xml new file mode 100644 index 000000000..6a39d6175 --- /dev/null +++ b/core/src/main/res/values-v14/styles.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <style name="AntennaPod.TextView.UnreadIndicator" parent="@android:style/TextAppearance.Small"> + <item name="android:textSize">@dimen/text_size_micro</item> + <item name="android:textColor">@color/new_indicator_green</item> + <item name="android:text">@string/new_label</item> + <item name="android:textAllCaps">true</item> + </style> +</resources>
\ No newline at end of file diff --git a/core/src/main/res/values-v16/styles.xml b/core/src/main/res/values-v16/styles.xml new file mode 100644 index 000000000..e7c56b5f5 --- /dev/null +++ b/core/src/main/res/values-v16/styles.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <style name="AntennaPod.TextView.Heading" parent="@android:style/TextAppearance.Medium"> + <item name="android:textSize">@dimen/text_size_large</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:fontFamily">sans-serif-light</item> + </style> + + <style name="AntennaPod.Dialog.Title" parent="@android:style/TextAppearance.Medium"> + <item name="android:textSize">@dimen/text_size_medium</item> + <item name="android:textColor">@color/bright_blue</item> + <item name="android:maxLines">2</item> + <item name="android:ellipsize">end</item> + <item name="android:fontFamily">sans-serif-light</item> + </style> +</resources>
\ No newline at end of file diff --git a/core/src/main/res/values-v19/colors.xml b/core/src/main/res/values-v19/colors.xml new file mode 100644 index 000000000..16c065d75 --- /dev/null +++ b/core/src/main/res/values-v19/colors.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="selection_background_color_dark">#484B4D</color> + <color name="selection_background_color_light">#E3E3E3</color> +</resources>
\ No newline at end of file diff --git a/core/src/main/res/values-zh-rCN/strings.xml b/core/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 000000000..59958ec35 --- /dev/null +++ b/core/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,316 @@ +<?xml version='1.0' encoding='UTF-8'?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <!--Activitiy and fragment titles--> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">订阅</string> + <string name="add_feed_label">添加博客</string> + <string name="podcasts_label">播客</string> + <string name="episodes_label">曲目</string> + <string name="new_episodes_label">新曲目</string> + <string name="all_episodes_label">所有曲目</string> + <string name="new_label">最新</string> + <string name="waiting_list_label">等待列表</string> + <string name="settings_label">设置</string> + <string name="add_new_feed_label">添加播客</string> + <string name="downloads_label">下载</string> + <string name="downloads_running_label">正在运行</string> + <string name="downloads_completed_label">已完成</string> + <string name="downloads_log_label">日志</string> + <string name="cancel_download_label">取消下载</string> + <string name="playback_history_label">播放历史</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net 登录</string> + <!--New episodes fragment--> + <string name="recently_published_episodes_label">最近发布</string> + <string name="episode_filter_label">仅显示新曲目</string> + <!--Main activity--> + <string name="drawer_open">打开菜单</string> + <string name="drawer_close">关闭菜单</string> + <!--Webview actions--> + <string name="open_in_browser_label">在浏览器打开</string> + <string name="copy_url_label">复制 URL</string> + <string name="share_url_label">分享 URL</string> + <string name="copied_url_msg">复制 URL 到剪贴板.</string> + <!--Playback history--> + <string name="clear_history_label">清空历史</string> + <!--Other--> + <string name="confirm_label">确定</string> + <string name="cancel_label">取消</string> + <string name="author_label">作者</string> + <string name="language_label">语言</string> + <string name="podcast_settings_label">设置</string> + <string name="cover_label">图片</string> + <string name="error_label">错误</string> + <string name="error_msg_prefix">出错:</string> + <string name="refresh_label">刷新</string> + <string name="external_storage_error_msg">没有可用的外部存储. 请确保安装外部存储器, 这样本应用才可以正常工作.</string> + <string name="chapters_label">章节</string> + <string name="shownotes_label">笔记</string> + <string name="description_label">描述</string> + <string name="most_recent_prefix">最近曲目:\u0020</string> + <string name="episodes_suffix">\u0020 曲</string> + <string name="length_prefix">长度:\u0020</string> + <string name="size_prefix">大小:\u0020</string> + <string name="processing_label">处理中</string> + <string name="loading_label">加载中...</string> + <string name="save_username_password_label">保存用户名密码</string> + <string name="close_label">关闭</string> + <string name="retry_label">重试</string> + <string name="auto_download_label">包含到自动下载</string> + <!--'Add Feed' Activity labels--> + <string name="feedurl_label">订阅 URL</string> + <string name="txtvfeedurl_label">添加播客 URL</string> + <string name="podcastdirectories_descr">您可以在 gpodder.net 通过名称、类别或热门来搜索新播客</string> + <string name="browse_gpoddernet_label">浏览 gpodder.net</string> + <!--Actions on feeds--> + <string name="mark_all_read_label">全部标识已读</string> + <string name="mark_all_read_msg">将所有曲目标记为已读</string> + <string name="show_info_label">查看信息</string> + <string name="remove_feed_label">删除播客</string> + <string name="share_link_label">分享网站链接</string> + <string name="share_source_label">分享订阅链接</string> + <string name="feed_delete_confirmation_msg">确认要删除这些订阅吗? 该订阅所有已经下载的曲目将一并删除. </string> + <string name="feed_remover_msg">删除订阅</string> + <!--actions on feeditems--> + <string name="download_label">下载</string> + <string name="play_label">播放</string> + <string name="pause_label">暂停</string> + <string name="stream_label">流媒体</string> + <string name="remove_label">删除</string> + <string name="remove_episode_lable">移除曲目</string> + <string name="mark_read_label">标记已读</string> + <string name="mark_unread_label">标记未读</string> + <string name="add_to_queue_label">添加到播放列表</string> + <string name="remove_from_queue_label">从播放列表中删除</string> + <string name="visit_website_label">访问网站</string> + <string name="support_label">Flattr 他</string> + <string name="enqueue_all_new">全部添加到播放列表</string> + <string name="download_all">全部下载</string> + <string name="skip_episode_label">跳过曲目</string> + <!--Download messages and labels--> + <string name="download_successful">成功</string> + <string name="download_failed">失败</string> + <string name="download_pending">下载等待</string> + <string name="download_running">下载中</string> + <string name="download_error_device_not_found">没有找到存储设备</string> + <string name="download_error_insufficient_space">空间不足</string> + <string name="download_error_file_error">文件错误</string> + <string name="download_error_http_data_error">HTTP 数据错误</string> + <string name="download_error_error_unknown">未知错误</string> + <string name="download_error_parser_exception">解析异常</string> + <string name="download_error_unsupported_type">未提供的订阅类型</string> + <string name="download_error_connection_error">链接错误</string> + <string name="download_error_unknown_host">未知主机</string> + <string name="download_error_unauthorized">认证错误</string> + <string name="cancel_all_downloads_label">取消所有下载</string> + <string name="download_cancelled_msg">已取消下载</string> + <string name="download_report_title">下载完成</string> + <string name="download_error_malformed_url">畸形 URL</string> + <string name="download_error_io_error">IO 错误</string> + <string name="download_error_request_error">请求出错</string> + <string name="download_error_db_access">数据库访问错误</string> + <string name="downloads_left">\u0020 下载剩余</string> + <string name="downloads_processing">正在处理下载</string> + <string name="download_notification_title">下载播客数据</string> + <string name="download_report_content">%1$d 下载成功, %2$d 失败</string> + <string name="download_log_title_unknown">未知标题</string> + <string name="download_type_feed">订阅</string> + <string name="download_type_media">媒体文件</string> + <string name="download_type_image">图片</string> + <string name="download_request_error_dialog_message_prefix">尝试下载文件:\u0020 时出错</string> + <string name="authentication_notification_title">需要认证</string> + <string name="authentication_notification_msg">您所请求的资源需要用户名和密码</string> + <!--Mediaplayer messages--> + <string name="player_error_msg">错误!</string> + <string name="player_stopped_msg">没有可播放媒体</string> + <string name="player_preparing_msg">预备</string> + <string name="player_ready_msg">准备</string> + <string name="player_seeking_msg">查找</string> + <string name="playback_error_server_died">服务器宕机</string> + <string name="playback_error_unknown">未知错误</string> + <string name="no_media_playing_label">没有可播放的媒体</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">缓冲中</string> + <string name="playbackservice_notification_title">播客播放中</string> + <!--Queue operations--> + <string name="clear_queue_label">清空播放列表</string> + <string name="undo">撤消</string> + <string name="removed_from_queue">已删除项</string> + <string name="move_to_top_label">移到顶端</string> + <string name="move_to_bottom_label">移到下部</string> + <!--Flattr--> + <string name="flattr_auth_label">Flattr 登录</string> + <string name="flattr_auth_explanation">按下面的按钮开始身份验证过程. 将在浏览器中打开 Flattr 登录界面并要求给予 AntennaPod 访问 Flattr 的权限. 权限许可后, 将自动回到这个界面.</string> + <string name="authenticate_label">验证</string> + <string name="return_home_label">返回主页</string> + <string name="flattr_auth_success">验证成功! 现在可以使用应用内 Flattr 相关功能了.</string> + <string name="no_flattr_token_title">没有找到 Flattr 验证令牌信息</string> + <string name="no_flattr_token_msg">您的 flattr 账户似乎并没有连接到 AntennaPod. You can either connect your account to AntennaPod to flattr things within the app or you can visit the website of the thing to flattr it there.</string> + <string name="authenticate_now_label">验证</string> + <string name="action_forbidden_title">被禁止</string> + <string name="action_forbidden_msg">AntennaPod 没有权限执行本动作. 原因可能是: AntennaPod 对您账户的访问令牌被撤销. 你可以重新\"验证\"或访问该网站来授权.</string> + <string name="access_revoked_title">撤销访问</string> + <string name="access_revoked_info">您已经成功撤销 AntennaPod 对账户令牌的访问. 为了完成这个过程, 您必须到 Flattr 网站 \"账户设置->已批准应用\" 列表内删除本应用.</string> + <!--Flattr--> + <!--Variable Speed--> + <string name="download_plugin_label">插件下载</string> + <string name="no_playback_plugin_title">插件没有安装</string> + <string name="set_playback_speed_label">播放速度</string> + <!--Empty list labels--> + <string name="no_items_label">列表为空.</string> + <string name="no_feeds_label">还没有任何订阅.</string> + <!--Preferences--> + <string name="other_pref">其他</string> + <string name="about_pref">关于</string> + <string name="queue_label">播放列表</string> + <string name="services_label">服务</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">耳机断开时暂停播放 </string> + <string name="pref_followQueue_sum">播放完成跳转到播放列表下一项</string> + <string name="playback_pref">播放</string> + <string name="network_pref">网络</string> + <string name="pref_autoUpdateIntervall_title">更新周期</string> + <string name="pref_autoUpdateIntervall_sum">设置订阅自动刷新周期</string> + <string name="pref_downloadMediaOnWifiOnly_sum">仅在 WIFI 情况下载媒体文件</string> + <string name="pref_followQueue_title">连续播放</string> + <string name="pref_downloadMediaOnWifiOnly_title">仅在 WIFI 情况下载</string> + <string name="pref_pauseOnHeadsetDisconnect_title">耳机断开</string> + <string name="pref_mobileUpdate_title">数据网络时更新</string> + <string name="pref_mobileUpdate_sum">允许移动数据网络情况下进行数据链接</string> + <string name="refreshing_label">刷新中</string> + <string name="flattr_settings_label">Flattr 设置</string> + <string name="pref_flattr_auth_title">Flattr 登录</string> + <string name="pref_flattr_auth_sum">登录 Flattr 账户, 以便直接使用本应用中的相关功能.</string> + <string name="pref_flattr_this_app_title">Flattr 本应用</string> + <string name="pref_flattr_this_app_sum">支持 AntennaPod 发展, 请 Flattring 他. 谢谢!! +</string> + <string name="pref_revokeAccess_title">撤销访问</string> + <string name="pref_revokeAccess_sum">撤销访问本应用对您 Flattr 账户的访问权限.</string> + <string name="user_interface_label">界面</string> + <string name="pref_set_theme_title">主题选择</string> + <string name="pref_set_theme_sum">改变 AntennaPod 外观</string> + <string name="pref_automatic_download_title">自动下载</string> + <string name="pref_automatic_download_sum">配置自动下载的曲目</string> + <string name="pref_autodl_wifi_filter_title">打开 Wi-Fi 过滤器</string> + <string name="pref_autodl_wifi_filter_sum">只允许在 Wi-Fi 网络下自动下载</string> + <string name="pref_episode_cache_title">曲目缓存</string> + <string name="pref_theme_title_light">浅色</string> + <string name="pref_theme_title_dark">暗色</string> + <string name="pref_episode_cache_unlimited">无限</string> + <string name="pref_update_interval_hours_plural">小时</string> + <string name="pref_update_interval_hours_singular">时</string> + <string name="pref_update_interval_hours_manual">手动</string> + <string name="pref_gpodnet_authenticate_title">登录</string> + <string name="pref_gpodnet_authenticate_sum">登录 gpodder.net 账户同步订阅</string> + <string name="pref_gpodnet_logout_title">注销</string> + <string name="pref_gpodnet_logout_toast">注销成功</string> + <string name="pref_gpodnet_setlogin_information_title">改变登录信息</string> + <string name="pref_gpodnet_setlogin_information_sum">改变 gpodder.net 账户登录信息.</string> + <string name="pref_playback_speed_title">播放速度</string> + <string name="pref_playback_speed_sum">自定义音频播放速度</string> + <string name="pref_gpodnet_sethostname_title">设置主机名</string> + <string name="pref_gpodnet_sethostname_use_default_host">使用默认主机</string> + <!--Auto-Flattr dialog--> + <!--Search--> + <string name="search_hint">搜索订阅或者曲目</string> + <string name="found_in_shownotes_label">笔记中查找</string> + <string name="found_in_chapters_label">章节中查找</string> + <string name="search_status_no_results">没有找到任何结果</string> + <string name="search_label">搜索</string> + <string name="found_in_title_label">标题中查找</string> + <!--OPML import and export--> + <string name="opml_import_txtv_button_lable">OPML 文件可以方便的从别的播客转移数据过来。</string> + <string name="opml_import_explanation">导入 OPML 文件, 您必须将它放在以下目录, 之后按下面的按钮开始导入处理. </string> + <string name="start_import_label">开始导入</string> + <string name="opml_import_label">OPML 导入</string> + <string name="opml_directory_error">错误!</string> + <string name="reading_opml_label">OPML 文件读取中</string> + <string name="opml_reader_error">读取 OPML 文件内容出错:</string> + <string name="opml_import_error_dir_empty">导入目录为空.</string> + <string name="select_all_label">全选</string> + <string name="deselect_all_label">取消所有选择</string> + <string name="choose_file_to_import_label">选择导入文件</string> + <string name="opml_export_label">OPML 导出</string> + <string name="exporting_label">导出中...</string> + <string name="export_error_label">导出出错</string> + <string name="opml_export_success_title">OPML 导出成功.</string> + <string name="opml_export_success_sum">.opml 文件已保存到:\u0020</string> + <!--Sleep timer--> + <string name="set_sleeptimer_label">设置休眠计时器</string> + <string name="disable_sleeptimer_label">禁用休眠计时器</string> + <string name="enter_time_here_label">输入时间</string> + <string name="sleep_timer_label">休眠计时器</string> + <string name="time_left_label">计时剩余:\u0020</string> + <string name="time_dialog_invalid_input">无效的输入, 时间是一个整数</string> + <string name="time_unit_seconds">秒</string> + <string name="time_unit_minutes">分钟</string> + <string name="time_unit_hours">小时</string> + <!--gpodder.net--> + <string name="gpodnet_taglist_header">目录</string> + <string name="gpodnet_toplist_header">头条播客</string> + <string name="gpodnet_suggestions_header">建议</string> + <string name="gpodnet_search_hint">搜索 gpodder.net</string> + <string name="gpodnetauth_login_title">登录</string> + <string name="gpodnetauth_login_descr">欢迎进入 gpodder.net 登录流程. 首先, 输入请你的登录信息:</string> + <string name="gpodnetauth_login_butLabel">登录</string> + <string name="username_label">用户名</string> + <string name="password_label">密码</string> + <string name="gpodnetauth_device_title">设备选择</string> + <string name="gpodnetauth_device_descr">为你的 gpodder.net 账户创建一个新设备或者选择一个已存在的:</string> + <string name="gpodnetauth_device_deviceID">设备编号: \u0020</string> + <string name="gpodnetauth_device_caption">标题</string> + <string name="gpodnetauth_device_butCreateNewDevice">创建新设备</string> + <string name="gpodnetauth_device_chooseExistingDevice">选择已存在设备</string> + <string name="gpodnetauth_device_errorEmpty">设备编号必须填写</string> + <string name="gpodnetauth_device_errorAlreadyUsed">设备编号已被使用</string> + <string name="gpodnetauth_device_butChoose">选择</string> + <string name="gpodnetauth_finish_title">登录成功!</string> + <string name="gpodnetauth_finish_descr">恭喜! 你的 gpodder.net 帐户与设备已连结完成. 现在开始 AntennaPod 将自动同步你 gpodder.net 帐户内的订阅信息到设备上.</string> + <string name="gpodnetauth_finish_butsyncnow">开始同步</string> + <string name="gpodnetauth_finish_butgomainscreen">返回主屏</string> + <string name="gpodnetsync_auth_error_title">gpodder.net 验证错误</string> + <string name="gpodnetsync_auth_error_descr">错误的用户名或者密码</string> + <string name="gpodnetsync_error_title">gpodder.net 同步错误</string> + <string name="gpodnetsync_error_descr">同步过程中发生错误: \u0020</string> + <!--Directory chooser--> + <string name="selected_folder_label">已选文件夹:</string> + <string name="create_folder_label">穿件文件夹</string> + <string name="choose_data_directory">选择数据文件夹</string> + <string name="create_folder_msg">确实创建 \"%1$s\" 文件夹?</string> + <string name="create_folder_success">创建新文件夹</string> + <string name="create_folder_error_no_write_access">本文件夹不能写入</string> + <string name="create_folder_error_already_exists">文件夹已存在</string> + <string name="create_folder_error">不能创建文件夹</string> + <string name="folder_not_empty_dialog_title">文件夹不能为空</string> + <string name="folder_not_empty_dialog_msg">您所选择的文件夹不能为空. 媒体下载和其他文件将被直接放在本文件夹. 确认继续吗?</string> + <string name="set_to_default_folder">选择默认文件夹</string> + <string name="pref_pausePlaybackForFocusLoss_sum">当另一个应用程序要播放声音时暂停播放, 而不是降低音量</string> + <string name="pref_pausePlaybackForFocusLoss_title">中断暂停</string> + <!--Online feed view--> + <string name="subscribe_label">订阅</string> + <string name="subscribed_label">已订阅</string> + <string name="downloading_label">下载中...</string> + <!--Content descriptions for image buttons--> + <string name="show_chapters_label">显示章节</string> + <string name="show_shownotes_label">显示笔记</string> + <string name="show_cover_label">显示图片</string> + <string name="rewind_label">回放</string> + <string name="fast_forward_label">快进</string> + <string name="media_type_audio_label">音频</string> + <string name="media_type_video_label">视频</string> + <string name="navigate_upwards_label">向上导航</string> + <string name="butAction_label">更多动作</string> + <string name="status_playing_label">曲目正在播放</string> + <string name="status_downloading_label">曲目正在下载</string> + <string name="status_downloaded_label">曲目已下载</string> + <string name="status_unread_label">新项目</string> + <string name="in_queue_label">曲目已经在播放列表中</string> + <string name="new_episodes_count_label">新曲目数</string> + <string name="in_progress_episodes_count_label">已收听曲目数</string> + <string name="drag_handle_content_description">拖动以变更本项目的位置</string> + <!--Feed information screen--> + <string name="authentication_label">验证</string> + <string name="authentication_descr">给本播客及曲目变更用户名及密码</string> + <!--AntennaPodSP--> + <string name="sp_apps_importing_feeds_msg">正在从选定的应用中导入订阅...</string> +</resources> diff --git a/core/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml new file mode 100644 index 000000000..9b9079021 --- /dev/null +++ b/core/src/main/res/values/arrays.xml @@ -0,0 +1,120 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="seek_delta_values"> + <item>5</item> + <item>10</item> + <item>15</item> + <item>20</item> + <item>30</item> + <item>45</item> + <item>60</item> + </string-array> + + <string-array name="update_intervall_options"> + <item>Manual</item> + <item>1 hour</item> + <item>2 hours</item> + <item>4 hours</item> + <item>8 hours</item> + <item>12 hours</item> + <item>24 hours</item> + </string-array> + + <string-array name="update_intervall_values"> + <item>0</item> + <item>1</item> + <item>2</item> + <item>4</item> + <item>8</item> + <item>12</item> + <item>24</item> + </string-array> + <string-array name="episode_cache_size_entries"> + <item>@string/pref_episode_cache_unlimited</item> + <item>1</item> + <item>2</item> + <item>5</item> + <item>10</item> + <item>20</item> + <item>40</item> + <item>60</item> + <item>80</item> + <item>100</item> + </string-array> + <string-array name="episode_cache_size_values"> + <item>-1</item> + <item>1</item> + <item>2</item> + <item>5</item> + <item>10</item> + <item>20</item> + <item>40</item> + <item>60</item> + <item>80</item> + <item>100</item> + </string-array> + <string-array name="playback_speed_values"> + <item>0.5</item> + <item>0.6</item> + <item>0.7</item> + <item>0.8</item> + <item>0.9</item> + <item>1.0</item> + <item>1.05</item> + <item>1.10</item> + <item>1.15</item> + <item>1.20</item> + <item>1.25</item> + <item>1.30</item> + <item>1.35</item> + <item>1.40</item> + <item>1.45</item> + <item>1.50</item> + <item>1.55</item> + <item>1.60</item> + <item>1.65</item> + <item>1.70</item> + <item>1.75</item> + <item>1.80</item> + <item>1.85</item> + <item>1.90</item> + <item>1.95</item> + <item>2.00</item> + <item>2.10</item> + <item>2.20</item> + <item>2.30</item> + <item>2.40</item> + <item>2.50</item> + <item>2.60</item> + <item>2.70</item> + <item>2.80</item> + <item>2.90</item> + <item>3.00</item> + <item>3.10</item> + <item>3.20</item> + <item>3.30</item> + <item>3.40</item> + <item>3.50</item> + <item>3.60</item> + <item>3.70</item> + <item>3.80</item> + <item>3.90</item> + <item>4.00</item> + </string-array> + + <string-array name="autodl_select_networks_default_entries"> + <item>N/A</item> + </string-array> + <string-array name="autodl_select_networks_default_values"> + <item>0</item> + </string-array> + <string-array name="theme_options"> + <item>@string/pref_theme_title_light</item> + <item>@string/pref_theme_title_dark</item> + </string-array> + <string-array name="theme_values"> + <item>0</item> + <item>1</item> + </string-array> +</resources> diff --git a/core/src/main/res/values/attrs.xml b/core/src/main/res/values/attrs.xml new file mode 100644 index 000000000..c46a2118f --- /dev/null +++ b/core/src/main/res/values/attrs.xml @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <attr name="action_about" format="reference"/> + <attr name="action_search" format="reference"/> + <attr name="action_stream" format="reference"/> + <attr name="av_download" format="reference"/> + <attr name="av_fast_forward" format="reference"/> + <attr name="av_pause" format="reference"/> + <attr name="av_play" format="reference"/> + <attr name="av_rewind" format="reference"/> + <attr name="content_discard" format="reference"/> + <attr name="content_new" format="reference"/> + <attr name="device_access_time" format="reference"/> + <attr name="location_web_site" format="reference"/> + <attr name="navigation_accept" format="reference"/> + <attr name="navigation_cancel" format="reference"/> + <attr name="navigation_expand" format="reference"/> + <attr name="navigation_refresh" format="reference"/> + <attr name="navigation_up" format="reference"/> + <attr name="navigation_shownotes" format="reference"/> + <attr name="navigation_chapters" format="reference"/> + <attr name="social_share" format="reference"/> + <attr name="stat_playlist" format="reference"/> + <attr name="type_audio" format="reference"/> + <attr name="type_video" format="reference"/> + <attr name="borderless_button" format="reference"/> + <attr name="overlay_drawable" format="reference"/> + <attr name="dragview_background" format="reference"/> + <attr name="dragview_float_background" format="reference"/> + <attr name="ic_action_overflow" format="reference"/> + <attr name="ic_new" format="reference"/> + <attr name="ic_history" format="reference"/> + <attr name="av_play_big" format="reference"/> + <attr name="av_pause_big" format="reference"/> + <attr name="av_ff_big" format="reference"/> + <attr name="av_rew_big" format="reference"/> + + <!-- Used in itemdescription --> + <attr name="non_transparent_background" format="reference"/> + <attr name="overlay_background" format="color"/> + + <attr name="nav_drawer_background" format="color"/> +</resources>
\ No newline at end of file diff --git a/core/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml new file mode 100644 index 000000000..ab48fafe7 --- /dev/null +++ b/core/src/main/res/values/colors.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <color name="white">#FFFFFF</color> + <color name="gray">#808080</color> + <color name="black">#000000</color> + <color name="bright_blue">#33B5E5</color> + <color name="ics_gray">#858585</color> + <color name="actionbar_gray">#DDDDDD</color> + <color name="download_success_green">#669900</color> + <color name="download_failed_red">#CC0000</color> + <color name="status_progress">#E033B5E5</color> + <color name="status_playing">#E0EE5F52</color> + <color name="overlay_dark">#262C31</color> + <color name="overlay_light">#DDDDDD</color> + <color name="swipe_refresh_secondary_color_light">#EDEDED</color> + <color name="swipe_refresh_secondary_color_dark">#060708</color> + <color name="new_indicator_green">#669900</color> + + <!-- Use Gingerbread-orange --> + <color name="selection_background_color_dark">#FEBB20</color> + <color name="selection_background_color_light">#FEBB20</color> + + <!-- Theme colors --> + <color name="primary_light">#FFFFFF</color> + <color name="color_accent">#009EC8</color> + + +</resources>
\ No newline at end of file diff --git a/core/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml new file mode 100644 index 000000000..38c14b024 --- /dev/null +++ b/core/src/main/res/values/dimens.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <dimen name="widget_margin">8dp</dimen> + <dimen name="thumbnail_length">70dp</dimen> + <dimen name="external_player_height">70dp</dimen> + <dimen name="enc_icons_size">20dp</dimen> + <dimen name="text_size_micro">12sp</dimen> + <dimen name="text_size_small">14sp</dimen> + <dimen name="text_size_navdrawer">16sp</dimen> + <dimen name="text_size_medium">18sp</dimen> + <dimen name="text_size_large">22sp</dimen> + <dimen name="status_indicator_width">32dp</dimen> + <dimen name="thumbnail_length_itemlist">64dp</dimen> + <dimen name="thumbnail_length_queue_item">64dp</dimen> + <dimen name="thumbnail_length_downloaded_item">64dp</dimen> + <dimen name="thumbnail_length_onlinefeedview">100dp</dimen> + <dimen name="feeditemlist_header_height">132dp</dimen> + <dimen name="thumbnail_length_navlist">42dp</dimen> + <dimen name="listview_secondary_button_width">48dp</dimen> + <dimen name="drawer_width">280dp</dimen> + <dimen name="listitem_iconwithtext_height">56dp</dimen> + <dimen name="listitem_iconwithtext_textleftpadding">14dp</dimen> + <dimen name="listitem_iconwithtext_textverticalpadding">16dp</dimen> + + <dimen name="listitem_threeline_height">96dp</dimen> + <dimen name="listitem_threeline_textleftpadding">14dp</dimen> + <dimen name="listitem_threeline_textrightpadding">8dp</dimen> + <dimen name="listitem_threeline_verticalpadding">16dp</dimen> + <dimen name="listitem_threeline_horizontalpadding">16dp</dimen> + + <dimen name="list_vertical_padding">8dp</dimen> + <dimen name="listitem_icon_leftpadding">16dp</dimen> + <dimen name="listitem_icon_rightpadding">16dp</dimen> + +</resources>
\ No newline at end of file diff --git a/core/src/main/res/values/ids.xml b/core/src/main/res/values/ids.xml new file mode 100644 index 000000000..90e405fde --- /dev/null +++ b/core/src/main/res/values/ids.xml @@ -0,0 +1,27 @@ +<resources> + + <item name="action_bar_refresh" type="id"/> + <item name="action_bar_add" type="id"/> + <item name="clear_queue_item" type="id"/> + <item name="select_all_item" type="id"/> + <item name="deselect_all_item" type="id"/> + <item name="search_item" type="id"/> + <item name="enqueue_all_item" type="id"/> + <item name="download_all_item" type="id"/> + <item name="clear_history_item" type="id"/> + <item name="open_in_browser_item" type="id"/> + <item name="copy_url_item" type="id"/> + <item name="share_url_item" type="id"/> + <item name="go_to_position_item" type="id"/> + <item name="organize_queue_item" type="id"/> + <item name="drag_handle" type="id"/> + <item name="skip_episode_item" type="id"/> + <item name="move_to_top_item" type="id"/> + <item name="move_to_bottom_item" type="id"/> + <item name="image_disk_cache_key" type="id"/> + <item name="imageloader_key" type="id"/> + <item name="notification_gpodnet_sync_error" type="id"/> + <item name="notification_gpodnet_sync_autherror" type="id"/> + <item name="undobar_button" type="id"/> + <item name="undobar_message" type="id"/> +</resources>
\ No newline at end of file diff --git a/core/src/main/res/values/integers.xml b/core/src/main/res/values/integers.xml new file mode 100644 index 000000000..33501d9fb --- /dev/null +++ b/core/src/main/res/values/integers.xml @@ -0,0 +1,4 @@ +<resources> + <integer name="undobar_hide_delay">5000</integer> + <integer name="episode_cache_size_unlimited">-1</integer> +</resources> diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml new file mode 100644 index 000000000..86f46a330 --- /dev/null +++ b/core/src/main/res/values/strings.xml @@ -0,0 +1,382 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources + xmlns:tools="http://schemas.android.com/tools" + tools:ignore="MissingTranslation" + > + + <!-- Activitiy and fragment titles --> + <string name="app_name">AntennaPod</string> + <string name="feeds_label">Feeds</string> + <string name="add_feed_label">Add podcast</string> + <string name="podcasts_label">PODCASTS</string> + <string name="episodes_label">EPISODES</string> + <string name="new_episodes_label">New episodes</string> + <string name="all_episodes_label">All episodes</string> + <string name="new_label">New</string> + <string name="waiting_list_label">Waiting list</string> + <string name="settings_label">Settings</string> + <string name="add_new_feed_label">Add podcast</string> + <string name="downloads_label">Downloads</string> + <string name="downloads_running_label">Running</string> + <string name="downloads_completed_label">Completed</string> + <string name="downloads_log_label">Log</string> + <string name="cancel_download_label">Cancel Download</string> + <string name="playback_history_label">Playback history</string> + <string name="gpodnet_main_label">gpodder.net</string> + <string name="gpodnet_auth_label">gpodder.net login</string> + + <!-- New episodes fragment --> + <string name="recently_published_episodes_label">Recently published</string> + <string name="episode_filter_label">Show only new episodes</string> + + <!-- Main activity --> + <string name="drawer_open">Open menu</string> + <string name="drawer_close">Close menu</string> + + <!-- Webview actions --> + <string name="open_in_browser_label">Open in browser</string> + <string name="copy_url_label">Copy URL</string> + <string name="share_url_label">Share URL</string> + <string name="copied_url_msg">Copied URL to clipboard.</string> + <string name="go_to_position_label">Go to this position</string> + + <!-- Playback history --> + <string name="clear_history_label">Clear history</string> + + <!-- Other --> + <string name="confirm_label">Confirm</string> + <string name="cancel_label">Cancel</string> + <string name="author_label">Author</string> + <string name="language_label">Language</string> + <string name="podcast_settings_label">Settings</string> + <string name="cover_label">Picture</string> + <string name="error_label">Error</string> + <string name="error_msg_prefix">An error occurred:</string> + <string name="refresh_label">Refresh</string> + <string name="external_storage_error_msg">No external storage is available. Please make sure that external storage is mounted so that the app can work properly.</string> + <string name="chapters_label">Chapters</string> + <string name="shownotes_label">Shownotes</string> + <string name="description_label">Description</string> + <string name="most_recent_prefix">Most Recent Episode:\u0020</string> + <string name="episodes_suffix">\u0020episodes</string> + <string name="length_prefix">Length:\u0020</string> + <string name="size_prefix">Size:\u0020</string> + <string name="processing_label">Processing</string> + <string name="loading_label">Loading...</string> + <string name="save_username_password_label">Save username and password</string> + <string name="close_label">Close</string> + <string name="retry_label">Retry</string> + <string name="auto_download_label">Include in auto downloads</string> + + <!-- 'Add Feed' Activity labels --> + <string name="feedurl_label">Feed URL</string> + <string name="etxtFeedurlHint">URL of feed or website</string> + <string name="txtvfeedurl_label">Add Podcast by URL</string> + <string name="podcastdirectories_label">Find podcast in directory</string> + <string name="podcastdirectories_descr">You can search for new podcasts by name, category or popularity in the gpodder.net directory.</string> + <string name="browse_gpoddernet_label">Browse gpodder.net</string> + + <!-- Actions on feeds --> + <string name="mark_all_read_label">Mark all as read</string> + <string name="mark_all_read_msg">Marked all episodes as read</string> + <string name="show_info_label">Show information</string> + <string name="remove_feed_label">Remove podcast</string> + <string name="share_link_label">Share website link</string> + <string name="share_source_label">Share feed link</string> + <string name="feed_delete_confirmation_msg">Please confirm that you want to delete this feed and ALL episodes of this feed that you have downloaded.</string> + <string name="feed_remover_msg">Removing feed</string> + <string name="load_complete_feed">Refresh complete feed</string> + + <!-- actions on feeditems --> + <string name="download_label">Download</string> + <string name="play_label">Play</string> + <string name="pause_label">Pause</string> + <string name="stop_label">Stop</string> + <string name="stream_label">Stream</string> + <string name="remove_label">Remove</string> + <string name="remove_episode_lable">Remove episode</string> + <string name="mark_read_label">Mark as read</string> + <string name="mark_unread_label">Mark as unread</string> + <string name="add_to_queue_label">Add to Queue</string> + <string name="remove_from_queue_label">Remove from Queue</string> + <string name="visit_website_label">Visit Website</string> + <string name="support_label">Flattr this</string> + <string name="enqueue_all_new">Enqueue all</string> + <string name="download_all">Download all</string> + <string name="skip_episode_label">Skip episode</string> + + <!-- Download messages and labels --> + <string name="download_successful">successful</string> + <string name="download_failed">failed</string> + <string name="download_pending">Download pending</string> + <string name="download_running">Download running</string> + <string name="download_error_device_not_found">Storage device not found</string> + <string name="download_error_insufficient_space">Insufficient space</string> + <string name="download_error_file_error">File error</string> + <string name="download_error_http_data_error">HTTP Data Error</string> + <string name="download_error_error_unknown">Unknown Error</string> + <string name="download_error_parser_exception">Parser Exception</string> + <string name="download_error_unsupported_type">Unsupported Feed type</string> + <string name="download_error_connection_error">Connection error</string> + <string name="download_error_unknown_host">Unknown host</string> + <string name="download_error_unauthorized">Authentication error</string> + <string name="cancel_all_downloads_label">Cancel all downloads</string> + <string name="download_cancelled_msg">Download cancelled</string> + <string name="download_report_title">Downloads completed</string> + <string name="download_error_malformed_url">Malformed URL</string> + <string name="download_error_io_error">IO Error</string> + <string name="download_error_request_error">Request error</string> + <string name="download_error_db_access">Database access error</string> + <string name="downloads_left">\u0020Downloads left</string> + <string name="downloads_processing">Processing downloads</string> + <string name="download_notification_title">Downloading podcast data</string> + <string name="download_report_content">%1$d downloads succeeded, %2$d failed</string> + <string name="download_log_title_unknown">Unknown title</string> + <string name="download_type_feed">Feed</string> + <string name="download_type_media">Media file</string> + <string name="download_type_image">Image</string> + <string name="download_request_error_dialog_message_prefix">An error occurred when trying to download the file:\u0020</string> + <string name="authentication_notification_title">Authentication required</string> + <string name="authentication_notification_msg">The resource you requested requires a username and a password</string> + + <!-- Mediaplayer messages --> + <string name="player_error_msg">Error!</string> + <string name="player_stopped_msg">No media playing</string> + <string name="player_preparing_msg">Preparing</string> + <string name="player_ready_msg">Ready</string> + <string name="player_seeking_msg">Seeking</string> + <string name="playback_error_server_died">Server died</string> + <string name="playback_error_unknown">Unknown Error</string> + <string name="no_media_playing_label">No media playing</string> + <string name="position_default_label">00:00:00</string> + <string name="player_buffering_msg">Buffering</string> + <string name="playbackservice_notification_title">Playing podcast</string> + <string name="unknown_media_key">AntennaPod - Unknown media key: %1$d</string> + + <!-- Queue operations --> + <string name="clear_queue_label">Clear queue</string> + <string name="undo">Undo</string> + <string name="removed_from_queue">Item removed</string> + <string name="move_to_top_label">Move to top</string> + <string name="move_to_bottom_label">Move to bottom</string> + + <!-- Flattr --> + <string name="flattr_auth_label">Flattr sign-in</string> + <string name="flattr_auth_explanation">Press the button below to start the authentication process. You will be forwarded to the flattr login screen in your browser and be asked to give AntennaPod the permission to flattr things. After you have given permission, you will return to this screen automatically.</string> + <string name="authenticate_label">Authenticate</string> + <string name="return_home_label">Return to home</string> + <string name="flattr_auth_success">Authentication was successful! You can now flattr things within the app.</string> + <string name="no_flattr_token_title">No Flattr token found</string> + <string name="no_flattr_token_notification_msg">Your flattr account does not seem to be connected to AntennaPod. Tap here to authenticate.</string> + <string name="no_flattr_token_msg">Your flattr account does not seem to be connected to AntennaPod. You can either connect your account to AntennaPod to flattr things within the app or you can visit the website of the thing to flattr it there.</string> + <string name="authenticate_now_label">Authenticate</string> + <string name="action_forbidden_title">Action forbidden</string> + <string name="action_forbidden_msg">AntennaPod has no permission for this action. The reason for this could be that the access token of AntennaPod to your account has been revoked. You can either re-reauthenticate or visit the website of the thing instead.</string> + <string name="access_revoked_title">Access revoked</string> + <string name="access_revoked_info">You have successfully revoked AntennaPod\'s access token to your account. In order to complete the process, you have to remove this app from the list of approved applications in your account settings on the flattr website.</string> + + <!-- Flattr --> + <string name="flattr_click_success">Flattr\'ed one thing!</string> + <string name="flattr_click_success_count">Flattr\'ed %d things!</string> + <string name="flattr_click_success_queue">Flattr\'ed: %s.</string> + <string name="flattr_click_failure_count">Failed to flattr %d things!</string> + <string name="flattr_click_failure">Not flattr\'ed: %s.</string> + <string name="flattr_click_enqueued">Thing will be flattr\'ed later</string> + <string name="flattring_thing">Flattring %s</string> + <string name="flattring_label">AntennaPod is flattring</string> + <string name="flattrd_label">AntennaPod has flattr\'ed</string> + <string name="flattrd_failed_label">AntennaPod flattr failed</string> + <string name="flattr_retrieving_status">Retrieving flattr\'ed things</string> + + <!-- Variable Speed --> + <string name="download_plugin_label">Download Plugin</string> + <string name="no_playback_plugin_title">Plugin Not Installed</string> + <string name="no_playback_plugin_msg">For variable speed playback to work, a third party library must be installed.\n\nTap \'Download Plugin\' to download a free plugin from the Play Store\n\nAny problems found using this plugin are not the responsibility of AntennaPod and should be reported to the plugin owner.</string> + <string name="set_playback_speed_label">Playback Speeds</string> + + <!-- Empty list labels --> + <string name="no_items_label">There are no items in this list.</string> + <string name="no_feeds_label">You haven\'t subscribed to any feeds yet.</string> + + <!-- Preferences --> + <string name="other_pref">Other</string> + <string name="about_pref">About</string> + <string name="queue_label">Queue</string> + <string name="services_label">Services</string> + <string name="flattr_label">Flattr</string> + <string name="pref_pauseOnHeadsetDisconnect_sum">Pause playback when the headphones are disconnected</string> + <string name="pref_followQueue_sum">Jump to next queue item when playback completes</string> + <string name="playback_pref">Playback</string> + <string name="network_pref">Network</string> + <string name="pref_autoUpdateIntervall_title">Update interval</string> + <string name="pref_autoUpdateIntervall_sum">Specify an interval in which the feeds are refreshed automatically or disable it</string> + <string name="pref_downloadMediaOnWifiOnly_sum">Download media files only over WiFi</string> + <string name="pref_followQueue_title">Continuous playback</string> + <string name="pref_downloadMediaOnWifiOnly_title">WiFi media download</string> + <string name="pref_pauseOnHeadsetDisconnect_title">Headphones disconnect</string> + <string name="pref_mobileUpdate_title">Mobile updates</string> + <string name="pref_mobileUpdate_sum">Allow updates over the mobile data connection</string> + <string name="refreshing_label">Refreshing</string> + <string name="flattr_settings_label">Flattr settings</string> + <string name="pref_flattr_auth_title">Flattr sign-in</string> + <string name="pref_flattr_auth_sum">Sign in to your flattr account to flattr things directly from the app.</string> + <string name="pref_flattr_this_app_title">Flattr this app</string> + <string name="pref_flattr_this_app_sum">Support the development of AntennaPod by flattring it. Thanks!</string> + <string name="pref_revokeAccess_title">Revoke access</string> + <string name="pref_revokeAccess_sum">Revoke the access permission to your flattr account for this app.</string> + <string name="pref_auto_flattr_title">Automatic Flattr</string> + <string name="pref_auto_flattr_sum">Configure automatic flattring</string> + <string name="user_interface_label">User Interface</string> + <string name="pref_set_theme_title">Select theme</string> + <string name="pref_set_theme_sum">Change the appearance of AntennaPod.</string> + <string name="pref_automatic_download_title">Automatic download</string> + <string name="pref_automatic_download_sum">Configure the automatic download of episodes.</string> + <string name="pref_autodl_wifi_filter_title">Enable Wi-Fi filter</string> + <string name="pref_autodl_wifi_filter_sum">Allow automatic download only for selected Wi-Fi networks.</string> + <string name="pref_episode_cache_title">Episode cache</string> + <string name="pref_theme_title_light">Light</string> + <string name="pref_theme_title_dark">Dark</string> + <string name="pref_episode_cache_unlimited">Unlimited</string> + <string name="pref_update_interval_hours_plural">hours</string> + <string name="pref_update_interval_hours_singular">hour</string> + <string name="pref_update_interval_hours_manual">Manual</string> + <string name="pref_gpodnet_authenticate_title">Login</string> + <string name="pref_gpodnet_authenticate_sum">Login with your gpodder.net account in order to sync your subscriptions.</string> + <string name="pref_gpodnet_logout_title">Logout</string> + <string name="pref_gpodnet_logout_toast">Logout was successful</string> + <string name="pref_gpodnet_setlogin_information_title">Change login information</string> + <string name="pref_gpodnet_setlogin_information_sum">Change the login information for your gpodder.net account.</string> + <string name="pref_playback_speed_title">Playback Speeds</string> + <string name="pref_playback_speed_sum">Customize the speeds available for variable speed audio playback</string> + <string name="pref_seek_delta_title">Seek time</string> + <string name="pref_seek_delta_sum">Seek this many seconds when rewinding or fast-forwarding</string> + <string name="pref_gpodnet_sethostname_title">Set hostname</string> + <string name="pref_gpodnet_sethostname_use_default_host">Use default host</string> + <string name="pref_expandNotify_title">Expand Notification</string> + <string name="pref_expandNotify_sum">Always expand the notification to show playback buttons.</string> + <string name="pref_persistNotify_title">Persistent playback controls</string> + <string name="pref_persistNotify_sum">Keep notification and lockscreen controls when playback is paused.</string> + <string name="pref_expand_notify_unsupport_toast">Android versions before 4.1 do not support expanded notifications.</string> + + <!-- Auto-Flattr dialog --> + <string name="auto_flattr_enable">Enable automatic flattring</string> + <string name="auto_flattr_after_percent">Flattr episode as soon as %d percent have been played</string> + <string name="auto_flattr_ater_beginning">Flattr episode when playback starts</string> + <string name="auto_flattr_ater_end">Flattr episode when playback ends</string> + + <!-- Search --> + <string name="search_hint">Search for Feeds or Episodes</string> + <string name="found_in_shownotes_label">Found in shownotes</string> + <string name="found_in_chapters_label">Found in chapters</string> + <string name="search_status_no_results">No results were found</string> + <string name="search_label">Search</string> + <string name="found_in_title_label">Found in title</string> + + <!-- OPML import and export --> + <string name="opml_import_txtv_button_lable">OPML files allow you to move your podcasts from one podcatcher to another.</string> + <string name="opml_import_explanation">To import an OPML file, you have to place it in the following directory and press the button below to start the import process. </string> + <string name="start_import_label">Start import</string> + <string name="opml_import_label">OPML import</string> + <string name="opml_directory_error">ERROR!</string> + <string name="reading_opml_label">Reading OPML file</string> + <string name="opml_reader_error">An error has occurred while reading the opml document:</string> + <string name="opml_import_error_dir_empty">The import directory is empty.</string> + <string name="select_all_label">Select all</string> + <string name="deselect_all_label">Deselect all</string> + <string name="choose_file_to_import_label">Choose file to import</string> + <string name="opml_export_label">OPML export</string> + <string name="exporting_label">Exporting...</string> + <string name="export_error_label">Export error</string> + <string name="opml_export_success_title">Opml export successful.</string> + <string name="opml_export_success_sum">The .opml file was written to:\u0020</string> + + <!-- Sleep timer --> + <string name="set_sleeptimer_label">Set sleep timer</string> + <string name="disable_sleeptimer_label">Disable sleep timer</string> + <string name="enter_time_here_label">Enter time</string> + <string name="sleep_timer_label">Sleep timer</string> + <string name="time_left_label">Time left:\u0020</string> + <string name="time_dialog_invalid_input">Invalid input, time has to be an integer</string> + <string name="time_unit_seconds">seconds</string> + <string name="time_unit_minutes">minutes</string> + <string name="time_unit_hours">hours</string> + + <!-- gpodder.net --> + <string name="gpodnet_taglist_header">CATEGORIES</string> + <string name="gpodnet_toplist_header">TOP PODCASTS</string> + <string name="gpodnet_suggestions_header">SUGGESTIONS</string> + <string name="gpodnet_search_hint">Search gpodder.net</string> + <string name="gpodnetauth_login_title">Login</string> + <string name="gpodnetauth_login_descr">Welcome to the gpodder.net login process. First, type in your login information:</string> + <string name="gpodnetauth_login_butLabel">Login</string> + <string name="gpodnetauth_login_register">If you do not have an account yet, you can create one here:\nhttps://gpodder.net/register/</string> + <string name="username_label">Username</string> + <string name="password_label">Password</string> + <string name="gpodnetauth_device_title">Device Selection</string> + <string name="gpodnetauth_device_descr">Create a new device to use for your gpodder.net account or choose an existing one:</string> + <string name="gpodnetauth_device_deviceID">Device ID:\u0020</string> + <string name="gpodnetauth_device_caption">Caption</string> + <string name="gpodnetauth_device_butCreateNewDevice">Create new device</string> + <string name="gpodnetauth_device_chooseExistingDevice">Choose existing device:</string> + <string name="gpodnetauth_device_errorEmpty">Device ID must not be empty</string> + <string name="gpodnetauth_device_errorAlreadyUsed">Device ID already in use</string> + + <string name="gpodnetauth_device_butChoose">Choose</string> + <string name="gpodnetauth_finish_title">Login successful!</string> + <string name="gpodnetauth_finish_descr">Congratulations! Your gpodder.net account is now linked with your device. AntennaPod will from now on automatically sync subscriptions on your device with your gpodder.net account.</string> + <string name="gpodnetauth_finish_butsyncnow">Start sync now</string> + <string name="gpodnetauth_finish_butgomainscreen">Go to main screen</string> + + <string name="gpodnetsync_auth_error_title">gpodder.net authentication error</string> + <string name="gpodnetsync_auth_error_descr">Wrong username or password</string> + <string name="gpodnetsync_error_title">gpodder.net sync error</string> + <string name="gpodnetsync_error_descr">An error occurred during syncing:\u0020</string> + + <!-- Directory chooser --> + <string name="selected_folder_label">Selected folder:</string> + <string name="create_folder_label">Create folder</string> + <string name="choose_data_directory">Choose data folder</string> + <string name="create_folder_msg">Create new folder with name "%1$s"?</string> + <string name="create_folder_success">Created new folder</string> + <string name="create_folder_error_no_write_access">Cannot write to this folder</string> + <string name="create_folder_error_already_exists">Folder already exists</string> + <string name="create_folder_error">Could not create folder</string> + <string name="folder_not_empty_dialog_title">Folder is not empty</string> + <string name="folder_not_empty_dialog_msg">The folder you have selected is not empty. Media downloads and other files will be placed directly in this folder. Continue anyway?</string> + <string name="set_to_default_folder">Choose default folder</string> + <string name="pref_pausePlaybackForFocusLoss_sum">Pause playback instead of lowering volume when another app wants to play sounds</string> + <string name="pref_pausePlaybackForFocusLoss_title">Pause for interruptions</string> + + <!-- Online feed view --> + <string name="subscribe_label">Subscribe</string> + <string name="subscribed_label">Subscribed</string> + <string name="downloading_label">Downloading...</string> + + <!-- Content descriptions for image buttons --> + <string name="show_chapters_label">Show chapters</string> + <string name="show_shownotes_label">Show shownotes</string> + <string name="show_cover_label">Show picture</string> + <string name="rewind_label">Rewind</string> + <string name="fast_forward_label">Fast forward</string> + <string name="media_type_audio_label">Audio</string> + <string name="media_type_video_label">Video</string> + <string name="navigate_upwards_label">Navigate upwards</string> + <string name="butAction_label">More actions</string> + <string name="status_playing_label">Episode is being played</string> + <string name="status_downloading_label">Episode is being downloaded</string> + <string name="status_downloaded_label">Episode is downloaded</string> + <string name="status_unread_label">Item is new</string> + <string name="in_queue_label">Episode is in the queue</string> + <string name="new_episodes_count_label">Number of new episodes</string> + <string name="in_progress_episodes_count_label">Number of episodes you have started listening to</string> + <string name="drag_handle_content_description">Drag to change the position of this item</string> + <string name="load_next_page_label">Load next page</string> + + <!-- Feed information screen --> + <string name="authentication_label">Authentication</string> + <string name="authentication_descr">Change your username and password for this podcast and its episodes.</string> + + <!-- AntennaPodSP --> + + <string name="sp_apps_importing_feeds_msg">Importing subscriptions from single-purpose apps…</string> +</resources> diff --git a/core/src/main/res/values/styles.xml b/core/src/main/res/values/styles.xml new file mode 100644 index 000000000..df7ae385c --- /dev/null +++ b/core/src/main/res/values/styles.xml @@ -0,0 +1,160 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="Theme.AntennaPod.Light" parent="@style/Theme.AppCompat.Light"> + <item name="colorPrimary">@color/primary_light</item> + <item name="colorAccent">@color/color_accent</item> + <item name="attr/action_about">@drawable/ic_info_grey600_24dp</item> + <item name="attr/action_search">@drawable/ic_search_grey600_24dp</item> + <item name="attr/action_stream">@drawable/ic_settings_input_antenna_grey600_24dp</item> + <item name="attr/av_download">@drawable/ic_file_download_grey600_24dp</item> + <item name="attr/av_fast_forward">@drawable/ic_fast_forward_grey600_24dp</item> + <item name="attr/av_pause">@drawable/ic_pause_grey600_24dp</item> + <item name="attr/av_play">@drawable/ic_play_arrow_grey600_24dp</item> + <item name="attr/av_rewind">@drawable/ic_fast_rewind_grey600_24dp</item> + <item name="attr/content_discard">@drawable/ic_delete_grey600_24dp</item> + <item name="attr/content_new">@drawable/ic_add_grey600_24dp</item> + <item name="attr/device_access_time">@drawable/ic_timer_grey600_24dp</item> + <item name="attr/location_web_site">@drawable/ic_web_grey600_24dp</item> + <item name="attr/navigation_accept">@drawable/ic_done_grey600_24dp</item> + <item name="attr/navigation_cancel">@drawable/ic_cancel_grey600_24dp</item> + <item name="attr/navigation_expand">@drawable/ic_expand_more_grey600_36dp</item> + <item name="attr/navigation_refresh">@drawable/ic_refresh_grey600_24dp</item> + <item name="attr/navigation_up">@drawable/navigation_up</item> + <item name="attr/navigation_shownotes">@drawable/ic_description_grey600_36dp</item> + <item name="attr/navigation_chapters">@drawable/ic_toc_grey600_36dp</item> + <item name="attr/social_share">@drawable/ic_share_grey600_24dp</item> + <item name="attr/stat_playlist">@drawable/ic_list_grey600_24dp</item> + <item name="attr/type_audio">@drawable/ic_hearing_grey600_18dp</item> + <item name="attr/type_video">@drawable/ic_remove_red_eye_grey600_18dp</item> + <item name="attr/non_transparent_background">@color/white</item> + <item name="attr/overlay_background">@color/overlay_light</item> + <item name="attr/overlay_drawable">@drawable/overlay_drawable</item> + <item name="attr/dragview_background">@drawable/ic_drag_handle</item> + <item name="attr/dragview_float_background">@color/white</item> + <item name="attr/nav_drawer_background">@color/white</item> + <item name="attr/ic_action_overflow">@drawable/ic_more_vert_grey600_24dp</item> + <item name="attr/ic_new">@drawable/ic_new_releases_grey600_24dp</item> + <item name="attr/ic_history">@drawable/ic_history_grey600_24dp</item> + <item name="attr/av_play_big">@drawable/ic_play_arrow_grey600_36dp</item> + <item name="attr/av_pause_big">@drawable/ic_pause_grey600_36dp</item> + <item name="attr/av_ff_big">@drawable/ic_fast_forward_grey600_36dp</item> + <item name="attr/av_rew_big">@drawable/ic_fast_rewind_grey600_36dp</item> + </style> + + <style name="Theme.AntennaPod.Dark" parent="@style/Theme.AppCompat"> + <item name="colorAccent">@color/color_accent</item> + <item name="attr/action_about">@drawable/ic_info_white_24dp</item> + <item name="attr/action_search">@drawable/ic_search_white_24dp</item> + <item name="attr/action_stream">@drawable/ic_settings_input_antenna_white_24dp</item> + <item name="attr/av_download">@drawable/ic_file_download_white_24dp</item> + <item name="attr/av_fast_forward">@drawable/ic_fast_forward_white_24dp</item> + <item name="attr/av_pause">@drawable/ic_pause_white_24dp</item> + <item name="attr/av_play">@drawable/ic_play_arrow_white_24dp</item> + <item name="attr/av_rewind">@drawable/ic_fast_rewind_white_24dp</item> + <item name="attr/content_discard">@drawable/ic_delete_white_24dp</item> + <item name="attr/content_new">@drawable/ic_add_white_24dp</item> + <item name="attr/device_access_time">@drawable/ic_timer_white_24dp</item> + <item name="attr/location_web_site">@drawable/ic_web_white_24dp</item> + <item name="attr/navigation_accept">@drawable/ic_done_white_24dp</item> + <item name="attr/navigation_cancel">@drawable/ic_cancel_white_24dp</item> + <item name="attr/navigation_expand">@drawable/ic_expand_more_white_36dp</item> + <item name="attr/navigation_refresh">@drawable/ic_refresh_white_24dp</item> + <item name="attr/navigation_up">@drawable/navigation_up_dark</item> + <item name="attr/navigation_shownotes">@drawable/ic_description_white_36dp</item> + <item name="attr/navigation_chapters">@drawable/ic_toc_white_36dp</item> + <item name="attr/social_share">@drawable/ic_share_white_24dp</item> + <item name="attr/stat_playlist">@drawable/ic_list_white_24dp</item> + <item name="attr/type_audio">@drawable/ic_hearing_white_18dp</item> + <item name="attr/type_video">@drawable/ic_remove_red_eye_white_18dp</item> + <item name="attr/non_transparent_background">@color/black</item> + <item name="attr/overlay_background">@color/overlay_dark</item> + <item name="attr/overlay_drawable">@drawable/overlay_drawable_dark</item> + <item name="attr/dragview_background">@drawable/ic_drag_handle_dark</item> + <item name="attr/dragview_float_background">@color/black</item> + <item name="attr/nav_drawer_background">#3B3B3B</item> + <item name="attr/ic_action_overflow">@drawable/ic_more_vert_white_24dp</item> + <item name="attr/ic_new">@drawable/ic_new_releases_white_24dp</item> + <item name="attr/ic_history">@drawable/ic_history_white_24dp</item> + <item name="attr/av_play_big">@drawable/ic_play_arrow_white_36dp</item> + <item name="attr/av_pause_big">@drawable/ic_pause_white_36dp</item> + <item name="attr/av_ff_big">@drawable/ic_fast_forward_white_36dp</item> + <item name="attr/av_rew_big">@drawable/ic_fast_rewind_white_36dp</item> + </style> + + <style name="Theme.AntennaPod.VideoPlayer" parent="@style/Theme.AntennaPod.Dark"> + <item name="windowActionBarOverlay">true</item> + </style> + + <style name="UndoBar"> + <item name="android:layout_width">match_parent</item> + <item name="android:layout_height">48dp</item> + <item name="android:layout_gravity">bottom</item> + <item name="android:layout_marginLeft">8dp</item> + <item name="android:layout_marginRight">8dp</item> + <item name="android:layout_marginBottom">16dp</item> + <item name="android:orientation">horizontal</item> + <item name="android:background">@drawable/undobar</item> + <item name="android:clickable">true</item> + <item name="android:divider">@drawable/undobar_divider</item> + </style> + + <style name="UndoBarMessage"> + <item name="android:layout_width">0dp</item> + <item name="android:layout_weight">1</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_marginLeft">16dp</item> + <item name="android:layout_gravity">center_vertical</item> + <item name="android:layout_marginRight">16dp</item> + <item name="android:textAppearance">?android:textAppearanceMedium</item> + <item name="android:textColor">#fff</item> + </style> + + <style name="UndoBarButton"> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">match_parent</item> + <item name="android:paddingLeft">16dp</item> + <item name="android:paddingRight">16dp</item> + <item name="android:background">@drawable/undobar_button</item> + <item name="android:drawableLeft">@drawable/ic_undobar_undo</item> + <item name="android:drawablePadding">12dp</item> + <item name="android:textAppearance">?android:textAppearanceSmall</item> + <item name="android:textStyle">bold</item> + <item name="android:textColor">#fff</item> + <item name="android:text">@string/undo</item> + </style> + + <style name="AntennaPod.TextView.Heading" parent="@android:style/TextAppearance.Medium"> + <item name="android:textSize">@dimen/text_size_large</item> + <item name="android:textStyle">italic</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + </style> + + <style name="AntennaPod.TextView.ListItemPrimaryTitle" parent="@android:style/TextAppearance.Small"> + <item name="android:textSize">16sp</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:lines">2</item> + <item name="android:ellipsize">end</item> + </style> + + <style name="AntennaPod.TextView.ListItemSecondaryTitle" parent="@android:style/TextAppearance.Small"> + <item name="android:textSize">14sp</item> + <item name="android:textColor">?android:attr/textColorSecondary</item> + <item name="android:lines">1</item> + <item name="android:ellipsize">end</item> + </style> + + <style name="AntennaPod.Dialog.Title" parent="@android:style/TextAppearance.Medium"> + <item name="android:textSize">@dimen/text_size_medium</item> + <item name="android:textColor">?android:attr/textColorPrimary</item> + <item name="android:maxLines">2</item> + <item name="android:ellipsize">end</item> + </style> + + <style name="AntennaPod.TextView.UnreadIndicator" parent="@android:style/TextAppearance.Small"> + <item name="android:textSize">@dimen/text_size_micro</item> + <item name="android:textColor">@color/new_indicator_green</item> + <item name="android:text">@string/new_label</item> + </style> + +</resources> |